Skip to content

feat(spider-py): Add support for parsing TDL types into native Python types. - #188

Merged
sitaowang1998 merged 148 commits into
y-scope:mainfrom
sitaowang1998:python_type_parse
Aug 24, 2025
Merged

feat(spider-py): Add support for parsing TDL types into native Python types.#188
sitaowang1998 merged 148 commits into
y-scope:mainfrom
sitaowang1998:python_type_parse

Conversation

@sitaowang1998

@sitaowang1998 sitaowang1998 commented Aug 9, 2025

Copy link
Copy Markdown
Collaborator

Description

Note

This PR depends on #187.

This PR:

  • Adds lark parser as dependency.
  • Adds TDL type parsing using lark.
  • Adds conversion from TDL types to Python native types.
  • Adds unit tests for parsing and conversion.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  • Newly added unit tests for TDL parsing and conversion to Python type pass.
  • GitHub workflows pass.

Summary by CodeRabbit

  • New Features

    • Public API to parse TDL type strings into native Python-compatible types (primitives, user classes, lists, maps), now exposed at the package level.
    • Types now provide native-type resolution (including generic list/dict mappings and runtime class resolution).
  • Tests

    • Comprehensive tests for primitives, classes, generics, nested parsing and native-type resolution, plus error cases.
  • Chores

    • Added runtime dependency: lark.

sitaowang1998 and others added 30 commits July 15, 2025 13:40
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>
…ider into dep-concurrency"

This reverts commit 1769c95, reversing
changes made to 90aa5a2.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
python/src/spider/type/utils.py (3)

15-21: Docstring tweaks: clarify input format and fix raises clause

Clarify that the input must be a fully qualified dotted path and align the raises note with the actual parameter name.

-    """
-    Gets class by name.
-    :param name:
-    :return:
-    :raise: TypeError if `class_name` is not a valid class.
-    """
+    """
+    Gets a class by its fully qualified dotted name.
+    :param name: e.g., 'package.module.Class' or 'package.module.Outer.Inner'.
+    :return: Class object.
+    :raises TypeError: if the name does not resolve to a class.
+    """

3-5: Remove now-unused typing.cast import (post-refactor)

If you adopt the runtime isinstance check suggested above, cast is no longer used.

-from typing import cast

15-30: Security: importing arbitrary module paths executes module-level code

If names come from untrusted input (e.g., user-supplied TDL), import_module will execute import-time code. Consider:

  • Restricting imports to an allowlist of modules/namespaces.
  • Providing a resolver callback that maps allowed identifiers to classes.
  • Failing fast if the module prefix is not in an approved set.

Please confirm whether inputs to get_class_by_name can be user-controlled. If so, we should add an allowlist or resolver strategy before merging.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b18170 and 49acaee.

📒 Files selected for processing (6)
  • python/src/spider/type/tdl_convert.py (1 hunks)
  • python/src/spider/type/tdl_parse.py (1 hunks)
  • python/src/spider/type/tdl_type.py (1 hunks)
  • python/src/spider/type/utils.py (1 hunks)
  • python/tests/type/test_to_native.py (1 hunks)
  • python/tests/type/test_to_tdl.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • python/tests/type/test_to_native.py
  • python/src/spider/type/tdl_convert.py
  • python/tests/type/test_to_tdl.py
  • python/src/spider/type/tdl_parse.py
  • python/src/spider/type/tdl_type.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: non-storage-unit-tests (ubuntu-22.04)
  • GitHub Check: non-storage-unit-tests (ubuntu-24.04)
  • GitHub Check: lint
🔇 Additional comments (1)
python/src/spider/type/utils.py (1)

7-13: LGTM: fully qualified class name generation is correct

Using module + qualname is appropriate and preserves nested class paths.

Comment thread python/src/spider/type/utils.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/spider-py/src/spider_py/type/tdl_convert.py (1)

111-113: Guard the issubclass call to avoid unexpected errors on non-class inputs

Without an isinstance(native_type, type) guard, passing values like 123, object() or a module into to_tdl_type() makes the bare issubclass(native_type, Collection) raise Python’s built-in
TypeError: issubclass() arg 1 must be a class, bypassing your custom error message. Add a type check so only classes reach issubclass, yielding a consistent TypeError(f"{native_type} is not a valid TDL type.") when appropriate.

