ssc_codegen 0.8.5__tar.gz → 0.8.7__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.5 → ssc_codegen-0.8.7}/PKG-INFO +1 -1
  2. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/pyproject.toml +1 -1
  3. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/__init__.py +1 -1
  4. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/__init__.py +2 -1
  5. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/nodes_validate.py +16 -2
  6. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/go_goquery.py +49 -3
  7. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/js_pure.py +40 -3
  8. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/py_base.py +39 -3
  9. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/templates/go_goquery.py +39 -0
  10. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/document.py +120 -20
  11. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/document_utlis.py +52 -37
  12. ssc_codegen-0.8.7/ssc_codegen/pseudo_selectors.py +64 -0
  13. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/static_checker/base.py +3 -1
  14. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/static_checker/callbacks.py +1 -1
  15. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/tokens.py +5 -3
  16. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/.gitignore +0 -0
  17. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/LICENSE +0 -0
  18. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/README.md +0 -0
  19. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/_compat.py +0 -0
  20. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/base.py +0 -0
  21. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/nodes_array.py +0 -0
  22. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/nodes_cast.py +0 -0
  23. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/nodes_core.py +0 -0
  24. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  25. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_/nodes_string.py +0 -0
  26. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_build/__init__.py +0 -0
  27. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_build/main.py +0 -0
  28. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/ast_build/utils.py +0 -0
  29. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/__init__.py +0 -0
  30. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/cli_callbacks.py +0 -0
  31. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/cli_utils.py +0 -0
  32. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/code_callbacks.py +0 -0
  33. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/consts.py +0 -0
  34. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/main.py +0 -0
  35. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  36. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/compiler.py +0 -0
  37. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/consts.py +0 -0
  38. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/__init__.py +0 -0
  39. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/base.py +0 -0
  40. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/helpers.py +0 -0
  41. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/py_bs4.py +0 -0
  42. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/py_parsel.py +0 -0
  43. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/py_selectolax.py +0 -0
  44. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/templates/__init__.py +0 -0
  45. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/converters/templates/js_pure.py +0 -0
  46. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/json_struct.py +0 -0
  47. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/json_to_scc.py +0 -0
  48. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/logs.py +0 -0
  49. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/schema.py +0 -0
  50. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/selector_utils.py +0 -0
  51. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/ssc_codegen/static_checker/__init__.py +0 -0
  52. {ssc_codegen-0.8.5 → ssc_codegen-0.8.7}/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.5
3
+ Version: 0.8.7
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssc_codegen"
3
- version = "0.8.5"
3
+ version = "0.8.7"
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.5"
10
+ VERSION = "0.8.7"
11
11
 
12
12
 
13
13
  class __MISSING(object):
@@ -29,4 +29,5 @@ from .nodes_string import ExprStringTrim, ExprStringRegex, ExprStringSplit, Expr
29
29
  ExprListStringRegexSub, \
30
30
  ExprStringRmPrefix, ExprStringRmSuffix, ExprStringRmPrefixAndSuffix, ExprListStringRmPrefixAndSuffix, \
31
31
  ExprListStringRmPrefix, ExprListStringRmSuffix
32
- from .nodes_validate import ExprIsCss, ExprIsEqual, ExprIsRegex, ExprIsXpath, ExprIsNotEqual, ExprIsContains
32
+ from .nodes_validate import ExprIsCss, ExprIsEqual, ExprStringIsRegex, ExprIsXpath, ExprIsNotEqual, ExprIsContains, \
33
+ ExprListStringAnyRegex, ExprListStringAllRegex
@@ -59,12 +59,26 @@ ARGS_IS_REGEX = tuple[str, bool, str]
59
59
 
60
60
 
61
61
  @dataclass(kw_only=True)
62
- class ExprIsRegex(BaseAstNode[KW_IS_REGEX, ARGS_IS_REGEX]):
63
- kind: ClassVar[TokenType] = TokenType.IS_REGEX_MATCH
62
+ class ExprStringIsRegex(BaseAstNode[KW_IS_REGEX, ARGS_IS_REGEX]):
63
+ kind: ClassVar[TokenType] = TokenType.IS_STRING_REGEX_MATCH
64
64
  accept_type: VariableType = VariableType.STRING
65
65
  ret_type: VariableType = VariableType.STRING
66
66
 
67
67
 
68
+ @dataclass(kw_only=True)
69
+ class ExprListStringAnyRegex(BaseAstNode[KW_IS_REGEX, ARGS_IS_REGEX]):
70
+ kind: ClassVar[TokenType] = TokenType.ANY_LIST_STRING_REGEX_MATCH
71
+ accept_type: VariableType = VariableType.LIST_STRING
72
+ ret_type: VariableType = VariableType.LIST_STRING
73
+
74
+
75
+ @dataclass(kw_only=True)
76
+ class ExprListStringAllRegex(BaseAstNode[KW_IS_REGEX, ARGS_IS_REGEX]):
77
+ kind: ClassVar[TokenType] = TokenType.ALL_LIST_STRING_REGEX_MATCH
78
+ accept_type: VariableType = VariableType.LIST_STRING
79
+ ret_type: VariableType = VariableType.LIST_STRING
80
+
81
+
68
82
  KW_IS_SELECT = TypedDict("KW_IS_SELECT", {"query": str, "msg": str})
