ssc_codegen 0.10.1__tar.gz → 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ssc_codegen might be problematic. Click here for more details.

Files changed (63) hide show
  1. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/PKG-INFO +6 -2
  2. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/README.md +5 -1
  3. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/pyproject.toml +1 -1
  4. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/__init__.py +4 -3
  5. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/base.py +7 -1
  6. ssc_codegen-0.11.0/ssc_codegen/ast_build/builder.py +422 -0
  7. ssc_codegen-0.11.0/ssc_codegen/ast_build/main.py +91 -0
  8. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/compiler.py +5 -23
  9. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/base.py +117 -22
  10. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/helpers.py +18 -10
  11. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/js_pure.py +241 -61
  12. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/lua_htmlparser.py +109 -64
  13. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/py_base.py +151 -104
  14. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/js_pure.py +0 -31
  15. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/document.py +229 -327
  16. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/schema.py +1 -1
  17. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/static_checker/__init__.py +2 -0
  18. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/static_checker/base.py +4 -2
  19. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/static_checker/callbacks.py +54 -4
  20. ssc_codegen-0.10.1/ssc_codegen/ast_build/main.py +0 -516
  21. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/.gitignore +0 -0
  22. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/LICENSE +0 -0
  23. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/_compat.py +0 -0
  24. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/__init__.py +0 -0
  25. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_array.py +0 -0
  26. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_cast.py +0 -0
  27. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_core.py +0 -0
  28. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_filter.py +0 -0
  29. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  30. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_string.py +0 -0
  31. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_/nodes_validate.py +0 -0
  32. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_build/__init__.py +0 -0
  33. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_build/utils.py +0 -0
  34. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_grep_rules/js_rules.yml +0 -0
  35. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/ast_grep_rules/py_rules.yml +0 -0
  36. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/__init__.py +0 -0
  37. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/ast_grep.py +0 -0
  38. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/cli_callbacks.py +0 -0
  39. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/cli_utils.py +0 -0
  40. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/code_callbacks.py +0 -0
  41. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/consts.py +0 -0
  42. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/main.py +0 -0
  43. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  44. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/__init__.py +0 -0
  45. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/go_goquery.py +0 -0
  46. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/py_bs4.py +0 -0
  47. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/py_lxml.py +0 -0
  48. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/py_parsel.py +0 -0
  49. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/py_selectolax.py +0 -0
  50. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/__init__.py +0 -0
  51. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/go_goquery.py +0 -0
  52. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/lua_base.py +0 -0
  53. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/lua_css_compat.py +0 -0
  54. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/lua_re_compat.py +0 -0
  55. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/converters/templates/py_base.py +0 -0
  56. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/document_utlis.py +0 -0
  57. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/json_struct.py +0 -0
  58. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/json_to_scc.py +0 -0
  59. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/logs.py +0 -0
  60. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/pseudo_selectors.py +0 -0
  61. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/selector_utils.py +0 -0
  62. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/str_utils.py +0 -0
  63. {ssc_codegen-0.10.1 → ssc_codegen-0.11.0}/ssc_codegen/tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssc_codegen
3
- Version: 0.10.1
3
+ Version: 0.11.0
4
4
  Summary: Python-dsl code converter to html parser for web scraping
5
5
  Project-URL: Documentation, https://github.com/vypivshiy/selector_schema_codegen#readme
6
6
  Project-URL: Issues, https://github.com/vypivshiy/selector_schema_codegen/issues
@@ -247,7 +247,11 @@ You can use any html source:
247
247
 
248
248
  ## See also
249
249
 
250
- - [Brief](docs/briefing.md) about css selectors and regular expressions.
250
+
251
+ - [Brief](docs/briefing.md) about css selectors and regular expressions
252
+ - [Explain](docs/explain.md) short document on how to understand DSL syntax
253
+ - [LLM](llm.md) experimental prompt for generate code
254
+ - [Explain](docs/explain.md) short note how to explain and read sscgen schema configs
251
255
  - [Quickstart](docs/quickstart.md) about css selectors and regular expressions.