• File: python/spider-py/src/spider_py/type/tdl_convert.py, lines 111–113
• Replace:

-    if issubclass(native_type, Collection):
+    if isinstance(native_type, type) and issubclass(native_type, Collection):
         msg = f"{native_type} is not a valid TDL type."
         raise TypeError(msg)

This ensures non-class inputs fall through (and can be handled or reported elsewhere) rather than triggering a misleading built-in exception.

🧹 Nitpick comments (16)
python/spider-py/src/spider_py/type/tdl_type.py (4)

3-7: Ensure typing_extensions is available or provide a safe runtime fallback for override.

The module imports typing_extensions.override on Python 3.10. Either declare typing-extensions as a dependency (see pyproject suggestion) or provide a no-op fallback to avoid runtime ImportError.

Example import pattern:

try:
    from typing import override  # Python 3.12+
except Exception:  # noqa: BLE001
    try:
        from typing_extensions import override  # type: ignore[assignment]
    except Exception:  # As last resort, define a no-op
        def override(func):  # type: ignore[no-redef]
            return func

123-129: Surface clearer errors for invalid class names and rely on improved resolver.

native_type delegates to get_class_by_name(self._name), which is good. Once utils.get_class_by_name supports nested classes and better diagnostics (see my comment there), this method will inherit those improvements. Consider referencing that utility’s message in the docstring.

No code change here if utils.py is updated as suggested; otherwise, add a brief format check (must contain a dot) to fail fast with a clearer message.

Please verify that TDL class strings may include nested class paths (e.g., pkg.mod.Outer.Inner) and add a test if so.


184-186: Use type_str() in the map key-type error for better UX.

Printing the object directly yields a less helpful repr. Using type_str() makes the message user-facing and consistent with TDL syntax.

Apply:

-            msg = f"{key_type} is not a supported type for map key."
+            msg = f"{key_type.type_str()} is not a supported type for map key."

194-197: Map generic native typing is fine; one caveat about “string-as-List”.

dict[key.native_type(), value.native_type()] is correct. However, since TDL “string” is modelled as List, map keys representing strings will translate to list[Int8] at the typing level, which are not hashable at runtime. If these types are only used for type representation (not runtime enforcement), it’s fine; otherwise, consider a dedicated StringType that maps to bytes or str for hashability.

I can sketch a StringType with native_type -> bytes (or str) and adapt is_string accordingly if desired.

Confirm whether map keys will ever be realised at runtime to actual dict keys; if yes, we should ensure the key’s native type is hashable.

python/spider-py/src/spider_py/type/tdl_convert.py (2)

24-46: Deduplicate primitive mapping logic; delegate to the single source of truth.

to_primitive_tdl_type reimplements the same mapping already provided by TypeDict/_to_primitive_tdl_type. Keep one mapping to prevent drift.

Replace the body with a simple dictionary lookup:

 def to_primitive_tdl_type(native_type: type | GenericAlias) -> TdlType | None:
-    """
-    Converts a native type to primitive TDL type.
-    :param native_type:
-    :return: Converted TDL primitive. None if `native_type` is not a supported primitive type.
-    """
-    tdl_type: TdlType | None = None
-    if native_type is Int8:
-        tdl_type = Int8Type()
-    elif native_type is Int16:
-        tdl_type = Int16Type()
-    elif native_type is Int32:
-        tdl_type = Int32Type()
-    elif native_type is Int64:
-        tdl_type = Int64Type()
-    elif native_type is Float:
-        tdl_type = FloatType()
-    elif native_type is Double:
-        tdl_type = DoubleType()
-    elif native_type is bool:
-        tdl_type = BoolType()
-    return tdl_type
+    """
+    Converts a native type to primitive TDL type.
+    :param native_type:
+    :return: The TDL primitive if supported; otherwise None.
+    """
+    return TypeDict.get(native_type)

If you prefer that this public function also raises on unsupported built-ins (int/float/str/complex/bytes), delegate directly to _to_primitive_tdl_type and update the docstring accordingly.


59-76: Good centralisation and guardrails for primitive resolution.

