ssc_codegen 0.8.13__tar.gz → 0.8.15__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 (52) hide show
  1. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/PKG-INFO +2 -2
  2. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/README.md +1 -1
  3. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/pyproject.toml +1 -1
  4. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/__init__.py +1 -1
  5. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/__init__.py +1 -1
  6. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/nodes_validate.py +18 -0
  7. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_build/main.py +28 -13
  8. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/go_goquery.py +3 -0
  9. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/js_pure.py +22 -0
  10. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/py_bs4.py +28 -0
  11. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/py_parsel.py +31 -0
  12. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/py_selectolax.py +31 -0
  13. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/templates/js_pure.py +9 -0
  14. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/document.py +29 -0
  15. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/document_utlis.py +8 -2
  16. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/tokens.py +2 -0
  17. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/.gitignore +0 -0
  18. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/LICENSE +0 -0
  19. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/_compat.py +0 -0
  20. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/base.py +0 -0
  21. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/nodes_array.py +0 -0
  22. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/nodes_cast.py +0 -0
  23. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/nodes_core.py +0 -0
  24. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  25. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_/nodes_string.py +0 -0
  26. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_build/__init__.py +0 -0
  27. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/ast_build/utils.py +0 -0
  28. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/__init__.py +0 -0
  29. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/cli_callbacks.py +0 -0
  30. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/cli_utils.py +0 -0
  31. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/code_callbacks.py +0 -0
  32. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/consts.py +0 -0
  33. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/main.py +0 -0
  34. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  35. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/compiler.py +0 -0
  36. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/consts.py +0 -0
  37. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/__init__.py +0 -0
  38. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/base.py +0 -0
  39. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/helpers.py +0 -0
  40. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/py_base.py +0 -0
  41. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/templates/__init__.py +0 -0
  42. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/converters/templates/go_goquery.py +0 -0
  43. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/json_struct.py +0 -0
  44. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/json_to_scc.py +0 -0
  45. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/logs.py +0 -0
  46. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/pseudo_selectors.py +0 -0
  47. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/schema.py +0 -0
  48. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/selector_utils.py +0 -0
  49. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/static_checker/__init__.py +0 -0
  50. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/static_checker/base.py +0 -0
  51. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/static_checker/callbacks.py +0 -0
  52. {ssc_codegen-0.8.13 → ssc_codegen-0.8.15}/ssc_codegen/str_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssc_codegen
3
- Version: 0.8.13
3
+ Version: 0.8.15
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
@@ -52,7 +52,7 @@ Current support converters
52
52
  |---------------|-------------------------------|---------------|-------------|------------------------------------------|----------------|
53
53
  | Python (3.8+) | bs4 | N | Y | TypedDict*, list, dict | ruff |
54
54
  | ... | parsel | Y | Y | ... | - |
55
- | ... | selectolax (modest) | N | Y | ... | - |
55
+ | ... | selectolax (lexbor) | N | Y | ... | - |
56
56
  | js (ES6) | pure (firefox/chrome/nodejs) | Y | Y | Array, Map** | - |
57
57
  | go (1.10+) | goquery | N | Y | struct(json anchors include), array, map | gofmt |
58
58
 
@@ -19,7 +19,7 @@ Current support converters
19
19
  |---------------|-------------------------------|---------------|-------------|------------------------------------------|----------------|
20
20
  | Python (3.8+) | bs4 | N | Y | TypedDict*, list, dict | ruff |
21
21
  | ... | parsel | Y | Y | ... | - |
22
- | ... | selectolax (modest) | N | Y | ... | - |
22
+ | ... | selectolax (lexbor) | N | Y | ... | - |
23
23
  | js (ES6) | pure (firefox/chrome/nodejs) | Y | Y | Array, Map** | - |
24
24
  | go (1.10+) | goquery | N | Y | struct(json anchors include), array, map | gofmt |
25
25
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssc_codegen"
3
- version = "0.8.13"
3
+ version = "0.8.15"
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.8.13"
10
+ VERSION = "0.8.15"
11
11
 
12
12
 
13
13
  class __MISSING(object):
@@ -30,4 +30,4 @@ from .nodes_string import ExprStringTrim, ExprStringRegex, ExprStringSplit, Expr
30
30
  ExprStringRmPrefix, ExprStringRmSuffix, ExprStringRmPrefixAndSuffix, ExprListStringRmPrefixAndSuffix, \
31
31
  ExprListStringRmPrefix, ExprListStringRmSuffix
