ssc_codegen 0.8.16__tar.gz → 0.8.18__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.16 → ssc_codegen-0.8.18}/PKG-INFO +2 -1
  2. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/pyproject.toml +2 -1
  3. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/__init__.py +7 -2
  4. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/__init__.py +4 -1
  5. ssc_codegen-0.8.18/ssc_codegen/ast_/nodes_filter.py +151 -0
  6. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_build/main.py +19 -1
  7. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/base.py +31 -6
  8. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/go_goquery.py +1 -0
  9. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/helpers.py +9 -0
  10. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/js_pure.py +206 -6
  11. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_base.py +193 -0
  12. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/document.py +210 -2
  13. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/str_utils.py +4 -1
  14. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/tokens.py +22 -0
  15. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/.gitignore +0 -0
  16. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/LICENSE +0 -0
  17. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/README.md +0 -0
  18. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/_compat.py +0 -0
  19. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/base.py +0 -0
  20. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_array.py +0 -0
  21. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_cast.py +0 -0
  22. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_core.py +0 -0
  23. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  24. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_string.py +0 -0
  25. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_/nodes_validate.py +0 -0
  26. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_build/__init__.py +0 -0
  27. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/ast_build/utils.py +0 -0
  28. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/__init__.py +0 -0
  29. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/cli_callbacks.py +0 -0
  30. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/cli_utils.py +0 -0
  31. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/code_callbacks.py +0 -0
  32. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/consts.py +0 -0
  33. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/main.py +0 -0
  34. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  35. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/compiler.py +0 -0
  36. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/consts.py +0 -0
  37. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/__init__.py +0 -0
  38. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_bs4.py +0 -0
  39. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_parsel.py +0 -0
  40. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/py_selectolax.py +0 -0
  41. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/templates/__init__.py +0 -0
  42. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/templates/go_goquery.py +0 -0
  43. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/converters/templates/js_pure.py +0 -0
  44. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/document_utlis.py +0 -0
  45. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/json_struct.py +0 -0
  46. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/json_to_scc.py +0 -0
  47. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/logs.py +0 -0
  48. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/pseudo_selectors.py +0 -0
  49. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/schema.py +0 -0
  50. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/selector_utils.py +0 -0
  51. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/static_checker/__init__.py +0 -0
  52. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/ssc_codegen/static_checker/base.py +0 -0
  53. {ssc_codegen-0.8.16 → ssc_codegen-0.8.18}/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.16
3
+ Version: 0.8.18
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
@@ -19,6 +19,7 @@ Classifier: Topic :: Software Development :: Code Generators
19
19
  Classifier: Topic :: Text Processing :: Markup :: HTML
20
20
  Classifier: Topic :: Utilities
21
21
  Requires-Python: >=3.10
22
+ Requires-Dist: bs4>=0.0.2
22
23
  Requires-Dist: colorama>=0.4.6; sys_platform == 'win32'
23
24
  Requires-Dist: cssselect>=1.2.0