Using TypeDict and explicitly erroring on built-in Python primitives makes misuses visible early. Consider expanding the message with guidance (e.g., “int -> Int32/Int64, float -> Double/Float, str -> List[Int8]”) if that helps users.

No code changes required; just a messaging improvement if desired.

python/spider-py/tests/type/test_to_native.py (5)

8-8: Import from the public API surface instead of the module path.

Prefer importing parse_tdl_type from spider_py.type to keep tests coupled to the public API and reduce churn if internals move.

-from spider_py.type.tdl_parse import parse_tdl_type
+from spider_py.type import parse_tdl_type

25-33: Parametrize primitive mappings to reduce repetition and improve diagnostics on failures.

Parametrization makes failures pinpoint which mapping broke and shortens the test.

-    def test_to_primitive_native_type(self) -> None:
-        """Test converting primitive TDL type to native type."""
-        assert string_to_native("bool") is bool
-        assert string_to_native("double") is spider_py.Double
-        assert string_to_native("float") is spider_py.Float
-        assert string_to_native("int8") is spider_py.Int8
-        assert string_to_native("int16") is spider_py.Int16
-        assert string_to_native("int32") is spider_py.Int32
-        assert string_to_native("int64") is spider_py.Int64
+    @pytest.mark.parametrize(
+        ("tdl", "expected"),
+        [
+            ("bool", bool),
+            ("double", spider_py.Double),
+            ("float", spider_py.Float),
+            ("int8", spider_py.Int8),
+            ("int16", spider_py.Int16),
+            ("int32", spider_py.Int32),
+            ("int64", spider_py.Int64),
+        ],
+    )
+    def test_to_primitive_native_type(self, tdl: str, expected: type) -> None:
+        """Test converting primitive TDL type to native type."""
+        assert string_to_native(tdl) is expected

35-39: Use identity comparison for classes.

When asserting class resolution, is communicates identity intent more clearly than ==.

-        assert string_to_native(get_class_name(TestToNativeType)) == TestToNativeType
+        assert string_to_native(get_class_name(TestToNativeType)) is TestToNativeType

48-54: Confirm that List keys in Map are intentional (Python dict keys are unhashable if they are lists).

The test accepts Map<List<int8>, ...> which maps to dict[list[Int8], ...]. In Python, list is unhashable and cannot be used as a dict key at runtime. If TDL intentionally allows non-hashable keys (e.g., because “Map” semantics differ from Python dicts), then this is fine. If not, we should reject such keys earlier (likely via is_map_key) and add a negative test.

If you want to enforce hashable keys, I can propose a patch in MapType.__init__ and add a failing test like:

with pytest.raises(TypeError):
    string_to_native("Map<List<int8>, int8>")

Do you want me to open a follow-up issue/PR for this?


22-54: Add a few edge-case parsing tests (whitespace, invalid syntax, and case-sensitivity).

These help harden the grammar and error paths without adding much maintenance overhead.

Examples (new tests to add; outside current diff):

@pytest.mark.parametrize(
    "s, expected",
    [
        (" List < int8 > ", list[spider_py.Int8]),
        ("Map< int8 ,  double >", dict[spider_py.Int8, spider_py.Double]),
        ("List<List< int64 >>", list[list[spider_py.Int64]]),
    ],
)
def test_whitespace_tolerance(s, expected):
    assert string_to_native(s) == expected

@pytest.mark.parametrize("s", ["Map<int8 int8>", "List<>", "Map<,>", "Map<int8,>", "List"])
def test_invalid_strings_raise(s):
    with pytest.raises(TypeError):
        string_to_native(s)

def test_case_sensitivity_of_keywords():
    with pytest.raises(TypeError):
        string_to_native("list<int8>")  # grammar requires capitalized List/Map
python/spider-py/src/spider_py/type/tdl_parse.py (5)

57-59: Rename key to element in list_type for clarity.

Minor naming nit: the parameter represents the element type, not a key.

-    def list_type(self, key: TdlType) -> TdlType:
+    def list_type(self, element: TdlType) -> TdlType:
         """Transforms list node into List type."""
-        return ListType(key)
+        return ListType(element)

62-66: Docstring should reflect both primitive and class outcomes.

base_type returns either a primitive or a ClassType.