32
32
  from .nodes_validate import ExprIsCss, ExprIsEqual, ExprStringIsRegex, ExprIsXpath, ExprIsNotEqual, ExprIsContains, \
33
- ExprListStringAnyRegex, ExprListStringAllRegex
33
+ ExprListStringAnyRegex, ExprListStringAllRegex, ExprHasAttr, ExprListHasAttr
@@ -97,6 +97,24 @@ class ExprIsXpath(BaseAstNode[KW_IS_SELECT, ARGS_IS_SELECT]):
97
97
  ret_type: VariableType = VariableType.DOCUMENT
98
98
 
99
99
 
100
+ KW_HAS_ATTR = TypedDict("KW_HAS_ATTR", {"key": str, "msg": str})
101
+ ARGS_HAS_ATTR = tuple[str, str]
102
+
103
+
104
+ @dataclass(kw_only=True)
105
+ class ExprHasAttr(BaseAstNode[KW_HAS_ATTR, ARGS_HAS_ATTR]):
106
+ kind: ClassVar[TokenType] = TokenType.HAS_ATTR
107
+ accept_type: VariableType = VariableType.DOCUMENT
108
+ ret_type: VariableType = VariableType.DOCUMENT
109
+
110
+
111
+ @dataclass(kw_only=True)
112
+ class ExprListHasAttr(BaseAstNode[KW_HAS_ATTR, ARGS_HAS_ATTR]):
113
+ kind: ClassVar[TokenType] = TokenType.HAS_LIST_ATTR
114
+ accept_type: VariableType = VariableType.DOCUMENT
115
+ ret_type: VariableType = VariableType.DOCUMENT
116
+
117
+
100
118
  # TODO:
101
119
  # ExprIsBiggerThan
102
120
  # ExprIsBiggerOrEq
