Repository navigation
feat(spider-py): Add support for parsing TDL types into native Python types. #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 143 commits
d2ac198
cc8a097
7f68bdc
2394649
6ff3e33
7ae8a78
56502f9
6f6becf
a74523d
2345d22
77ee494
38b86c8
3698339
369e9f1
90aa5a2
1769c95
edaa834
7faac8f
f494a90
eb01bb2
ff2fe1c
d476e42
850126d
65841a0
8564fc2
a6e7d29
573b448
6ad72c3
ba7c6e5
209acb1
406b514
66892f5
80f0a10
3186a42
0925938
6936a9b
7407090
6a95b24
f29dcba
453f209
d1011c7
a7dc642
a7d92c2
1c6213b
0f13d07
14d6326
55a727a
c98fea1
ef64454
09ba8ad
4e7237e
1f944d0
68992fc
1127b22
4c51c91
56878b2
bb428c9
944bd5e
d75bc15
4b7eca2
8ddbda7
0d998ec
6004d64
bcab5db
a50d25a
fbebb96
0322bd5
2a08155
0b62f73
7384158
70ef90a
b09515c
bad9675
e009ff4
a0407a7
aa9a8de
3c7e794
b4d6576
23f31cd
0167ac0
f447614
a84de83
2d1b6fc
061d101
de149ee
d332dbe
80b6772
f2ebaa6
8c27b16
b7f1884
a7aa6b0
95b98c8
5765acf
427dd6b
213d2fc
a1a86f3
97af71b
74f31e2
7d95706
96b5324
14ea6fb
854cd11
5565c9e
2504429
7ea4928
01dd0c6
4cec09b
e022404
8894b90
7753153
dd59915
ac1d4ee
0d38c43
438f6e7
2caad5d
c6815ce
61e0651
8b13abd
50b2b25
519dcd8
8e2786b
41d5de2
0a377f5
a8c8641
9fabc44
ade589b
0b18170
0bfbdb5
e558f67
73e8f0c
6b58ff0
90ab334
49acaee
d3f3eb1
43343aa
2f81cd9
ff60b0f
8223792
311e041
3e75f87
de28844
346a45a
d348ed0
84aea87
1f02daf
138ef8d
9d2d70e
af5dd2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,16 @@ | ||
| """Spider type package.""" | ||
|
|
||
| from spider_py.type.tdl_convert import to_tdl_type_str | ||
| from spider_py.type.tdl_parse import parse_tdl_type | ||
| from spider_py.type.type import Double, Float, Int8, Int16, Int32, Int64 | ||
|
|
||
| __all__ = ["Double", "Float", "Int8", "Int16", "Int32", "Int64", "to_tdl_type_str"] | ||
| __all__ = [ | ||
| "Double", | ||
| "Float", | ||
| "Int8", | ||
| "Int16", | ||
| "Int32", | ||
| "Int64", | ||
| "parse_tdl_type", | ||
| "to_tdl_type_str", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| """Parse TDL type string.""" | ||
|
|
||
| from lark import Lark, LarkError, Token, Transformer, v_args | ||
|
|
||
| from spider_py.type.tdl_type import ( | ||
| BoolType, | ||
| ClassType, | ||
| DoubleType, | ||
| FloatType, | ||
| Int8Type, | ||
| Int16Type, | ||
| Int32Type, | ||
| Int64Type, | ||
| ListType, | ||
| MapType, | ||
| TdlType, | ||
| ) | ||
|
|
||
| grammar = r""" | ||
| type: map_type | list_type | base_type | ||
|
|
||
| map_type: "Map" "<" type "," type ">" | ||
| list_type: "List" "<" type ">" | ||
| base_type: ID | ||
|
|
||
| ID: /[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*/ | ||
|
|
||
| %import common.WS | ||
| %ignore WS | ||
| """ | ||
|
|
||
| PrimitiveTypeDict = { | ||
| "bool": BoolType(), | ||
| "double": DoubleType(), | ||
| "float": FloatType(), | ||
| "int8": Int8Type(), | ||
| "int16": Int16Type(), | ||
| "int32": Int32Type(), | ||
| "int64": Int64Type(), | ||
| } | ||
|
|
||
|
|
||
| class TypeTransformer(Transformer[Token, TdlType]): | ||
| """Transforms Lark parse tree into TDL type.""" | ||
|
|
||
| @v_args(inline=True) | ||
| def type(self, value_type: TdlType) -> TdlType: | ||
| """ | ||
| Unwraps the type node to return the TdlType. | ||
| :param value_type: The type node to unwrap. | ||
| :return: The unwrapped TdlType. | ||
| """ | ||
| return value_type | ||
|
|
||
| @v_args(inline=True) | ||
| def map_type(self, key_type: TdlType, value_type: TdlType) -> TdlType: | ||
| """ | ||
| Transforms map node into Map type. | ||
| :param key_type: The key type of the map. | ||
| :param value_type: The value type of the map. | ||
| :return: The MapType with the given `key_type` and `value_type`. | ||
| """ | ||
| return MapType(key_type, value_type) | ||
|
|
||
| @v_args(inline=True) | ||
| def list_type(self, element_type: TdlType) -> TdlType: | ||
| """ | ||
| Transforms list node into List type. | ||
| :param element_type: The element type of the list. | ||
| :return: The ListType with the given `element_type`. | ||
| """ | ||
| return ListType(element_type) | ||
|
|
||
| @v_args(inline=True) | ||
| def base_type(self, token_id: str) -> TdlType: | ||
| """ | ||
| Transforms primitive node into primitive type. | ||
| :param token_id: The token id of the primitive type. | ||
| :return: The corresponding TdlType of the `token_id`. | ||
| """ | ||
| if token_id in PrimitiveTypeDict: | ||
| return PrimitiveTypeDict[token_id] | ||
| return ClassType(token_id) | ||
|
|
||
|
|
||
| _parser = Lark(grammar, start="type", parser="lalr") | ||
| _transformer = TypeTransformer(visit_tokens=False) | ||
|
|
||
|
|
||
| def parse_tdl_type(type_string: str) -> TdlType: | ||
| """ | ||
| Parses TDL type string into TDL type. | ||
| :param type_string: TDL type string. | ||
| :return: Parsed TDL type. | ||
| :raises TypeError: If string is not a valid TDL type. | ||
| """ | ||
| try: | ||
| tree = _parser.parse(type_string) | ||
| return _transformer.transform(tree) | ||
| except LarkError as e: | ||
| msg = f"Cannot parse TDL type `{type_string}`" | ||
| raise TypeError(msg) from e |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,30 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Utility for TDL types.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from importlib import import_module | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import cast | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_class_name(cls: type) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :param cls: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :return: Full name of `cls`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f"{cls.__module__}.{cls.__qualname__}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_class_by_name(name: str) -> type: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Gets class by name. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :param name: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :return: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
sitaowang1998 marked this conversation as resolved.
Resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :raise: TypeError if `class_name` is not a valid class. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts = name.split(".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module_name = ".".join(parts[:-1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class_name = parts[-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
sitaowang1998 marked this conversation as resolved.
Resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module = import_module(module_name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cast("type", getattr(module, class_name)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (ValueError, ModuleNotFoundError, AttributeError) as exc: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
sitaowang1998 marked this conversation as resolved.
Resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg = f"{name} is not a valid TDL type." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise TypeError(msg) from exc | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_excConsider 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| """Test converting TDL type to native type.""" | ||
|
|
||
| from types import GenericAlias | ||
|
|
||
| import pytest | ||
|
|
||
| import spider_py | ||
| from spider_py.type.tdl_parse import parse_tdl_type | ||
| from spider_py.type.utils import get_class_name | ||
|
|
||
|
|
||
| def string_to_native(s: str) -> type | GenericAlias: | ||
|
sitaowang1998 marked this conversation as resolved.
Resolved
|
||
| """ | ||
| Converts TDL string to native type. | ||
| :param s: TDL string to convert. | ||
| :return: The converted native type. | ||
|
sitaowang1998 marked this conversation as resolved.
Resolved
|
||
| :raises: TypeError forwards from `parse_tdl_type` and `native_type`. | ||
|
sitaowang1998 marked this conversation as resolved.
Resolved
|
||
| """ | ||
| return parse_tdl_type(s).native_type() | ||
|
|
||
|
|
||
| class TestToNativeType: | ||
| """Test converting TDL type to native type.""" | ||
|
|
||
| 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 | ||
|
|
||
| def test_to_class_type(self) -> None: | ||
| """Test converting class TDL type to native type.""" | ||
| assert string_to_native(get_class_name(TestToNativeType)) == TestToNativeType | ||
| with pytest.raises(TypeError): | ||
| string_to_native("NonExistType") | ||
| with pytest.raises(TypeError): | ||
| string_to_native("NonExistModule.NonExistType") | ||
|
|
||
| def test_to_list_type(self) -> None: | ||
| """Test converting list TDL type to native type.""" | ||
| assert string_to_native("List<int8>") == list[spider_py.Int8] | ||
| assert string_to_native("List<List<int8>>") == list[list[spider_py.Int8]] | ||
|
|
||
| def test_to_map_type(self) -> None: | ||
| """Test converting map TDL type to native type.""" | ||
| assert string_to_native("Map<int8,int8>") == dict[spider_py.Int8, spider_py.Int8] | ||
| assert ( | ||
| string_to_native("Map<List<int8>,Map<int8,double>>") | ||
| == dict[list[spider_py.Int8], dict[spider_py.Int8, spider_py.Double]] | ||
| ) | ||
There was a problem hiding this comment.
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.
Apply this diff to add typing-extensions and (optionally) cap Lark to <2:
Run this to confirm usage of typing_extensions across the repo and catch any other missing deps:
🏁 Script executed:
Length of output: 1312
Action Required: Add
typing-extensionsas a runtime dependency and constrain Lark’s major versionThe code imports
overridefrom typing_extensions (seepython/spider-py/src/spider_py/type/tdl_type.py:6), buttyping-extensionsisn’t declared inpyproject.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:
typing-extensionsif you need features introduced after 4.8.📝 Committable suggestion
🤖 Prompt for AI Agents