-    def base_type(self, children: list[Token]) -> TdlType:
-        """Transforms primitive node into primitive type."""
+    def base_type(self, children: list[Token]) -> TdlType:
+        """Transforms base node into a primitive or class TDL type."""
         name = str(children[0])
         if name in primitive_type_map:
             return primitive_type_map[name]()  # type: ignore[abstract]
         return ClassType(name)

5-17: Avoid duplicating primitive mappings; reuse the converter from tdl_convert.

There is an existing primitive mapping in tdl_convert. Reusing it prevents drift when adding new primitives and keeps a single source of truth.

 from spider_py.type.tdl_type import (
     BoolType,
     ClassType,
     DoubleType,
     FloatType,
     Int8Type,
     Int16Type,
     Int32Type,
     Int64Type,
     ListType,
     MapType,
     TdlType,
 )
+from spider_py.type.tdl_convert import to_primitive_tdl_type
@@
-primitive_type_map = {
-    "bool": BoolType,
-    "double": DoubleType,
-    "float": FloatType,
-    "int8": Int8Type,
-    "int16": Int16Type,
-    "int32": Int32Type,
-    "int64": Int64Type,
-}
+# Primitive resolution is delegated to `to_primitive_tdl_type` for consistency.
@@
-    def base_type(self, children: list[Token]) -> TdlType:
-        """Transforms base node into primitive type."""
-        name = str(children[0])
-        if name in primitive_type_map:
-            return primitive_type_map[name]()  # type: ignore[abstract]
-        return ClassType(name)
+    def base_type(self, children: list[Token]) -> TdlType:
+        """Transforms base node into a primitive or class TDL type."""
+        name = str(children[0])
+        primitive = to_primitive_tdl_type(name)
+        return primitive if primitive is not None else ClassType(name)

Also applies to: 32-40, 61-66


69-69: Optional: consider renaming the start rule from type to avoid clashing with the Python built-in.

This is purely stylistic. Using a name like tdl_type avoids shadowing in transformer methods and improves readability.


3-3: Consider caching parse results if this function is on a hot path.

If parse_tdl_type is called frequently with repeated inputs (e.g., schema parsing), an LRU cache can materially reduce parse overhead. Ensure returned TdlType instances are immutable (they appear to be).

+from functools import lru_cache
@@
-def parse_tdl_type(string: str) -> TdlType:
+@lru_cache(maxsize=1024)
+def parse_tdl_type(string: str) -> TdlType:

Also applies to: 72-72

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d3f3eb1 and 2f81cd9.

⛔ Files ignored due to path filters (1)
  • python/spider-py/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • python/spider-py/pyproject.toml (1 hunks)
  • python/spider-py/src/spider_py/type/__init__.py (1 hunks)
  • python/spider-py/src/spider_py/type/tdl_convert.py (1 hunks)
  • python/spider-py/src/spider_py/type/tdl_parse.py (1 hunks)
  • python/spider-py/src/spider_py/type/tdl_type.py (12 hunks)
  • python/spider-py/src/spider_py/type/utils.py (1 hunks)
  • python/spider-py/tests/type/test_to_native.py (1 hunks)
  • taskfiles/lint.yaml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
python/spider-py/src/spider_py/type/tdl_parse.py (1)
python/spider-py/src/spider_py/type/tdl_type.py (11)
  • BoolType (96-105)
  • ClassType (108-128)
  • DoubleType (24-33)
  • FloatType (36-45)
  • Int8Type (48-57)
  • Int16Type (60-69)
  • Int32Type (72-81)
  • Int64Type (84-93)
  • ListType (131-147)
  • MapType (174-196)
  • TdlType (12-21)
python/spider-py/tests/type/test_to_native.py (4)
python/spider-py/src/spider_py/type/tdl_parse.py (1)
  • parse_tdl_type (72-84)
python/spider-py/src/spider_py/type/utils.py (1)
  • get_class_name (7-12)
python/spider-py/src/spider_py/type/tdl_type.py (11)
  • native_type (20-21)
  • native_type (32-33)
  • native_type (44-45)
  • native_type (56-57)
  • native_type (68-69)
  • native_type (80-81)
  • native_type (92-93)
  • native_type (104-105)
  • native_type (123-128)
  • native_type (146-147)
  • native_type (195-196)
