ssc_codegen 0.13.0__tar.gz → 0.13.2__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.
Files changed (63) hide show
  1. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/PKG-INFO +1 -1
  2. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/pyproject.toml +1 -1
  3. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/__init__.py +1 -1
  4. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/__init__.py +0 -1
  5. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_build/main.py +4 -1
  6. ssc_codegen-0.13.2/ssc_codegen/ast_grep_rules/py_drop_prefix_suffix_backport.yml +47 -0
  7. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/ast_grep.py +1 -0
  8. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/main.py +8 -1
  9. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/base.py +183 -24
  10. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/js_pure.py +4 -5
  11. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/py_base.py +1 -1
  12. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/py_lxml.py +52 -45
  13. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/py_selectolax.py +1 -1
  14. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/lua_re_compat.py +1 -1
  15. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/logs.py +2 -2
  16. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/static_checker/base.py +0 -1
  17. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/.gitignore +0 -0
  18. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/LICENSE +0 -0
  19. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/README.md +0 -0
  20. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/_compat.py +0 -0
  21. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/base.py +0 -0
  22. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_array.py +0 -0
  23. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_cast.py +0 -0
  24. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_core.py +0 -0
  25. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_filter.py +0 -0
  26. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  27. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_string.py +0 -0
  28. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_/nodes_validate.py +0 -0
  29. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_build/__init__.py +0 -0
  30. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_build/builder.py +0 -0
  31. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_build/utils.py +0 -0
  32. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_grep_rules/js_rules.yml +0 -0
  33. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/ast_grep_rules/py_rules.yml +0 -0
  34. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/__init__.py +0 -0
  35. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/cli_callbacks.py +0 -0
  36. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/cli_utils.py +0 -0
  37. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/code_callbacks.py +0 -0
  38. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/consts.py +0 -0
  39. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  40. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/compiler.py +0 -0
  41. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/__init__.py +0 -0
  42. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/go_goquery.py +0 -0
  43. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/helpers.py +0 -0
  44. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/lua_htmlparser.py +0 -0
  45. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/py_bs4.py +0 -0
  46. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/py_parsel.py +0 -0
  47. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/__init__.py +0 -0
  48. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/go_goquery.py +0 -0
  49. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/js_pure.py +0 -0
  50. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/lua_base.py +0 -0
  51. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/lua_css_compat.py +0 -0
  52. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/converters/templates/py_base.py +0 -0
  53. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/document.py +0 -0
  54. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/document_utlis.py +0 -0
  55. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/json_struct.py +0 -0
  56. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/json_to_scc.py +0 -0
  57. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/pseudo_selectors.py +0 -0
  58. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/schema.py +0 -0
  59. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/selector_utils.py +0 -0
  60. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/static_checker/__init__.py +0 -0
  61. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/static_checker/callbacks.py +0 -0
  62. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/str_utils.py +0 -0
  63. {ssc_codegen-0.13.0 → ssc_codegen-0.13.2}/ssc_codegen/tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssc_codegen
3
- Version: 0.13.0
3
+ Version: 0.13.2
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.13.0"
3
+ version = "0.13.2"
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.13.0"
10
+ VERSION = "0.13.2"
11
11
 
12
12
 
13
13
  class __MISSING(object):
