ssc_codegen 0.13.1__tar.gz → 0.13.3__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 (63) hide show
  1. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/PKG-INFO +1 -1
  2. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/pyproject.toml +1 -1
  3. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/__init__.py +1 -1
  4. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_build/main.py +4 -1
  5. ssc_codegen-0.13.3/ssc_codegen/ast_grep_rules/py_drop_prefix_suffix_backport.yml +47 -0
  6. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/ast_grep.py +3 -0
  7. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/main.py +18 -1
  8. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/py_lxml.py +86 -48
  9. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/lua_re_compat.py +1 -1
  10. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/logs.py +2 -2
  11. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/.gitignore +0 -0
  12. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/LICENSE +0 -0
  13. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/README.md +0 -0
  14. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/_compat.py +0 -0
  15. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/__init__.py +0 -0
  16. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/base.py +0 -0
  17. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_array.py +0 -0
  18. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_cast.py +0 -0
  19. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_core.py +0 -0
  20. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_filter.py +0 -0
  21. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_selectors.py +0 -0
  22. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_string.py +0 -0
  23. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_/nodes_validate.py +0 -0
  24. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_build/__init__.py +0 -0
  25. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_build/builder.py +0 -0
  26. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_build/utils.py +0 -0
  27. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_grep_rules/js_rules.yml +0 -0
  28. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/ast_grep_rules/py_rules.yml +0 -0
  29. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/__init__.py +0 -0
  30. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/cli_callbacks.py +0 -0
  31. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/cli_utils.py +0 -0
  32. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/code_callbacks.py +0 -0
  33. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/consts.py +0 -0
  34. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/cli/runtime_parse_runners.py +0 -0
  35. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/compiler.py +0 -0
  36. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/__init__.py +0 -0
  37. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/base.py +0 -0
  38. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/go_goquery.py +0 -0
  39. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/helpers.py +0 -0
  40. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/js_pure.py +0 -0
  41. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/lua_htmlparser.py +0 -0
  42. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/py_base.py +0 -0
  43. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/py_bs4.py +0 -0
  44. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/py_parsel.py +0 -0
  45. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/py_selectolax.py +0 -0
  46. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/__init__.py +0 -0
  47. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/go_goquery.py +0 -0
  48. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/js_pure.py +0 -0
  49. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/lua_base.py +0 -0
  50. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/lua_css_compat.py +0 -0
  51. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/converters/templates/py_base.py +0 -0
  52. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/document.py +0 -0
  53. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/document_utlis.py +0 -0
  54. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/json_struct.py +0 -0
  55. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/json_to_scc.py +0 -0
  56. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/pseudo_selectors.py +0 -0
  57. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/schema.py +0 -0
  58. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/selector_utils.py +0 -0
  59. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/static_checker/__init__.py +0 -0
  60. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/static_checker/base.py +0 -0
  61. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/static_checker/callbacks.py +0 -0
  62. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/ssc_codegen/str_utils.py +0 -0
  63. {ssc_codegen-0.13.1 → ssc_codegen-0.13.3}/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.1
3
+ Version: 0.13.3
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.1"
3
+ version = "0.13.3"
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.1"
10
+ VERSION = "0.13.3"
11
11
 
12
12
 
13
13
  class __MISSING(object):
@@ -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,7 @@ 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(
8
+ RULES_PATH.joinpath("py_drop_prefix_suffix_backport.yml")
9
+ )
7
10
  JS_RULES = str(RULES_PATH.joinpath("js_rules.yml"))
@@ -11,7 +11,11 @@ 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 (
15
+ JS_RULES,
16
+ PY_RULES,
17
+ PY_NEW_RM_PREFIX_SUFFIX_SYNTAX,
18
+ )
15
19
  from ssc_codegen.cli.cli_callbacks import cb_check_ssc_files, cb_folder_out
16
20
  from ssc_codegen.cli.cli_utils import (
17
21
  ConverterLike,
@@ -168,6 +172,14 @@ def gen_py(
168
172
  docstring: Annotated[
169
173
  bool, Option(help=HELP_DOCSTRING, is_flag=True)
170
174
  ] = True,
175
+ fmt_prefix_suffix_backport: Annotated[
176
+ bool,
177
+ Option(
178
+ "-frps",
179
+ "--fmt-remove-prefix-suffix",
180
+ help="[PY39+]: replace backport funcs to str.removeprefix(...) and str.removesuffix(...)",
181
+ ),
182
+ ] = False,
171
183
  ) -> None:
172
184
  converter = import_converter(f"py_{lib.value}")
173
185
  if fmt:
@@ -176,6 +188,11 @@ def gen_py(
176
188
  "ruff check {} --fix",
177
189
  f"ast-grep scan --rule {PY_RULES} -U " + "{}",
178
190
  ]
191
+ if fmt_prefix_suffix_backport:
192
+ commands.append(
193
+ f"ast-grep scan --rule {PY_NEW_RM_PREFIX_SUFFIX_SYNTAX} -U "
194
+ + "{}"
195
+ )
179
196
  fmt_cmd = create_fmt_cmd(ssc_files, prefix, suffix, out, commands)
180
197
  else:
181
198
  fmt_cmd = []
@@ -62,22 +62,44 @@ from ssc_codegen.converters.templates.py_base import IMPORTS_MIN
62
62
 
63
63
  CONVERTER = BasePyCodeConverter()
64
64
 
65
+ """
66
+ # HtmlElement → сразу принимаем
67
+ if isinstance(document, html.HtmlElement):
68
+ self._document = document
69
+ return
70
+
71
+ # Строка → пробуем парсить
72
+ if isinstance(document, str):
73
+ try:
74
+ self._document = html.fromstring(document.strip() or self.FALLBACK_HTML)
75
+ return
76
+ except (ParserError, ValueError):
77
+ pass
78
+
79
+ # Всё остальное или ошибка → заглушка
80
+ self._document = html.fromstring(self.FALLBACK_HTML)
81
+ """
82
+
65
83
 
66
84
  @CONVERTER(StructInitMethod.kind)
67
85
  def pre_init(_node: StructInitMethod) -> str:
68
- return (
86
+ parts = [
69
87
  INDENT_METHOD
70
- + "def __init__(self, document: Union[str, etree._Element]) -> None:\n"
71
- + INDENT_METHOD_BODY
72
- + "self._document = etree.HTML(document) if isinstance(document, str) else document"
73
- )
88
+ + "def __init__(self, document: Union[str, html.HtmlElement]) -> None:",
89
+ INDENT_METHOD * 2 + "if isinstance(document, html.HtmlElement):",
90
+ INDENT_METHOD * 3 + "self._document = document",
91
+ INDENT_METHOD * 2 + "elif isinstance(document, str):",
92
+ INDENT_METHOD * 3
93
+ + "self._document = html.fromstring(document.strip() or FALLBACK_HTML_STR)",
94
+ # in codegen, passed only str or html.HtmlElement objects
95
+ ]
96
+ return "\n".join(parts)
74
97
 
75
98
 
76
99
  @CONVERTER(StructPreValidateMethod.kind)
77
100
  def pre_struct_pre_validate(_node: StructPreValidateMethod) -> str:
78
101
  return (
79
- INDENT_METHOD
80
- + "def _pre_validate(self, v: Union[etree._Element, List[etree._Element]]) -> None:"
102
+ INDENT_METHOD + "def _pre_validate(self, v: html.HtmlElement) -> None:"
81
103
  )
82
104
 
83
105
 
@@ -85,7 +107,7 @@ def pre_struct_pre_validate(_node: StructPreValidateMethod) -> str:
85
107
  def pre_struct_part_doc_method(_node: StructPartDocMethod) -> str:
86
108
  return (
87
109
  INDENT_METHOD
88
- + "def _split_doc(self, v: Union[etree._Element, List[etree._Element]]) -> List[etree._Element]:"
110
+ + "def _split_doc(self, v: html.HtmlElement) -> List[html.HtmlElement]:"
89
111
  )
90
112
 
91
113
 
@@ -96,13 +118,21 @@ def pre_struct_field_method(node: StructFieldMethod) -> str:
96
118
  name = MAGIC_METHODS.get(name, name)
97
119
  return (
98
120
  INDENT_METHOD
99
- + f"def _parse_{name}(self, v: Union[etree._Element, List[etree._Element]]) -> {type_}:"
121
+ + f"def _parse_{name}(self, v: html.HtmlElement) -> {type_}:"
100
122
  )
101
123
 
102
124
 
103
125
  @CONVERTER(ModuleImports.kind)
104
126
  def pre_imports(_: ModuleImports) -> str:
105
- return IMPORTS_MIN + """from lxml import etree, html"""
127
+ # lxml throw parse error if passed empty string
128
+ # most mainsteram html parser libs use `FALLBACK_HTML_STR` stub value
129
+ return (
130
+ IMPORTS_MIN
131
+ + """from lxml import html, etree
132
+
133
+ FALLBACK_HTML_STR = "<html><body></body></html>"
134
+ """
135
+ )
106
136
 
107
137
 
108
138
  @CONVERTER(ExprCss.kind)
@@ -112,7 +142,7 @@ def pre_css(node: ExprCss) -> str:
112
142
  )
113
143
  prv, nxt = prev_next_var(node)
114
144
  query = py_get_classvar_hook_or_value(node, "query")
115
- return indent + f"{nxt} = etree.CSSSelector({query})({prv})[0]"
145
+ return indent + f"{nxt} = {prv}.cssselect({query})[0]"
116
146
 
117
147
 
118
148
  @CONVERTER(ExprCssAll.kind)
@@ -122,7 +152,7 @@ def pre_css_all(node: ExprCssAll) -> str:
122
152
  )
123
153
  prv, nxt = prev_next_var(node)
124
154
  query = py_get_classvar_hook_or_value(node, "query")
125
- return indent + f"{nxt} = etree.CSSSelector({query})({prv})"
155
+ return indent + f"{nxt} = {prv}.cssselect({query})"
126
156
 
127
157
 
128
158
  @CONVERTER(ExprXpath.kind)
@@ -154,8 +184,9 @@ def pre_html_attr(node: ExprGetHtmlAttr) -> str:
154
184
  keys = node.kwargs["key"]
155
185
  if len(keys) == 1:
156
186
  key = keys[0]
157
- return indent + f"{nxt} = {prv}.attrib[{key!r}]"
158
- return indent + f"{nxt} = [{prv}.attrib[k] for k in {keys}]"
187
+ return indent + f"{nxt} = {prv}.get({key!r})"
188
+ key_accesses = [f"{prv}.get({k!r})" for k in keys]
189
+ return indent + f"{nxt} = [{', '.join(key_accesses)}]"
159
190
 
160
191
 
161
192
  @CONVERTER(ExprGetHtmlAttrAll.kind)
@@ -167,8 +198,8 @@ def pre_html_attr_all(node: ExprGetHtmlAttrAll) -> str:
167
198
  keys = node.kwargs["key"]
168
199
  if len(keys) == 1:
169
200
  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}]"
201
+ return indent + f"{nxt} = [e.get({key!r}) for e in {prv}]"
202
+ return indent + f"{nxt} = [e.get(k) for e in {prv} for k in {keys}]"
172
203
 
173
204
 
174
205
  @CONVERTER(ExprGetHtmlText.kind)
@@ -177,7 +208,7 @@ def pre_html_text(node: ExprGetHtmlText) -> str:
177
208
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
178
209
  )
179
210
  prv, nxt = prev_next_var(node)
180
- return indent + f"{nxt} = ''.join({prv}.itertext())"
211
+ return indent + f"{nxt} = {prv}.text_content()"
181
212
 
182
213
 
183
214
  @CONVERTER(ExprGetHtmlTextAll.kind)
@@ -186,7 +217,7 @@ def pre_html_text_all(node: ExprGetHtmlTextAll) -> str:
186
217
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
187
218
  )
188
219
  prv, nxt = prev_next_var(node)
189
- return indent + f"{nxt} = [text for e in {prv} for text in e.itertext()]"
220
+ return indent + f"{nxt} = [e.text_content() for e in {prv}]"
190
221
 
191
222
 
192
223
  @CONVERTER(ExprGetHtmlRaw.kind)
@@ -195,7 +226,7 @@ def pre_html_raw(node: ExprGetHtmlRaw) -> str:
195
226
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
196
227
  )
197
228
  prv, nxt = prev_next_var(node)
198
- return indent + f"{nxt} = etree.tostring({prv}, encoding='unicode')"
229
+ return indent + f"{nxt} = html.tostring({prv}, encoding='unicode')"
199
230
 
200
231
 
201
232
  @CONVERTER(ExprGetHtmlRawAll.kind)
@@ -206,7 +237,7 @@ def pre_html_raw_all(node: ExprGetHtmlRawAll) -> str:
206
237
  prv, nxt = prev_next_var(node)
207
238
  return (
208
239
  indent
209
- + f"{nxt} = [etree.tostring(e, encoding='unicode') for e in {prv}]"
240
+ + f"{nxt} = [html.tostring(e, encoding='unicode') for e in {prv}]"
210
241
  )
211
242
 
212
243
 
@@ -220,9 +251,9 @@ def pre_is_css(node: ExprIsCss) -> str:
220
251
  msg = py_get_classvar_hook_or_value(node, "msg")
221
252
  invert = node.kwargs["invert"]
222
253
 
223
- expr = f"etree.CSSSelector({query})({prv})"
254
+ expr = f"{prv}.cssselect({query})"
224
255
  if invert:
225
- expr = f"not ({expr})"
256
+ expr = f"not {expr}"
226
257
 
227
258
  expr = indent + f"assert {expr}, {msg}"
228
259
  if is_last_var_no_ret(node):
@@ -255,19 +286,19 @@ def pre_has_attr(node: ExprHasAttr) -> str:
255
286
  INDENT_DEFAULT_BODY if have_default_expr(node) else INDENT_METHOD_BODY
256
287
  )
257
288
  prv, nxt = prev_next_var(node)
258
- key, msg = node.unpack_args()
259
289
  key = py_get_classvar_hook_or_value(node, "key")
260
290
  msg = py_get_classvar_hook_or_value(node, "msg")
261
291
  invert = node.kwargs["invert"]
262
292
 
263
- expr = f"{prv}.attrib[{key}]"
293
+ expr = f"{prv}.get({key}) is not None"
264
294
  if invert:
265
295
  expr = f"not ({expr})"
266
296
 
267
297
  expr = indent + f"assert {expr}, {msg}"
268
298
  if is_last_var_no_ret(node):
269
299
  return expr
270
- return "\n".join(expr, indent + f"{nxt} = {prv}")
300
+ # Исправлено: добавлена запятая в join
301
+ return "\n".join([expr, indent + f"{nxt} = {prv}"])
271
302
 
272
303
 
273
304
  @CONVERTER(ExprListHasAttr.kind)
@@ -280,7 +311,7 @@ def pre_list_has_attr(node: ExprListHasAttr) -> str:
280
311
  msg = py_get_classvar_hook_or_value(node, "msg")
281
312
  invert = node.kwargs["invert"]
282
313
 
283
- expr = f"all(i.attrib[{key}] for i in {prv})"
314
+ expr = f"all(i.get({key}) is not None for i in {prv})"
284
315
  if invert:
285
316
  expr = f"not ({expr})"
286
317
 
@@ -318,7 +349,7 @@ def pre_css_remove(node: ExprCssElementRemove) -> str:
318
349
 
319
350
  return (
320
351
  indent
321
- + f"[e.getparent().remove(e) for e in etree.CSSSelector({query})({prv}) if e.getparent() is not None]\n"
352
+ + f"[e.getparent().remove(e) for e in {prv}.cssselect({query}) if e.getparent() is not None]\n"
322
353
  + indent
323
354
  + f"{nxt} = {prv}"
324
355
  )
@@ -365,9 +396,9 @@ def pre_doc_filter_css(node: FilterDocCss) -> str:
365
396
  def pre_doc_filter_has_attr(node: FilterDocHasAttr) -> str:
366
397
  keys = node.kwargs["keys"]
367
398
  if len(keys) == 1:
368
- expr = f"{keys[0]!r} in e.attrib"
399
+ expr = f"e.get({keys[0]!r}) is not None"
369
400
  else:
370
- expr = f"any(i in e.attrib for i in {keys})"
401
+ expr = f"any(e.get(i) is not None for i in {keys})"
371
402
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
372
403
  return "and " + expr
373
404
  return expr
@@ -377,9 +408,9 @@ def pre_doc_filter_has_attr(node: FilterDocHasAttr) -> str:
377
408
  def pre_doc_filter_attr_eq(node: FilterDocAttrEqual) -> str:
378
409
  key, values = node.unpack_args()
379
410
  if len(values) == 1:
380
- expr = f"e.attrib[{key!r}] == {values[0]!r}"
411
+ expr = f"e.get({key!r}) == {values[0]!r}"
381
412
  else:
382
- expr = f"e.attrib[{key!r}] in {values}"
413
+ expr = f"e.get({key!r}) in {values}"
383
414
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
384
415
  return "and " + expr
385
416
  return expr
@@ -389,9 +420,9 @@ def pre_doc_filter_attr_eq(node: FilterDocAttrEqual) -> str:
389
420
  def pre_doc_filter_attr_contains(node: FilterDocAttrContains) -> str:
390
421
  key, values = node.unpack_args()
391
422
  if len(values) == 1:
392
- expr = f"{values[0]!r} in e.attrib[{key!r}]"
423
+ expr = f"{values[0]!r} in (e.get({key!r}) or '')"
393
424
  else:
394
- expr = f"any(i in e.attrib[{key!r}] for i in {values})"
425
+ expr = f"any(i in (e.get({key!r}) or '') for i in {values})"
395
426
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
396
427
  return "and " + expr
397
428
  return expr
@@ -401,10 +432,10 @@ def pre_doc_filter_attr_contains(node: FilterDocAttrContains) -> str:
401
432
  def pre_doc_filter_attr_starts(node: FilterDocAttrContains) -> str:
402
433
  key, values = node.unpack_args()
403
434
  if len(values) == 1:
404
- expr = f"e.attrib[{key!r}].startswith({values[0]!r})"
435
+ expr = f"(e.get({key!r}) or '').startswith({values[0]!r})"
405
436
  else:
406
437
  # str.startswith() arg allow tuple[str, ...]
407
- expr = f"e.attrib[{key!r}].startswith({values})"
438
+ expr = f"(e.get({key!r}) or '').startswith({values})"
408
439
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
409
440
  return "and " + expr
410
441
  return expr
@@ -414,10 +445,10 @@ def pre_doc_filter_attr_starts(node: FilterDocAttrContains) -> str:
414
445
  def pre_doc_filter_attr_ends(node: FilterDocAttrContains) -> str:
415
446
  key, values = node.unpack_args()
416
447
  if len(values) == 1:
417
- expr = f"e.attrib[{key!r}].endswith({values[0]!r})"
448
+ expr = f"(e.get({key!r}) or '').endswith({values[0]!r})"
418
449
  else:
419
450
  # str.endswith() arg allow tuple[str, ...]
420
- expr = f"e.attrib[{key!r}].endswith({values})"
451
+ expr = f"(e.get({key!r}) or '').endswith({values})"
421
452
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
422
453
  return "and " + expr
423
454
  return expr
@@ -427,11 +458,12 @@ def pre_doc_filter_attr_ends(node: FilterDocAttrContains) -> str:
427
458
  def pre_doc_filter_attr_re(node: FilterDocAttrRegex) -> str:
428
459
  key, pattern, ignore_case = node.unpack_args()
429
460
  flags = py_regex_flags(ignore_case)
461
+
462
+ attr_value = f"e.get({key!r}) or ''"
430
463
  if flags:
431
- expr = f"re.search({pattern!r}, e.attrib[{key!r}], {flags})"
464
+ expr = f"re.search({pattern!r}, {attr_value}, {flags})"
432
465
  else:
433
- expr = f"re.search({pattern!r}, e.attrib[{key!r}], ''))"
434
- expr = f"bool({expr})"
466
+ expr = f"re.search({pattern!r}, {attr_value})"
435
467
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
436
468
  return "and " + expr
437
469
  return expr
@@ -442,10 +474,12 @@ def pre_doc_filter_is_regex_text(node: FilterDocIsRegexText) -> str:
442
474
  pattern, ignore_case = node.unpack_args()
443
475
 
444
476
  flags = py_regex_flags(ignore_case)
477
+
478
+ text_content_call = "e.text_content()"
445
479
  if flags:
446
- expr = f"re.search({pattern!r}, ''.join(e.itertext()), {flags})"
480
+ expr = f"re.search({pattern!r}, {text_content_call}, {flags})"
447
481
  else:
448
- expr = f"re.search({pattern!r}, ''.join(e.itertext()))"
482
+ expr = f"re.search({pattern!r}, {text_content_call})"
449
483
  expr = f"bool({expr})"
450
484
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
451
485
  return "and " + expr
@@ -457,10 +491,11 @@ def pre_doc_filter_is_regex_raw(node: FilterDocIsRegexText) -> str:
457
491
  pattern, ignore_case = node.unpack_args()
458
492
 
459
493
  flags = py_regex_flags(ignore_case)
494
+ raw_content_call = "etree.tostring(e, encoding='unicode')"
460
495
  if flags:
461
- expr = f"re.search({pattern!r}, etree.tostring(e, encoding='unicode'), {flags})"
496
+ expr = f"re.search({pattern!r}, {raw_content_call}, {flags})"
462
497
  else:
463
- expr = f"re.search({pattern!r}, etree.tostring(e, encoding='unicode'))"
498
+ expr = f"re.search({pattern!r}, {raw_content_call})"
464
499
  expr = f"bool({expr})"
465
500
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
466
501
  return "and " + expr
@@ -470,10 +505,12 @@ def pre_doc_filter_is_regex_raw(node: FilterDocIsRegexText) -> str:
470
505
  @CONVERTER(FilterDocHasText.kind)
471
506
  def pre_doc_filter_has_text(node: FilterDocHasText) -> str:
472
507
  values = node.kwargs["values"]
508
+
509
+ text_content_call = "e.text_content()"
473
510
  if len(values) == 1:
474
- expr = f"{values[0]!r} in ''.join(e.itertext())"
511
+ expr = f"{values[0]!r} in {text_content_call}"
475
512
  else:
476
- expr = f"any(i in ''.join(e.itertext()) for i in {values})"
513
+ expr = f"any(i in {text_content_call} for i in {values})"
477
514
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
478
515
  return "and " + expr
479
516
  return expr
@@ -482,11 +519,12 @@ def pre_doc_filter_has_text(node: FilterDocHasText) -> str:
482
519
  @CONVERTER(FilterDocHasRaw.kind)
483
520
  def pre_doc_filter_has_raw(node: FilterDocHasText) -> str:
484
521
  values = node.kwargs["values"]
522
+
485
523
  if len(values) == 1:
486
- expr = f"{values[0]!r} in etree.tostring(e, encoding='unicode')"
524
+ expr = f"{values[0]!r} in html.tostring(e, encoding='unicode')"
487
525
  else:
488
526
  expr = (
489
- f"any(i in etree.tostring(e, encoding='unicode') for i in {values})"
527
+ f"any(i in html.tostring(e, encoding='unicode') for i in {values})"
490
528
  )
491
529
  if not is_first_node_cond(node) and is_prev_node_atomic_cond(node):
492
530
  return "and " + expr
@@ -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)
File without changes
File without changes
File without changes