python/spider-py/src/spider_py/type/type.py (6)
  • Double (69-74)
  • Float (61-66)
  • Int8 (29-34)
  • Int16 (37-42)
  • Int32 (45-50)
  • Int64 (53-58)
python/spider-py/src/spider_py/type/tdl_type.py (2)
python/spider-py/src/spider_py/type/type.py (6)
  • Double (69-74)
  • Float (61-66)
  • Int8 (29-34)
  • Int16 (37-42)
  • Int32 (45-50)
  • Int64 (53-58)
python/spider-py/src/spider_py/type/utils.py (1)
  • get_class_by_name (15-30)
python/spider-py/src/spider_py/type/__init__.py (1)
python/spider-py/src/spider_py/type/tdl_parse.py (1)
  • parse_tdl_type (72-84)
python/spider-py/src/spider_py/type/tdl_convert.py (2)
python/spider-py/src/spider_py/type/tdl_type.py (19)
  • native_type (20-21)
  • native_type (32-33)
  • native_type (44-45)
  • native_type (56-57)
  • native_type (68-69)
  • native_type (80-81)
  • native_type (92-93)
  • native_type (104-105)
  • native_type (123-128)
  • native_type (146-147)
  • native_type (195-196)
  • TdlType (12-21)
  • Int8Type (48-57)
  • Int16Type (60-69)
  • Int32Type (72-81)
  • Int64Type (84-93)
  • FloatType (36-45)
  • DoubleType (24-33)
  • BoolType (96-105)
python/spider-py/src/spider_py/type/type.py (6)
  • Int8 (29-34)
  • Int16 (37-42)
  • Int32 (45-50)
  • Int64 (53-58)
  • Float (61-66)
  • Double (69-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: non-storage-unit-tests (ubuntu-22.04)
  • GitHub Check: lint
  • GitHub Check: non-storage-unit-tests (ubuntu-24.04)
🔇 Additional comments (6)
python/spider-py/src/spider_py/type/tdl_type.py (3)

19-22: Good abstraction: native_type() on the base TdlType.

Introducing native_type in the ABC keeps resolution logic encapsulated per type. This aligns well with SOLID’s Open/Closed principle.


31-34: Primitive TDL → native type mappings look correct.

Double/Float/Int8/16/32/64/Bool map cleanly to their corresponding runtime types in spider_py.type.type and bool. The implementation is straightforward and side-effect free.

Also applies to: 43-46, 56-57, 68-69, 80-81, 92-93, 104-105


145-148: List generic native typing is fine.

Returning list[element_type.native_type()] is the right PEP 585 form and matches downstream usage.

python/spider-py/src/spider_py/type/tdl_convert.py (2)

85-107: GenericAlias handling for list/dict is correct and composable.

Origin/args flow and recursive to_tdl_type calls cover nested generics well. The “no element/key/value type” checks are also clear.


118-124: String conversion helper is concise and correct.

to_tdl_type_str correctly defers to to_tdl_type().type_str(), keeping a single conversion path.

python/spider-py/src/spider_py/type/__init__.py (1)

4-4: Re-export looks good and matches the new public API surface.

parse_tdl_type is now exposed via spider_py.type, and __all__ reflects it alongside existing exports. This aligns with the tests and keeps the internal parser module encapsulated.

Also applies to: 7-16

Comment on lines +12 to 15
"lark>=1.2.2",
"mariadb>=1.1.13",
"msgpack-types>=0.5.0",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add missing runtime dependency for typing_extensions and consider constraining Lark’s major version.

  • This repo imports typing_extensions.override (see python/spider-py/src/spider_py/type/tdl_type.py Line 6). On Python 3.10, typing_extensions is not in the stdlib, so the package must declare it as a dependency, otherwise imports will fail in clean environments.
  • Optional: Since Lark follows semver and 2.x may include breaking changes, favour pinning an upper bound to avoid unexpected resolver picks.

Apply this diff to add typing-extensions and (optionally) cap Lark to <2:

 dependencies = [
-  "lark>=1.2.2",
+  "lark>=1.2.2,<2",
   "mariadb>=1.1.13",
   "msgpack-types>=0.5.0",
+  "typing-extensions>=4.8",
 ]

Run this to confirm usage of typing_extensions across the repo and catch any other missing deps:


🏁 Script executed:

#!/bin/bash
rg -n --glob 'python/**' -C2 -e '\btyping_extensions\b'

Length of output: 1312


Action Required: Add typing-extensions as a runtime dependency and constrain Lark’s major version

The code imports override from typing_extensions (see python/spider-py/src/spider_py/type/tdl_type.py:6), but typing-extensions isn’t declared in pyproject.toml. In a clean Python 3.10 environment this import will fail. Additionally, since Lark follows semver and a 2.x release may introduce breaking changes, it’s safer to pin an upper bound on its major version.

Please update python/spider-py/pyproject.toml accordingly:

 dependencies = [
-  "lark>=1.2.2",
+  "lark>=1.2.2,<2",
   "mariadb>=1.1.13",
   "msgpack-types>=0.5.0",
+  "typing-extensions>=4.8",
 ]
  • Confirm that any CI or packaging tooling picks up the new dependency.
  • Optional: adjust the minimum version of typing-extensions if you need features introduced after 4.8.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"lark>=1.2.2",
"mariadb>=1.1.13",
"msgpack-types>=0.5.0",
]
# python/spider-py/pyproject.toml
dependencies = [
"lark>=1.2.2,<2",
"mariadb>=1.1.13",
"msgpack-types>=0.5.0",
"typing-extensions>=4.8",
]
🤖 Prompt for AI Agents
In python/spider-py/pyproject.toml around lines 12–15, add typing-extensions as
a runtime dependency and constrain Lark’s major version to avoid accidental 2.x
breakage; specifically add an entry for "typing-extensions" (optionally with a
minimum like ">=4.8" if you rely on newer features) to the dependencies list and
change the Lark entry to include an upper bound such as "lark>=1.2.2,<2.0.0";
after editing, ensure CI/packaging picks up the updated pyproject (e.g., by
re-running dependency install steps or updating lock files) so the new
dependency is propagated.