@@ -77,15 +77,32 @@ def build_ast_module_parser(
77
77
  json_structs = extract_json_structs_from_module(py_module)
78
78
  # TODO: API for build single json
79
79
  ast_jsons = build_ast_json(ast_module, *json_structs)
80
- ast_schemas = [
81
- build_ast_struct_parser(
82
- sc,
83
- ast_module,
84
- css_to_xpath=css_to_xpath,
85
- xpath_to_css=xpath_to_css,
86
- )
87
- for sc in schemas
88
- ]
80
+ # TODO: implement analyze-only AST function
81
+ try:
82
+ ast_schemas = [
83
+ build_ast_struct_parser(
84
+ sc,
85
+ ast_module,
86
+ css_to_xpath=css_to_xpath,
87
+ xpath_to_css=xpath_to_css,
88
+ )
89
+ for sc in schemas
90
+ ]
91
+ except SyntaxError:
92
+ # get all schemas exceptions and throw to logs:
93
+ errors = []
94
+ for sc in schemas:
95
+ try:
96
+ build_ast_struct_parser(
97
+ sc,
98
+ ast_module,
99
+ css_to_xpath=css_to_xpath,
100
+ xpath_to_css=xpath_to_css,
101
+ )
102
+ except SyntaxError as e:
103
+ errors.append(e)
104
+ raise SyntaxError(f"{path}: Founded errors: {len(errors)}")
105
+
89
106
  # TODO: API for build single typedef
90
107
  ast_typedefs = build_ast_typedef(ast_module, *ast_schemas)
91
108
  module_body.extend(ast_jsons)
@@ -173,7 +190,6 @@ def build_ast_struct_parser(
173
190
  ) -> StructParser:
174
191
  errors_count = run_analyze_schema(schema)
175
192
  if errors_count > 0:
176
- # todo: exc
177
193
  msg = f"{schema.__name__} founded errors: {errors_count}"
178
194
  raise SyntaxError(msg)
179
195
  docstring = (
@@ -237,8 +253,7 @@ def _fetch_field_nodes(
237
253
  )
238
254
  # TODO: add ast tests
239
255
  # in inheritance schemas, child classes use same fields are used as in the parent class
240
- # avoid duplicate ExprReturn node
241
- # second case used in ExprDefaultValueWrapper cases
256
+ # avoid duplicate ExprReturn or ExprDefaultValueWrapper node
242
257
  if (
243
258
  document.stack[-1].kind != ExprReturn.kind
244
259
  and document.stack[-1].kind != TokenType.EXPR_DEFAULT_END
@@ -335,7 +350,7 @@ def _try_fetch_split_doc_node(
335
350
  method = StructPartDocMethod(parent=st_ref)
336
351
 
337
352
  # in inheritance schemas, child classes use same fields are used as in the parent class
338
- # second case used in ExprDefaultValueWrapper cases
353
+ # avoid duplicate ExprReturn or ExprDefaultValueWrapper node
339
354
  if (
340
355
  split_doc.stack[-1].kind != ExprReturn.kind
341
356
  and split_doc.stack[-1].kind != TokenType.EXPR_DEFAULT_END
@@ -1259,3 +1259,6 @@ def pre_list_str_rm_prefix_and_suffix(
1259
1259
  return J2_PRE_LIST_STR_RM_PREFIX_AND_SUFFIX.render(
1260
1260
  prv=prv, nxt=nxt, substr=substr, tmp_var=tmp_var
1261
1261
  )
1262
+
1263
+
1264
+ # TODO impl ExprHasAttr, ExprListHasAttr
@@ -83,6 +83,8 @@ from ssc_codegen.ast_ import (
83
83
  ExprListStringRmPrefixAndSuffix,
84
84
  ExprListStringAnyRegex,
85
85
  ExprListStringAllRegex,
86
+ ExprListHasAttr,
87
+ ExprHasAttr,
86
88
  )
87
89
  from ssc_codegen.converters.base import BaseCodeConverter
88
90
  from ssc_codegen.converters.helpers import (
@@ -690,3 +692,23 @@ def pre_list_str_rm_prefix_and_suffix(
690
692
  f"let {nxt} = {prv}.map((e) => e.startsWith({substr}) ? e.slice({substr}.length) : e);"
691
693
  + f"{nxt} = {nxt}.map((e) => e.endsWith({substr}) ? e.slice(0, -{substr}.length) : e);"
692
694
  )
695
+
696
+
697
+ @CONVERTER(ExprHasAttr.kind)
698
+ def pre_has_attr(node: ExprHasAttr) -> str:
699
+ prv, nxt = prev_next_var(node)
700
+ key, msg = node.unpack_args()
701
+ expr = f"if (!{prv}?.hasAttribute({key!r}) throw new Error({msg!r});"
702
+ if is_last_var_no_ret(node):
703
+ return expr
704
+ return expr + f"let {nxt} = {prv};"
705
+
706
+
707
+ @CONVERTER(ExprListHasAttr.kind)
708
+ def pre_list_has_attr(node: ExprListHasAttr) -> str:
709
+ prv, nxt = prev_next_var(node)
710
+ key, msg = node.unpack_args()
711
+ expr = f"if (!{prv}.every(e => e?.hasAttribute({key!r}))) throw new Error({msg!r});"
712
+ if is_last_var_no_ret(node):
713
+ return expr
714
+ return expr + f"let {nxt} = {prv};"
@@ -16,6 +16,8 @@ from ssc_codegen.ast_ import (
16
16
  StructFieldMethod,
17
17
  StructPreValidateMethod,
18
18
  StructPartDocMethod,
19
+ ExprHasAttr,
20
+ ExprListHasAttr,
19
21
  )
20
22
  from ssc_codegen.converters.helpers import (
21
23
  prev_next_var,
@@ -173,6 +175,32 @@ def pre_is_css(node: ExprIsCss) -> str:
173
175
  return expr + "\n" + indent + f"{nxt} = {prv}"
174
176
 
175
177
 
178
+ @CONVERTER(ExprHasAttr.kind)
179
+ def pre_has_attr(node: ExprHasAttr) -> str:
180
+ indent = (
181
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
182
+ )
183
+ prv, nxt = prev_next_var(node)
184
+ key, msg = node.unpack_args()
185
+ expr = indent + f"assert {prv}.get({key!r}, None), {msg!r}"
186
+ if is_last_var_no_ret(node):
187
+ return expr
188
+ return expr + "\n" + indent + f"{nxt} = {prv}"
189
+
190
+
191
+ @CONVERTER(ExprListHasAttr.kind)
192
+ def pre_list_has_attr(node: ExprListHasAttr) -> str:
193
+ indent = (
194
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
195
+ )
196
+ prv, nxt = prev_next_var(node)
197
+ key, msg = node.unpack_args()
198
+ expr = indent + f"assert all(i.get({key!r}, None) for i in {prv}), {msg!r}"
199
+ if is_last_var_no_ret(node):
200
+ return expr
201
+ return expr + "\n" + indent + f"{nxt} = {prv}"
202
+
203
+
176
204
  # NOT SUPPORT
177
205
  @CONVERTER(ExprXpath.kind)
178
206
  def pre_xpath(_: ExprXpath) -> str:
@@ -16,6 +16,8 @@ from ssc_codegen.ast_ import (
16
16
  StructFieldMethod,
17
17
  StructPreValidateMethod,
18
18
  StructPartDocMethod,
19
+ ExprListHasAttr,
20
+ ExprHasAttr,
19
21
  )
20
22
  from ssc_codegen.converters.helpers import (
21
23
  prev_next_var,
@@ -196,3 +198,32 @@ def pre_is_xpath(node: ExprIsXpath) -> str:
196
198
  if is_last_var_no_ret(node):
197
199
  return expr
198
200
  return expr + "\n" + indent + f"{nxt} = {prv}"
201
+
202
+
203
+ @CONVERTER(ExprHasAttr.kind)
204
+ def pre_has_attr(node: ExprHasAttr) -> str:
205
+ indent = (
206
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
207
+ )
208
+ prv, nxt = prev_next_var(node)
209
+ key, msg = node.unpack_args()
210
+ expr = indent + f"assert {prv}.attrib.get({key!r}, None), {msg!r}"
211
+ if is_last_var_no_ret(node):
212
+ return expr
213
+ return expr + "\n" + indent + f"{nxt} = {prv}"
214
+
215
+
216
+ @CONVERTER(ExprListHasAttr.kind)
217
+ def pre_list_has_attr(node: ExprListHasAttr) -> str:
218
+ indent = (
219
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
220
+ )
221
+ prv, nxt = prev_next_var(node)
222
+ key, msg = node.unpack_args()
223
+ expr = (
224
+ indent
225
+ + f"assert all(i.attrib.get({key!r}, None) for i in {prv}), {msg!r}"
226
+ )
227
+ if is_last_var_no_ret(node):
228
+ return expr
229
+ return expr + "\n" + indent + f"{nxt} = {prv}"
@@ -16,6 +16,8 @@ from ssc_codegen.ast_ import (
16
16
  StructFieldMethod,
17
17
  StructPreValidateMethod,
18
18
  StructPartDocMethod,
19
+ ExprListHasAttr,
20
+ ExprHasAttr,
19
21
  )
20
22
  from ssc_codegen.converters.helpers import (
21
23
  prev_next_var,
@@ -171,6 +173,35 @@ def pre_is_css(node: ExprIsCss) -> str:
171
173
  return expr + "\n" + indent + f"{nxt} = {prv}"
172
174
 
173
175
 
176
+ @CONVERTER(ExprHasAttr.kind)
177
+ def pre_has_attr(node: ExprHasAttr) -> str:
178
+ indent = (
179
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
180
+ )
181
+ prv, nxt = prev_next_var(node)
182
+ key, msg = node.unpack_args()
183
+ expr = indent + f"assert {prv}.attributes.get({key!r}, None), {msg!r}"
184
+ if is_last_var_no_ret(node):
185
+ return expr
186
+ return expr + "\n" + indent + f"{nxt} = {prv}"
187
+
188
+
189
+ @CONVERTER(ExprListHasAttr.kind)
190
+ def pre_list_has_attr(node: ExprListHasAttr) -> str:
191
+ indent = (
192
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
193
+ )
194
+ prv, nxt = prev_next_var(node)
195
+ key, msg = node.unpack_args()
196
+ expr = (
197
+ indent
198
+ + f"assert all(i.attributes.get({key!r}, None) for i in {prv}), {msg!r}"
199
+ )
200
+ if is_last_var_no_ret(node):
201
+ return expr
202
+ return expr + "\n" + indent + f"{nxt} = {prv}"
203
+
204
+
174
205
  # NOT SUPPORT
175
206
  @CONVERTER(ExprXpath.kind)
176
207
  def pre_xpath(_: ExprXpath) -> str:
@@ -100,3 +100,12 @@ let {{ nxt }} = Array.from({ length: {{ snapshot_var }}.snapshotLength }, (_, i)
100
100
  J2_IS_XPATH = Template("""
101
101
  if (document.evaluate("{{ query }}", {{ prv }}, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue === null) throw new Error({{ msg }});
102
102
  """)
103
+ # TODO: use in covverter
104
+ JS_HAS_ATTR = Template("""
105
+ if (!{{prv}}?.hasAttribute({{key}}) throw new Error({{ msg }});
106
+ """)
107
+
108
+
109
+ JS_HAS_ATTR_ALL = Template("""
110
+ if (!{{prv}}.every(e => e?.hasAttribute({{key}}));}) throw new Error({{ msg }});
111
+ """)
@@ -57,6 +57,8 @@ from ssc_codegen.ast_ import (
57
57
  ExprListStringRmPrefixAndSuffix,
58
58
  ExprListStringAllRegex,
59
59
  ExprListStringAnyRegex,
60
+ ExprHasAttr,
61
+ ExprListHasAttr,
60
62
  )
61
63
  from ssc_codegen.document_utlis import (
62
64
  analyze_re_expression,
@@ -1086,6 +1088,33 @@ class AssertDocument(BaseDocument):
1086
1088
  )
1087
1089
  return self
1088
1090
 
1091
+ def has_attr(self, key: str, msg: str = "") -> Self:
1092
+ """assert document has attribute key. If in generated code check failed - throw exception with passed msg
1093
+
1094
+ EXPR DO NOT MODIFY variable
1095
+
1096
+ - accept DOCUMENT, return DOCUMENT
1097
+ - accept LIST_DOCUMENT, return LIST_DOCUMENT
1098
+ """
1099
+ if self.stack_last_ret == VariableType.DOCUMENT:
1100
+ self._add(ExprHasAttr(kwargs={"key": key, "msg": msg}))
1101
+ elif self.stack_last_ret == VariableType.LIST_DOCUMENT:
1102
+ self._add(ExprListHasAttr(kwargs={"key": key, "msg": msg}))
1103
+ else:
1104
+ LOGGER.warning(
1105
+ "has_attr(%s): Expected type(s) %s got %s",
1106
+ repr(key),
1107
+ (VariableType.DOCUMENT.name, VariableType.LIST_DOCUMENT.name),
1108
+ self.stack_last_ret.name,
1109
+ )
1110
+ self._add(
1111
+ ExprHasAttr(
1112
+ kwargs={"key": key, "msg": msg},
1113
+ ret_type=self.stack_last_ret,
1114
+ )
1115
+ )
1116
+ return self
1117
+
1089
1118
 
1090
1119
  class NestedDocument(BaseDocument):
1091
1120
  def sub_parser(self, schema: Type["BaseSchema"]) -> Self:
@@ -31,6 +31,8 @@ CM1_RX = r"(?m)(?<!\\)((\\{2})*)#.*$"
31
31
  CM2_RX = r"(\\)?((\\{2})*)(#)"
32
32
  WS_RX = r"(\\)?((\\{2})*)(\s)\s*"
33
33
 
34
+ RE_CAPTURED_GROUPS = re.compile(r"(?<!\()\((?!\?:)[^)]+\)")
35
+
34
36
 
35
37
  def is_ignore_case_regex(pattern: str | Pattern) -> bool:
36
38
  if isinstance(pattern, str):
@@ -76,10 +78,14 @@ def analyze_re_expression(
76
78
  try:
77
79
  re_pattern = re.compile(pattern)
78
80
  if not allow_empty_groups and re_pattern.groups == 0:
79
- msg = f"`{re_pattern.pattern}` pattern groups is empty"
81
+ msg = f"`{re_pattern.pattern}` pattern groups is empty."
82
+ msg += "\nTIP: maybe you remember wrap pattern to brackets `()`?"
80
83
  return AnalyzeResult.error(msg)
81
84
  elif max_groups != -1 and re_pattern.groups > max_groups:
82
- msg = f"`{re_pattern.pattern}` too many groups in pattern, expected {max_groups}"
85
+ captured_groups = RE_CAPTURED_GROUPS.findall(re_pattern.pattern)
86
+ msg = f"`{re_pattern.pattern}` too many groups in pattern, expected groups count: {max_groups}."
87
+ msg += f"\nTIP: fix regular expression for extract {max_groups}:"
88
+ msg += f"\nGroups founded: {captured_groups}"
83
89
  return AnalyzeResult.error(msg)
84
90
  except re.error as e:
85
91
  msg = f"`{pattern}` wrong regex pattern syntax: {e!r}"
@@ -156,6 +156,8 @@ class TokenType(IntEnum):
156
156
  IS_STRING_REGEX_MATCH = auto()
157
157
  ANY_LIST_STRING_REGEX_MATCH = auto()
158
158
  ALL_LIST_STRING_REGEX_MATCH = auto()
159
+ HAS_ATTR = auto()
160
+ HAS_LIST_ATTR = auto()
159
161
 
160
162
  # NUMERIC
161
163
  TO_INT = auto()
File without changes
File without changes