69
83
  ARGS_IS_SELECT = tuple[str, str]
70
84
 
@@ -90,7 +90,7 @@ from ssc_codegen.ast_ import (
90
90
  ExprIsEqual,
91
91
  ExprIsNotEqual,
92
92
  ExprIsContains,
93
- ExprIsRegex,
93
+ ExprStringIsRegex,
94
94
  ExprIsCss,
95
95
  ExprToInt,
96
96
  ExprToListInt,
@@ -122,6 +122,8 @@ from ssc_codegen.ast_ import (
122
122
  ExprListStringRmSuffix,
123
123
  ExprStringRmPrefixAndSuffix,
124
124
  ExprListStringRmPrefixAndSuffix,
125
+ ExprListStringAnyRegex,
126
+ ExprListStringAllRegex,
125
127
  )
126
128
  from ssc_codegen.converters.base import BaseCodeConverter
127
129
  from ssc_codegen.converters.helpers import (
@@ -166,6 +168,8 @@ from ssc_codegen.converters.templates.go_goquery import (
166
168
  J2_PRE_LIST_STR_RM_SUFFIX,
167
169
  J2_PRE_STR_RM_PREFIX_AND_SUFFIX,
168
170
  J2_PRE_LIST_STR_RM_PREFIX_AND_SUFFIX,
171
+ J2_PRE_LIST_STR_ANY_IS_RE,
172
+ J2_PRE_LIST_STR_ALL_IS_RE,
169
173
  )
170
174
  from ssc_codegen.str_utils import (
171
175
  wrap_backtick,
@@ -853,8 +857,8 @@ def pre_is_contains(node: ExprIsContains) -> str:
853
857
  return J2_PRE_IS_CONTAINS.render(**context)
854
858
 
855
859
 
856
- @CONVERTER(ExprIsRegex.kind)
857
- def pre_is_regex(node: ExprIsRegex) -> str:
860
+ @CONVERTER(ExprStringIsRegex.kind)
861
+ def pre_is_regex(node: ExprStringIsRegex) -> str:
858
862
  prv, nxt = prev_next_var(node)
859
863
  pattern, ignore_case, msg = node.unpack_args()
860
864
 
@@ -880,6 +884,48 @@ def pre_is_regex(node: ExprIsRegex) -> str:
880
884
  return J2_PRE_IS_REGEX.render(**context)
881
885
 
882
886
 
887
+ @CONVERTER(ExprListStringAnyRegex.kind)
888
+ def pre_list_str_any_is_regex(node: ExprListStringAnyRegex) -> str:
889
+ prv, nxt = prev_next_var(node)
890
+ pattern, ignore_case, msg = node.unpack_args()
891
+ if ignore_case:
892
+ pattern = f"(?i){pattern}"
893
+ pattern = wrap_backtick(pattern)
894
+ msg = wrap_double_quotes(msg)
895
+ context = {
896
+ "prv": prv,
897
+ "nxt": nxt,
898
+ "pattern": pattern, # Already processed with ignore_case and backticks
899
+ "msg": msg,
900
+ "have_default_expr": have_default_expr(node),
901
+ "is_pre_validate_parent": is_pre_validate_parent(node),
902
+ "is_last_var_no_ret": is_last_var_no_ret(node),
903
+ "return_type": RETURN_ERR_TYPES.get(get_last_ret_type(node), "nil"),
904
+ }
905
+ return J2_PRE_LIST_STR_ANY_IS_RE.render(**context)
906
+
907
+
908
+ @CONVERTER(ExprListStringAllRegex.kind)
909
+ def pre_list_str_all_is_regex(node: ExprListStringAllRegex) -> str:
910
+ prv, nxt = prev_next_var(node)
911
+ pattern, ignore_case, msg = node.unpack_args()
912
+ if ignore_case:
913
+ pattern = f"(?i){pattern}"
914
+ pattern = wrap_backtick(pattern)
915
+ msg = wrap_double_quotes(msg)
916
+ context = {
917
+ "prv": prv,
918
+ "nxt": nxt,
919
+ "pattern": pattern, # Already processed with ignore_case and backticks
920
+ "msg": msg,
921
+ "have_default_expr": have_default_expr(node),
922
+ "is_pre_validate_parent": is_pre_validate_parent(node),
923
+ "is_last_var_no_ret": is_last_var_no_ret(node),
924
+ "return_type": RETURN_ERR_TYPES.get(get_last_ret_type(node), "nil"),
925
+ }
926
+ return J2_PRE_LIST_STR_ALL_IS_RE.render(**context)
927
+
928
+
883
929
  @CONVERTER(ExprIsCss.kind)
884
930
  def pre_is_css(node: ExprIsCss) -> str:
885
931
  prv, nxt = prev_next_var(node)
@@ -54,7 +54,7 @@ from ssc_codegen.ast_ import (
54
54
  ExprIsEqual,
55
55
  ExprIsNotEqual,
56
56
  ExprIsContains,
57
- ExprIsRegex,
57
+ ExprStringIsRegex,
58
58
  ExprToInt,
59
59
  ExprToListInt,
60
60
  ExprToFloat,
@@ -81,6 +81,8 @@ from ssc_codegen.ast_ import (
81
81
  ExprListStringRmSuffix,
82
82
  ExprStringRmPrefixAndSuffix,
83
83
  ExprListStringRmPrefixAndSuffix,
84
+ ExprListStringAnyRegex,
85
+ ExprListStringAllRegex,
84
86
  )
85
87
  from ssc_codegen.converters.base import BaseCodeConverter
86
88
  from ssc_codegen.converters.helpers import (
@@ -442,11 +444,14 @@ def pre_is_contains(node: ExprIsContains) -> str:
442
444
  return expr + f"let {nxt} = {prv};"
443
445
 
444
446
 
445
- @CONVERTER(ExprIsRegex.kind)
446
- def pre_is_regex(node: ExprIsRegex) -> str:
447
+ @CONVERTER(ExprStringIsRegex.kind)
448
+ def pre_is_regex(node: ExprStringIsRegex) -> str:
447
449
  prv, nxt = prev_next_var(node)
448
450
  pattern, ignore_case, msg = node.unpack_args()
449
451
  pattern = f"/{node.pattern}/g"
452
+ if ignore_case:
453
+ pattern += "i"
454
+
450
455
  expr = f"if ({prv}.match({pattern}) === null) throw new Error({msg!r});"
451
456
  if is_last_var_no_ret(node):
452
457
  return expr
@@ -454,6 +459,38 @@ def pre_is_regex(node: ExprIsRegex) -> str:
454
459
  return expr + f"let {nxt} = {prv};"
455
460
 
456
461
 
462
+ @CONVERTER(ExprListStringAnyRegex.kind)
463
+ def pre_list_str_any_is_regex(node: ExprListStringAnyRegex) -> str:
464
+ # a.some(i => (new RegExp("foo")).test(i))
465
+ prv, nxt = prev_next_var(node)
466
+ pattern, ignore_case, msg = node.unpack_args()
467
+ pattern = f"/{node.pattern}/g"
468
+ if ignore_case:
469
+ pattern += "i"
470
+
471
+ expr = f"if (!{prv}.some(i => (new RegExp({pattern})).test(i))) throw new Error({msg!r});"
472
+ if is_last_var_no_ret(node):
473
+ return expr
474
+ # HACK: avoid recalc variables
475
+ return expr + f"let {nxt} = {prv};"
476
+
477
+
478
+ @CONVERTER(ExprListStringAllRegex.kind)
479
+ def pre_list_str_all_is_regex(node: ExprListStringAllRegex) -> str:
480
+ prv, nxt = prev_next_var(node)
481
+ # a.every(i => (new RegExp("foo")).test)
482
+ pattern, ignore_case, msg = node.unpack_args()
483
+ pattern = f"/{node.pattern}/g"
484
+ if ignore_case:
485
+ pattern += "i"
486
+
487
+ expr = f"if (!{prv}.every(i => (new RegExp({pattern})).test(i))) throw new Error({msg!r});"
488
+ if is_last_var_no_ret(node):
489
+ return expr
490
+ # HACK: avoid recalc variables
491
+ return expr + f"let {nxt} = {prv};"
492
+
493
+
457
494
  @CONVERTER(ExprIsCss.kind)
458
495
  def pre_is_css(node: ExprIsCss) -> str:
459
496
  prv, nxt = prev_next_var(node)
@@ -72,7 +72,7 @@ from ssc_codegen.ast_ import (
72
72
  ExprIsEqual,
73
73
  ExprIsNotEqual,
74
74
  ExprIsContains,
75
- ExprIsRegex,
75
+ ExprStringIsRegex,
76
76
  ExprToInt,
77
77
  ExprToListInt,
78
78
  ExprToFloat,
@@ -85,6 +85,8 @@ from ssc_codegen.ast_ import (
85
85
  ExprListStringRmSuffix,
86
86
  ExprStringRmPrefixAndSuffix,
87
87
  ExprListStringRmPrefixAndSuffix,
88
+ ExprListStringAnyRegex,
89
+ ExprListStringAllRegex,
88
90
  )
89
91
  from ssc_codegen.converters.base import (
90
92
  BaseCodeConverter,
@@ -222,7 +224,9 @@ class BasePyCodeConverter(BaseCodeConverter):
222
224
  ExprIsEqual.kind: pre_is_equal,
223
225
  ExprIsNotEqual.kind: pre_is_not_equal,
224
226
  ExprIsContains.kind: pre_is_contains,
225
- ExprIsRegex.kind: pre_is_regex,
227
+ ExprStringIsRegex.kind: pre_is_regex,
228
+ ExprListStringAllRegex.kind: pre_list_str_all_is_regex,
229
+ ExprListStringAnyRegex.kind: pre_list_str_any_is_regex,
226
230
  ExprToInt.kind: pre_to_int,
227
231
  ExprToListInt.kind: pre_to_list_int,
228
232
  ExprToFloat.kind: pre_to_float,
@@ -770,7 +774,7 @@ def pre_is_contains(node: ExprIsContains) -> str:
770
774
  return expr + "\n" + indent + f"{nxt} = {prv}"
771
775
 
772
776
 
773
- def pre_is_regex(node: ExprIsRegex) -> str:
777
+ def pre_is_regex(node: ExprStringIsRegex) -> str:
774
778
  indent = (
775
779
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
776
780
  )
@@ -783,6 +787,38 @@ def pre_is_regex(node: ExprIsRegex) -> str:
783
787
  return expr + "\n" + indent + f"{nxt} = {prv}"
784
788
 
785
789
 
790
+ def pre_list_str_any_is_regex(node: ExprListStringAnyRegex) -> str:
791
+ indent = (
792
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
793
+ )
794
+ prv, nxt = prev_next_var(node)
795
+ pattern, ignore_case, msg = node.unpack_args()
796
+ expr = (
797
+ indent
798
+ + f"assert any(re.search({pattern!r}, i) for i in {prv}), {msg!r}"
799
+ )
800
+ if is_last_var_no_ret(node):
801
+ return expr
802
+ # HACK: avoid recalc variables
803
+ return expr + "\n" + indent + f"{nxt} = {prv}"
804
+
805
+
806
+ def pre_list_str_all_is_regex(node: ExprListStringAllRegex) -> str:
807
+ indent = (
808
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
809
+ )
810
+ prv, nxt = prev_next_var(node)
811
+ pattern, ignore_case, msg = node.unpack_args()
812
+ expr = (
813
+ indent
814
+ + f"assert all(re.search({pattern!r}, i) for i in {prv}), {msg!r}"
815
+ )
816
+ if is_last_var_no_ret(node):
817
+ return expr
818
+ # HACK: avoid recalc variables
819
+ return expr + "\n" + indent + f"{nxt} = {prv}"
820
+
821
+
786
822
  def pre_to_int(node: ExprToInt) -> str:
787
823
  indent = (
788
824
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
@@ -409,3 +409,42 @@ for _, {{ tmp_var }} := range {{ prv }} {
409
409
  {{nxt}} = append({{ nxt }}, {{ tmp_var }});
410
410
  }
411
411
  """)
412
+
413
+
414
+ J2_PRE_LIST_STR_ANY_IS_RE = Template("""
415
+ re{{ nxt }} := regexp.MustCompile({{ pattern }});
416
+ for _, {{ tmp_var }} := range {{ prv }} {
417
+ if re{{ nxt }}.MatchString({{ tmp_var }}) {
418
+ break;
419
+ }
420
+ {% if have_default_expr %}
421
+ panic(fmt.Errorf({{ msg }}));
422
+ {% elif is_pre_validate_parent %}
423
+ return nil, fmt.Errorf({{ msg }});
424
+ {% else %}
425
+ return {{ return_type }}, fmt.Errorf({{ msg }});
426
+ {% endif %}
427
+ }
428
+ {% if not is_last_var_no_ret %}
429
+ {{ nxt }} := {{ prv }};
430
+ {% endif %}
431
+ """)
432
+
433
+
434
+ J2_PRE_LIST_STR_ALL_IS_RE = Template("""
435
+ re{{ nxt }} := regexp.MustCompile({{ pattern }});
436
+ for _, {{ tmp_var }} := range {{ prv }} {
437
+ if !re{{ nxt }}.MatchString({{ tmp_var }}) {
438
+ {% if have_default_expr %}
439
+ panic(fmt.Errorf({{ msg }}));
440
+ {% elif is_pre_validate_parent %}
441
+ return nil, fmt.Errorf({{ msg }});
442
+ {% else %}
443
+ return {{ return_type }}, fmt.Errorf({{ msg }});
444
+ {% endif %}
445
+ }
446
+ }
447
+ {% if not is_last_var_no_ret %}
448
+ {{ nxt }} := {{ prv }};
449
+ {% endif %}
450
+ """)
@@ -4,7 +4,7 @@ import logging
4
4
  from typing import Type, Pattern
5
5
 
6
6
  from cssselect import SelectorSyntaxError
7
- from typing_extensions import Self
7
+ from typing_extensions import Self, assert_never
8
8
 
9
9
  from ssc_codegen.ast_ import (
10
10
  BaseAstNode,
@@ -41,7 +41,7 @@ from ssc_codegen.ast_ import (
41
41
  ExprIsEqual,
42
42
  ExprIsNotEqual,
43
43
  ExprIsContains,
44
- ExprIsRegex,
44
+ ExprStringIsRegex,
45
45
  ExprNested,
46
46
  ExprToInt,
47
47
  ExprToListInt,
@@ -55,12 +55,19 @@ from ssc_codegen.ast_ import (
55
55
  ExprListStringRmSuffix,
56
56
  ExprStringRmPrefixAndSuffix,
57
57
  ExprListStringRmPrefixAndSuffix,
58
+ ExprListStringAllRegex,
59
+ ExprListStringAnyRegex,
58
60
  )
59
61
  from ssc_codegen.document_utlis import (
60
62
  analyze_re_expression,
61
63
  unverbosify_regex,
62
64
  is_ignore_case_regex,
63
65
  )
66
+ from ssc_codegen.pseudo_selectors import (
67
+ parse_pseudo_xpath_query,
68
+ parse_pseudo_css_query,
69
+ PseudoAction,
70
+ )
64
71
  from ssc_codegen.json_struct import Json
65
72
  from ssc_codegen.schema import BaseSchema
66
73
  from ssc_codegen.selector_utils import validate_css_query, validate_xpath_query
@@ -70,6 +77,8 @@ LOGGER = logging.getLogger("ssc_gen")
70
77
 
71
78
 
72
79
  class BaseDocument:
80
+ LOGGER = LOGGER
81
+
73
82
  def __init__(self) -> None:
74
83
  self._stack: list[BaseAstNode] = []
75
84
 
@@ -96,14 +105,6 @@ class BaseDocument:
96
105
  return VariableType.DOCUMENT
97
106
  return self.stack[-1].ret_type
98
107
 
99
- @staticmethod
100
- def _raise_wrong_type_error(
101
- type_: VariableType, *expected: VariableType
102
- ) -> None:
103
- fmt_types = "(" + ",".join(i.name for i in expected) + ")"
104
- msg = f"Expected type(s): {fmt_types}, got {type_.name}"
105
- raise SyntaxError(msg)
106
-
107
108
  def _add(self, expr: BaseAstNode) -> None:
108
109
  self._stack.append(expr)
109
110
 
@@ -138,6 +139,7 @@ class HTMLDocument(BaseDocument):
138
139
  - accept: DOCUMENT, return DOCUMENT
139
140
  """
140
141
  query = " ".join(query.splitlines())
142
+ query, action = parse_pseudo_css_query(query)
141
143
  try:
142
144
  validate_css_query(query)
143
145
  except SelectorSyntaxError:
@@ -151,13 +153,29 @@ class HTMLDocument(BaseDocument):
151
153
  )
152
154
 
153
155
  self._add(ExprCss(kwargs={"query": query}))
156
+ self._pseudo_query_action_to_expr(action)
154
157
  return self
155
158
 
159
+ def _pseudo_query_action_to_expr(
160
+ self, action: tuple[PseudoAction | None, str | None]
161
+ ) -> None:
162
+ if action[0]:
163
+ match action[0]:
164
+ case "text":
165
+ self.text()
166
+ case "raw":
167
+ self.raw()
168
+ case "attr":
169
+ self.attr(action[1]) # type: ignore
170
+ case _:
171
+ assert_never(action[0])
172
+
156
173
  def xpath(self, query: str) -> Self:
157
174
  """Xpath query. returns first founded element
158
175
  - accept: DOCUMENT, return DOCUMENT
159
176
  """
160
177
  query = " ".join(query.splitlines())
178
+ query, action = parse_pseudo_xpath_query(query)
161
179
  try:
162
180
  validate_xpath_query(query)
163
181
  except SelectorSyntaxError:
@@ -170,22 +188,15 @@ class HTMLDocument(BaseDocument):
170
188
  self.stack_last_ret.name,
171
189
  )
172
190
  self._add(ExprXpath(kwargs={"query": query}))
191
+ self._pseudo_query_action_to_expr(action)
173
192
  return self
174
- # match self.stack_last_ret:
175
- # case VariableType.DOCUMENT:
176
- # self._add(HtmlXpathExpression(query=query))
177
- # case VariableType.ANY:
178
- # self._add(HtmlXpathExpression(query=query))
179
- # case _:
180
- # self._raise_wrong_type_error(
181
- # self.stack_last_ret, VariableType.DOCUMENT
182
- # )
183
193
 
184
194
  def css_all(self, query: str) -> Self:
185
195
  """Css query. returns all founded elements
186
196
  - accept: DOCUMENT, return: LIST_DOCUMENT
187
197
  """
188
198
  query = " ".join(query.splitlines())
199
+ query, action = parse_pseudo_css_query(query)
189
200
  try:
190
201
  validate_css_query(query)
191
202
  except SelectorSyntaxError:
@@ -197,6 +208,7 @@ class HTMLDocument(BaseDocument):
197
208
  self.stack_last_ret.name,
198
209
  )
199
210
  self._add(ExprCssAll(kwargs={"query": query}))
211
+ self._pseudo_query_action_to_expr(action)
200
212
  return self
201
213
 
202
214
  def xpath_all(self, query: str) -> Self:
@@ -204,6 +216,7 @@ class HTMLDocument(BaseDocument):
204
216
  - accept: DOCUMENT, return: LIST_DOCUMENT
205
217
  """
206
218
  query = " ".join(query.splitlines())
219
+ query, action = parse_pseudo_xpath_query(query)
207
220
  try:
208
221
  validate_xpath_query(query)
209
222
  except SelectorSyntaxError as e:
@@ -215,6 +228,7 @@ class HTMLDocument(BaseDocument):
215
228
  self.stack_last_ret.name,
216
229
  )
217
230
  self._add(ExprXpathAll(kwargs={"query": query}))
231
+ self._pseudo_query_action_to_expr(action)
218
232
  return self
219
233
 
220
234
  def attr(self, key: str) -> Self:
@@ -766,6 +780,13 @@ class AssertDocument(BaseDocument):
766
780
  - accept DOCUMENT, return DOCUMENT
767
781
  """
768
782
  query = " ".join(query.splitlines())
783
+ new_query, action = parse_pseudo_css_query(query)
784
+ if action[0]:
785
+ LOGGER.warning(
786
+ "is_css(%s) not support pseudo parse classes, skip", repr(query)
787
+ )
788
+ query = new_query
789
+
769
790
  try:
770
791
  validate_css_query(query)
771
792
  except SelectorSyntaxError:
@@ -788,6 +809,13 @@ class AssertDocument(BaseDocument):
788
809
  - accept DOCUMENT, return DOCUMENT
789
810
  """
790
811
  query = " ".join(query.splitlines())
812
+ new_query, action = parse_pseudo_xpath_query(query)
813
+ if action[0]:
814
+ LOGGER.warning(
815
+ "is_xpath(%s) not support pseudo parse classes, skip",
816
+ repr(query),
817
+ )
818
+ query = new_query
791
819
  try:
792
820
  validate_xpath_query(query)
793
821
  except SelectorSyntaxError:
@@ -953,6 +981,78 @@ class AssertDocument(BaseDocument):
953
981
  )
954
982
  return self
955
983
 
984
+ def any_is_re(
985
+ self, pattern: str | Pattern, msg: str = "", ignore_case: bool = False
986
+ ) -> Self:
987
+ """assert any value matched in array of strings by regex.
988
+ If in generated code check failed - throw exception with passed msg
989
+
990
+ EXPR DO NOT MODIFY variable
991
+
992
+ - accept LIST_STRING, return LIST_STRING
993
+ """
994
+ if not isinstance(pattern, str):
995
+ ignore_case = is_ignore_case_regex(pattern)
996
+ pattern = unverbosify_regex(pattern)
997
+ analyze_re_expression(pattern, allow_empty_groups=True)
998
+ if self.stack_last_ret != VariableType.LIST_STRING:
999
+ LOGGER.warning(
1000
+ "any_is_re(%s): Expected type(s) %s got %s",
1001
+ repr(pattern),
1002
+ VariableType.LIST_STRING.name,
1003
+ self.stack_last_ret.name,
1004
+ )
1005
+
1006
+ self._add(
1007
+ ExprListStringAnyRegex(
1008
+ kwargs={
1009
+ "pattern": pattern,
1010
+ "ignore_case": ignore_case,
1011
+ "msg": msg,
1012
+ }
1013
+ )
1014
+ )
1015
+ return self
1016
+
1017
+ def all_is_re(
1018
+ self, pattern: str | Pattern, msg: str = "", ignore_case: bool = False
1019
+ ) -> Self:
1020
+ """assert all value matched in array of strings by regex.
1021
+ If in generated code check failed - throw exception with passed msg
1022
+
1023
+ EXPR DO NOT MODIFY variable
1024
+
1025
+ - accept LIST_STRING, return LIST_STRING
1026
+ """
1027
+ if not isinstance(pattern, str):
1028
+ ignore_case = is_ignore_case_regex(pattern)
1029
+ pattern = unverbosify_regex(pattern)
1030
+ analyze_re_expression(pattern, allow_empty_groups=True)
1031
+ if self.stack_last_ret != VariableType.LIST_STRING:
1032
+ LOGGER.warning(
1033
+ "all_is_re(%s): Expected type(s) %s got %s",
1034
+ repr(pattern),
1035
+ VariableType.LIST_STRING.name,
1036
+ self.stack_last_ret.name,
1037
+ )
1038
+
1039
+ self._add(
1040
+ ExprListStringAllRegex(
1041
+ kwargs={
1042
+ "pattern": pattern,
1043
+ "ignore_case": ignore_case,
1044
+ "msg": msg,
1045
+ }
1046
+ )
1047
+ )
1048
+ return self
1049
+
1050
+ def is_re(
1051
+ self, pattern: str | Pattern, msg: str = "", ignore_case: bool = False
1052
+ ) -> Self:
1053
+ """shortcut of is_regex() method"""
1054
+ return self.is_regex(pattern, msg, ignore_case)
1055
+
956
1056
  def is_regex(
957
1057
  self, pattern: str | Pattern, msg: str = "", ignore_case: bool = False
958
1058
  ) -> Self:
@@ -976,7 +1076,7 @@ class AssertDocument(BaseDocument):
976
1076
  )
977
1077
 
978
1078
  self._add(
979
- ExprIsRegex(
1079
+ ExprStringIsRegex(
980
1080
  kwargs={
981
1081
  "pattern": pattern,
982
1082
  "ignore_case": ignore_case,
@@ -1,6 +1,8 @@
1
+ import logging
1
2
  import re
2
3
  from typing import TYPE_CHECKING, Pattern
3
- import logging
4
+ from typing_extensions import assert_never
5
+
4
6
  from ssc_codegen.ast_ import (
5
7
  BaseAstNode,
6
8
  ExprIsCss,
@@ -10,6 +12,12 @@ from ssc_codegen.ast_ import (
10
12
  ExprXpath,
11
13
  ExprCssAll,
12
14
  )
15
+ from ssc_codegen.pseudo_selectors import (
16
+ parse_pseudo_xpath_query,
17
+ pseudo_action_to_pseudo_xpath,
18
+ parse_pseudo_css_query,
19
+ pseudo_action_to_pseudo_css,
20
+ )
13
21
  from ssc_codegen.selector_utils import css_to_xpath, xpath_to_css
14
22
  from ssc_codegen.static_checker.base import AnalyzeResult
15
23
 
@@ -18,7 +26,6 @@ LOGGER = logging.getLogger("ssc_gen")
18
26
  if TYPE_CHECKING:
19
27
  from .document import BaseDocument
20
28
 
21
-
22
29
  # https://stackoverflow.com/a/14919203
23
30
  CM1_RX = r"(?m)(?<!\\)((\\{2})*)#.*$"
24
31
  CM2_RX = r"(\\)?((\\{2})*)(#)"
@@ -88,35 +95,34 @@ def convert_css_to_xpath(
88
95
  new_stack: list[BaseAstNode] = []
89
96
 
90
97
  for expr in old_stack:
98
+ if expr.kind not in (ExprCss.kind, ExprCssAll.kind, ExprIsCss.kind):
99
+ new_stack.append(expr)
100
+ continue
101
+ query = expr.kwargs["query"]
102
+ query, pseudo_action = parse_pseudo_css_query(query)
103
+ new_query = css_to_xpath(query, prefix=prefix)
104
+ if pseudo_action[0]:
105
+ new_query += pseudo_action_to_pseudo_css(*pseudo_action)
91
106
  match expr.kind:
92
107
  case ExprCss.kind:
93
- new_expr = ExprXpath( # type: ignore[assignment]
94
- kwargs={
95
- "query": css_to_xpath(
96
- expr.kwargs["query"], prefix=prefix
97
- )
98
- }
99
- )
108
+ new_stack.append(ExprXpath(kwargs={"query": new_query}))
100
109
  case ExprCssAll.kind:
101
- new_expr = ExprXpathAll( # type: ignore[assignment]
102
- kwargs={
103
- "query": css_to_xpath(
104
- expr.kwargs["query"], prefix=prefix
105
- )
106
- }
110
+ new_stack.append(
111
+ ExprXpathAll( # type: ignore[assignment]
112
+ kwargs={"query": new_query}
113
+ )
107
114
  )
108
115
  case ExprIsCss.kind:
109
- new_expr = ExprIsXpath( # type: ignore[assignment]
110
- kwargs={
111
- "query": css_to_xpath(
112
- expr.kwargs["query"], prefix=prefix
113
- ),
114
- "msg": expr.kwargs["msg"],
115
- }
116
+ new_stack.append(
117
+ ExprIsXpath( # type: ignore[assignment]
118
+ kwargs={
119
+ "query": new_query,
120
+ "msg": expr.kwargs["msg"],
121
+ }
122
+ )
116
123
  )
117
124
  case _:
118
- new_expr = expr # type: ignore[assignment]
119
- new_stack.append(new_expr)
125
+ assert_never(expr.kind)
120
126
  doc._stack = new_stack
121
127
  return doc
122
128
 
@@ -126,24 +132,33 @@ def convert_xpath_to_css(doc: "BaseDocument") -> "BaseDocument":
126
132
  old_stack = doc.stack.copy()
127
133
  new_stack: list[BaseAstNode] = []
128
134
  for expr in old_stack:
135
+ if expr.kind not in (
136
+ ExprXpath.kind,
137
+ ExprXpathAll.kind,
138
+ ExprIsXpath.kind,
139
+ ):
140
+ new_stack.append(expr)
141
+ continue
142
+ query = expr.kwargs["query"]
143
+ query, pseudo_action = parse_pseudo_xpath_query(query)
144
+ new_query = xpath_to_css(query)
145
+ if pseudo_action[0]:
146
+ new_query += pseudo_action_to_pseudo_xpath(*pseudo_action)
129
147
  match expr.kind:
130
148
  case ExprXpath.kind:
131
- new_expr = ExprCss(
132
- kwargs={"query": xpath_to_css(expr.kwargs["query"])}
133
- )
149
+ new_stack.append(ExprCss(kwargs={"query": new_query}))
134
150
  case ExprXpathAll.kind:
135
- new_expr = ExprCssAll( # type: ignore[assignment]
136
- kwargs={"query": xpath_to_css(expr.kwargs["query"])}
137
- )
151
+ new_stack.append(ExprCssAll(kwargs={"query": new_query}))
138
152
  case ExprIsXpath.kind:
139
- new_expr = ExprIsCss( # type: ignore[assignment]
140
- kwargs={
141
- "query": xpath_to_css(expr.kwargs["query"]),
142
- "msg": expr.kwargs["msg"],
143
- }
153
+ new_stack.append(
154
+ ExprIsCss(
155
+ kwargs={
156
+ "query": new_query,
157
+ "msg": expr.kwargs["msg"],
158
+ }
159
+ )
144
160
  )
145
161
  case _:
146
- new_expr = expr # type: ignore[assignment]
147
- new_stack.append(new_expr)
162
+ assert_never(expr.kind)
148
163
  doc._stack = new_stack
149
164
  return doc
@@ -0,0 +1,64 @@
1
+ import re
2
+ from typing import Literal
3
+
4
+ PseudoAction = Literal["text", "raw", "attr"]
5
+
6
+
7
+ def parse_pseudo_css_query(
8
+ query: str,
9
+ ) -> tuple[str, tuple[PseudoAction | None, str | None]]:
10
+ """parse css selector and extract pseudo class action"""
11
+ query = query.strip()
12
+
13
+ if query.endswith("::text"):
14
+ return query[: -len("::text")], ("text", None)
15
+
16
+ if query.endswith("::raw"):
17
+ return query[: -len("::raw")], ("raw", None)
18
+
19
+ if match := re.search(r"::attr\(([^)]+)\)$", query):
20
+ return query[: match.start()], ("attr", match.group(1))
21
+
22
+ return query, (None, None)
23
+
24
+
25
+ def parse_pseudo_xpath_query(
26
+ query: str,
27
+ ) -> tuple[str, tuple[PseudoAction | None, str | None]]:
28
+ """parse xpath selector and extract pseudo class action"""
29
+ query = query.strip()
30
+
31
+ if query.endswith("/text()"):
32
+ return query[: -len("/text()")], ("text", None)
33
+
34
+ if query.endswith("/raw()"):
35
+ return query[: -len("/raw()")], ("raw", None)
36
+
37
+ if match := re.search(r"/@([\w:-]+)$", query):
38
+ return query[: match.start()], ("attr", match.group(1))
39
+
40
+ return query, (None, None)
41
+
42
+
43
+ def pseudo_action_to_pseudo_css(action: PseudoAction, arg: str | None) -> str:
44
+ match action:
45
+ case "text":
46
+ return "::text"
47
+ case "raw":
48
+ return "::raw"
49
+ case "attr":
50
+ return f"::attr({arg})"
51
+ case _:
52
+ return ""
53
+
54
+
55
+ def pseudo_action_to_pseudo_xpath(action: PseudoAction, arg: str | None) -> str:
56
+ match action:
57
+ case "text":
58
+ return "/text()"
59
+ case "raw":
60
+ return "/raw()"
61
+ case "attr":
62
+ return f"/@{arg}"
63
+ case _:
64
+ return ""
@@ -62,7 +62,9 @@ FMT_MAPPING_METHODS = {
62
62
  TokenType.IS_CONTAINS: "is_contains",
63
63
  TokenType.IS_CSS: "is_css",
64
64
  TokenType.IS_XPATH: "is_xpath",
65
- TokenType.IS_REGEX_MATCH: "is_regex",
65
+ TokenType.IS_STRING_REGEX_MATCH: "is_regex",
66
+ TokenType.ANY_LIST_STRING_REGEX_MATCH: "any_is_re",
67
+ TokenType.ALL_LIST_STRING_REGEX_MATCH: "all_is_re",
66
68
  TokenType.TO_INT: "to_int",
67
69
  TokenType.TO_INT_LIST: "to_int",
68
70
  TokenType.TO_FLOAT: "to_float",
@@ -329,7 +329,7 @@ def analyze_regex_expr(
329
329
  case (
330
330
  TokenType.EXPR_REGEX_SUB
331
331
  | TokenType.EXPR_LIST_REGEX_SUB
332
- | TokenType.IS_REGEX_MATCH
332
+ | TokenType.IS_STRING_REGEX_MATCH
333
333
  ):
334
334
  result = analyze_re_expression(
335
335
  re_expr.kwargs["pattern"], allow_empty_groups=True
@@ -153,7 +153,9 @@ class TokenType(IntEnum):
153
153
  IS_CONTAINS = auto()
154
154
  IS_CSS = auto()
155
155
  IS_XPATH = auto()
156
- IS_REGEX_MATCH = auto()
156
+ IS_STRING_REGEX_MATCH = auto()
157
+ ANY_LIST_STRING_REGEX_MATCH = auto()
158
+ ALL_LIST_STRING_REGEX_MATCH = auto()
157
159
 
158
160
  # NUMERIC
159
161
  TO_INT = auto()
@@ -186,7 +188,7 @@ class TokenType(IntEnum):
186
188
  TokenType.IS_EQUAL,
187
189
  TokenType.IS_NOT_EQUAL,
188
190
  TokenType.IS_CONTAINS,
189
- TokenType.IS_REGEX_MATCH,
191
+ TokenType.IS_STRING_REGEX_MATCH,
190
192
  )
191
193
 
192
194
  @classmethod
@@ -196,5 +198,5 @@ class TokenType(IntEnum):
196
198
  TokenType.EXPR_REGEX_ALL,
197
199
  TokenType.EXPR_REGEX_SUB,
198
200
  TokenType.EXPR_LIST_REGEX_SUB,
199
- TokenType.IS_REGEX_MATCH,
201
+ TokenType.IS_STRING_REGEX_MATCH,
200
202
  )
File without changes
File without changes
File without changes