Comment on lines +15 to +30
def get_class_by_name(name: str) -> type:
"""
Gets class by name.
:param name:
:return:
:raise: TypeError if `class_name` is not a valid class.
"""
parts = name.split(".")
module_name = ".".join(parts[:-1])
class_name = parts[-1]
try:
module = import_module(module_name)
return cast("type", getattr(module, class_name))
except (ValueError, ModuleNotFoundError, AttributeError) as exc:
msg = f"{name} is not a valid TDL type."
raise TypeError(msg) from exc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make class resolution robust to nested classes and improve diagnostics.

Current logic splits on the last dot and getattr’s a single attribute from the module. This fails for nested classes (e.g., pkg.mod.Outer.Inner) and for objects not actually being classes. It also conflates module import failures with attribute lookups in error messaging.

Refactor to resolve the longest importable module prefix, then getattr-chain the remaining components. Also validate the resolved object is a class and surface clearer messages:

 def get_class_by_name(name: str) -> type:
     """
     Gets class by name.
     :param name:
     :return:
     :raise: TypeError if `class_name` is not a valid class.
     """
-    parts = name.split(".")
-    module_name = ".".join(parts[:-1])
-    class_name = parts[-1]
-    try:
-        module = import_module(module_name)
-        return cast("type", getattr(module, class_name))
-    except (ValueError, ModuleNotFoundError, AttributeError) as exc:
-        msg = f"{name} is not a valid TDL type."
-        raise TypeError(msg) from exc
+    parts = name.split(".")
+    if len(parts) < 2:
+        raise TypeError(f"{name} is not a fully-qualified class path.")
+
+    last_exc: Exception | None = None
+    # Try longest module prefix first, then shorten until import succeeds.
+    for i in range(len(parts) - 1, 0, -1):
+        module_name = ".".join(parts[:i])
+        attrs = parts[i:]
+        try:
+            module = import_module(module_name)
+        except (ValueError, ModuleNotFoundError) as exc:
+            last_exc = exc
+            continue
+        obj: object = module
+        try:
+            for attr in attrs:
+                obj = getattr(obj, attr)
+        except AttributeError as exc:
+            last_exc = exc
+            continue
+        if not isinstance(obj, type):
+            raise TypeError(f"{name} resolved to {type(obj).__name__}, not a class.")
+        return cast("type", obj)
+
+    raise TypeError(f"{name} is not a valid class path.") from last_exc