252
256
  - [Tutorial](docs/tutorial.md) basic usage ssc-gen
253
257
  - [AST reference](docs/ast_reference.md) about generation code from AST
@@ -210,7 +210,11 @@ You can use any html source:
210
210
 
211
211
  ## See also
212
212
 
213
- - [Brief](docs/briefing.md) about css selectors and regular expressions.
213
+
214
+ - [Brief](docs/briefing.md) about css selectors and regular expressions
215
+ - [Explain](docs/explain.md) short document on how to understand DSL syntax
216
+ - [LLM](llm.md) experimental prompt for generate code
217
+ - [Explain](docs/explain.md) short note how to explain and read sscgen schema configs
214
218
  - [Quickstart](docs/quickstart.md) about css selectors and regular expressions.
215
219
  - [Tutorial](docs/tutorial.md) basic usage ssc-gen
216
220
  - [AST reference](docs/ast_reference.md) about generation code from AST
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssc_codegen"
3
- version = "0.10.1"
3
+ version = "0.11.0"
4
4
  description = "Python-dsl code converter to html parser for web scraping "
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -7,7 +7,7 @@ from ssc_codegen.schema import ItemSchema, DictSchema, ListSchema, FlatListSchem
7
7
 
8
8
  setup_logger()
9
9
 
10
- VERSION = "0.10.1"
10
+ VERSION = "0.11.0"
11
11
 
12
12
 
13
13
  class __MISSING(object):
@@ -28,10 +28,12 @@ class Document(
28
28
  JsonDocument,
29
29
  BooleanDocument
30
30
  ):
31
+ """Special Common Document or Element marker manipulations"""
31
32
  pass
32
33
 
33
34
 
34
35
  class Nested(HTMLDocument, NestedDocument, ArrayDocument, AssertDocument):
36
+ """Special Common Document or Element marker for provide Nested structure parsers"""
35
37
  pass
36
38
 
37
39
 
@@ -66,7 +68,7 @@ def F() -> DocumentFilter:
66
68
  return DocumentFilter()
67
69
 
68
70
 
69
- def CV(value, self_cls_path: str | None = None, *, returns: bool = False) -> ClassVarDocument:
71
+ def CV(value: int | str | float | bool | None | list, self_cls_path: str | None = None, *, returns: bool = False) -> ClassVarDocument:
70
72
  """Shortcut as ClassVarDocument(value, self_cls, parse_returns) object
71
73
 
72
74
  pass `self_cls_path` argument if need use this var inner schema context:
@@ -88,5 +90,4 @@ class A(ItemShema):
88
90
  ```
89
91
 
90
92
  """
91
-
92
93
  return ClassVarDocument(value, self_cls_path, parse_returns=returns)
@@ -45,10 +45,16 @@ class BaseAstNode(Generic[T_MAPPING_FIELD, T_ARGUMENTS]):
45
45
  """extract all values from kwargs field wout keys"""
46
46
  return tuple(self.kwargs.values()) # type: ignore
47
47
 
48
- # todo: cached property?
49
48
  @property
50
49
  def index(self) -> int:
51
50
  if self.parent:
51
+ # self.parent.body.index(self) add side effect if expr has same operations:
52
+ # 0 1 1 1
53
+ # D().raw().trim().trim().trim()
54
+ # use compare by id
55
+ for i, child in enumerate(self.parent.body):
56
+ if id(child) == id(self):
57
+ return i
52
58
  return self.parent.body.index(self)
53
59
  return -1
54
60
 