@@ -120,7 +120,6 @@ from .nodes_validate import (
120
120
  ExprIsEqual,
121
121
  ExprStringIsRegex,
122
122
  ExprIsXpath,
123
- ExprIsNotEqual,
124
123
  ExprIsContains,
125
124
  ExprListStringAnyRegex,
126
125
  ExprListStringAllRegex,
@@ -13,7 +13,7 @@ from ssc_codegen.schema import BaseSchema
13
13
  from ssc_codegen.static_checker import run_analyze_schema
14
14
  import logging
15
15
 
16
- LOGGER = logging.getLogger("ssc-gen")
16
+ LOGGER = logging.getLogger("ssc_gen")
17
17
 
18
18
 
19
19
  def build_ast_module_parser(
@@ -37,6 +37,9 @@ def build_ast_module_parser(
37
37
  py_module = exec_module_code(path)
38
38
  # check code configs before build AST
39
39
  schemas = extract_schemas_from_module(py_module)
40
+ if len(schemas) == 0:
41
+ LOGGER.warning(f"{path} does not contains defined schemas")
42
+
40
43
  count_errors = 0
41
44
  for schema in schemas:
42
45
  errors = run_analyze_schema(schema)
@@ -0,0 +1,47 @@
1
+ id: py_drop_prefix_backport
2
+ language: python
3
+ rule:
4
+ pattern:
5
+ context: |-
6
+ def ssc_rm_prefix($$$) -> str:$$$
7
+ fix: ''
8
+ ---
9
+ id: py_fix_new_prefix_syntax
10
+ language: python
11
+ rule:
12
+ pattern:
13
+ context: ssc_rm_prefix($V, $P)
14
+ fix: '$V.removeprefix($P)'
15
+
16
+ ---
17
+ id: py_drop_suffix_backport
18
+ language: python
19
+ rule:
20
+ pattern:
21
+ context: |-
22
+ def ssc_rm_suffix($$$) -> str:$$$
23
+ fix: ''
24
+ ---
25
+ id: py_fix_new_suffix_syntax
26
+ language: python
27
+ rule:
28
+ pattern:
29
+ context: ssc_rm_suffix($V, $P)
30
+ fix: '$V.removesuffix($P)'
31
+ ---
32
+
33
+ id: py_drop_prefix_suffix_backport
34
+ language: python
35
+ rule:
36
+ pattern:
37
+ context: |-
38
+ def ssc_rm_prefix_and_suffix($$$) -> str:$$$
39
+ fix: ''
40
+ ---
41
+ id: py_fix_new_prefix_suffix_syntax
42
+ language: python
43
+ rule:
44
+ pattern:
45
+ context: ssc_rm_prefix_and_suffix($V, $P1, $P2)
46
+ fix:
47
+ '$V.removeprefix($P1).removesuffix($P2)'
@@ -4,4 +4,5 @@ from importlib import resources
4
4
  RULES_PATH = resources.files("ssc_codegen.ast_grep_rules")
5
5
 
6
6
  PY_RULES = str(RULES_PATH.joinpath("py_rules.yml"))
7
+ PY_NEW_RM_PREFIX_SUFFIX_SYNTAX = str(RULES_PATH.joinpath("py_drop_prefix_suffix_backport.yml"))
7
8
  JS_RULES = str(RULES_PATH.joinpath("js_rules.yml"))
@@ -11,7 +11,7 @@ from ichrome.exceptions import ChromeRuntimeError
11
11
  from typer import Argument, BadParameter, Option, Typer
12
12
 
13
13
  from ssc_codegen.ast_build import build_ast_module_parser
14
- from ssc_codegen.cli.ast_grep import JS_RULES, PY_RULES
14
+ from ssc_codegen.cli.ast_grep import JS_RULES, PY_RULES, PY_NEW_RM_PREFIX_SUFFIX_SYNTAX
15
15
  from ssc_codegen.cli.cli_callbacks import cb_check_ssc_files, cb_folder_out
16
16
  from ssc_codegen.cli.cli_utils import (
17
17
  ConverterLike,
@@ -168,6 +168,9 @@ def gen_py(
168
168
  docstring: Annotated[
169
169
  bool, Option(help=HELP_DOCSTRING, is_flag=True)
170
170
  ] = True,
171
+ fmt_prefix_suffix_backport: Annotated[
172
+ bool, Option("-frps", '--fmt-remove-prefix-suffix',
173
+ help="[PY39+]: replace backport funcs to str.removeprefix(...) and str.removesuffix(...)")] = False
171
174
  ) -> None:
172
175
  converter = import_converter(f"py_{lib.value}")
173
176
  if fmt:
@@ -176,10 +179,14 @@ def gen_py(
176
179
  "ruff check {} --fix",
177
180
  f"ast-grep scan --rule {PY_RULES} -U " + "{}",
178
181
  ]
182
+ if fmt_prefix_suffix_backport:
183
+ commands.append(f"ast-grep scan --rule {PY_NEW_RM_PREFIX_SUFFIX_SYNTAX} -U " + "{}")
179
184
  fmt_cmd = create_fmt_cmd(ssc_files, prefix, suffix, out, commands)
180
185
  else:
181
186
  fmt_cmd = []
182
187
 
188
+
189
+
183
190
  generate_code(
184
191
  converter=converter,
185
192
  out=out,
@@ -28,6 +28,18 @@ CB_AST_DECORATOR = Callable[[CB_AST_BIND], CB_AST_BIND]
28
28
 
29
29
 
30
30
  def debug_comment_cb(node: BaseAstNode, comment_prefix: str) -> str:
31
+ """Generate a debug comment for an AST node.
32
+
33
+ Creates a formatted string containing information about the node's type,
34
+ return type (if applicable), and other relevant details for debugging purposes.
35
+
36
+ Args:
37
+ node: The AST node to generate a comment for.
38
+ comment_prefix: The prefix to add to each line of the comment.
39
+
40
+ Returns:
41
+ A formatted string containing debug information about the node.
42
+ """
31
43
  match node.kind:
32
44
  case TokenType.EXPR_RETURN:
33
45
  token = f"{comment_prefix}Token: {node.kind.name} ret_type: {node.ret_type.name}"
@@ -48,16 +60,60 @@ def debug_comment_cb(node: BaseAstNode, comment_prefix: str) -> str:
48
60
 
49
61
 
50
62
  class BaseCodeConverter:
51
- """base ast visitor class and code converter
52
-
53
- Visitor is set using decorators pointing to the TokenType
54
-
55
- decorators hooks can be overridden
56
-
57
- - as defaults __call__ methods - set @pre decorator
58
- - @pre - first trigger visitor entrypoint
59
- - @post - second trigger visitor entrypoint (for close brackets or other logic, for example)
60
-
63
+ """Base AST visitor class and code converter.
64
+
65
+ This class serves as the foundation for converting AST nodes into code.
66
+ Visitors are set using decorators that point to specific TokenTypes.
67
+
68
+ Decorator hooks can be overridden:
69
+ - As defaults, __call__ methods set @pre decorator
70
+ - @pre - First trigger visitor entrypoint
71
+ - @post - Second trigger visitor entrypoint (for closing brackets or other logic).
72
+ It will be called when the broadcast of all nodes and the container body is transpiled.
73
+
74
+ Example:
75
+ ```
76
+ CONVERTER = BaseCodeConverter()
77
+
78
+ # recommended use <node>.kind call as key in decorators instead TokenType.<TOKEN>
79
+ @CONVERTER(ModuleImports.kind)
80
+ def pre_imports(_node: ModuleImports) -> str:
81
+ # implement imports stmt
82
+ return '''from bs4 import BeautifulSoap'''
83
+
84
+ # TypeDef and StartParseMethod node allow add extra second shortcut as StructType for
85
+ # increase code readability and simplify codegen
86
+
87
+ # TYPEDEF Impl
88
+ # post_callback - shortcut for minify typical end expr (eg: close parens)
89
+
90
+ # generate type for StructType.ITEM parser
91
+ @CONVERTER(TypeDef.kind, StructType.ITEM, post_callback=lambda _: "})")
92
+ def pre_typedef_item(node: TypeDef) -> str:
93
+ name, _ = node.unpack_args()
94
+ return f'T_{name} = TypedDict("T_{name}", ' + "{"
95
+
96
+ # generate type for StructType.DICT parser
97
+ @CONVERTER(TypeDef.kind, StructType.DICT)
98
+ def pre_typedef_dict(node: TypeDef) -> str:
99
+ name, _ = node.unpack_args()
100
+ type_ = get_typedef_field_by_name(node, "__VALUE__")
101
+ return f"T_{name} = Dict[str, {type_}]"
102
+
103
+ # StartParse impl
104
+ @CONVERTER(StartParseMethod.kind, StructType.ITEM)
105
+ def pre_start_parse_item(node: StartParseMethod) -> str:
106
+ # some impl for ItEM struct
107
+ return ""
108
+
109
+ @CONVERTER(StartParseMethod.kind, StructType.LIST)
110
+ def pre_start_parse_list(node: StartParseMethod) -> str:
111
+ # some impl for LIST struct
112
+ return ""
113
+ ```
114
+
115
+ Attributes:
116
+ TEST_EXCLUDE_NODES: List of token types to exclude from tests.
61
117
  """
62
118
 
63
119
  TEST_EXCLUDE_NODES: ClassVar[list[TokenType]] = [
@@ -70,7 +126,7 @@ class BaseCodeConverter:
70
126
  TokenType.STRUCT_CALL_FUNCTION,
71
127
  TokenType.STRUCT_CALL_CLASSVAR,
72
128
  ]
73
- """dev classvar marks exclude nodes in tests"""
129
+ """Developer class variable that marks nodes to exclude in tests."""
74
130
 
75
131
  def __init__(
76
132
  self,
@@ -80,10 +136,10 @@ class BaseCodeConverter:
80
136
  debug_cb: CB_FMT_DEBUG_COMMENT = debug_comment_cb,
81
137
  ) -> None:
82
138
  """
83
-
84
- :param debug_instructions: enable debug instructions (add comment for every generated instruction)
85
- :param debug_comment_prefix: comment line prefix
86
- :param debug_cb: callback formatting comment
139
+ Arguments:
140
+ debug_instructions: enable debug instructions (add comment for every generated instruction. used in debug codegen output)
141
+ debug_comment_prefix: comment line prefix (used in debug codegen output)
142
+ debug_cb: callback formatting comment (used in debug codegen output)
87
143
  """
88
144
  self._debug_instructions = debug_instructions
89
145
  self._debug_comment_prefix = debug_comment_prefix
@@ -131,14 +187,14 @@ class BaseCodeConverter:
131
187
 
132
188
  optional allow set post callback for simple string casts like close brackets etc
133
189
 
134
- StartParseMethod has shortcut (StartParseMethod.kind, StructType.<TYPE>) for simplify callback generators
190
+ StartParseMethod and TypeDef nodes has shortcut (StartParseMethod.kind, StructType.<TYPE>) for simplify callback generators
135
191
  """
136
192
  if for_struct_definition and for_definition not in (
137
193
  StartParseMethod.kind,
138
194
  TypeDef.kind,
139
195
  ):
140
196
  raise TypeError(
141
- "Add struct definition allowed only for `StartParseMethod` nodes"
197
+ "Add struct definition allowed only for `StartParseMethod` and `TypeDef` nodes"
142
198
  )
143
199
 
144
200
  def decorator(func: CB_AST_BIND) -> CB_AST_BIND:
@@ -164,7 +220,23 @@ class BaseCodeConverter:
164
220
  for_definition: TokenType,
165
221
  for_struct_definition: StructType | None = None,
166
222
  ) -> CB_AST_DECORATOR:
167
- """Define a post-conversion decorator for the given TokenType."""
223
+ """Define a post-conversion decorator for the given TokenType.
224
+
225
+ This method creates a decorator that registers a function to be called
226
+ after the main conversion of a node of the specified type. It's typically
227
+ used for closing brackets or other logic that should happen after the
228
+ main node processing.
229
+
230
+ Args:
231
+ for_definition: The TokenType to associate with this post-conversion function.
232
+ for_struct_definition: Optional StructType for specific struct definitions (for StartParseMethod and TypeDef).
233
+
234
+ Returns:
235
+ A decorator function that registers the post-conversion handler.
236
+
237
+ Raises:
238
+ TypeError: If struct definition is provided for unsupported node types.
239
+ """
168
240
  if for_struct_definition and for_definition not in (
169
241
  StartParseMethod.kind,
170
242
  TypeDef.kind,
@@ -191,7 +263,19 @@ class BaseCodeConverter:
191
263
  *,
192
264
  post_callback: Callable[[BaseAstNode], str] | None = None,
193
265
  ) -> CB_AST_DECORATOR:
194
- """Alias for pre decorator."""
266
+ """Alias for pre decorator.
267
+
268
+ Provides a convenient way to use the pre decorator by calling the class instance
269
+ directly. This is equivalent to calling the pre() method.
270
+
271
+ Args:
272
+ for_definition: The TokenType to associate with this conversion function.
273
+ for_struct_definition: Optional StructType for specific struct definitions (only for StartParseMethod and TypeDef nodes).
274
+ post_callback: Optional callback function for post-processing.
275
+
276
+ Returns:
277
+ A decorator function that registers the conversion handler.
278
+ """
195
279
  return self.pre(
196
280
  for_definition, for_struct_definition, post_callback=post_callback
197
281
  )
@@ -199,7 +283,20 @@ class BaseCodeConverter:
199
283
  def _pre_convert_node(
200
284
  self, node: BaseAstNode, st_type: StructType | None = None
201
285
  ) -> str:
202
- """Convert the AST node using the pre-definition function."""
286
+ """Convert the AST node using the pre-definition function.
287
+
288
+ This method applies the registered pre-conversion function to an AST node,
289
+ optionally using a specific struct type for more granular control. It handles
290
+ both simple node conversions and struct-specific conversions.
291
+
292
+ Args:
293
+ node: The AST node to convert.
294
+ st_type: Optional StructType for struct-specific conversions.
295
+
296
+ Returns:
297
+ The result of applying the pre-conversion function to the node,
298
+ or an empty string if no matching function is found.
299
+ """
203
300
  # syntax sugar: split generate start_parse methods by part callbacks
204
301
  if (
205
302
  node.kind in (StartParseMethod.kind, TypeDef.kind)
@@ -220,7 +317,21 @@ class BaseCodeConverter:
220
317
  def _post_convert_node(
221
318
  self, node: BaseAstNode, st_type: StructType | None = None
222
319
  ) -> str:
223
- """Convert the AST node using the post-definition function."""
320
+ """Convert the AST node using the post-definition function.
321
+
322
+ This method applies the registered post-conversion function to an AST node,
323
+ optionally using a specific struct type for more granular control. It's typically
324
+ used for closing brackets or other logic that should happen after the main
325
+ node processing.
326
+
327
+ Args:
328
+ node: The AST node to convert.
329
+ st_type: Optional StructType for struct-specific conversions.
330
+
331
+ Returns:
332
+ The result of applying the post-conversion function to the node,
333
+ or an empty string if no matching function is found.
334
+ """
224
335
  # syntax sugar: split generate start_parse methods by part callbacks
225
336
  if (
226
337
  node.kind in (StartParseMethod.kind, TypeDef.kind)
@@ -239,7 +350,19 @@ class BaseCodeConverter:
239
350
  def convert_program(
240
351
  self, ast_program: ModuleProgram, comment: str = ""
241
352
  ) -> list[str]:
242
- """Convert the module AST to code parts."""
353
+ """Convert the module AST to code parts.
354
+
355
+ This method converts an entire module AST into a list of code parts,
356
+ starting with an optional comment and processing all nodes in the AST.
357
+
358
+ Args:
359
+ ast_program: The ModuleProgram AST node to convert.
360
+ comment: An optional comment to include at the beginning of the output.
361
+
362
+ Returns:
363
+ A list of strings representing the converted code parts,
364
+ with empty strings filtered out.
365
+ """
243
366
  acc = [comment]
244
367
  result = self.convert(ast_program, acc)
245
368
  return [i for i in result if i]
@@ -247,7 +370,19 @@ class BaseCodeConverter:
247
370
  def convert(
248
371
  self, ast_entry: BaseAstNode, acc: list[str] | None = None
249
372
  ) -> list[str]:
250
- """Convert an AST node into code parts."""
373
+ """Convert an AST node into code parts.
374
+
375
+ This method dispatches the conversion of an AST node to the appropriate
376
+ conversion method based on the node's type. It builds up a list of code
377
+ parts by processing the node and its children.
378
+
379
+ Args:
380
+ ast_entry: The AST node to convert.
381
+ acc: An optional accumulator list to append code parts to.
382
+
383
+ Returns:
384
+ A list of strings representing the converted code parts.
385
+ """
251
386
  acc = acc or []
252
387
  match ast_entry.kind:
253
388
  case TokenType.MODULE:
@@ -307,6 +442,22 @@ class BaseCodeConverter:
307
442
  def _convert_logic_filter(
308
443
  self, ast_entry: FilterOr | FilterAnd | FilterNot, acc: list[str]
309
444
  ) -> None:
445
+ """Convert a logical filter node (OR, AND, NOT) and its children.
446
+
447
+ This method handles the conversion of logical filter operations, appending
448
+ the pre-conversion result, processing all child nodes, and then appending
449
+ the post-conversion result.
450
+
451
+ For example, an AND filter with two children would be converted as:
452
+ AND(body=[F1, F2]) ->
453
+ AND ( (OPEN)
454
+ F1 and F2
455
+ ) (CLOSE)
456
+
457
+ Args:
458
+ ast_entry: The logical filter node (FilterOr, FilterAnd, or FilterNot) to convert.
459
+ acc: The accumulator list to append code parts to.
460
+ """
310
461
  # AND(body=[F1, F2]) ->
311
462
  # AND ( (OPEN)
312
463
  # F1 and F2
@@ -317,7 +468,15 @@ class BaseCodeConverter:
317
468
  acc.append(self._post_convert_node(ast_entry))
318
469
 
319
470
  def _convert_module(self, ast_entry: ModuleProgram, acc: list[str]) -> None:
320
- """Handle module conversion."""
471
+ """Handle module conversion.
472
+
473
+ This method processes all nodes in a module's body by recursively converting
474
+ each one and appending the results to the accumulator.
475
+
476
+ Args:
477
+ ast_entry: The ModuleProgram node to convert.
478
+ acc: The accumulator list to append code parts to.
479
+ """
321
480
  for node in ast_entry.body:
322
481
  self.convert(node, acc)
323
482
 
@@ -51,7 +51,6 @@ from ssc_codegen.ast_ import (
51
51
  ExprIndex,
52
52
  ExprListStringJoin,
53
53
  ExprIsEqual,
54
- ExprIsNotEqual,
55
54
  ExprIsContains,
56
55
  ExprStringIsRegex,
57
56
  ExprToInt,
@@ -848,7 +847,7 @@ def pre_is_css(node: ExprIsCss) -> str:
848
847
  expr = f"({prv}.querySelector({query}) === null)"
849
848
  if invert:
850
849
  expr = f"!{expr}"
851
- expr = f'if {expr}'
850
+ expr = f"if {expr}"
852
851
  expr = f"{expr} throw new Error({msg});"
853
852
  if is_last_var_no_ret(node):
854
853
  return expr
@@ -865,7 +864,7 @@ def pre_is_xpath(node: ExprIsXpath) -> str:
865
864
  expr = f"(document.evaluate({query}, {prv}, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue === null)"
866
865
  if invert:
867
866
  expr = f"!{expr}"
868
- expr = f'if {expr}'
867
+ expr = f"if {expr}"
869
868
  code = [
870
869
  f"{expr}",
871
870
  "{",
@@ -1076,8 +1075,8 @@ def pre_has_attr(node: ExprHasAttr) -> str:
1076
1075
  invert = node.kwargs["invert"]
1077
1076
  expr = f"!{prv}?.hasAttribute({key})"
1078
1077
  if invert:
1079
- expr = f'!({expr})'
1080
- expr = f"if ({expr})"
1078
+ expr = f"!({expr})"
1079
+ expr = f"if ({expr}) throw new Error({msg}); "
1081
1080
  if is_last_var_no_ret(node):
1082
1081
  return expr
1083
1082
  return expr + f"let {nxt} = {prv};"
@@ -963,7 +963,7 @@ def pre_is_contains(node: ExprIsContains) -> str:
963
963
 
964
964
  item = py_get_classvar_hook_or_value(node, "item")
965
965
  msg = py_get_classvar_hook_or_value(node, "msg")
966
-
966
+
967
967
  if invert:
968
968
  expr = f"{item} not in {prv}"
969
969
  else:
@@ -67,17 +67,16 @@ CONVERTER = BasePyCodeConverter()
67
67
  def pre_init(_node: StructInitMethod) -> str:
68
68
  return (
69
69
  INDENT_METHOD
70
- + "def __init__(self, document: Union[str, etree._Element]) -> None:\n"
70
+ + "def __init__(self, document: Union[str, html.HtmlElement]) -> None:\n"
71
71
  + INDENT_METHOD_BODY
72
- + "self._document = etree.HTML(document) if isinstance(document, str) else document"
72
+ + "self._document = html.fromstring(document) if isinstance(document, str) else document"
73
73
  )
74
74
 
75
75
 
76
76
  @CONVERTER(StructPreValidateMethod.kind)
77
77
  def pre_struct_pre_validate(_node: StructPreValidateMethod) -> str:
78
78
  return (
79
- INDENT_METHOD
80
- + "def _pre_validate(self, v: Union[etree._Element, List[etree._Element]]) -> None:"
79
+ INDENT_METHOD + "def _pre_validate(self, v: html.HtmlElement) -> None:"
81
80
  )
82
81
 
83
82
 
@@ -85,7 +84,7 @@ def pre_struct_pre_validate(_node: StructPreValidateMethod) -> str:
85
84
  def pre_struct_part_doc_method(_node: StructPartDocMethod) -> str:
86
85
  return (
87
86
  INDENT_METHOD
88
- + "def _split_doc(self, v: Union[etree._Element, List[etree._Element]]) -> List[etree._Element]:"
87
+ + "def _split_doc(self, v: html.HtmlElement) -> List[html.HtmlElement]:"
89
88
  )
90
89
 
91
90
 
@@ -96,13 +95,13 @@ def pre_struct_field_method(node: StructFieldMethod) -> str:
96
95
  name = MAGIC_METHODS.get(name, name)
97
96
  return (
98
97
  INDENT_METHOD
99
- + f"def _parse_{name}(self, v: Union[etree._Element, List[etree._Element]]) -> {type_}:"
98
+ + f"def _parse_{name}(self, v: html.HtmlElement) -> {type_}:"
100
99
  )
101
100
 
102
101
 
103
102
  @CONVERTER(ModuleImports.kind)
104
103
  def pre_imports(_: ModuleImports) -> str:
105
- return IMPORTS_MIN + """from lxml import etree, html"""
104
+ return IMPORTS_MIN + """from lxml import html, etree"""
106
105
 
107
106
 
108
107
  @CONVERTER(ExprCss.kind)
@@ -112,7 +111,7 @@ def pre_css(node: ExprCss) -> str:
112
111
  )
113
112
  prv, nxt = prev_next_var(node)
114
113
  query = py_get_classvar_hook_or_value(node, "query")
115
- return indent + f"{nxt} = etree.CSSSelector({query})({prv})[0]"
114
+ return indent + f"{nxt} = {prv}.cssselect({query})[0]"
116
115
 
117
116
 
118
117
  @CONVERTER(ExprCssAll.kind)
@@ -122,7 +121,7 @@ def pre_css_all(node: ExprCssAll) -> str:
122
121
  )
123
122
  prv, nxt = prev_next_var(node)
124
123
  query = py_get_classvar_hook_or_value(node, "query")
125
- return indent + f"{nxt} = etree.CSSSelector({query})({prv})"
124
+ return indent + f"{nxt} = {prv}.cssselect({query})"
126
125
 
127
126
 
128
127
  @CONVERTER(ExprXpath.kind)
@@ -154,8 +153,9 @@ def pre_html_attr(node: ExprGetHtmlAttr) -> str:
154
153
  keys = node.kwargs["key"]
155
154
  if len(keys) == 1:
156
155
  key = keys[0]
157
- return indent + f"{nxt} = {prv}.attrib[{key!r}]"
158
- return indent + f"{nxt} = [{prv}.attrib[k] for k in {keys}]"
156
+ return indent + f"{nxt} = {prv}.get({key!r})"
157
+ key_accesses = [f"{prv}.get({k!r})" for k in keys]
158
+ return indent + f"{nxt} = [{', '.join(key_accesses)}]"
159
159
 
160
160
 
161
161
  @CONVERTER(ExprGetHtmlAttrAll.kind)
@@ -167,8 +167,8 @@ def pre_html_attr_all(node: ExprGetHtmlAttrAll) -> str:
167
167
  keys = node.kwargs["key"]
168
168
  if len(keys) == 1:
169
169
  key = keys[0]
170
- return indent + f"{nxt} = [e.attrib[{key!r}] for e in {prv}]"
171
- return indent + f"{nxt} = [e.attrib[k] for e in {prv} for k in {keys}]"
170
+ return indent + f"{nxt} = [e.get({key!r}) for e in {prv}]"
171
+ return indent + f"{nxt} = [e.get(k) for e in {prv} for k in {keys}]"
172
172
 
173
173
 
174
174
  @CONVERTER(ExprGetHtmlText.kind)
@@ -177,7 +177,7 @@ def pre_html_text(node: ExprGetHtmlText) -> str:
177
177
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
178
178
  )
179
179
  prv, nxt = prev_next_var(node)
180
- return indent + f"{nxt} = ''.join({prv}.itertext())"
180
+ return indent + f"{nxt} = {prv}.text_content()"
181
181
 
182
182
 
183
183
  @CONVERTER(ExprGetHtmlTextAll.kind)
@@ -186,7 +186,7 @@ def pre_html_text_all(node: ExprGetHtmlTextAll) -> str:
186
186
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
187
187
  )
188
188
  prv, nxt = prev_next_var(node)
189
- return indent + f"{nxt} = [text for e in {prv} for text in e.itertext()]"
189
+ return indent + f"{nxt} = [e.text_content() for e in {prv}]"
190
190
 
191
191
 
192
192
  @CONVERTER(ExprGetHtmlRaw.kind)
@@ -195,7 +195,7 @@ def pre_html_raw(node: ExprGetHtmlRaw) -> str:
195
195
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
196
196
  )
197
197
  prv, nxt = prev_next_var(node)
198
- return indent + f"{nxt} = etree.tostring({prv}, encoding='unicode')"
198
+ return indent + f"{nxt} = html.tostring({prv}, encoding='unicode')"
199
199
 
200
200
 
201
201
  @CONVERTER(ExprGetHtmlRawAll.kind)
@@ -206,7 +206,7 @@ def pre_html_raw_all(node: ExprGetHtmlRawAll) -> str:
206
206
  prv, nxt = prev_next_var(node)
207
207
  return (
208
208
  indent
209
- + f"{nxt} = [etree.tostring(e, encoding='unicode') for e in {prv}]"
209
+ + f"{nxt} = [html.tostring(e, encoding='unicode') for e in {prv}]"
210
210
  )
211
211
 
212
212
 
@@ -220,9 +220,9 @@ def pre_is_css(node: ExprIsCss) -> str:
220
220
  msg = py_get_classvar_hook_or_value(node, "msg")
221
221
  invert = node.kwargs["invert"]
222
222
 
223
- expr = f"etree.CSSSelector({query})({prv})"
223
+ expr = f"{prv}.cssselect({query})"
224
224
  if invert:
225
- expr = f"not ({expr})"
225
+ expr = f"not {expr}"
226
226
 
227
227
  expr = indent + f"assert {expr}, {msg}"
228
228
  if is_last_var_no_ret(node):
@@ -255,19 +255,19 @@ def pre_has_attr(node: ExprHasAttr) -> str:
255
255
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
256
256
  )
257
257
  prv, nxt = prev_next_var(node)
258
- key, msg = node.unpack_args()
259
258
  key = py_get_classvar_hook_or_value(node, "key")
260
259
  msg = py_get_classvar_hook_or_value(node, "msg")
261
260
  invert = node.kwargs["invert"]
262
261
 
263
- expr = f"{prv}.attrib[{key}]"
262
+ expr = f"{prv}.get({key}) is not None"
264
263
  if invert:
265
264
  expr = f"not ({expr})"
266
265
 
267
266
  expr = indent + f"assert {expr}, {msg}"
268
267
  if is_last_var_no_ret(node):
269
268
  return expr
270
- return "\n".join(expr, indent + f"{nxt} = {prv}")
269
+ # Исправлено: добавлена запятая в join
270
+ return "\n".join([expr, indent + f"{nxt} = {prv}"])
271
271
 
272
272
 
273
273
  @CONVERTER(ExprListHasAttr.kind)
@@ -280,7 +280,7 @@ def pre_list_has_attr(node: ExprListHasAttr) -> str:
280
280
  msg = py_get_classvar_hook_or_value(node, "msg")
281
281
  invert = node.kwargs["invert"]
282
282
 
283
- expr = f"all(i.attrib[{key}] for i in {prv})"
283
+ expr = f"all(i.get({key}) is not None for i in {prv})"
284
284
  if invert:
285
285
  expr = f"not ({expr})"
286
286
 
@@ -318,7 +318,7 @@ def pre_css_remove(node: ExprCssElementRemove) -> str:
318
318
 
319
319
  return (
320
320
  indent
321
- + f"[e.getparent().remove(e) for e in etree.CSSSelector({query})({prv}) if e.getparent() is not None]\n"
321
+ + f"[e.getparent().remove(e) for e in {prv}.cssselect({query}) if e.getparent() is not None]\n"
322
322
  + indent
323
323
  + f"{nxt} = {prv}"
324
324
  )
@@ -365,9 +365,9 @@ def pre_doc_filter_css(node: FilterDocCss) -> str:
365
365
  def pre_doc_filter_has_attr(node: FilterDocHasAttr) -> str:
366
366
  keys = node.kwargs["keys"]
367
367
  if len(keys) == 1:
368
- expr = f"{keys[0]!r} in e.attrib"
368
+ expr = f"e.get({keys[0]!r}) is not None"
369
369
  else:
370
- expr = f"any(i in e.attrib for i in {keys})"
370
+ expr = f"any(e.get(i) is not None for i in {keys})"
371
371
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
372
372
  return "and " + expr
373
373
  return expr
@@ -377,9 +377,9 @@ def pre_doc_filter_has_attr(node: FilterDocHasAttr) -> str:
377
377
  def pre_doc_filter_attr_eq(node: FilterDocAttrEqual) -> str:
378
378
  key, values = node.unpack_args()
379
379
  if len(values) == 1:
380
- expr = f"e.attrib[{key!r}] == {values[0]!r}"
380
+ expr = f"e.get({key!r}) == {values[0]!r}"
381
381
  else:
382
- expr = f"e.attrib[{key!r}] in {values}"
382
+ expr = f"e.get({key!r}) in {values}"
383
383
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
384
384
  return "and " + expr
385
385
  return expr
@@ -389,9 +389,9 @@ def pre_doc_filter_attr_eq(node: FilterDocAttrEqual) -> str:
389
389
  def pre_doc_filter_attr_contains(node: FilterDocAttrContains) -> str:
390
390
  key, values = node.unpack_args()
391
391
  if len(values) == 1:
392
- expr = f"{values[0]!r} in e.attrib[{key!r}]"
392
+ expr = f"{values[0]!r} in (e.get({key!r}) or '')"
393
393
  else:
394
- expr = f"any(i in e.attrib[{key!r}] for i in {values})"
394
+ expr = f"any(i in (e.get({key!r}) or '') for i in {values})"
395
395
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
396
396
  return "and " + expr
397
397
  return expr
@@ -401,10 +401,10 @@ def pre_doc_filter_attr_contains(node: FilterDocAttrContains) -> str:
401
401
  def pre_doc_filter_attr_starts(node: FilterDocAttrContains) -> str:
402
402
  key, values = node.unpack_args()
403
403
  if len(values) == 1:
404
- expr = f"e.attrib[{key!r}].startswith({values[0]!r})"
404
+ expr = f"(e.get({key!r}) or '').startswith({values[0]!r})"
405
405
  else:
406
406
  # str.startswith() arg allow tuple[str, ...]
407
- expr = f"e.attrib[{key!r}].startswith({values})"
407
+ expr = f"(e.get({key!r}) or '').startswith({values})"
408
408
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
409
409
  return "and " + expr
410
410
  return expr
@@ -414,10 +414,10 @@ def pre_doc_filter_attr_starts(node: FilterDocAttrContains) -> str:
414
414
  def pre_doc_filter_attr_ends(node: FilterDocAttrContains) -> str:
415
415
  key, values = node.unpack_args()
416
416
  if len(values) == 1:
417
- expr = f"e.attrib[{key!r}].endswith({values[0]!r})"
417
+ expr = f"(e.get({key!r}) or '').endswith({values[0]!r})"
418
418
  else:
419
419
  # str.endswith() arg allow tuple[str, ...]
420
- expr = f"e.attrib[{key!r}].endswith({values})"
420
+ expr = f"(e.get({key!r}) or '').endswith({values})"
421
421
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
422
422
  return "and " + expr
423
423
  return expr
@@ -427,11 +427,12 @@ def pre_doc_filter_attr_ends(node: FilterDocAttrContains) -> str:
427
427
  def pre_doc_filter_attr_re(node: FilterDocAttrRegex) -> str:
428
428
  key, pattern, ignore_case = node.unpack_args()
429
429
  flags = py_regex_flags(ignore_case)
430
+
431
+ attr_value = f"e.get({key!r}) or ''"
430
432
  if flags:
431
- expr = f"re.search({pattern!r}, e.attrib[{key!r}], {flags})"
433
+ expr = f"re.search({pattern!r}, {attr_value}, {flags})"
432
434
  else:
433
- expr = f"re.search({pattern!r}, e.attrib[{key!r}], ''))"
434
- expr = f"bool({expr})"
435
+ expr = f"re.search({pattern!r}, {attr_value})"
435
436
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
436
437
  return "and " + expr
437
438
  return expr
@@ -442,10 +443,12 @@ def pre_doc_filter_is_regex_text(node: FilterDocIsRegexText) -> str:
442
443
  pattern, ignore_case = node.unpack_args()
443
444
 
444
445
  flags = py_regex_flags(ignore_case)
446
+
447
+ text_content_call = "e.text_content()"
445
448
  if flags:
446
- expr = f"re.search({pattern!r}, ''.join(e.itertext()), {flags})"
449
+ expr = f"re.search({pattern!r}, {text_content_call}, {flags})"
447
450
  else:
448
- expr = f"re.search({pattern!r}, ''.join(e.itertext()))"
451
+ expr = f"re.search({pattern!r}, {text_content_call})"
449
452
  expr = f"bool({expr})"
450
453
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
451
454
  return "and " + expr
@@ -457,10 +460,11 @@ def pre_doc_filter_is_regex_raw(node: FilterDocIsRegexText) -> str:
457
460
  pattern, ignore_case = node.unpack_args()
458
461
 
459
462
  flags = py_regex_flags(ignore_case)
463
+ raw_content_call = "etree.tostring(e, encoding='unicode')"
460
464
  if flags:
461
- expr = f"re.search({pattern!r}, etree.tostring(e, encoding='unicode'), {flags})"
465
+ expr = f"re.search({pattern!r}, {raw_content_call}, {flags})"
462
466
  else:
463
- expr = f"re.search({pattern!r}, etree.tostring(e, encoding='unicode'))"
467
+ expr = f"re.search({pattern!r}, {raw_content_call})"
464
468
  expr = f"bool({expr})"
465
469
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
466
470
  return "and " + expr
@@ -470,10 +474,12 @@ def pre_doc_filter_is_regex_raw(node: FilterDocIsRegexText) -> str:
470
474
  @CONVERTER(FilterDocHasText.kind)
471
475
  def pre_doc_filter_has_text(node: FilterDocHasText) -> str:
472
476
  values = node.kwargs["values"]
477
+
478
+ text_content_call = "e.text_content()"
473
479
  if len(values) == 1:
474
- expr = f"{values[0]!r} in ''.join(e.itertext())"
480
+ expr = f"{values[0]!r} in {text_content_call}"
475
481
  else:
476
- expr = f"any(i in ''.join(e.itertext()) for i in {values})"
482
+ expr = f"any(i in {text_content_call} for i in {values})"
477
483
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
478
484
  return "and " + expr
479
485
  return expr
@@ -482,11 +488,12 @@ def pre_doc_filter_has_text(node: FilterDocHasText) -> str:
482
488
  @CONVERTER(FilterDocHasRaw.kind)
483
489
  def pre_doc_filter_has_raw(node: FilterDocHasText) -> str:
484
490
  values = node.kwargs["values"]
491
+
485
492
  if len(values) == 1:
486
- expr = f"{values[0]!r} in etree.tostring(e, encoding='unicode')"
493
+ expr = f"{values[0]!r} in html.tostring(e, encoding='unicode')"
487
494
  else:
488
495
  expr = (
489
- f"any(i in etree.tostring(e, encoding='unicode') for i in {values})"
496
+ f"any(i in html.tostring(e, encoding='unicode') for i in {values})"
490
497
  )
491
498
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
492
499
  return "and " + expr
@@ -211,7 +211,7 @@ def pre_is_css(node: ExprIsCss) -> str:
211
211
  msg = py_get_classvar_hook_or_value(node, "msg")
212
212
  invert = node.kwargs["invert"]
213
213
 
214
- expr = {prv}.css_first({query})
214
+ expr = f"{prv}.css_first({query})"
215
215
  if invert:
216
216
  expr = f"not {expr}"
217
217
 
@@ -79,7 +79,7 @@ import logging
79
79
  __all__ = ["py_regex_to_lua_pattern"]
80
80
 
81
81
 
82
- LOGGER = logging.getLogger("ssc-gen")
82
+ LOGGER = logging.getLogger("ssc_gen")
83
83
 
84
84
 
85
85
  @dataclass
@@ -26,8 +26,8 @@ def setup_logger(
26
26
  name: str = "ssc_gen",
27
27
  level: int = logging.INFO,
28
28
  colored: bool = True,
29
- fmt: str = "[%(levelname)-8s] %(name)s: %(asctime)s - %(message)s",
30
- datefmt: str = "%Y-%m-%d %H:%M:%S",
29
+ fmt: str = "[%(levelname)-8s] %(name)s: - %(message)s", # %(asctime)s
30
+ datefmt: str | None = None, # "%Y-%m-%d %H:%M:%S",
31
31
  ) -> logging.Logger:
32
32
  logger = logging.getLogger(name)
33
33
  logger.setLevel(level)
@@ -58,7 +58,6 @@ FMT_MAPPING_METHODS = {
58
58
  TokenType.EXPR_LIST_JOIN: "join",
59
59
  TokenType.EXPR_LIST_LEN: "to_len",
60
60
  TokenType.IS_EQUAL: "is_equal",
61
- TokenType.IS_NOT_EQUAL: "is_not_equal",
62
61
  TokenType.IS_CONTAINS: "is_contains",
63
62
  TokenType.IS_CSS: "is_css",
64
63
  TokenType.IS_XPATH: "is_xpath",
File without changes
File without changes
File without changes