ssc_codegen 0.8.15__tar.gz → 0.8.17__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 (53) hide show
  1. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/PKG-INFO +1 -1
  2. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/pyproject.toml +1 -1
  3. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/__init__.py +7 -2
  4. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/__init__.py +2 -1
  5. ssc_codegen-0.8.17/ssc_codegen/ast_/nodes_filter.py +110 -0
  6. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_build/main.py +19 -1
  7. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/base.py +31 -6
  8. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/go_goquery.py +1 -0
  9. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/helpers.py +9 -0
  10. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/js_pure.py +138 -18
  11. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/py_base.py +121 -0
  12. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/document.py +99 -0
  13. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/str_utils.py +4 -1
  14. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/tokens.py +15 -0
  15. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/.gitignore +0 -0
  16. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/LICENSE +0 -0
  17. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/README.md +0 -0
  18. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/_compat.py +0 -0
  19. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/base.py +0 -0
  20. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/nodes_array.py +0 -0
  21. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/nodes_cast.py +0 -0
  22. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/nodes_core.py +0 -0
  23. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  24. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/nodes_string.py +0 -0
  25. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_/nodes_validate.py +0 -0
  26. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_build/__init__.py +0 -0
  27. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/ast_build/utils.py +0 -0
  28. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/__init__.py +0 -0
  29. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/cli_callbacks.py +0 -0
  30. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/cli_utils.py +0 -0
  31. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/code_callbacks.py +0 -0
  32. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/consts.py +0 -0
  33. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/main.py +0 -0
  34. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  35. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/compiler.py +0 -0
  36. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/consts.py +0 -0
  37. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/__init__.py +0 -0
  38. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/py_bs4.py +0 -0
  39. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/py_parsel.py +0 -0
  40. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/py_selectolax.py +0 -0
  41. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/templates/__init__.py +0 -0
  42. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/templates/go_goquery.py +0 -0
  43. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/converters/templates/js_pure.py +0 -0
  44. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/document_utlis.py +0 -0
  45. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/json_struct.py +0 -0
  46. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/json_to_scc.py +0 -0
  47. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/logs.py +0 -0
  48. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/pseudo_selectors.py +0 -0
  49. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/schema.py +0 -0
  50. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/selector_utils.py +0 -0
  51. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/static_checker/__init__.py +0 -0
  52. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/static_checker/base.py +0 -0
  53. {ssc_codegen-0.8.15 → ssc_codegen-0.8.17}/ssc_codegen/static_checker/callbacks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssc_codegen
3
- Version: 0.8.15
3
+ Version: 0.8.17
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.15"
3
+ version = "0.8.17"
4
4
  description = "Python-dsl code converter to html parser for web scraping "
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,13 +1,13 @@
1
1
  from ssc_codegen.document import HTMLDocument, StringDocument, ArrayDocument, AssertDocument, NestedDocument, \
2
2
  DefaultDocument, \
3
- NumericDocument, JsonDocument, BooleanDocument
3
+ NumericDocument, JsonDocument, BooleanDocument, DocumentFilter
4
4
  from ssc_codegen.json_struct import Json
5
5
  from ssc_codegen.logs import setup_logger
6
6
  from ssc_codegen.schema import ItemSchema, DictSchema, ListSchema, FlatListSchema
7
7
 
8
8
  setup_logger()
9
9
 
10
- VERSION = "0.8.15"
10
+ VERSION = "0.8.17"
11
11
 
12
12
 
13
13
  class __MISSING(object):
@@ -59,3 +59,8 @@ def R(default: None | str | int | float | list | __MISSING = _NO_DEFAULT) -> Doc
59
59
  if default == _NO_DEFAULT:
60
60
  return Document().raw()
61
61
  return Document().default(default).raw() # type: ignore
62
+
63
+
64
+ def F() -> DocumentFilter:
65
+ """Shortcut as a DocumentFilter() object"""
66
+ return DocumentFilter()
@@ -20,9 +20,10 @@ from .nodes_core import (ExprReturn,
20
20
  TypeDef,
21
21
  TypeDefField,
22
22
  ExprCallStructMethod)
23
+ from .nodes_filter import Filter, FilterOr, FilterAnd, FilterNot, FilterNotEqual, FilterEqual, FilterStrIn, \
24
+ FilterStrStarts, FilterStrEnds, FilterStrRe, ExprFilter
23
25
  from .nodes_selectors import ExprCss, ExprCssAll, ExprXpathAll, ExprXpath, ExprGetHtmlText, ExprGetHtmlRaw, \
24
26
  ExprGetHtmlAttr, ExprGetHtmlAttrAll, ExprGetHtmlRawAll, ExprGetHtmlTextAll
25
-
26
27
  from .nodes_string import ExprStringTrim, ExprStringRegex, ExprStringSplit, ExprStringReplace, ExprStringLeftTrim, \