24
25
  Requires-Dist: httpx>=0.28.1
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "ssc_codegen"
3
- version = "0.8.16"
3
+ version = "0.8.18"
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
  dependencies = [
8
+ "bs4>=0.0.2",
8
9
  "colorama>=0.4.6 ; sys_platform == 'win32'",
9
10
  "cssselect>=1.2.0",
10
11
  "httpx>=0.28.1",
@@ -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.16"
10
+ VERSION = "0.8.18"
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,12 @@ from .nodes_core import (ExprReturn,
20
20
  TypeDef,
21
21
  TypeDefField,
22
22
  ExprCallStructMethod)
23
+ from .nodes_filter import FilterOr, FilterAnd, FilterNot, FilterNotEqual, FilterEqual, FilterStrIn, \
24
+ FilterStrStarts, FilterStrEnds, FilterStrRe, ExprFilter, FilterStrLenEq, FilterStrLenNe, FilterStrLenLt, \
25
+ FilterStrLenLe, FilterStrLenGt, FilterStrLenGe
26
+
23
27
  from .nodes_selectors import ExprCss, ExprCssAll, ExprXpathAll, ExprXpath, ExprGetHtmlText, ExprGetHtmlRaw, \
24
28
  ExprGetHtmlAttr, ExprGetHtmlAttrAll, ExprGetHtmlRawAll, ExprGetHtmlTextAll
25
-
26
29
  from .nodes_string import ExprStringTrim, ExprStringRegex, ExprStringSplit, ExprStringReplace, ExprStringLeftTrim, \
27
30
  ExprStringFormat, ExprListStringTrim, ExprStringRegexAll, ExprStringRegexSub, ExprStringRightTrim, \
28
31
  ExprListStringFormat, ExprListStringReplace, ExprListStringRightTrim, ExprListStringLeftTrim, \
@@ -0,0 +1,151 @@
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
+
11
+
12
+ @dataclass(kw_only=True)
13
+ class FilterOr(BaseAstNode[T_EMPTY_KWARGS, tuple]):
14
+ kind: ClassVar[TokenType] = TokenType.FILTER_OR
15
+ accept_type: VariableType = VariableType.STRING
16
+ ret_type: VariableType = VariableType.STRING
17
+ # TODO: typing body accept Filter-like nodes
18
+
19
+
20
+ @dataclass(kw_only=True)
21
+ class FilterAnd(BaseAstNode[T_EMPTY_KWARGS, tuple]):
22
+ kind: ClassVar[TokenType] = TokenType.FILTER_AND
23
+ accept_type: VariableType = VariableType.STRING
24
+ ret_type: VariableType = VariableType.STRING
25
+ # TODO: typing body accept Filter-like nodes
26
+
27
+
28
+ @dataclass(kw_only=True)
29
+ class FilterNot(BaseAstNode[T_EMPTY_KWARGS, tuple]):
30
+ kind: ClassVar[TokenType] = TokenType.FILTER_NOT
31
+ accept_type: VariableType = VariableType.STRING
32
+ ret_type: VariableType = VariableType.STRING
33
+ # TODO: typing body accept Filter-like nodes
34
+
35
+
36
+ KW_STR_IN = TypedDict("KW_STR_IN", {"substr": tuple[str, ...]})
37
+ ARGS_STR_IN = tuple[tuple[str, ...]]
38
+
39
+ KW_STR_STARTS_OR_ENDS = TypedDict(
40
+ "KW_STR_STARTS_OR_ENDS", {"substr": tuple[str, ...]}
41
+ )
42
+ ARGS_STR_STARTS_OR_ENDS = tuple[tuple[str, ...]]
43
+
44
+
45
+ @dataclass(kw_only=True)
46
+ class FilterStrIn(BaseAstNode[KW_STR_IN, ARGS_STR_IN]):
47
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_IN
48
+ accept_type: VariableType = VariableType.STRING
49
+ ret_type: VariableType = VariableType.STRING
50
+
51
+
52
+ @dataclass(kw_only=True)
53
+ class FilterStrStarts(
54
+ BaseAstNode[KW_STR_STARTS_OR_ENDS, ARGS_STR_STARTS_OR_ENDS]
55
+ ):
56
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_STARTS
57
+ accept_type: VariableType = VariableType.STRING
58
+ ret_type: VariableType = VariableType.STRING
59
+
60
+
61
+ @dataclass(kw_only=True)
62
+ class FilterStrEnds(
63
+ BaseAstNode[KW_STR_STARTS_OR_ENDS, ARGS_STR_STARTS_OR_ENDS]
64
+ ):
65
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_ENDS
66
+ accept_type: VariableType = VariableType.STRING
67
+ ret_type: VariableType = VariableType.STRING
68
+
69
+
70
+ KW_STR_RE = TypedDict("KW_STR_RE", {"pattern": str, "ignore_case": bool})
71
+ ARGS_STR_RE = tuple[str, bool]
72
+
73
+
74
+ @dataclass(kw_only=True)
75
+ class FilterStrRe(BaseAstNode[KW_STR_RE, ARGS_STR_RE]):
76
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_RE
77
+ accept_type: VariableType = VariableType.STRING
78
+ ret_type: VariableType = VariableType.STRING
79
+
80
+
81
+ KW_STR_LEN = TypedDict("KW_STR_LEN", {"length": int})
82
+ ARGS_STR_LEN = tuple[int]
83
+
84
+
85
+ @dataclass(kw_only=True)
86
+ class FilterStrLenEq(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
87
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_EQ
88
+ accept_type: VariableType = VariableType.STRING
89
+ ret_type: VariableType = VariableType.STRING
90
+
91
+
92
+ @dataclass(kw_only=True)
93
+ class FilterStrLenNe(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
94
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_NE
95
+ accept_type: VariableType = VariableType.STRING
96
+ ret_type: VariableType = VariableType.STRING
97
+
98
+
99
+ @dataclass(kw_only=True)
100
+ class FilterStrLenLt(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
101
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_LT
102
+ accept_type: VariableType = VariableType.STRING
103
+ ret_type: VariableType = VariableType.STRING
104
+
105
+
106
+ @dataclass(kw_only=True)
107
+ class FilterStrLenLe(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
108
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_LE
109
+ accept_type: VariableType = VariableType.STRING
110
+ ret_type: VariableType = VariableType.STRING
111
+
112
+
113
+ @dataclass(kw_only=True)
114
+ class FilterStrLenGt(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
115
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_GT
116
+ accept_type: VariableType = VariableType.STRING
117
+ ret_type: VariableType = VariableType.STRING
118
+
119
+
120
+ @dataclass(kw_only=True)
121
+ class FilterStrLenGe(BaseAstNode[KW_STR_LEN, ARGS_STR_LEN]):
122
+ kind: ClassVar[TokenType] = TokenType.FILTER_STR_LEN_GE
123
+ accept_type: VariableType = VariableType.STRING
124
+ ret_type: VariableType = VariableType.STRING
125
+
126
+
127
+ # TODO: provide API for int, float, etc
128
+ KW_STR_EQ_OR_NE = TypedDict("KW_STR_EQ_OR_NE", {"values": tuple[str, ...]})
129
+ ARGS_STR_EQ_OR_NE = tuple[tuple[str, ...]]
130
+
131
+
132
+ @dataclass(kw_only=True)
133
+ class FilterEqual(BaseAstNode[KW_STR_EQ_OR_NE, ARGS_STR_EQ_OR_NE]):
134
+ kind: ClassVar[TokenType] = TokenType.FILTER_EQ
135
+ accept_type: VariableType = VariableType.STRING
136
+ ret_type: VariableType = VariableType.STRING
137
+
138
+
139
+ @dataclass(kw_only=True)
140
+ class FilterNotEqual(BaseAstNode[KW_STR_EQ_OR_NE, ARGS_STR_EQ_OR_NE]):
141
+ kind: ClassVar[TokenType] = TokenType.FILTER_NE
142
+ accept_type: VariableType = VariableType.STRING
143
+ ret_type: VariableType = VariableType.STRING
144
+
145
+
146
+ @dataclass(kw_only=True)
147
+ class ExprFilter(BaseAstNode[T_EMPTY_KWARGS, tuple]):
148
+ kind: ClassVar[TokenType] = TokenType.EXPR_FILTER
149
+ accept_type: VariableType = VariableType.LIST_STRING
150
+ ret_type: VariableType = VariableType.LIST_STRING
151
+ # 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,30 @@ 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,
98
+ FilterStrLenEq,
99
+ FilterStrLenNe,
100
+ FilterStrLenLt,
101
+ FilterStrLenLe,
102
+ FilterStrLenGt,
103
+ FilterStrLenGe,
88
104
  )
89
105
  from ssc_codegen.converters.base import BaseCodeConverter
90
106
  from ssc_codegen.converters.helpers import (
91
107
  prev_next_var,
92
108
  is_last_var_no_ret,
93
109
  have_pre_validate_call,
110
+ is_first_node_cond,
111
+ is_prev_node_atomic_cond,
94
112
  )
95
113
  from ssc_codegen.converters.templates.js_pure import (
96
114
  J2_STRUCT_INIT,
@@ -130,11 +148,15 @@ CONVERTER = BaseCodeConverter(debug_comment_prefix="// ")
130
148
 
131
149
 
132
150
  # TODO: move to string_utils
133
- def to_js_regexp(pattern: str, ignore_case: bool = False) -> str:
151
+ def to_js_regexp(
152
+ pattern: str, ignore_case: bool = False, is_global: bool = True
153
+ ) -> str:
134
154
  """helper function for convert string pattern to js"""
135
155
  pattern = pattern.replace("<\\/", "</")
136
156
  pattern = pattern.replace("/", "\\/")
137
- pattern = f"/{pattern}/g"
157
+ pattern = f"/{pattern}/"
158
+ if is_global:
159
+ pattern += "g"
138
160
  if ignore_case:
139
161
  pattern += "i"
140
162
  return pattern
@@ -368,8 +390,8 @@ def pre_list_str_replace(node: ExprListStringReplace) -> str:
368
390
  def pre_str_regex(node: ExprStringRegex) -> str:
369
391
  prv, nxt = prev_next_var(node)
370
392
  pattern, group, ignore_case = node.unpack_args()
371
- pattern = to_js_regexp(pattern, ignore_case)
372
- return f"let {nxt} = (new RegExp({pattern})).exec({prv})[{group}];"
393
+ pattern = to_js_regexp(pattern, ignore_case, is_global=False)
394
+ return f"let {nxt} = {prv}.match({pattern})[{group}];"
373
395
 
374
396
 
375
397
  @CONVERTER(ExprStringRegexAll.kind)
@@ -377,8 +399,7 @@ def pre_str_regex_all(node: ExprStringRegexAll) -> str:
377
399
  prv, nxt = prev_next_var(node)
378
400
  pattern, ignore_case = node.unpack_args()
379
401
  pattern = to_js_regexp(pattern, ignore_case)
380
-
381
- return f"let {nxt} = (new RegExp({pattern})).exec({prv});"
402
+ return f"let {nxt} = Array.from({prv}.match({pattern}));"
382
403
 
383
404
 
384
405
  @CONVERTER(ExprStringRegexSub.kind)
@@ -714,3 +735,182 @@ def pre_list_has_attr(node: ExprListHasAttr) -> str:
714
735
  if is_last_var_no_ret(node):
715
736
  return expr
716
737
  return expr + f"let {nxt} = {prv};"
738
+
739
+
740
+ @CONVERTER(ExprFilter.kind)
741
+ def pre_expr_filter(node: ExprFilter) -> str:
742
+ prv, nxt = prev_next_var(node)
743
+ return f"let {nxt} = {prv}.filter(i => "
744
+
745
+
746
+ @CONVERTER.post(ExprFilter.kind)
747
+ def post_expr_filter(_node: ExprFilter) -> str:
748
+ return ");"
749
+
750
+
751
+ @CONVERTER(FilterOr.kind)
752
+ def pre_filter_or(_node: FilterOr) -> str:
753
+ return " || ("
754
+
755
+
756
+ @CONVERTER.post(FilterOr.kind)
757
+ def post_filter_or(_node: FilterOr) -> str:
758
+ return ")"
759
+
760
+
761
+ @CONVERTER(FilterAnd.kind)
762
+ def pre_filter_and(_node: FilterAnd) -> str:
763
+ return " && ("
764
+
765
+
766
+ @CONVERTER.post(FilterAnd.kind)
767
+ def post_filter_and(_node: FilterAnd) -> str:
768
+ return ")"
769
+
770
+
771
+ @CONVERTER(FilterNot.kind)
772
+ def pre_filter_not(_node: FilterNot) -> str: # type: ignore
773
+ return "!("
774
+
775
+
776
+ @CONVERTER.post(FilterNot.kind)
777
+ def post_filter_not(_node: FilterNot) -> str:
778
+ return ")"
779
+
780
+
781
+ @CONVERTER(FilterStrIn.kind)
782
+ def pre_filter_in(node: FilterStrIn) -> str:
783
+ values, *_ = node.unpack_args()
784
+ if len(values) == 1:
785
+ expr = f"i.includes({values[0]!r})"
786
+ else:
787
+ # to js array
788
+ val_arr = str(values)
789
+ val_arr = "[" + val_arr[1:-1] + "]"
790
+ expr = f"{val_arr}.some(e => i.includes(e))"
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(FilterStrStarts.kind)
797
+ def pre_filter_starts_with(node: FilterStrStarts) -> str:
798
+ values, *_ = node.unpack_args()
799
+ if len(values) == 1:
800
+ expr = f"i.startsWith({values[0]!r})"
801
+ else:
802
+ # to js array
803
+ val_arr = str(values)
804
+ val_arr = "[" + val_arr[1:-1] + "]"
805
+ expr = f"{val_arr}.some(e => i.startsWith(e))"
806
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
807
+ return f" && {expr}"
808
+ return expr
809
+
810
+
811
+ @CONVERTER(FilterStrEnds.kind)
812
+ def pre_filter_ends_with(node: FilterStrEnds) -> str:
813
+ values, *_ = node.unpack_args()
814
+ if len(values) == 1:
815
+ expr = f"i.endsWith({values[0]!r})"
816
+ else:
817
+ # to js array
818
+ val_arr = str(values)
819
+ val_arr = "[" + val_arr[1:-1] + "]"
820
+ expr = f"{val_arr}.some(e => i.endsWith(e))"
821
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
822
+ return f" && {expr}"
823
+ return expr
824
+
825
+
826
+ @CONVERTER(FilterStrRe.kind)
827
+ def pre_filter_re(node: FilterStrRe) -> str:
828
+ pattern, ignore_case, *_ = node.unpack_args()
829
+ pattern = to_js_regexp(pattern, ignore_case, is_global=False)
830
+
831
+ expr = f"(new RegExp({pattern})).test(i)"
832
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
833
+ return f" && {expr}"
834
+ return expr
835
+
836
+
837
+ @CONVERTER(FilterEqual.kind)
838
+ def pre_filter_eq(node: FilterEqual) -> str:
839
+ values, *_ = node.unpack_args()
840
+ if len(values) == 1:
841
+ expr = f"i === {values[0]!r}"
842
+ else:
843
+ val_arr = str(values)
844
+ val_arr = "[" + val_arr[1:-1] + "]"
845
+ expr = f"{val_arr}.every(e => i === e)"
846
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
847
+ return f" && {expr}"
848
+ return expr
849
+
850
+
851
+ @CONVERTER(FilterNotEqual.kind)
852
+ def pre_filter_ne(node: FilterNotEqual) -> str:
853
+ values, *_ = node.unpack_args()
854
+ if len(values) == 1:
855
+ expr = f"i !== {values[0]!r}"
856
+ else:
857
+ val_arr = str(values)
858
+ val_arr = "[" + val_arr[1:-1] + "]"
859
+ expr = f"{val_arr}.every(e => i !== e)"
860
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
861
+ return f" && {expr}"
862
+ return expr
863
+
864
+
865
+ @CONVERTER(FilterStrLenEq.kind)
866
+ def pre_filter_str_len_eq(node: FilterStrLenEq) -> str:
867
+ length, *_ = node.unpack_args()
868
+ expr = f"i.length == {length}"
869
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
870
+ return f" && {expr}"
871
+ return expr
872
+
873
+
874
+ @CONVERTER(FilterStrLenNe.kind)
875
+ def pre_filter_str_len_ne(node: FilterStrLenNe) -> str:
876
+ length, *_ = node.unpack_args()
877
+ expr = f"i.length != {length}"
878
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
879
+ return f" && {expr}"
880
+ return expr
881
+
882
+
883
+ @CONVERTER(FilterStrLenLt.kind)
884
+ def pre_filter_str_len_lt(node: FilterStrLenLt) -> str:
885
+ length, *_ = node.unpack_args()
886
+ expr = f"i.length < {length}"
887
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
888
+ return f" && {expr}"
889
+ return expr
890
+
891
+
892
+ @CONVERTER(FilterStrLenLe.kind)
893
+ def pre_filter_str_len_le(node: FilterStrLenLe) -> str:
894
+ length, *_ = node.unpack_args()
895
+ expr = f"i.length <= {length}"
896
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
897
+ return f" && {expr}"
898
+ return expr
899
+
900
+
901
+ @CONVERTER(FilterStrLenGt.kind)
902
+ def pre_filter_str_len_gt(node: FilterStrLenGt) -> str:
903
+ length, *_ = node.unpack_args()
904
+ expr = f"i.length > {length}"
905
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
906
+ return f" && {expr}"
907
+ return expr
908
+
909
+
910
+ @CONVERTER(FilterStrLenGe.kind)
911
+ def pre_filter_str_len_ge(node: FilterStrLenGe) -> str:
912
+ length, *_ = node.unpack_args()
913
+ expr = f"i.length >= {length}"
914
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
915
+ return f" && {expr}"
916
+ return expr
@@ -87,6 +87,22 @@ 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,
100
+ FilterStrLenEq,
101
+ FilterStrLenNe,
102
+ FilterStrLenLt,
103
+ FilterStrLenLe,
104
+ FilterStrLenGt,
105
+ FilterStrLenGe,
90
106
  )
91
107
  from ssc_codegen.converters.base import (
92
108
  BaseCodeConverter,
@@ -98,6 +114,8 @@ from ssc_codegen.converters.helpers import (
98
114
  have_pre_validate_call,
99
115
  prev_next_var,
100
116
  is_last_var_no_ret,
117
+ is_prev_node_atomic_cond,
118
+ is_first_node_cond,
101
119
  )
102
120
  from ssc_codegen.tokens import StructType, VariableType, JsonVariableType
103
121
 
@@ -181,6 +199,8 @@ class BasePyCodeConverter(BaseCodeConverter):
181
199
  )
182
200
  from ssc_codegen.ast_ import ExprListStringRmPrefix
183
201
 
202
+ from ssc_codegen.ast_ import FilterNotEqual
203
+
184
204
  self.pre_definitions = {
185
205
  Docstring.kind: pre_docstring,
186
206
  ModuleImports.kind: pre_imports,
@@ -234,12 +254,33 @@ class BasePyCodeConverter(BaseCodeConverter):
234
254
  ExprToListLength.kind: pre_to_len,
235
255
  ExprToBool.kind: pre_to_bool,
236
256
  ExprJsonify.kind: pre_jsonify,
257
+ # FILTER,
258
+ ExprFilter.kind: pre_expr_filter,
259
+ FilterAnd.kind: pre_filter_and,
260
+ FilterOr.kind: pre_filter_or,
261
+ FilterNot.kind: pre_filter_not,
262
+ FilterStrStarts.kind: pre_filter_starts_with,
263
+ FilterStrEnds.kind: pre_filter_ends_with,
264
+ FilterStrIn.kind: pre_filter_in,
265
+ FilterStrRe.kind: pre_filter_re,
266
+ FilterEqual.kind: pre_filter_eq,
267
+ FilterNotEqual.kind: pre_filter_ne,
268
+ FilterStrLenEq.kind: pre_filter_str_len_eq,
269
+ FilterStrLenNe.kind: pre_filter_str_len_ne,
270
+ FilterStrLenLt.kind: pre_filter_str_len_lt,
271
+ FilterStrLenLe.kind: pre_filter_str_len_le,
272
+ FilterStrLenGe.kind: pre_filter_str_len_ge,
273
+ FilterStrLenGt.kind: pre_filter_str_len_gt,
237
274
  }
238
275
 
239
276
  self.post_definitions = {
240
277
  TypeDef.kind: post_typedef,
241
278
  JsonStruct.kind: post_json_struct,
242
279
  StartParseMethod.kind: post_start_parse,
280
+ ExprFilter.kind: post_expr_filter,
281
+ FilterAnd.kind: post_filter_and,
282
+ FilterOr.kind: post_filter_or,
283
+ FilterNot.kind: post_filter_not,
243
284
  }
244
285
 
245
286
 
@@ -876,3 +917,155 @@ def pre_jsonify(node: ExprJsonify) -> str:
876
917
  prv, nxt = prev_next_var(node)
877
918
  _name, _is_array = node.unpack_args()
878
919
  return indent + f"{nxt} = json.loads({prv})"
920
+
921
+
922
+ # FILTERS
923
+
924
+
925
+ def pre_expr_filter(node: ExprFilter) -> str:
926
+ indent = (
927
+ INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
928
+ )
929
+ prv, nxt = prev_next_var(node)
930
+ return indent + f"{nxt} = [i for i in {prv} if "
931
+
932
+
933
+ def post_expr_filter(_node: ExprFilter) -> str:
934
+ return "]"
935
+
936
+
937
+ def pre_filter_or(_node: FilterOr) -> str:
938
+ return " or ("
939
+
940
+
941
+ def post_filter_or(_node: FilterOr) -> str:
942
+ return ")"
943
+
944
+
945
+ def pre_filter_and(_node: FilterAnd) -> str:
946
+ return " and ("
947
+
948
+
949
+ def post_filter_and(_node: FilterAnd) -> str:
950
+ return ")"
951
+
952
+
953
+ def pre_filter_not(_node: FilterNot) -> str:
954
+ return "not ("
955
+
956
+
957
+ def post_filter_not(_node: FilterNot) -> str:
958
+ return ")"
959
+
960
+
961
+ def pre_filter_in(node: FilterStrIn) -> str:
962
+ values, *_ = node.unpack_args()
963
+
964
+ if len(values) == 1:
965
+ expr = f"{values[0]!r} in i"
966
+ else:
967
+ expr = f"any(s in i for s in {values})"
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_starts_with(node: FilterStrStarts) -> str:
974
+ start_, *_ = node.unpack_args()
975
+ # build-in python startswith accept tuple[str, ...]
976
+ expr = f"i.startswith({start_})"
977
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
978
+ return f" and {expr}"
979
+ return expr
980
+
981
+
982
+ def pre_filter_ends_with(node: FilterStrEnds) -> str:
983
+ suffix_, *_ = node.unpack_args()
984
+ # build-in python endswith accept tuple[str, ...]
985
+ expr = f"i.endswith({suffix_})"
986
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
987
+ return f" and {expr}"
988
+ return expr
989
+
990
+
991
+ def pre_filter_re(node: FilterStrRe) -> str:
992
+ pattern, ignore_case, *_ = node.unpack_args()
993
+ if ignore_case:
994
+ expr = f"re.search({pattern!r}, i, re.IGNORECASE)"
995
+ else:
996
+ expr = f"re.search({pattern!r}, i)"
997
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
998
+ return f" and {expr}"
999
+ return expr
1000
+
1001
+
1002
+ def pre_filter_eq(node: FilterEqual) -> str:
1003
+ values, *_ = node.unpack_args()
1004
+ # currently support only str
1005
+ if len(values) == 1:
1006
+ expr = f"i == {values[0]!r}"
1007
+ else:
1008
+ expr = f"all(s == i for s in {values})"
1009
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1010
+ return f" and {expr}"
1011
+ return expr
1012
+
1013
+
1014
+ def pre_filter_ne(node: FilterNotEqual) -> str:
1015
+ values, *_ = node.unpack_args()
1016
+ # currently support only str
1017
+ if len(values) == 1:
1018
+ expr = f"i != {values[0]!r}"
1019
+ else:
1020
+ expr = f"all(s != i for s in {values})"
1021
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1022
+ return f" and {expr}"
1023
+ return expr
1024
+
1025
+
1026
+ def pre_filter_str_len_eq(node: FilterStrLenEq) -> str:
1027
+ length, *_ = node.unpack_args()
1028
+ expr = f"len(i) == {length}"
1029
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1030
+ return f" and {expr}"
1031
+ return expr
1032
+
1033
+
1034
+ def pre_filter_str_len_ne(node: FilterStrLenNe) -> str:
1035
+ length, *_ = node.unpack_args()
1036
+ expr = f"len(i) != {length}"
1037
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1038
+ return f" and {expr}"
1039
+ return expr
1040
+
1041
+
1042
+ def pre_filter_str_len_lt(node: FilterStrLenLt) -> str:
1043
+ length, *_ = node.unpack_args()
1044
+ expr = f"len(i) < {length}"
1045
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1046
+ return f" and {expr}"
1047
+ return expr
1048
+
1049
+
1050
+ def pre_filter_str_len_le(node: FilterStrLenLe) -> str:
1051
+ length, *_ = node.unpack_args()
1052
+ expr = f"len(i) <= {length}"
1053
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1054
+ return f" and {expr}"
1055
+ return expr
1056
+
1057
+
1058
+ def pre_filter_str_len_gt(node: FilterStrLenGt) -> str:
1059
+ length, *_ = node.unpack_args()
1060
+ expr = f"len(i) > {length}"
1061
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1062
+ return f" and {expr}"
1063
+ return expr
1064
+
1065
+
1066
+ def pre_filter_str_len_ge(node: FilterStrLenGe) -> str:
1067
+ length, *_ = node.unpack_args()
1068
+ expr = f"len(i) >= {length}"
1069
+ if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
1070
+ return f" and {expr}"
1071
+ return expr
@@ -1,7 +1,7 @@
1
1
  """high-level AST builder interface"""
2
2
 
3
3
  import logging
4
- from typing import Type, Pattern
4
+ from typing import Type, Pattern, Sequence
5
5
 
6
6
  from cssselect import SelectorSyntaxError
7
7
  from typing_extensions import Self, assert_never
@@ -59,18 +59,34 @@ 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,
72
+ FilterStrLenEq,
73
+ FilterStrLenNe,
74
+ FilterStrLenLt,
75
+ FilterStrLenLe,
76
+ FilterStrLenGt,
77
+ FilterStrLenGe,
62
78
  )
63
79
  from ssc_codegen.document_utlis import (
64
80
  analyze_re_expression,
65
81
  unverbosify_regex,
66
82
  is_ignore_case_regex,
67
83
  )
84
+ from ssc_codegen.json_struct import Json
68
85
  from ssc_codegen.pseudo_selectors import (
69
86
  parse_pseudo_xpath_query,
70
87
  parse_pseudo_css_query,
71
88
  PseudoAction,
72
89
  )
73
- from ssc_codegen.json_struct import Json
74
90
  from ssc_codegen.schema import BaseSchema
75
91
  from ssc_codegen.selector_utils import validate_css_query, validate_xpath_query
76
92
  from ssc_codegen.tokens import VariableType
@@ -457,6 +473,20 @@ class ArrayDocument(BaseDocument):
457
473
  self._add(ExprToListLength(accept_type=expected_type))
458
474
  return self
459
475
 
476
+ def filter(self, expr: "DocumentFilter") -> Self:
477
+ """filter array of resuts by expr
478
+
479
+ - accept LIST_STRING, return LIST_STRING
480
+ """
481
+ if self.stack_last_ret != VariableType.LIST_STRING:
482
+ LOGGER.warning(
483
+ "to_len(): Expected type(s) %s got %s",
484
+ (VariableType.LIST_STRING.name,),
485
+ self.stack_last_ret.name,
486
+ )
487
+ self._add(ExprFilter(body=expr.stack))
488
+ return self
489
+
460
490
 
461
491
  class StringDocument(BaseDocument):
462
492
  def rm_prefix(self, substr: str) -> Self:
@@ -1236,3 +1266,181 @@ class JsonDocument(BaseDocument):
1236
1266
  # HACK: store to class instance for generate docstring signature
1237
1267
  BaseSchema.__JSON_SCHEMAS__[struct.__name__] = struct # type: ignore
1238
1268
  return self
1269
+
1270
+
1271
+ class DocumentFilter(BaseDocument):
1272
+ def eq(self, *values: str) -> Self:
1273
+ """check if value equal
1274
+
1275
+ multiple values converts to logical AND
1276
+
1277
+ pseudocode example:
1278
+
1279
+ F().eq("foo") -> value == "foo"
1280
+
1281
+ F().eq("bar", "foo") -> (value == "bar" && value == "foo")
1282
+ """
1283
+ self._add(FilterEqual(kwargs={"values": values}))
1284
+ return self
1285
+
1286
+ def ne(self, *values: str) -> Self:
1287
+ """check if value not equal
1288
+
1289
+ multiple values converts to logical AND
1290
+
1291
+ pseudocode example:
1292
+
1293
+ F().eq("foo") -> value == "foo"
1294
+
1295
+ F().eq("bar", "foo") -> (value != "bar" && value != "foo")
1296
+ """
1297
+ self._add(FilterNotEqual(kwargs={"values": values}))
1298
+ return self
1299
+
1300
+ def starts(self, *values: str) -> Self:
1301
+ """check if value starts by substring
1302
+
1303
+ multiple values converts to logical OR
1304
+
1305
+ pseudocode example:
1306
+
1307
+ F().eq("foo") -> value == "foo"
1308
+
1309
+ F().eq("foo", "bar") -> (value.starts_with("bar") || value.starts_with("foo"))
1310
+ """
1311
+ self._add(FilterStrStarts(kwargs={"substr": values}))
1312
+ return self
1313
+
1314
+ def ends(self, *values: str) -> Self:
1315
+ """check if value starts by substring
1316
+
1317
+ multiple values converts to logical OR
1318
+
1319
+ pseudocode example:
1320
+
1321
+ F().eq("foo") -> value == "foo"
1322
+
1323
+ F().eq("foo", "bar") -> (value.ends_with("bar") || value.ends_with("foo"))
1324
+ """
1325
+ self._add(FilterStrEnds(kwargs={"substr": values}))
1326
+ return self
1327
+
1328
+ def contains(self, *values: str) -> Self:
1329
+ """check if value contains by substring
1330
+
1331
+ multiple values converts to logical OR
1332
+
1333
+ pseudocode example:
1334
+
1335
+ F().eq("foo") -> value == "foo"
1336
+
1337
+ F().eq("foo", "bar") -> (value.include("bar") || value.include("foo"))
1338
+ """
1339
+ self._add(FilterStrIn(kwargs={"substr": values}))
1340
+ return self
1341
+
1342
+ def re(
1343
+ self, pattern: str | Pattern[str], ignore_case: bool = False
1344
+ ) -> Self:
1345
+ """check if pattern matched result in value"""
1346
+ if not isinstance(pattern, str):
1347
+ ignore_case = is_ignore_case_regex(pattern)
1348
+
1349
+ pattern = unverbosify_regex(pattern)
1350
+ result = analyze_re_expression(pattern, allow_empty_groups=True)
1351
+ if not result:
1352
+ LOGGER.warning(result.msg)
1353
+ self._add(
1354
+ FilterStrRe(kwargs={"pattern": pattern, "ignore_case": ignore_case})
1355
+ )
1356
+ return self
1357
+
1358
+ def and_(self, filter_expr: "DocumentFilter") -> Self:
1359
+ if self.stack[0].kind in (FilterAnd.kind, FilterOr.kind):
1360
+ LOGGER.warning(
1361
+ "logic AND: first node is `%s`", self.stack[0].kind.name
1362
+ )
1363
+ tmp_stack = filter_expr.stack.copy()
1364
+ self._add(FilterAnd(body=tmp_stack))
1365
+ return self
1366
+
1367
+ def or_(self, filter_expr: "DocumentFilter") -> Self:
1368
+ if self.stack[0].kind in (FilterAnd.kind, FilterOr.kind):
1369
+ LOGGER.warning(
1370
+ "logic OR: first node is `%s`", self.stack[0].kind.name
1371
+ )
1372
+ tmp_stack = filter_expr.stack.copy()
1373
+ self._add(FilterOr(body=tmp_stack))
1374
+ return self
1375
+
1376
+ def len_eq(self, value: int) -> Self:
1377
+ self._add(FilterStrLenEq(kwargs={"length": value}))
1378
+ return self
1379
+
1380
+ def len_ne(self, value: int) -> Self:
1381
+ self._add(FilterStrLenNe(kwargs={"length": value}))
1382
+ return self
1383
+
1384
+ def len_lt(self, value: int) -> Self:
1385
+ self._add(FilterStrLenLt(kwargs={"length": value}))
1386
+ return self
1387
+
1388
+ def len_le(self, value: int) -> Self:
1389
+ self._add(FilterStrLenLe(kwargs={"length": value}))
1390
+ return self
1391
+
1392
+ def len_gt(self, value: int) -> Self:
1393
+ self._add(FilterStrLenGt(kwargs={"length": value}))
1394
+ return self
1395
+
1396
+ def len_ge(self, value: int) -> Self:
1397
+ self._add(FilterStrLenGe(kwargs={"length": value}))
1398
+ return self
1399
+
1400
+ def not_(self, filter_expr: "DocumentFilter") -> Self:
1401
+ tmp_stack = filter_expr.stack.copy()
1402
+ self._add(FilterNot(body=tmp_stack))
1403
+ return self
1404
+
1405
+ def __or__(self, other: "DocumentFilter") -> "DocumentFilter":
1406
+ """syntax suger F().or_(...)"""
1407
+
1408
+ return self.or_(other)
1409
+
1410
+ def __and__(self, other: "DocumentFilter") -> "DocumentFilter":
1411
+ """syntax sugar F().and_(...)"""
1412
+ return self.and_(other)
1413
+
1414
+ def __invert__(self) -> "DocumentFilter":
1415
+ """syntax sugar F().not_(...)"""
1416
+ return DocumentFilter().not_(self)
1417
+
1418
+ def __eq__(self, other: int | str | Sequence[str]) -> Self:
1419
+ """syntax sugar F().eq(...)"""
1420
+ if isinstance(other, int):
1421
+ return self.len_eq(other)
1422
+
1423
+ if isinstance(other, str):
1424
+ other = (other,)
1425
+ return self.eq(*other)
1426
+
1427
+ def __ne__(self, other: int | str | Sequence[str]) -> Self:
1428
+ """syntax sugar F().ne(...)"""
1429
+ if isinstance(other, int):
1430
+ return self.len_ne(other)
1431
+
1432
+ if isinstance(other, str):
1433
+ other = (other,)
1434
+ return self.eq(*other)
1435
+
1436
+ def __lt__(self, other: int) -> Self:
1437
+ return self.len_lt(other)
1438
+
1439
+ def __le__(self, other: int) -> Self:
1440
+ return self.len_le(other)
1441
+
1442
+ def __gt__(self, other: int) -> Self:
1443
+ return self.len_gt(other)
1444
+
1445
+ def __ge__(self, other: int) -> Self:
1446
+ return self.len_ge(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,28 @@ class TokenType(IntEnum):
174
174
  # bool
175
175
  TO_BOOL = auto()
176
176
 
177
+ # FILTER
178
+ EXPR_FILTER = auto()
179
+
180
+ FILTER_OR = auto()
181
+ FILTER_AND = auto()
182
+ FILTER_NOT = auto()
183
+ # FILTER EXPR (STR)
184
+ FILTER_STR_IN = auto()
185
+ FILTER_STR_STARTS = auto()
186
+ FILTER_STR_ENDS = auto()
187
+ FILTER_STR_RE = auto()
188
+
189
+ FILTER_STR_LEN_EQ = auto()
190
+ FILTER_STR_LEN_NE = auto()
191
+ FILTER_STR_LEN_LT = auto()
192
+ FILTER_STR_LEN_LE = auto()
193
+ FILTER_STR_LEN_GT = auto()
194
+ FILTER_STR_LEN_GE = auto()
195
+
196
+ FILTER_EQ = auto()
197
+ FILTER_NE = auto()
198
+
177
199
  @classmethod
178
200
  def default_tokens(cls) -> tuple["TokenType", ...]:
179
201
  return (
File without changes
File without changes
File without changes