Consider aligning the error text with the utility’s purpose, e.g., “valid class path” instead of “valid TDL type,” to reduce confusion.

If you’d like, I can add unit tests covering nested classes (Outer.Inner) resolution.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_class_by_name(name: str) -> type:
"""
Gets class by name.
:param name:
:return:
:raise: TypeError if `class_name` is not a valid class.
"""
parts = name.split(".")
module_name = ".".join(parts[:-1])
class_name = parts[-1]
try:
module = import_module(module_name)
return cast("type", getattr(module, class_name))
except (ValueError, ModuleNotFoundError, AttributeError) as exc:
msg = f"{name} is not a valid TDL type."
raise TypeError(msg) from exc
def get_class_by_name(name: str) -> type:
"""
Gets class by name.
:param name:
:return:
:raise: TypeError if `class_name` is not a valid class.
"""
parts = name.split(".")
if len(parts) < 2:
raise TypeError(f"{name} is not a fully-qualified class path.")
last_exc: Exception | None = None
# Try longest module prefix first, then shorten until import succeeds.
for i in range(len(parts) - 1, 0, -1):
module_name = ".".join(parts[:i])
attrs = parts[i:]
try:
module = import_module(module_name)
except (ValueError, ModuleNotFoundError) as exc:
last_exc = exc
continue
obj: object = module
try:
for attr in attrs:
obj = getattr(obj, attr)
except AttributeError as exc:
last_exc = exc
continue
if not isinstance(obj, type):
raise TypeError(f"{name} resolved to {type(obj).__name__}, not a class.")
return cast("type", obj)
raise TypeError(f"{name} is not a valid class path.") from last_exc

Comment thread taskfiles/lint.yaml Outdated

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed tdl_parse. Still working on the rest.

Comment thread python/spider-py/src/spider_py/type/tdl_parse.py Outdated
Comment thread python/spider-py/src/spider_py/type/tdl_parse.py Outdated
Comment thread python/spider-py/src/spider_py/type/tdl_parse.py Outdated
Comment thread python/spider-py/src/spider_py/type/tdl_parse.py Outdated
Comment thread python/spider-py/src/spider_py/type/tdl_parse.py Outdated

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed utils

Comment thread python/spider-py/src/spider_py/type/utils.py Outdated
Comment thread python/spider-py/src/spider_py/type/utils.py Outdated
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
python/spider-py/src/spider_py/type/utils.py (1)

15-31: Make resolution robust to nested classes and validate the resolved object is a class.

Current logic splits once and does a single getattr, which breaks for nested classes (e.g., pkg.mod.Outer.Inner) and may return non-class attributes without validation. Recommend resolving the longest importable module, then getattr-chain the remainder, and validate the final object is a class. Also improve error messaging to reflect the utility’s purpose. This mirrors a prior suggestion on this PR thread.

Apply this diff:

-def get_class_by_name(name: str) -> type:
-    """
-    Gets class by name.
-    :param name:
-    :return: The type object identified by `name`.
-    :raise: TypeError if `class_name` is not a valid class.
-    """
-    module_name, _, class_name = name.rpartition(".")
-    if "" == module_name:
-        msg = f"{name} does not contain a valid Python module."
-        raise TypeError(msg)
-    try:
-        module = import_module(module_name)
-        return cast("type", getattr(module, class_name))
-    except (ValueError, ModuleNotFoundError, AttributeError) as exc:
-        msg = f"{name} is not a valid TDL type."
-        raise TypeError(msg) from exc
+def get_class_by_name(name: str) -> type:
+    """
+    Resolve a fully-qualified class path to a class object.
+    :param name: Fully-qualified path like 'pkg.mod.Outer.Inner'.
+    :return: The class object identified by `name`.
+    :raise TypeError: if `name` does not resolve to a class.
+    """
+    parts = name.split(".")
+    if len(parts) < 2:
+        raise TypeError(f"{name} is not a fully-qualified class path.")
+
+    last_exc: Exception | None = None
+    # Try longest module prefix first, then shorten until import succeeds.
+    for i in range(len(parts) - 1, 0, -1):
+        module_name = ".".join(parts[:i])
+        attrs = parts[i:]
+        try:
+            module = import_module(module_name)
+        except (ValueError, ModuleNotFoundError) as exc:
+            last_exc = exc
+            continue
+        obj: object = module
+        try:
+            for attr in attrs:
+                obj = getattr(obj, attr)
+        except AttributeError as exc:
+            last_exc = exc
+            continue
+        if not isinstance(obj, type):
+            raise TypeError(f"{name} resolved to {type(obj).__name__}, not a class.")
+        return cast(type, obj)
+
+    raise TypeError(f"{name} is not a valid class path.") from last_exc