27
28
  ExprStringFormat, ExprListStringTrim, ExprStringRegexAll, ExprStringRegexSub, ExprStringRightTrim, \
28
29
  ExprListStringFormat, ExprListStringReplace, ExprListStringRightTrim, ExprListStringLeftTrim, \
@@ -0,0 +1,110 @@
1
+ from dataclasses import dataclass
2
+ from typing import TypedDict, ClassVar
3
+
4
+ from ssc_codegen.ast_.base import BaseAstNode, T_EMPTY_KWARGS
5
+ from ssc_codegen.tokens import TokenType, VariableType
6
+
7
+
8
+ # TODO: static analyzer for filter operations
9
+ # TODO: implement operators for DOCUMENT, INT, FLOAT
10
+ @dataclass(kw_only=True)
11
+ class Filter(BaseAstNode):
12
+ kind: ClassVar[TokenType] = TokenType.FILTER
13
+ accept_type: VariableType = VariableType.STRING
14
+ ret_type: VariableType = VariableType.STRING
15
+
16
+
17
+ @dataclass(kw_only=True)
18
+ class FilterOr(BaseAstNode[T_EMPTY_KWARGS, tuple]):
19
+ kind: ClassVar[TokenType] = TokenType.FILTER_OR
20
+ accept_type: VariableType = VariableType.STRING
21
+ ret_type: VariableType = VariableType.STRING
22
+ # TODO: typing body accept Filter-like nodes
23
+
24
+
25
+ @dataclass(kw_only=True)
26
+ class FilterAnd(BaseAstNode[T_EMPTY_KWARGS, tuple]):
27
+ kind: ClassVar[TokenType] = TokenType.FILTER_AND
28
+ accept_type: VariableType = VariableType.STRING
29
+ ret_type: VariableType = VariableType.STRING
30
+ # TODO: typing body accept Filter-like nodes
31
+
32
+
33
+ @dataclass(kw_only=True)
34
+ class FilterNot(BaseAstNode[T_EMPTY_KWARGS, tuple]):
35
+ kind: ClassVar[TokenType] = TokenType.FILTER_NOT
36
+ accept_type: VariableType = VariableType.STRING
37
+ ret_type: VariableType = VariableType.STRING
38
+ # TODO: typing body accept Filter-like nodes
39
+
40
+
41
+ KW_STR_IN = TypedDict("KW_STR_IN", {"substr": str})
42
+ ARGS_STR_IN = tuple[str]
43
+
44
+ KW_STR_STARTS_OR_ENDS = TypedDict(
45
+ "KW_STR_STARTS_OR_ENDS", {"substr": tuple[str, ...]}
46
+ )
47
+ ARGS_STR_STARTS_OR_ENDS = tuple[tuple[str, ...]]
48
+
49
+
50
+ @dataclass(kw_only=True)
51
+ class FilterStrIn(BaseAstNode[KW_STR_IN, ARGS_STR_IN]):
52
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_IN
53
+ accept_type: VariableType = VariableType.STRING
54
+ ret_type: VariableType = VariableType.STRING
55
+
56
+
57
+ @dataclass(kw_only=True)
58
+ class FilterStrStarts(
59
+ BaseAstNode[KW_STR_STARTS_OR_ENDS, ARGS_STR_STARTS_OR_ENDS]
60
+ ):
61
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_STARTS
62
+ accept_type: VariableType = VariableType.STRING
63
+ ret_type: VariableType = VariableType.STRING
64
+
65
+
66
+ @dataclass(kw_only=True)
67
+ class FilterStrEnds(
68
+ BaseAstNode[KW_STR_STARTS_OR_ENDS, ARGS_STR_STARTS_OR_ENDS]
69
+ ):
70
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_ENDS
71
+ accept_type: VariableType = VariableType.STRING
72
+ ret_type: VariableType = VariableType.STRING
73
+
74
+
75
+ KW_STR_RE = TypedDict("KW_STR_RE", {"pattern": str, "ignore_case": bool})
76
+ ARGS_STR_RE = tuple[str, bool]
77
+
78
+
79
+ @dataclass(kw_only=True)
80
+ class FilterStrRe(BaseAstNode[KW_STR_RE, ARGS_STR_RE]):
81
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_RE
82
+ accept_type: VariableType = VariableType.STRING
83
+ ret_type: VariableType = VariableType.STRING
84
+
85
+
86
+ # TODO: provide API for int, float, etc
87
+ KW_STR_EQ_OR_NE = TypedDict("KW_STR_EQ_OR_NE", {"value": str})
88
+ ARGS_STR_EQ_OR_NE = tuple[str]
89
+
90
+
91
+ @dataclass(kw_only=True)
92
+ class FilterEqual(BaseAstNode[KW_STR_EQ_OR_NE, ARGS_STR_EQ_OR_NE]):
93
+ kind: ClassVar[TokenType] = TokenType.FILTER_EQ
94
+ accept_type: VariableType = VariableType.STRING
95
+ ret_type: VariableType = VariableType.STRING
96
+
97
+
98
+ @dataclass(kw_only=True)
99
+ class FilterNotEqual(BaseAstNode[KW_STR_EQ_OR_NE, ARGS_STR_EQ_OR_NE]):
100
+ kind: ClassVar[TokenType] = TokenType.FILTER_EQ
101
+ accept_type: VariableType = VariableType.STRING
102
+ ret_type: VariableType = VariableType.STRING
103
+
104
+
105
+ @dataclass(kw_only=True)
106
+ class ExprFilter(BaseAstNode[T_EMPTY_KWARGS, tuple]):
107
+ kind: ClassVar[TokenType] = TokenType.EXPR_FILTER
108
+ accept_type: VariableType = VariableType.LIST_STRING
109
+ ret_type: VariableType = VariableType.LIST_STRING
110
+ # TODO: typing body accept Filter-like nodes
@@ -1,6 +1,6 @@
1
1
  import warnings