@@ -0,0 +1,422 @@
1
+ from types import ModuleType
2
+ from typing import MutableSequence, Type, cast
3
+ import warnings
4
+ from typing_extensions import Self
5
+
6
+ from ssc_codegen.ast_.base import BaseAstNode
7
+ from ssc_codegen.ast_.nodes_cast import ExprJsonify, ExprNested
8
+ from ssc_codegen.ast_.nodes_core import (
9
+ CodeEnd,
10
+ ExprCallStructClassVar,
11
+ ExprCallStructMethod,
12
+ ExprClassVar,
13
+ ExprDefaultValueEnd,
14
+ ExprDefaultValueStart,
15
+ ExprDefaultValueWrapper,
16
+ ExprNoReturn,
17
+ ExprReturn,
18
+ JsonStruct,
19
+ JsonStructField,
20
+ ModuleProgram,
21
+ Docstring,
22
+ ModuleImports,
23
+ CodeStart,
24
+ StartParseMethod,
25
+ StructFieldMethod,
26
+ StructInitMethod,
27
+ StructParser,
28
+ StructPartDocMethod,
29
+ StructPreValidateMethod,
30
+ TypeDef,
31
+ TypeDefField,
32
+ )
33
+ from ssc_codegen.ast_build.utils import (
34
+ generate_docstring_signature,
35
+ extract_schemas_from_module,
36
+ extract_json_structs_from_module,
37
+ is_literals_only_schema,
38
+ )
39
+ from ssc_codegen.document import BaseDocument, ClassVarDocument
40
+ from ssc_codegen.document_utlis import (
41
+ convert_css_to_xpath,
42
+ convert_xpath_to_css,
43
+ )
44
+ from ssc_codegen.json_struct import Json
45
+ from ssc_codegen.schema import BaseSchema
46
+ from ssc_codegen.tokens import StructType, TokenType, VariableType
47
+
48
+
49
+ class AstBuilder:
50
+ """USAGE:
51
+
52
+ builder = AstBuilder()
53
+ builder.add_header("test").add_json_types(*[]).add_struct_parsers(*[]).module
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ gen_docstr: bool = True,
59
+ css_to_xpath: bool = False,
60
+ xpath_to_css: bool = False,
61
+ ):
62
+ self.module = ModuleProgram()
63
+ self.gen_docstr = gen_docstr
64
+ if css_to_xpath and xpath_to_css:
65
+ raise TypeError("allowed css_to_xpath=True or xpath_to_css=True")
66
+ self.css_to_xpath = css_to_xpath
67
+ self.xpath_to_css = xpath_to_css
68
+
69
+ @classmethod
70
+ def build_from_moduletype(
71
+ cls,
72
+ module: ModuleType,
73
+ gen_docstr: bool = True,
74
+ css_to_xpath: bool = False,
75
+ xpath_to_css: bool = False,
76
+ ) -> ModuleProgram:
77
+ builder = cls(
78
+ gen_docstr=gen_docstr,
79
+ css_to_xpath=css_to_xpath,
80
+ xpath_to_css=xpath_to_css,
81
+ )
82
+ structs = extract_schemas_from_module(module)
83
+ jsons = extract_json_structs_from_module(module)
84
+ return (
85
+ builder.add_header(module.__dict__.get("__doc__", None))
86
+ .add_json_types(*jsons)
87
+ .add_struct_parsers(*structs)
88
+ .module
89
+ )
90
+
91
+ @classmethod
92
+ def build_from_ssc_schemas(
93
+ cls,
94
+ *schemas: Type[BaseSchema], # TODO: it not support json serialized
95
+ gen_docstr: bool = False,
96
+ css_to_xpath: bool = False,
97
+ xpath_to_css: bool = False,
98
+ ) -> ModuleProgram:
99
+ builder = cls(
100
+ gen_docstr=gen_docstr,
101
+ css_to_xpath=css_to_xpath,
102
+ xpath_to_css=xpath_to_css,
103
+ )
104
+ return (
105
+ builder.add_header(module_doc=None)
106
+ .add_struct_parsers(*schemas)
107
+ .module
108
+ )
109
+
110
+ def add_header(self, module_doc: str | None = None) -> Self:
111
+ if self.gen_docstr and module_doc:
112
+ self.module.body.append(
113
+ Docstring(kwargs={"value": module_doc}, parent=self.module)
114
+ )
115
+ # import node
116
+ self.module.body.append(ModuleImports(parent=self.module))
117
+ # CodeStart hook node
118
+ self.module.body.append(CodeStart(parent=self.module))
119
+ return self
120
+
121
+ def _json_field_nodes(
122
+ self, jsn_obj: Type[Json], ast_jsn: JsonStruct
123
+ ) -> None:
124
+ for name, field_type in jsn_obj.get_fields().items():
125
+ ast_jsn.body.append(
126
+ JsonStructField(
127
+ kwargs={"name": name, "type": field_type}, parent=ast_jsn
128
+ )
129
+ )
130
+
131
+ def add_json_types(self, *json_structs: Type[Json]) -> Self:
132
+ ast_jsons = []
133
+ for jsn_st in json_structs:
134
+ ast_jsn = JsonStruct(
135
+ kwargs={
136
+ "name": jsn_st.__name__,
137
+ "is_array": jsn_st.__IS_ARRAY__,
138
+ },
139
+ parent=self.module,
140
+ )
141
+ self._json_field_nodes(jsn_st, ast_jsn)
142
+ ast_jsons.append(ast_jsn)
143
+ self.module.body.extend(ast_jsons)
144
+ return self
145
+
146
+ def _ref_body_parent(self, node: BaseAstNode) -> None:
147
+ parent = node
148
+ for child in node.body:
149
+ child.parent = parent
150
+
151
+ def _literal_struct_node(self, schema: Type[BaseSchema]) -> StructParser:
152
+ # dont required TypeDef struct generate
153
+ literals = schema.__get_mro_literals__().copy()
154
+ st = StructParser(
155
+ kwargs={
156
+ "name": schema.__name__,
157
+ "struct_type": StructType.CONFIG_CLASSVARS,
158
+ "docstring": schema.__doc__ or "" if self.gen_docstr else "",
159
+ },
160
+ body=[f.expr() for f in literals.values()],
161
+ parent=self.module,
162
+ )
163
+ self._ref_body_parent(st)
164
+ return st
165
+
166
+ def _ref_filter_expr(
167
+ self, parent: BaseAstNode, parent_body: MutableSequence[BaseAstNode]
168
+ ) -> None:
169
+ for i in parent_body:
170
+ i.parent = parent
171
+ if i.body:
172
+ self._ref_filter_expr(i, i.body)
173
+
174
+ def _ref_method_exprs(self, node_method: BaseAstNode) -> None:
175
+ parent = node_method
176
+ for expr in node_method.body:
177
+ expr.parent = parent
178
+ if expr.kind == TokenType.EXPR_FILTER:
179
+ self._ref_filter_expr(expr, expr.body)
180
+
181
+ def _unwrap_default_node(
182
+ self,
183
+ stack: list[BaseAstNode], # noqa (use sideeffect for path stack exprs)
184
+ ret_type: VariableType,
185
+ ) -> None:
186
+ if stack[0].kind == ExprDefaultValueWrapper.kind:
187
+ default_expr = stack.pop(0)
188
+ value = default_expr.kwargs["value"]
189
+ classvar_hooks = default_expr.classvar_hooks
190
+ default_type = ret_type
191
+ if value is None:
192
+ match ret_type:
193
+ case VariableType.STRING:
194
+ default_type = VariableType.OPTIONAL_STRING
195
+ case VariableType.INT:
196
+ default_type = VariableType.OPTIONAL_INT
197
+ case VariableType.FLOAT:
198
+ default_type = VariableType.OPTIONAL_FLOAT
199
+ case VariableType.LIST_STRING:
200
+ default_type = VariableType.OPTIONAL_LIST_STRING
201
+ case VariableType.LIST_INT:
202
+ default_type = VariableType.OPTIONAL_LIST_INT
203
+ case VariableType.LIST_FLOAT:
204
+ default_type = VariableType.OPTIONAL_LIST_FLOAT
205
+ # TODO: warning for BOOL type
206
+ case _:
207
+ warnings.warn(
208
+ f"'None' default value not allowed return type '{ret_type.name}'. ",
209
+ category=SyntaxWarning,
210
+ )
211
+ default_type = VariableType.ANY
212
+ elif isinstance(value, list):
213
+ # todo: check if empty list passed
214
+ match ret_type:
215
+ case VariableType.LIST_STRING:
216
+ default_type = VariableType.LIST_STRING
217
+ case VariableType.LIST_INT:
218
+ default_type = VariableType.LIST_INT
219
+ case VariableType.LIST_FLOAT:
220
+ default_type = VariableType.LIST_FLOAT
221
+ case _:
222
+ warnings.warn(
223
+ f"`empty list` default value not allowed return type `{ret_type.name}`. "
224
+ f"Expected types `{(VariableType.LIST_STRING.name, VariableType.LIST_INT, VariableType.LIST_FLOAT)}`",
225
+ category=SyntaxWarning,
226
+ )
227
+ default_type = VariableType.ANY
228
+ expr_default_start = ExprDefaultValueStart(
229
+ kwargs={"value": value}, classvar_hooks=classvar_hooks
230
+ )
231
+ expr_default_end = ExprDefaultValueEnd(
232
+ kwargs={"value": value},
233
+ ret_type=default_type,
234
+ classvar_hooks=classvar_hooks,
235
+ )
236
+ stack.insert(0, expr_default_start)
237
+ stack.append(expr_default_end)
238
+
239
+ def _struct_pop_pre_validate(
240
+ self,
241
+ node_start_parse: StartParseMethod,
242
+ fields: dict[str, BaseDocument],
243
+ ) -> StructPreValidateMethod:
244
+ # fmt: off
245
+ assert "__PRE_VALIDATE__" in fields, "check `__PRE_VALIDATE__` key before extract"
246
+
247
+ validate_field = fields.pop("__PRE_VALIDATE__")
248
+ method = StructPreValidateMethod()
249
+ exprs = validate_field.stack.copy()
250
+ # not allowed use default exprs, skip check
251
+ exprs.append(ExprNoReturn())
252
+ method.body.extend(exprs)
253
+ self._ref_method_exprs(method)
254
+ node_start_parse.body.append(ExprCallStructMethod(kwargs={"type": VariableType.NULL, "name": "__PRE_VALIDATE__",}))
255
+ return method
256
+ # fmt: on
257
+
258
+ def _struct_pop_split_doc(
259
+ self,
260
+ node_start_parse: StartParseMethod,
261
+ fields: dict[str, BaseDocument],
262
+ ) -> StructPartDocMethod:
263
+ # fmt: off
264
+ assert "__SPLIT_DOC__" in fields, "check `__SPLIT_DOC__` key before extract"
265
+
266
+ split_field = fields.pop("__SPLIT_DOC__")
267
+ method = StructPartDocMethod()
268
+ exprs = split_field.stack.copy()
269
+ # not allowed use default exprs, skip check
270
+ exprs.append(ExprReturn(ret_type=VariableType.LIST_DOCUMENT))
271
+ method.body.extend(exprs)
272
+ self._ref_method_exprs(method)
273
+ node_start_parse.body.append(ExprCallStructMethod(kwargs={"type": VariableType.LIST_DOCUMENT, "name": "__SPLIT_DOC__",}))
274
+ return method
275
+ # fmt: on
276
+
277
+ def _struct_call_literals(
278
+ self,
279
+ node_start_parse: StartParseMethod,
280
+ literals: dict[str, ClassVarDocument],
281
+ ) -> None:
282
+ # fmt: off
283
+ for literal in literals.values():
284
+ expr = literal.expr()
285
+ if expr.kwargs["parse_returns"]:
286
+ _, struct_name, field_name, _, _ = expr.unpack_args()
287
+ node_start_parse.body.append(
288
+ ExprCallStructClassVar(
289
+ kwargs={"struct_name": struct_name, "field_name": field_name, "type": expr.ret_type,}))
290
+ # fmt: on
291
+
292
+ def _struct_fields_parse(
293
+ self,
294
+ node_start_parse: StartParseMethod,
295
+ fields: dict[str, BaseDocument],
296
+ ) -> list[StructFieldMethod]:
297
+ st_fields = []
298
+ # fmt: off
299
+ for field_name, document in fields.items():
300
+ if self.css_to_xpath:
301
+ document = convert_css_to_xpath(document)
302
+ elif self.xpath_to_css:
303
+ document = convert_xpath_to_css(document)
304
+
305
+ ret_type = document.stack_last_ret
306
+ method = StructFieldMethod(kwargs={"name": field_name}, ret_type=ret_type,)
307
+ exprs = document.stack.copy()
308
+ # in inheritance schemas, child classes use same fields are used as in the parent class
309
+ # avoid duplicate ExprReturn or ExprDefaultValueWrapper node
310
+ if (exprs[-1].kind != ExprReturn.kind and exprs[-1].kind != ExprDefaultValueEnd.kind):
311
+ exprs.append(ExprReturn(accept_type=ret_type, ret_type=ret_type))
312
+ self._unwrap_default_node(exprs, exprs[-1].ret_type)
313
+ method.body.extend(exprs)
314
+ self._ref_method_exprs(method)
315
+ st_fields.append(method)
316
+ node_start_parse.body.append(
317
+ ExprCallStructMethod(kwargs={"type": exprs[-1].ret_type, "name": field_name,}))
318
+ return st_fields
319
+ # fmt: on
320
+
321
+ def _node_typedef(self, struct: StructParser) -> TypeDef | None:
322
+ # fmt: off
323
+ if struct.struct_type == StructType.CONFIG_CLASSVARS:
324
+ return None
325
+ typedef = TypeDef(
326
+ parent=self.module,
327
+ kwargs={"name": struct.kwargs["name"], "struct_type": struct.struct_type},
328
+ )
329
+ for sc_field in struct.body:
330
+ if sc_field.kind not in (TokenType.STRUCT_FIELD, TokenType.CLASSVAR,):
331
+ continue
332
+ elif sc_field.kind == ExprClassVar.kind and sc_field.kwargs["parse_returns"]:
333
+ _value, _struct_name, field_name, _parse_returns, _is_regex = sc_field.unpack_args()
334
+ tdef_field = TypeDefField(
335
+ parent=typedef,
336
+ # cls_nested: always self-used field, set None
337
+ kwargs={"name": field_name, "type": sc_field.ret_type, "cls_nested": None, "cls_nested_type": None,},
338
+ )
339
+ typedef.body.append(tdef_field)
340
+ elif sc_field.kind == StructFieldMethod.kind:
341
+ if sc_field.body[-1].ret_type == VariableType.NESTED:
342
+ node = [i for i in sc_field.body if i.kind == TokenType.EXPR_NESTED][0]
343
+ node = cast(ExprNested, node)
344
+ cls_name = node.kwargs["schema_name"]
345
+ cls_nested_type = node.kwargs["schema_type"]
346
+ elif sc_field.body[-1].ret_type == VariableType.JSON:
347
+ node = [i for i in sc_field.body if i.kind == TokenType.TO_JSON][0]
348
+ node = cast(ExprJsonify, node)
349
+ cls_name = node.kwargs["json_struct_name"]
350
+ # hack: for provide more consistent Typedef field gen api reuse StructType enum
351
+ # or perceive it as bad AST design
352
+ cls_nested_type = StructType.LIST if node.kwargs["is_array"] else StructType.ITEM
353
+ else:
354
+ cls_name, cls_nested_type = None, None
355
+ tdef_field = TypeDefField(
356
+ parent=typedef,
357
+ kwargs={"name": sc_field.kwargs["name"], "type": sc_field.body[-1].ret_type,
358
+ "cls_nested": cls_name, "cls_nested_type": cls_nested_type,}
359
+ )
360
+ typedef.body.append(tdef_field)
361
+ return typedef
362
+ # fmt: on
363
+
364
+ def _parser_struct_node(self, schema: Type[BaseSchema]) -> StructParser:
365
+ # fmt: off
366
+ docstring = ((schema.__doc__ or "") + "\n\n" + generate_docstring_signature(schema))
367
+ st = StructParser(
368
+ kwargs={"name": schema.__name__, "struct_type": schema.__SCHEMA_TYPE__, "docstring": docstring if self.gen_docstr else "",},
369
+ parent=self.module,
370
+ )
371
+ literals = schema.__get_mro_literals__().copy()
372
+
373
+ for literal in literals.values():
374
+ st.body.append(literal.expr())
375
+ st.body.append(StructInitMethod())
376
+
377
+ fields = schema.__get_mro_fields__().copy()
378
+ node_start_parse = StartParseMethod(parent=st) # insert to END
379
+ if "__PRE_VALIDATE__" in fields:
380
+ pre_validate = self._struct_pop_pre_validate(node_start_parse, fields)
381
+ st.body.append(pre_validate)
382
+
383
+ if fields.get("__SPLIT_DOC__"):
384
+ split_doc = self._struct_pop_split_doc(node_start_parse, fields)
385
+ st.body.append(split_doc)
386
+ # literals call (if required return)
387
+ self._struct_call_literals(node_start_parse, literals)
388
+
389
+ fields_st = self._struct_fields_parse(node_start_parse, fields)
390
+ # assembly struct node
391
+ st.body.extend(fields_st)
392
+ st.body.append(node_start_parse)
393
+ self._ref_body_parent(st)
394
+ # generate typedef as annotation node
395
+ return st
396
+
397
+ def add_struct_parsers(self, *struct_parsers: Type[BaseSchema]) -> Self:
398
+ ssc_structs = []
399
+ ssc_typedefs = []
400
+ for schema in struct_parsers:
401
+ if is_literals_only_schema(schema):
402
+ if schema.__SCHEMA_TYPE__ != StructType.ITEM:
403
+ raise TypeError(
404
+ "Literals-only init allowed only ItemSchema"
405
+ )
406
+ struct = self._literal_struct_node(schema)
407
+ ssc_structs.append(struct)
408
+ continue
409
+ else:
410
+ struct = self._parser_struct_node(schema)
411
+ ssc_structs.append(struct)
412
+ for struct in ssc_structs:
413
+ typedef = self._node_typedef(struct)
414
+ # literals-only schema skip
415
+ if not typedef:
416
+ continue
417
+ ssc_typedefs.append(typedef)
418
+ # assembly
419
+ self.module.body.extend(ssc_typedefs)
420
+ self.module.body.extend(ssc_structs)
421
+ self.module.body.append(CodeEnd(parent=self.module))
422
+ return self
@@ -0,0 +1,91 @@
1
+ from pathlib import Path
2
+ from typing import Type
3
+
4
+ from ssc_codegen.ast_ import ModuleProgram
5
+
6
+ from ssc_codegen.ast_build.builder import AstBuilder
7
+ from ssc_codegen.ast_build.utils import (
8
+ exec_module_code,
9
+ extract_schemas_from_module,
10
+ )
11
+
12
+ from ssc_codegen.schema import BaseSchema
13
+ from ssc_codegen.static_checker import run_analyze_schema
14
+ import logging
15
+
16
+ LOGGER = logging.getLogger("ssc-gen")
17
+
18
+
19
+ def build_ast_module_parser(
20
+ path: str | Path,
21
+ *,
22
+ css_to_xpath: bool = False,
23
+ xpath_to_css: bool = False,
24
+ gen_docstring: bool = True,
25
+ ) -> ModuleProgram:
26
+ """build ast from python sscgen config file
27
+
28
+ WARNING!!!
29
+ DO NOT PASS MODULES FROM UNKNOWN SOURCE/INPUT FOR SECURITY REASONS.
30
+
31
+ THIS FUNCTION COMPILE AND EXEC PYTHON CODE in runtime WOUT CHECKS
32
+ """
33
+ if css_to_xpath and xpath_to_css:
34
+ raise AttributeError(
35
+ "Should be chosen one variant (css_to_xpath OR xpath_to_css)"
36
+ )
37
+ py_module = exec_module_code(path)
38
+ # check code configs before build AST
39
+ schemas = extract_schemas_from_module(py_module)
40
+ count_errors = 0
41
+ for schema in schemas:
42
+ errors = run_analyze_schema(schema)
43
+ if errors > 0:
44
+ msg = f"{schema.__name__} founded errors: {errors}"
45
+ LOGGER.error(msg)
46
+ count_errors += errors
47
+
48
+ if count_errors > 0:
49
+ msg = f"{path} errors count: {count_errors}"
50
+ LOGGER.error(msg)
51
+ raise SyntaxError("")
52
+
53
+ ast_module = AstBuilder.build_from_moduletype(
54
+ py_module,
55
+ css_to_xpath=css_to_xpath,
56
+ xpath_to_css=xpath_to_css,
57
+ gen_docstr=gen_docstring,
58
+ )
59
+ return ast_module
60
+ # static check
61
+
62
+
63
+ def build_ast_schemas(
64
+ *schemas: Type[BaseSchema],
65
+ css_to_xpath: bool = False,
66
+ xpath_to_css: bool = False,
67
+ gen_docstring: bool = False,
68
+ ) -> ModuleProgram:
69
+ if css_to_xpath and xpath_to_css:
70
+ raise AttributeError(
71
+ "Should be chosen one variant (css_to_xpath OR xpath_to_css)"
72
+ )
73
+ count_errors = 0
74
+ for schema in schemas:
75
+ errors = run_analyze_schema(schema)
76
+ if errors > 0:
77
+ msg = f"{schema.__name__} founded errors: {errors}"
78
+ LOGGER.error(msg)
79
+ count_errors += errors
80
+
81
+ if count_errors > 0:
82
+ msg = f"scehmas errors count: {count_errors}"
83
+ LOGGER.error(msg)
84
+ raise SyntaxError
85
+ ast_module = AstBuilder.build_from_ssc_schemas(
86
+ *schemas,
87
+ css_to_xpath=css_to_xpath,
88
+ xpath_to_css=xpath_to_css,
89
+ gen_docstr=gen_docstring,
90
+ )
91
+ return ast_module
@@ -5,12 +5,8 @@ import types
5
5
  from os import PathLike
6
6
  from typing import Any, Type
7
7
 
8
- from ssc_codegen.ast_.nodes_core import ModuleImports, ModuleProgram
9
- from ssc_codegen.ast_build import build_ast_module_parser
10
- from ssc_codegen.ast_build.main import (
11
- build_ast_struct_parser,
12
- build_ast_typedef,
13
- )
8
+ from ssc_codegen.ast_build.builder import AstBuilder
9
+ from ssc_codegen.ast_build.main import build_ast_module_parser
14
10
  from ssc_codegen.converters.py_base import BasePyCodeConverter
15
11
  from ssc_codegen.schema import BaseSchema
16
12
 
@@ -46,22 +42,8 @@ def compile(
46
42
  print(module.Demo(document.text).parse())
47
43
  ```
48
44
  """
49
-
50
- module = ModuleProgram()
51
- module.body.append(ModuleImports())
52
-
53
- structs = []
54
-
55
- # TODO: add json structs support
56
- for s in schemas:
57
- struct = build_ast_struct_parser(s, module, gen_docstring=False)
58
- structs.append(struct)
59
-
60
- t_defs = build_ast_typedef(module, *structs)
61
- module.body.extend(t_defs)
62
- module.body.extend(structs)
63
-
64
- code_parts = converter.convert_program(module)
45
+ ast_module = AstBuilder.build_from_ssc_schemas(*schemas)
46
+ code_parts = converter.convert_program(ast_module)
65
47
  module = types.ModuleType("_ssc_single_eval")
66
48
  code = "\n".join(code_parts)
67
49
  exec(code, module.__dict__)
@@ -99,7 +81,7 @@ class Compiler:
99
81
  return instance
100
82
 
101
83
  @property
102
- def cache(self):
84
+ def cache(self) -> dict[str, "Compiler"]:
103
85
  return self._cache
104
86
 
105
87
  def get_class(self, class_name: str) -> Any: