This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 6778dca2e refactor(dev): use minijinja (#5494)
6778dca2e is described below

commit 6778dca2ec834d4e378972f7e107fc7942f28210
Author: tison <[email protected]>
AuthorDate: Thu Jan 2 14:37:27 2025 +0800

    refactor(dev): use minijinja (#5494)
---
 bindings/python/Cargo.toml                         |   2 +-
 bindings/python/python/opendal/__base.pyi          |  78 +++++++++++--
 dev/Cargo.lock                                     | 123 +--------------------
 dev/Cargo.toml                                     |   9 +-
 dev/src/generate/binding_python.rs                 |  89 ---------------
 dev/src/generate/mod.rs                            |  14 +--
 dev/src/generate/parser.rs                         |  40 +++----
 .../python.py.jinja2 => src/generate/python.j2}    |  35 +++---
 dev/src/generate/python.rs                         |  71 ++++++++++++
 dev/src/main.rs                                    |  23 ++--
 10 files changed, 206 insertions(+), 278 deletions(-)

diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml
index bf5739794..84c38d73e 100644
--- a/bindings/python/Cargo.toml
+++ b/bindings/python/Cargo.toml
@@ -47,7 +47,7 @@ default = [
 
 # NOTE: this is the feature we used to build pypi wheels.
 # When enable or disable some features,
-# also need to update dev/src/generate/binding_python.rs `enabled_service` to 
match it.
+# Also, you need to update the `enabled_service` function in 
dev/src/generate/python.rs to match it.
 services-all = [
   "default",
   "services-aliyun-drive",
diff --git a/bindings/python/python/opendal/__base.pyi 
b/bindings/python/python/opendal/__base.pyi
index c9e794b81..6096af429 100644
--- a/bindings/python/python/opendal/__base.pyi
+++ b/bindings/python/python/opendal/__base.pyi
@@ -1,9 +1,3 @@
-"""
-this file is generated by opendal/dev/generate/binding_python.rs, and 
opendal.__base doesn't exists.
-
-DO NOT EDIT IT Manually
-"""
-
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -21,6 +15,12 @@ DO NOT EDIT IT Manually
 # specific language governing permissions and limitations
 # under the License.
 
+"""
+> DO NOT EDIT IT MANUALLY <
+
+This file is generated by opendal/dev/generate/python.rs. `opendal.__base` 
doesn't exists.
+"""
+
 from typing import overload, Literal, TypeAlias
 
 # `true`/`false`` in any case, for example, `true`/`True`/`TRUE` 
`false`/`False`/`FALSE`
@@ -33,6 +33,7 @@ _int: TypeAlias = str
 # for more details
 _duration: TypeAlias = str
 
+
 # A "," separated string, for example `"127.0.0.1:1,127.0.0.1:2"`
 _strings: TypeAlias = str
 
@@ -42,6 +43,7 @@ class _Base:
     The services list here is support by opendal pypi wheel.
     """
 
+
     @overload
     def __init__(
         self,
@@ -55,6 +57,7 @@ class _Base:
         client_secret: str = ...,
         refresh_token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -64,6 +67,7 @@ class _Base:
         root: str = ...,
         endpoint: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -76,6 +80,7 @@ class _Base:
         public_key: str = ...,
         parent_resource_id: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -93,6 +98,7 @@ class _Base:
         sas_token: str = ...,
         batch_max_operations: _int = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -105,6 +111,7 @@ class _Base:
         account_name: str = ...,
         account_key: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -118,6 +125,7 @@ class _Base:
         account_key: str = ...,
         sas_token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -130,6 +138,7 @@ class _Base:
         application_key_id: str = ...,
         application_key: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -138,6 +147,7 @@ class _Base:
         *,
         datadir: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -148,6 +158,7 @@ class _Base:
         root: str = ...,
         api_key: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -159,6 +170,7 @@ class _Base:
         namespace_id: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -167,6 +179,7 @@ class _Base:
         *,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -180,6 +193,7 @@ class _Base:
         bucket: str = ...,
         disable_config_load: _bool = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -194,6 +208,7 @@ class _Base:
         key_field: str = ...,
         value_field: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -202,6 +217,7 @@ class _Base:
         *,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -212,6 +228,7 @@ class _Base:
         endpoint: str = ...,
         token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -224,6 +241,7 @@ class _Base:
         client_id: str = ...,
         client_secret: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -233,6 +251,7 @@ class _Base:
         root: str = ...,
         atomic_write_dir: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -253,6 +272,7 @@ class _Base:
         disable_config_load: _bool = ...,
         token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -265,6 +285,7 @@ class _Base:
         client_id: str = ...,
         client_secret: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -276,6 +297,7 @@ class _Base:
         endpoint: str = ...,
         runtime_token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -287,6 +309,7 @@ class _Base:
         root: str = ...,
         token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -299,6 +322,7 @@ class _Base:
         chunk_size: _int = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -309,6 +333,7 @@ class _Base:
         url: str = ...,
         enable_append: _bool = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -321,6 +346,7 @@ class _Base:
         token: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -333,6 +359,7 @@ class _Base:
         root: str = ...,
         token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -346,6 +373,7 @@ class _Base:
         ds_web_auth_token: str = ...,
         is_china_mainland: _bool = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -355,6 +383,7 @@ class _Base:
         endpoint: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -364,6 +393,7 @@ class _Base:
         root: str = ...,
         endpoint: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -375,6 +405,7 @@ class _Base:
         root: str = ...,
         password: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -388,6 +419,7 @@ class _Base:
         repository: str = ...,
         branch: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -401,6 +433,7 @@ class _Base:
         value_field: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -413,6 +446,7 @@ class _Base:
         password: str = ...,
         default_ttl: _duration = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -421,6 +455,7 @@ class _Base:
         *,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -432,6 +467,7 @@ class _Base:
         time_to_idle: _duration = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -445,6 +481,7 @@ class _Base:
         num_segments: _int = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -458,6 +495,7 @@ class _Base:
         key_field: str = ...,
         value_field: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -466,6 +504,7 @@ class _Base:
         *,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -478,6 +517,7 @@ class _Base:
         value_field: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -494,6 +534,7 @@ class _Base:
         value_field: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -506,6 +547,7 @@ class _Base:
         secret_access_key: str = ...,
         bucket: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -515,6 +557,7 @@ class _Base:
         access_token: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -539,6 +582,7 @@ class _Base:
         oidc_token_file: str = ...,
         sts_endpoint: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -550,6 +594,7 @@ class _Base:
         username: str = ...,
         password: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -560,6 +605,7 @@ class _Base:
         segment: str = ...,
         index: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -572,6 +618,7 @@ class _Base:
         key_field: str = ...,
         value_field: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -582,6 +629,7 @@ class _Base:
         root: str = ...,
         table: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -596,6 +644,7 @@ class _Base:
         root: str = ...,
         default_ttl: _duration = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -630,6 +679,7 @@ class _Base:
         checksum_algorithm: str = ...,
         disable_write_with_if_match: _bool = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -642,6 +692,7 @@ class _Base:
         username: str = ...,
         password: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -655,6 +706,7 @@ class _Base:
         known_hosts_strategy: str = ...,
         enable_copy: _bool = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -665,6 +717,7 @@ class _Base:
         root: str = ...,
         tree: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -677,6 +730,7 @@ class _Base:
         value_field: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -688,6 +742,7 @@ class _Base:
         endpoint: str = ...,
         key: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -704,6 +759,7 @@ class _Base:
         value_field: str = ...,
         root: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -715,6 +771,7 @@ class _Base:
         root: str = ...,
         token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -726,6 +783,7 @@ class _Base:
         operator: str = ...,
         password: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -734,6 +792,7 @@ class _Base:
         *,
         access_token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -743,6 +802,7 @@ class _Base:
         root: str = ...,
         token: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -756,6 +816,7 @@ class _Base:
         root: str = ...,
         disable_copy: _bool = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -768,6 +829,7 @@ class _Base:
         disable_list_batch: _bool = ...,
         atomic_write_dir: str = ...,
     ) -> None: ...
+
     @overload
     def __init__(
         self,
@@ -777,5 +839,7 @@ class _Base:
         access_token: str,
         root: str = ...,
     ) -> None: ...
+
+
     @overload
-    def __init__(self, scheme: str, /, **kwargs: str) -> None: ...
+    def __init__(self, scheme: str, /, **kwargs: str) -> None: ...
\ No newline at end of file
diff --git a/dev/Cargo.lock b/dev/Cargo.lock
index f0479e853..9557672df 100644
--- a/dev/Cargo.lock
+++ b/dev/Cargo.lock
@@ -66,15 +66,6 @@ version = "1.0.95"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
 
-[[package]]
-name = "basic-toml"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "clap"
 version = "4.5.23"
@@ -156,15 +147,6 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
-[[package]]
-name = "humansize"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
-dependencies = [
- "libm",
-]
-
 [[package]]
 name = "humantime"
 version = "2.1.0"
@@ -177,18 +159,6 @@ version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
-[[package]]
-name = "itoa"
-version = "1.0.14"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
-
-[[package]]
-name = "libm"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
-
 [[package]]
 name = "log"
 version = "0.4.22"
@@ -202,35 +172,12 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
-name = "mime"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-
-[[package]]
-name = "mime_guess"
-version = "2.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
-dependencies = [
- "mime",
- "unicase",
-]
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "nom"
-version = "7.1.3"
+name = "minijinja"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+checksum = "2c37e1b517d1dcd0e51dc36c4567b9d5a29262b3ec8da6cb5d35e27a8fb529b5"
 dependencies = [
- "memchr",
- "minimal-lexical",
+ "serde",
 ]
 
 [[package]]
@@ -241,17 +188,12 @@ dependencies = [
  "clap",
  "env_logger",
  "log",
+ "minijinja",
  "pretty_assertions",
- "rinja",
+ "serde",
  "syn",
 ]
 
-[[package]]
-name = "percent-encoding"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-
 [[package]]
 name = "pretty_assertions"
 version = "1.4.1"
@@ -309,53 +251,6 @@ version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
-[[package]]
-name = "rinja"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5"
-dependencies = [
- "humansize",
- "itoa",
- "percent-encoding",
- "rinja_derive",
-]
-
-[[package]]
-name = "rinja_derive"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b"
-dependencies = [
- "basic-toml",
- "memchr",
- "mime",
- "mime_guess",
- "proc-macro2",
- "quote",
- "rinja_parser",
- "rustc-hash",
- "serde",
- "syn",
-]
-
-[[package]]
-name = "rinja_parser"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
-dependencies = [
- "memchr",
- "nom",
- "serde",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
-
 [[package]]
 name = "serde"
 version = "1.0.216"
@@ -393,12 +288,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "unicase"
-version = "2.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
-
 [[package]]
 name = "unicode-ident"
 version = "1.0.14"
diff --git a/dev/Cargo.toml b/dev/Cargo.toml
index 9d0e98acd..2acaeecae 100644
--- a/dev/Cargo.toml
+++ b/dev/Cargo.toml
@@ -28,11 +28,12 @@ rust-version = "1.75"
 version = "0.0.1"
 
 [dependencies]
-anyhow = "1.0.95"
+anyhow = { version = "1.0.95" }
 clap = { version = "4.5.23", features = ["derive"] }
-env_logger = "0.11.6"
-log = "0.4.22"
-rinja = "0.3.5"
+env_logger = { version = "0.11.5" }
+log = { version = "0.4.22" }
+minijinja = { version = "2.5.0" }
+serde = { version = "1.0.216", features = ["derive"] }
 syn = { version = "2.0.91", features = ["visit", "full", "extra-traits"] }
 
 [dev-dependencies]
diff --git a/dev/src/generate/binding_python.rs 
b/dev/src/generate/binding_python.rs
deleted file mode 100644
index 68bd9b457..000000000
--- a/dev/src/generate/binding_python.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::generate::parser::Services;
-use anyhow::Result;
-use rinja::Template;
-use std::fs;
-use std::path::PathBuf;
-
-use super::parser::{ConfigType, Service};
-
-// Using the template in this path, relative
-// to the `templates` dir in the crate root
-#[derive(Template)]
-#[template(path = "python.py.jinja2", escape = "none")]
-struct PythonTemplate {
-    services: Vec<(String, Service)>,
-}
-
-/// TODO: add a common utils to parse enabled features from cargo.toml
-fn enabled_service(srv: &str) -> bool {
-    match srv {
-        // not enabled in bindings/python/Cargo.toml
-        "etcd" | "foundationdb" | "ftp" | "hdfs" | "rocksdb" | "tikv" => false,
-        _ => true,
-    }
-}
-
-pub fn generate(project_root: PathBuf, services: &Services) -> Result<()> {
-    let mut v = Vec::from_iter(
-        services
-            .clone()
-            .into_iter()
-            .filter(|x| enabled_service(x.0.as_str())),
-    );
-
-    // move required options at beginning.
-    for srv in &mut v {
-        let mut v = 
Vec::from_iter(srv.1.config.clone().into_iter().enumerate());
-
-        v.sort_by_key(|a| (a.1.optional, a.0));
-
-        srv.1.config = v.iter().map(|f| f.1.clone()).collect();
-    }
-
-    let tmpl = PythonTemplate { services: v };
-
-    let t = tmpl.render().expect("should render template");
-
-    let output_file: String = project_root
-        .join("bindings/python/python/opendal/__base.pyi")
-        .to_str()
-        .expect("should build output file path")
-        .into();
-
-    fs::write(output_file, t).expect("failed to write result to file");
-
-    Ok(())
-}
-
-impl ConfigType {
-    pub fn python_type(&self) -> String {
-        match self {
-            ConfigType::Bool => "_bool".into(),
-            ConfigType::Duration => "_duration".into(),
-            ConfigType::I64
-            | ConfigType::Usize
-            | ConfigType::U64
-            | ConfigType::U32
-            | ConfigType::U16 => "_int".into(),
-            ConfigType::Vec => "_strings".into(),
-            ConfigType::String => "str".into(),
-        }
-    }
-}
diff --git a/dev/src/generate/mod.rs b/dev/src/generate/mod.rs
index 71ec44429..780f19a74 100644
--- a/dev/src/generate/mod.rs
+++ b/dev/src/generate/mod.rs
@@ -16,20 +16,18 @@
 // under the License.
 
 mod parser;
+mod python;
 
-mod binding_python;
-
+use crate::workspace_dir;
 use anyhow::Result;
-use std::path::PathBuf;
 
 pub fn run(language: &str) -> Result<()> {
-    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
-    let services_path = 
manifest_dir.join("../core/src/services").canonicalize()?;
-    let project_root = manifest_dir.join("..").canonicalize()?;
+    let workspace_dir = workspace_dir();
+    let services_path = 
workspace_dir.join("core/src/services").canonicalize()?;
     let services = parser::parse(&services_path.to_string_lossy())?;
 
     match language {
-        "python" | "py" => binding_python::generate(project_root, &services),
-        _ => Err(anyhow::anyhow!("Unsupported language: {}", language)),
+        "python" | "py" => python::generate(workspace_dir, services),
+        _ => anyhow::bail!("unsupported language: {}", language),
     }
 }
diff --git a/dev/src/generate/parser.rs b/dev/src/generate/parser.rs
index 45cfe7cb4..086d7be5a 100644
--- a/dev/src/generate/parser.rs
+++ b/dev/src/generate/parser.rs
@@ -15,40 +15,29 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use anyhow::Result;
 use anyhow::{anyhow, Context};
+use anyhow::{bail, Result};
 use log::debug;
+use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
+use std::fs;
 use std::fs::read_dir;
 use std::str::FromStr;
-use std::{fs, vec};
 use syn::{
     Expr, ExprLit, Field, GenericArgument, Item, Lit, LitStr, Meta, 
PathArguments, Type, TypePath,
 };
 
-#[derive(Debug, Clone)]
-pub struct Services(HashMap<String, Service>);
-
-impl IntoIterator for Services {
-    type Item = (String, Service);
-    type IntoIter = vec::IntoIter<(String, Service)>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        let mut v = Vec::from_iter(self.0);
-        v.sort();
-        v.into_iter()
-    }
-}
+pub type Services = HashMap<String, Service>;
 
 /// Service represents a service supported by opendal core, like `s3` and `fs`
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
 pub struct Service {
     /// All configurations for this service.
     pub config: Vec<Config>,
 }
 
 /// Config represents a configuration item for a service.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
 pub struct Config {
     /// The name of this config, for example, `access_key_id` and 
`secret_access_key`
     pub name: String,
@@ -64,7 +53,7 @@ pub struct Config {
     pub comments: String,
 }
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, 
Deserialize)]
 pub enum ConfigType {
     /// Mapping to rust's `bool`
     Bool,
@@ -94,7 +83,7 @@ impl FromStr for ConfigType {
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> Result<Self> {
-        let ct = match s {
+        Ok(match s {
             "bool" => ConfigType::Bool,
             "String" => ConfigType::String,
             "Duration" => ConfigType::Duration,
@@ -106,10 +95,9 @@ impl FromStr for ConfigType {
             "u16" => ConfigType::U16,
 
             "Vec" => ConfigType::Vec,
-            v => return Err(anyhow!("unsupported config type {v:?}")),
-        };
 
-        Ok(ct)
+            v => bail!("unsupported config type {v}"),
+        })
     }
 }
 
@@ -135,11 +123,11 @@ impl FromStr for ConfigType {
 /// ```
 ///
 /// - since = "0.52.0"
-#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, 
Deserialize)]
 pub struct AttrDeprecated {
-    /// The since of this deprecated field.
+    /// The `since` of this deprecated field.
     pub since: String,
-    /// The note for this deprecated field.
+    /// The `note` for this deprecated field.
     pub note: String,
 }
 
@@ -163,7 +151,7 @@ pub fn parse(path: &str) -> Result<Services> {
         map.insert(parser.service, service);
     }
 
-    Ok(Services(map))
+    Ok(map)
 }
 
 /// ServiceParser is used to parse a service config file.
diff --git a/dev/templates/python.py.jinja2 b/dev/src/generate/python.j2
similarity index 71%
rename from dev/templates/python.py.jinja2
rename to dev/src/generate/python.j2
index 7c8b2732e..772407d52 100644
--- a/dev/templates/python.py.jinja2
+++ b/dev/src/generate/python.j2
@@ -1,9 +1,3 @@
-"""
-this file is generated by opendal/dev/generate/binding_python.rs, and 
opendal.__base doesn't exists.
-
-DO NOT EDIT IT Manually
-"""
-
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -21,6 +15,12 @@ DO NOT EDIT IT Manually
 # specific language governing permissions and limitations
 # under the License.
 
+"""
+> DO NOT EDIT IT MANUALLY <
+
+This file is generated by opendal/dev/generate/python.rs. `opendal.__base` 
doesn't exists.
+"""
+
 from typing import overload, Literal, TypeAlias
 
 # `true`/`false`` in any case, for example, `true`/`True`/`TRUE` 
`false`/`False`/`FALSE`
@@ -43,20 +43,21 @@ class _Base:
     The services list here is support by opendal pypi wheel.
     """
 
-{% for srv in services %}
+{% for srv in srvs %}
     @overload
-    def __init__(self,
-        scheme: Literal["{{srv.0}}"],
+    def __init__(
+        self,
+        scheme: Literal["{{srv}}"],
         /,
         *,
-    {% for field in srv.1.config.clone().into_iter() %}
-        {% if field.deprecated.is_some() %}
-        # deprecated: {{ field.deprecated.unwrap().note }}
-        {% endif %}
-        {{field.name}}: {{field.value.python_type()}} {% if field.optional %} 
= ... {% endif %},
-    {% endfor %}
-     ) -> None: ...
+    {%- for field in srvs[srv].config -%}
+        {% if field.deprecated %}
+        # deprecated: {{field.deprecated["note"]}}
+        {%- endif %}
+        {{field.name}}: {{make_pytype(field.value)}}{% if field.optional %} = 
...{% endif %},
+    {%- endfor %}
+    ) -> None: ...
 {% endfor %}
 
     @overload
-    def __init__(self, scheme:str, /, **kwargs: str) -> None: ...
+    def __init__(self, scheme: str, /, **kwargs: str) -> None: ...
diff --git a/dev/src/generate/python.rs b/dev/src/generate/python.rs
new file mode 100644
index 000000000..268a6e89d
--- /dev/null
+++ b/dev/src/generate/python.rs
@@ -0,0 +1,71 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::generate::parser::{ConfigType, Service, Services};
+use anyhow::Result;
+use minijinja::value::ViaDeserialize;
+use minijinja::{context, Environment};
+use std::fs;
+use std::path::PathBuf;
+
+/// TODO: add a common utils to parse enabled features from cargo.toml
+fn enabled_service(srv: &str) -> bool {
+    match srv {
+        // not enabled in bindings/python/Cargo.toml
+        "etcd" | "foundationdb" | "ftp" | "hdfs" | "rocksdb" | "tikv" => false,
+        _ => true,
+    }
+}
+
+pub fn generate(workspace_dir: PathBuf, services: Services) -> Result<()> {
+    let mut srvs = Services::new();
+    for (k, srv) in services.into_iter() {
+        if !enabled_service(k.as_str()) {
+            continue;
+        }
+
+        let mut sorted = 
srv.config.into_iter().enumerate().collect::<Vec<_>>();
+        sorted.sort_by_key(|(i, v)| (v.optional, *i));
+        let config = sorted.into_iter().map(|(_, v)| v).collect();
+        srvs.insert(k, Service { config });
+    }
+
+    let mut env = Environment::new();
+    env.add_template("python", include_str!("python.j2"))?;
+    env.add_function("make_pytype", make_pytype);
+    let tmpl = env.get_template("python")?;
+
+    let output = 
workspace_dir.join("bindings/python/python/opendal/__base.pyi");
+    fs::write(output, tmpl.render(context! { srvs => srvs })?)?;
+
+    Ok(())
+}
+
+fn make_pytype(ty: ViaDeserialize<ConfigType>) -> Result<String, 
minijinja::Error> {
+    Ok(match ty.0 {
+        ConfigType::Bool => "_bool",
+        ConfigType::Duration => "_duration",
+        ConfigType::I64
+        | ConfigType::Usize
+        | ConfigType::U64
+        | ConfigType::U32
+        | ConfigType::U16 => "_int",
+        ConfigType::Vec => "_strings",
+        ConfigType::String => "str",
+    }
+    .to_string())
+}
diff --git a/dev/src/main.rs b/dev/src/main.rs
index a56430abf..1858e89b1 100644
--- a/dev/src/main.rs
+++ b/dev/src/main.rs
@@ -19,10 +19,21 @@ mod generate;
 
 use anyhow::Result;
 use clap::{Parser, Subcommand};
+use std::path::PathBuf;
+
+fn manifest_dir() -> PathBuf {
+    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+        .canonicalize()
+        .unwrap()
+}
+
+fn workspace_dir() -> PathBuf {
+    manifest_dir().join("..").canonicalize().unwrap()
+}
 
 #[derive(Parser)]
 #[command(version, about, long_about = None)]
-struct Cli {
+struct Cmd {
     #[command(subcommand)]
     command: Commands,
 }
@@ -39,13 +50,7 @@ enum Commands {
 fn main() -> Result<()> {
     env_logger::init();
 
-    let cli = Cli::parse();
-
-    match cli.command {
-        Commands::Generate { language } => {
-            generate::run(&language)?;
-        }
+    match Cmd::parse().command {
+        Commands::Generate { language } => generate::run(&language),
     }
-
-    Ok(())
 }

Reply via email to