2
2
  from pathlib import Path
3
- from typing import Type, cast
3
+ from typing import Type, cast, MutableSequence
4
4
 
5
5
  from ssc_codegen import Json
6
6
  from ssc_codegen.ast_ import (
@@ -230,6 +230,15 @@ def build_ast_struct_parser(
230
230
  return st
231
231
 
232
232
 
233
+ def set_parent_for_expr_filter(
234
+ parent: BaseAstNode, parent_body: MutableSequence[BaseAstNode]
235
+ ) -> None:
236
+ for j in parent_body:
237
+ j.parent = parent
238
+ if j.body:
239
+ set_parent_for_expr_filter(j, j.body)
240
+
241
+
233
242
  def _fetch_field_nodes(
234
243
  body: list[BaseAstNode],
235
244
  body_parse_method_expr: list[BaseAstNode],
@@ -263,8 +272,12 @@ def _fetch_field_nodes(
263
272
  )
264
273
 
265
274
  _unwrap_default_node(document, ret_type)
275
+
266
276
  for i in document.stack:
267
277
  i.parent = method
278
+ if i.kind == TokenType.EXPR_FILTER:
279
+ set_parent_for_expr_filter(i, i.body)
280
+
268
281
  method.body = document.stack
269
282
 
270
283
  body.append(method)
@@ -361,6 +374,9 @@ def _try_fetch_split_doc_node(
361
374
  )
362
375
  for i in split_doc.stack:
363
376
  i.parent = method
377
+ if i.kind == TokenType.EXPR_FILTER:
378
+ set_parent_for_expr_filter(i, i.body)
379
+
364
380
  method.body = split_doc.stack
365
381
  body.append(method)
366
382
 
@@ -388,6 +404,8 @@ def _try_fetch_pre_validate_node(
388
404
  validate_field.stack.append(ExprNoReturn())
389
405
  for i in validate_field.stack:
390
406
  i.parent = method
407
+ if i.kind == TokenType.EXPR_FILTER:
408
+ set_parent_for_expr_filter(i, i.body)
391
409
 
392
410
  body.append(method)
393
411
  call_methods_expr.append(
@@ -14,6 +14,9 @@ from ssc_codegen.ast_ import (
14
14
  StructInitMethod,
15
15
  StructPartDocMethod,
16
16
  StructPreValidateMethod,
17
+ FilterOr,
18
+ FilterAnd,
19
+ FilterNot,
17
20
  )
18
21
  from ssc_codegen.tokens import TokenType
19
22
 
@@ -168,12 +171,22 @@ class BaseCodeConverter:
168
171
  case TokenType.JSON_FIELD:
169
172
  ast_entry = cast(JsonStructField, ast_entry)
170
173
  self._convert_json_struct_field(ast_entry, acc)
171
- case _ if ast_entry.kind in {
172
- TokenType.STRUCT_PART_DOCUMENT,
173
- TokenType.STRUCT_PRE_VALIDATE,
174
- TokenType.STRUCT_PARSE_START,
175
- TokenType.STRUCT_FIELD,
176
- }:
174
+
175
+ case (
176
+ TokenType.EXPR_FILTER
177
+ | TokenType.FILTER_OR
178
+ | TokenType.FILTER_AND
179
+ | TokenType.FILTER_NOT
180
+ ):
181
+ ast_entry = cast(FilterOr | FilterAnd | FilterNot, ast_entry)
182
+ self._convert_logic_filter(ast_entry, acc)
183
+
184
+ case (
185
+ TokenType.STRUCT_PART_DOCUMENT
186
+ | TokenType.STRUCT_PRE_VALIDATE
187
+ | TokenType.STRUCT_PARSE_START
188
+ | TokenType.STRUCT_FIELD
189
+ ):
177
190
  ast_entry = cast(
178
191
  StructFieldMethod
179
192
  | StructInitMethod
@@ -186,6 +199,18 @@ class BaseCodeConverter:
186
199
  self._convert_default(ast_entry, acc)
187
200
  return acc
188
201
 
202
+ def _convert_logic_filter(
203
+ self, ast_entry: FilterOr | FilterAnd | FilterNot, acc: list[str]
204
+ ) -> None:
205
+ # AND(body=[F1, F2]) ->
206
+ # AND ( (OPEN)
207
+ # F1 and F2
208
+ # ) (CLOSE)
209
+ acc.append(self._pre_convert_node(ast_entry))
210
+ for node in ast_entry.body:
211
+ self.convert(node, acc)
212
+ acc.append(self._post_convert_node(ast_entry))
213
+
189
214
  def _convert_module(self, ast_entry: ModuleProgram, acc: list[str]) -> None:
190
215
  """Handle module conversion."""
191
216
  for node in ast_entry.body:
@@ -1262,3 +1262,4 @@ def pre_list_str_rm_prefix_and_suffix(
1262
1262
 
1263
1263
 
1264
1264
  # TODO impl ExprHasAttr, ExprListHasAttr
1265
+ # TODO: impl Filter Expr
@@ -47,3 +47,12 @@ def get_struct_field_method_by_name(
47
47
  ][0]
48
48
  result = cast(StructFieldMethod, result)
49
49
  return result
50
+
51
+
52
+ def is_prev_node_atomic_cond(node: BaseAstNode) -> bool:
53
+ """return true if node is atomic condition (exclude body), NOT FIRST"""
54
+ return node.index != 0 and not node.parent.body[node.index_prev].body
55
+
56
+
57
+ def is_first_node_cond(node: BaseAstNode) -> bool:
58
+ return node.index == 0 and node.parent.kind == TokenType.EXPR_FILTER
@@ -85,12 +85,24 @@ from ssc_codegen.ast_ import (
85
85
  ExprListStringAllRegex,
86
86
  ExprListHasAttr,
87
87
  ExprHasAttr,
88
+ FilterEqual,
89
+ FilterNotEqual,
90
+ FilterStrRe,
91
+ FilterStrEnds,
92
+ FilterStrStarts,
93
+ FilterStrIn,
94
+ FilterNot,
95
+ FilterAnd,
96
+ FilterOr,
97
+ ExprFilter,
88
98
  )
89
99
  from ssc_codegen.converters.base import BaseCodeConverter
90
100
  from ssc_codegen.converters.helpers import (
91
101
  prev_next_var,
92
102
  is_last_var_no_ret,
93
103
  have_pre_validate_call,
104
+ is_first_node_cond,
105
+ is_prev_node_atomic_cond,
94
106
  )
95
107
  from ssc_codegen.converters.templates.js_pure import (
96
108
  J2_STRUCT_INIT,
@@ -129,6 +141,21 @@ DOCSTR_SEP = "* "
129
141
  CONVERTER = BaseCodeConverter(debug_comment_prefix="// ")
130
142
 
131
143
 
144
+ # TODO: move to string_utils
145
+ def to_js_regexp(
146
+ pattern: str, ignore_case: bool = False, is_global: bool = True
147
+ ) -> str:
148
+ """helper function for convert string pattern to js"""
149
+ pattern = pattern.replace("<\\/", "</")
150
+ pattern = pattern.replace("/", "\\/")
151
+ pattern = f"/{pattern}/"
152
+ if is_global:
153
+ pattern += "g"
154
+ if ignore_case:
155
+ pattern += "i"
156
+ return pattern
157
+
158
+
132
159
  def make_js_docstring(value: str) -> str:
133
160
  if not value:
134
161
  return ""
@@ -357,30 +384,23 @@ def pre_list_str_replace(node: ExprListStringReplace) -> str:
357
384
  def pre_str_regex(node: ExprStringRegex) -> str:
358
385
  prv, nxt = prev_next_var(node)
359
386
  pattern, group, ignore_case = node.unpack_args()
360
- pattern = pattern.replace("/", "\\/")
361
- pattern = f"/{pattern}/g"
362
- if ignore_case:
363
- pattern += "i"
364
- return f"let {nxt} = (new RegExp({pattern})).exec({prv})[{group}];"
387
+ pattern = to_js_regexp(pattern, ignore_case, is_global=False)
388
+ return f"let {nxt} = {prv}.match({pattern})[{group}];"
365
389
 
366
390
 
367
391
  @CONVERTER(ExprStringRegexAll.kind)
368
392
  def pre_str_regex_all(node: ExprStringRegexAll) -> str:
369
393
  prv, nxt = prev_next_var(node)
370
394
  pattern, ignore_case = node.unpack_args()
371
- pattern = pattern.replace("/", "\\/")
372
- pattern = f"/{pattern}/g"
373
- if ignore_case:
374
- pattern += "i"
375
- return f"let {nxt} = (new RegExp({pattern})).exec({prv});"
395
+ pattern = to_js_regexp(pattern, ignore_case)
396
+ return f"let {nxt} = Array.from({prv}.match({pattern}));"
376
397
 
377
398
 
378
399
  @CONVERTER(ExprStringRegexSub.kind)
379
400
  def pre_str_regex_sub(node: ExprStringRegexSub) -> str:
380
401
  prv, nxt = prev_next_var(node)
381
402
  pattern, repl = node.unpack_args()
382
- pattern = pattern.replace("/", "\\/")
383
- pattern = f"/{pattern}/g"
403
+ pattern = to_js_regexp(pattern)
384
404
 
385
405
  return f"let {nxt} = {prv}.replace({pattern}, {repl!r});"
386
406
 
@@ -389,8 +409,8 @@ def pre_str_regex_sub(node: ExprStringRegexSub) -> str:
389
409
  def pre_list_str_regex_sub(node: ExprListStringRegexSub) -> str:
390
410
  prv, nxt = prev_next_var(node)
391
411
  pattern, repl = node.unpack_args()
392
- pattern = pattern.replace("/", "\\/")
393
- pattern = f"/{pattern}/g"
412
+ pattern = to_js_regexp(pattern)
413
+
394
414
  return f"let {nxt} = {prv}.map(e => e.replace({pattern}, {repl!r}));"
395
415
 
396
416
 
@@ -456,10 +476,7 @@ def pre_is_contains(node: ExprIsContains) -> str:
456
476
  def pre_is_regex(node: ExprStringIsRegex) -> str:
457
477
  prv, nxt = prev_next_var(node)
458
478
  pattern, ignore_case, msg = node.unpack_args()
459
- pattern = pattern.replace("/", "\\/")
460
- pattern = f"/{pattern}/g"
461
- if ignore_case:
462
- pattern += "i"
479
+ pattern = to_js_regexp(pattern, ignore_case)
463
480
 
464
481
  expr = f"if ({prv}.match({pattern}) === null) throw new Error({msg!r});"
465
482
  if is_last_var_no_ret(node):
@@ -712,3 +729,106 @@ def pre_list_has_attr(node: ExprListHasAttr) -> str:
712
729
  if is_last_var_no_ret(node):
713
730
  return expr
714
731
  return expr + f"let {nxt} = {prv};"
732
+
733
+
734
+ @CONVERTER(ExprFilter.kind)
735
+ def pre_expr_filter(node: ExprFilter) -> str:
736
+ prv, nxt = prev_next_var(node)
737
+ return f"let {nxt} = {prv}.filter(i => "
738
+
739
+
740
+ @CONVERTER.post(ExprFilter.kind)
741
+ def post_expr_filter(_node: ExprFilter) -> str:
742
+ return ");"
743
+
744
+
745
+ @CONVERTER(FilterOr.kind)
746
+ def pre_filter_or(_node: FilterOr) -> str:
747
+ return " || ("
748
+
749
+
750
+ @CONVERTER.post(FilterOr.kind)
751
+ def post_filter_or(_node: FilterOr) -> str:
752
+ return ")"
753
+
754
+
755
+ @CONVERTER(FilterAnd.kind)
756
+ def pre_filter_and(_node: FilterAnd) -> str:
757
+ return " && ("
758
+
759
+
760
+ @CONVERTER.post(FilterAnd.kind)
761
+ def post_filter_and(_node: FilterAnd) -> str:
762
+ return ")"
763
+
764
+
765
+ @CONVERTER(FilterNot.kind)
766
+ def pre_filter_not(_node: FilterNot) -> str: # type: ignore
767
+ return "!("
768
+
769
+
770
+ @CONVERTER.post(FilterNot.kind)
771
+ def post_filter_not(_node: FilterNot) -> str:
772
+ return ")"
773
+
774
+
775
+ @CONVERTER(FilterStrIn.kind)
776
+ def pre_filter_in(node: FilterStrIn) -> str:
777
+ substr, *_ = node.unpack_args()
778
+ expr = f"i.includes({substr!r})"
779
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
780
+ return f" && {expr}"
781
+ return expr
782
+
783
+
784
+ @CONVERTER(FilterStrStarts.kind)
785
+ def pre_filter_starts_with(node: FilterStrStarts) -> str:
786
+ start_, *_ = node.unpack_args()
787
+ if len(start_) == 1:
788
+ expr = f"i.startsWith({start_[0]!r})"
789
+ else:
790
+ expr = "(" + " || ".join(f"i.startsWith({s!r})" for s in start_) + ")"
791
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
792
+ return f" && {expr}"
793
+ return expr
794
+
795
+
796
+ @CONVERTER(FilterStrEnds.kind)
797
+ def pre_filter_ends_with(node: FilterStrEnds) -> str:
798
+ suffix_, *_ = node.unpack_args()
799
+ if len(suffix_) == 1:
800
+ expr = f"i.endsWith({suffix_[0]!r})"
801
+ else:
802
+ expr = "(" + " || ".join(f"i.endsWith({s!r})" for s in suffix_) + ")"
803
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
804
+ return f" && {expr}"
805
+ return expr
806
+
807
+
808
+ @CONVERTER(FilterStrRe.kind)
809
+ def pre_filter_re(node: FilterStrRe) -> str:
810
+ pattern, ignore_case, *_ = node.unpack_args()
811
+ pattern = to_js_regexp(pattern, ignore_case, is_global=False)
812
+
813
+ expr = f"(new RegExp({pattern})).test(i)"
814
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
815
+ return f" && {expr}"
816
+ return expr
817
+
818
+
819
+ @CONVERTER(FilterEqual.kind)
820
+ def pre_filter_eq(node: FilterEqual) -> str:
821
+ value, *_ = node.unpack_args()
822
+ expr = f"i === {value!r}"
823
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
824
+ return f" && {expr}"
825
+ return expr
826
+
827
+
828
+ @CONVERTER(FilterNotEqual.kind)
829
+ def pre_filter_ne(node: FilterNotEqual) -> str:
830
+ value, *_ = node.unpack_args()
831
+ expr = f"i !== {value!r}"
832
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
833
+ return f" && {expr}"
834
+ return expr
@@ -87,6 +87,16 @@ from ssc_codegen.ast_ import (
87
87
  ExprListStringRmPrefixAndSuffix,
88
88
  ExprListStringAnyRegex,
89
89
  ExprListStringAllRegex,
90
+ FilterOr,
91
+ FilterAnd,
92
+ FilterNot,
93
+ ExprFilter,
94
+ FilterStrIn,
95
+ FilterStrStarts,
96
+ FilterStrEnds,
97
+ FilterStrRe,
98
+ FilterEqual,
99
+ FilterNotEqual,
90
100
  )
91
101
  from ssc_codegen.converters.base import (
92
102
  BaseCodeConverter,
@@ -98,6 +108,8 @@ from ssc_codegen.converters.helpers import (
98
108
  have_pre_validate_call,
99
109
  prev_next_var,
100
110
  is_last_var_no_ret,
111
+ is_prev_node_atomic_cond,
112
+ is_first_node_cond,
101
113
  )
102
114
  from ssc_codegen.tokens import StructType, VariableType, JsonVariableType
103
115
 
@@ -181,6 +193,8 @@ class BasePyCodeConverter(BaseCodeConverter):
181
193
  )
182
194
  from ssc_codegen.ast_ import ExprListStringRmPrefix
183
195
 
196
+ from ssc_codegen.ast_ import FilterNotEqual
197
+
184
198
  self.pre_definitions = {
185
199
  Docstring.kind: pre_docstring,
186
200
  ModuleImports.kind: pre_imports,
@@ -234,12 +248,27 @@ class BasePyCodeConverter(BaseCodeConverter):
234
248
  ExprToListLength.kind: pre_to_len,
235
249
  ExprToBool.kind: pre_to_bool,
236
250
  ExprJsonify.kind: pre_jsonify,
251
+ # FILTER,
252
+ ExprFilter.kind: pre_expr_filter,
253
+ FilterAnd.kind: pre_filter_and,
254
+ FilterOr.kind: pre_filter_or,
255
+ FilterNot.kind: pre_filter_not,
256
+ FilterStrStarts.kind: pre_filter_starts_with,
257
+ FilterStrEnds.kind: pre_filter_ends_with,
258
+ FilterStrIn.kind: pre_filter_in,
259
+ FilterStrRe.kind: pre_filter_re,
260
+ FilterEqual.kind: pre_filter_eq,
261
+ FilterNotEqual.kind: pre_filter_ne,
237
262
  }
238
263
 
239
264
  self.post_definitions = {
240
265
  TypeDef.kind: post_typedef,
241
266
  JsonStruct.kind: post_json_struct,
242
267
  StartParseMethod.kind: post_start_parse,
268
+ ExprFilter.kind: post_expr_filter,
269
+ FilterAnd.kind: post_filter_and,
270
+ FilterOr.kind: post_filter_or,
271
+ FilterNot.kind: post_filter_not,
243
272
  }
244
273
 
245
274
 
@@ -876,3 +905,95 @@ def pre_jsonify(node: ExprJsonify) -> str:
876
905
  prv, nxt = prev_next_var(node)
877
906
  _name, _is_array = node.unpack_args()
878
907
  return indent + f"{nxt} = json.loads({prv})"
908
+
909
+
910
+ # FILTERS
911
+
912
+
913
+ def pre_expr_filter(node: ExprFilter) -> str:
914
+ indent = (
915
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
916
+ )
917
+ prv, nxt = prev_next_var(node)
918
+ return indent + f"{nxt} = [i for i in {prv} if "
919
+
920
+
921
+ def post_expr_filter(_node: ExprFilter) -> str:
922
+ return "]"
923
+
924
+
925
+ def pre_filter_or(_node: FilterOr) -> str:
926
+ return " or ("
927
+
928
+
929
+ def post_filter_or(_node: FilterOr) -> str:
930
+ return ")"
931
+
932
+
933
+ def pre_filter_and(_node: FilterAnd) -> str:
934
+ return " and ("
935
+
936
+
937
+ def post_filter_and(_node: FilterAnd) -> str:
938
+ return ")"
939
+
940
+
941
+ def pre_filter_not(_node: FilterNot) -> str:
942
+ return "not ("
943
+
944
+
945
+ def post_filter_not(_node: FilterNot) -> str:
946
+ return ")"
947
+
948
+
949
+ def pre_filter_in(node: FilterStrIn) -> str:
950
+ substr, *_ = node.unpack_args()
951
+ expr = f"{substr!r} in i"
952
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
953
+ return f" and {expr}"
954
+ return expr
955
+
956
+
957
+ def pre_filter_starts_with(node: FilterStrStarts) -> str:
958
+ start_, *_ = node.unpack_args()
959
+ expr = f"i.startswith({start_})"
960
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
961
+ return f" and {expr}"
962
+ return expr
963
+
964
+
965
+ def pre_filter_ends_with(node: FilterStrEnds) -> str:
966
+ suffix_, *_ = node.unpack_args()
967
+ expr = f"i.endswith({suffix_})"
968
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
969
+ return f" and {expr}"
970
+ return expr
971
+
972
+
973
+ def pre_filter_re(node: FilterStrRe) -> str:
974
+ pattern, ignore_case, *_ = node.unpack_args()
975
+ if ignore_case:
976
+ expr = f"re.search({pattern!r}, i, re.IGNORECASE)"
977
+ else:
978
+ expr = f"re.search({pattern!r}, i)"
979
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
980
+ return f" and {expr}"
981
+ return expr
982
+
983
+
984
+ def pre_filter_eq(node: FilterEqual) -> str:
985
+ value, *_ = node.unpack_args()
986
+ # currently support only str
987
+ expr = f"i == {value!r}"
988
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
989
+ return f" and {expr}"
990
+ return expr
991
+
992
+
993
+ def pre_filter_ne(node: FilterNotEqual) -> str:
994
+ value, *_ = node.unpack_args()
995
+ # currently support only str
996
+ expr = f"i != {value!r}"
997
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
998
+ return f" and {expr}"
999
+ return expr
@@ -59,6 +59,16 @@ from ssc_codegen.ast_ import (
59
59
  ExprListStringAnyRegex,
60
60
  ExprHasAttr,
61
61
  ExprListHasAttr,
62
+ ExprFilter,
63
+ FilterEqual,
64
+ FilterNotEqual,
65
+ FilterStrStarts,
66
+ FilterStrEnds,
67
+ FilterStrIn,
68
+ FilterStrRe,
69
+ FilterAnd,
70
+ FilterOr,
71
+ FilterNot,
62
72
  )
63
73
  from ssc_codegen.document_utlis import (
64
74
  analyze_re_expression,
@@ -457,6 +467,20 @@ class ArrayDocument(BaseDocument):
457
467
  self._add(ExprToListLength(accept_type=expected_type))
458
468
  return self
459
469
 
470
+ def filter(self, expr: "DocumentFilter") -> Self:
471
+ """filter array of resuts by expr
472
+
473
+ - accept LIST_STRING, return LIST_STRING
474
+ """
475
+ if self.stack_last_ret != VariableType.LIST_STRING:
476
+ LOGGER.warning(
477
+ "to_len(): Expected type(s) %s got %s",
478
+ (VariableType.LIST_STRING.name,),
479
+ self.stack_last_ret.name,
480
+ )
481
+ self._add(ExprFilter(body=expr.stack))
482
+ return self
483
+
460
484
 
461
485
  class StringDocument(BaseDocument):
462
486
  def rm_prefix(self, substr: str) -> Self:
@@ -1236,3 +1260,78 @@ class JsonDocument(BaseDocument):
1236
1260
  # HACK: store to class instance for generate docstring signature
1237
1261
  BaseSchema.__JSON_SCHEMAS__[struct.__name__] = struct # type: ignore
1238
1262
  return self
1263
+
1264
+
1265
+ class DocumentFilter(BaseDocument):
1266
+ def eq(self, value: str) -> Self:
1267
+ self._add(FilterEqual(kwargs={"value": value}))
1268
+ return self
1269
+
1270
+ def ne(self, value: str) -> Self:
1271
+ self._add(FilterNotEqual(kwargs={"value": value}))
1272
+ return self
1273
+
1274
+ def starts(self, *values: str) -> Self:
1275
+ self._add(FilterStrStarts(kwargs={"substr": values}))
1276
+ return self
1277
+
1278
+ def ends(self, *values: str) -> Self:
1279
+ self._add(FilterStrEnds(kwargs={"substr": values}))
1280
+ return self
1281
+
1282
+ def contains(self, value: str) -> Self:
1283
+ self._add(FilterStrIn(kwargs={"substr": value}))
1284
+ return self
1285
+
1286
+ def re(
1287
+ self, pattern: str | Pattern[str], ignore_case: bool = False
1288
+ ) -> Self:
1289
+ if not isinstance(pattern, str):
1290
+ ignore_case = is_ignore_case_regex(pattern)
1291
+
1292
+ pattern = unverbosify_regex(pattern)
1293
+ result = analyze_re_expression(pattern, allow_empty_groups=True)
1294
+ if not result:
1295
+ LOGGER.warning(result.msg)
1296
+ self._add(
1297
+ FilterStrRe(kwargs={"pattern": pattern, "ignore_case": ignore_case})
1298
+ )
1299
+ return self
1300
+
1301
+ def and_(self, filter_expr: "DocumentFilter") -> Self:
1302
+ if self.stack[0].kind in (FilterAnd.kind, FilterOr.kind):
1303
+ LOGGER.warning(
1304
+ "logic AND: first node is `%s`", self.stack[0].kind.name
1305
+ )
1306
+ tmp_stack = filter_expr.stack.copy()
1307
+ self._add(FilterAnd(body=tmp_stack))
1308
+ return self
1309
+
1310
+ def or_(self, filter_expr: "DocumentFilter") -> Self:
1311
+ if self.stack[0].kind in (FilterAnd.kind, FilterOr.kind):
1312
+ LOGGER.warning(
1313
+ "logic OR: first node is `%s`", self.stack[0].kind.name
1314
+ )
1315
+ tmp_stack = filter_expr.stack.copy()
1316
+ self._add(FilterOr(body=tmp_stack))
1317
+ return self
1318
+
1319
+ def not_(self, filter_expr: "DocumentFilter") -> Self:
1320
+ tmp_stack = filter_expr.stack.copy()
1321
+ self._add(FilterNot(body=tmp_stack))
1322
+ return self
1323
+
1324
+ def __or__(self, other: "DocumentFilter") -> "DocumentFilter":
1325
+ return self.or_(other)
1326
+
1327
+ def __and__(self, other: "DocumentFilter") -> "DocumentFilter":
1328
+ return self.and_(other)
1329
+
1330
+ def __invert__(self) -> "DocumentFilter":
1331
+ return DocumentFilter().not_(self)
1332
+
1333
+ def __eq__(self, other: str) -> Self:
1334
+ return self.eq(other)
1335
+
1336
+ def __ne__(self, other: str) -> Self:
1337
+ return self.ne(other)
@@ -103,7 +103,7 @@ RE_PY_METHOD_BLOCK = re.compile(
103
103
  r"""
104
104
  def\s_(?:parse|split)\w+\(.*\:\s* # SPLIT_DOC STRUCT_PARSE HEAD
105
105
  (?:\n|.)*? # BLOCK OF CODE
106
- return\s(?P<ret_var>\w+) # RET STMT
106
+ return\s(?P<ret_var>\w+) # RET STMT
107
107
  """,
108
108
  re.X,
109
109
  )
@@ -148,6 +148,9 @@ def py_optimize_return_naive(py_code: str) -> str:
148
148
  re_expr = f"{ret_var}\\s*=\\s*(?P<expr>.*)"
149
149
  if match := re.search(re_expr, method_block[0]):
150
150
  expr = match["expr"] # noqa
151
+ # NAIVE apologize, its .filter(...) expr, skip optimization
152
+ if expr.strip().endswith(" if"):
153
+ continue
151
154
 
152
155
  # optimization function convert double backslash to single (\\ -> \) in regex patterns
153
156
  # add `r` prefix for avoid SyntaxWarning then compile generated code to cpython bytecode
@@ -174,6 +174,21 @@ class TokenType(IntEnum):
174
174
  # bool
175
175
  TO_BOOL = auto()
176
176
 
177
+ # FILTER
178
+ EXPR_FILTER = auto()
179
+
180
+ FILTER = auto() # TODO: REMOVE?
181
+ FILTER_OR = auto()
182
+ FILTER_AND = auto()
183
+ FILTER_NOT = auto()
184
+ # FILTER EXPR
185
+ FILTER_STR_IN = auto()
186
+ FILTER_STR_STARTS = auto()
187
+ FILTER_STR_ENDS = auto()
188
+ FILTER_STR_RE = auto()
189
+ FILTER_EQ = auto()
190
+ FILTER_NE = auto()
191
+
177
192
  @classmethod
178
193
  def default_tokens(cls) -> tuple["TokenType", ...]:
179
194
  return (
File without changes
File without changes
File without changes