If you want, I can add unit tests covering:

  • nested class resolution (Outer.Inner),
  • non-class attributes (should error),
  • ambiguous module vs. attribute errors.
🧹 Nitpick comments (4)
python/spider-py/src/spider_py/type/utils.py (4)

16-21: Docstring and error text clarity.

Minor polish: the docstring refers to “class_name” (not a parameter) and error text says “valid TDL type,” which is misleading for a utility that resolves class paths.

If you keep the current implementation, apply this minimal diff to improve wording:

-    """
-    Gets class by name.
-    :param name:
-    :return: The type object identified by `name`.
-    :raise: TypeError if `class_name` is not a valid class.
-    """
+    """
+    Resolve a fully-qualified class path to a class object.
+    :param name: Fully-qualified path like 'pkg.mod.Class'.
+    :return: The class object identified by `name`.
+    :raise TypeError: if `name` does not resolve to a class.
+    """
@@
-        msg = f"{name} is not a valid TDL type."
+        msg = f"{name} is not a valid class path."

Also applies to: 30-31


23-25: Fix Yoda condition flagged by Ruff (SIM300).

Style nit: prefer module_name == "" over "" == module_name.

If the current approach is kept, apply:

-    if "" == module_name:
+    if module_name == "":

28-28: Use non-string type in cast.

cast("type", ...) works, but cast(type, ...) is clearer and avoids unnecessary forward references.

-        return cast("type", getattr(module, class_name))
+        return cast(type, getattr(module, class_name))

15-31: Confirm threat model; importing arbitrary modules from strings can be risky.

If TDL strings are user-controlled, import_module will execute module top-level code. Consider restricting permissible modules (e.g., allowlist prefixes) or requiring prior registration.

Options:

  • Accept an optional allowed_prefixes: Sequence[str] argument and reject names outside it.
  • Resolve only within a curated registry map (name → class).
  • Restrict resolution to already-imported modules in sys.modules.

I can provide a patch once you confirm whether input is trusted or needs sandboxing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d348ed0 and 84aea87.

📒 Files selected for processing (1)
  • python/spider-py/src/spider_py/type/utils.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.12.2)
python/spider-py/src/spider_py/type/utils.py

23-23: Yoda condition detected

Rewrite as module_name == ""

(SIM300)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: lint
  • GitHub Check: non-storage-unit-tests (ubuntu-24.04)
  • GitHub Check: non-storage-unit-tests (ubuntu-22.04)
🔇 Additional comments (1)
python/spider-py/src/spider_py/type/utils.py (1)

3-4: Imports look correct and minimal.

import_module and cast are used below; nothing extraneous here.

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comments. Otherwise lgtm.

Comment thread python/spider-py/tests/type/test_to_native.py Outdated
Comment thread python/spider-py/tests/type/test_to_native.py Outdated
Comment thread python/spider-py/tests/type/test_to_native.py Outdated
Comment thread python/spider-py/src/spider_py/type/utils.py Outdated
sitaowang1998 and others added 4 commits August 23, 2025 18:11
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the PR title, how about:

feat(spider-py): Add support for parsing TDL types into native Python types.

@sitaowang1998 sitaowang1998 changed the title feat: Add TDL type parsing and conversion from TDL types to native Python types. feat(spider-py): Add support for parsing TDL types into native Python types. Aug 24, 2025
@sitaowang1998
sitaowang1998 merged commit 273e5d4 into y-scope:main Aug 24, 2025
6 checks passed
@sitaowang1998
sitaowang1998 deleted the python_type_parse branch August 24, 2025 02:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants