ssc_codegen 0.37.2__tar.gz → 0.38.0__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.
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/PKG-INFO +3 -2
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/README.md +2 -1
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/pyproject.toml +1 -1
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/__init__.py +18 -2
- ssc_codegen-0.38.0/ssc_codegen/ast/extension.py +80 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/module.py +5 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/contexts.py +3 -1
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/expressions.py +70 -1
- ssc_codegen-0.38.0/ssc_codegen/core/extensions.py +144 -0
- ssc_codegen-0.38.0/ssc_codegen/core/imports.py +544 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/linter.py +430 -10
- ssc_codegen-0.38.0/ssc_codegen/core/module_handler.py +147 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/reader.py +7 -4
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/struct_parser.py +10 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/type_checking.py +17 -1
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/generation/builder.py +37 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/generation/runtime.py +44 -1
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/main.py +29 -9
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/visitor.py +42 -1
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/visitor.py +50 -6
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/visitor.py +61 -8
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/traversal/utils.py +11 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/traversal/walker.py +2 -0
- ssc_codegen-0.37.2/ssc_codegen/core/module_handler.py +0 -281
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/.gitignore +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/LICENSE +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/function.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/rest.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/struct.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/typedef.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/ast/types.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/format.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/predicates.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/core/rest_artifacts.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/explore.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/generation/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/health.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/naming.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/parsers/curl.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/parsers/http.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/request_spec.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/literals.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/regex.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/rest.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/golang/runtime.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/javascript/rest.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/profile.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/base.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/base.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/python/rest.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/resolver.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/targets/spec.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/traversal/__init__.py +0 -0
- {ssc_codegen-0.37.2 → ssc_codegen-0.38.0}/ssc_codegen/traversal/context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.0
|
|
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
|
|
@@ -42,7 +42,8 @@ Code generator for web scraping parsers. Describe HTML extraction rules in a dec
|
|
|
42
42
|
|
|
43
43
|
- Declarative DSL based on KDL 2.0 syntax
|
|
44
44
|
- Static type checking and linting before code generation
|
|
45
|
-
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API)
|
|
45
|
+
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API), Go (goquery)
|
|
46
|
+
- Typed user extensions with lazy target imports and runtime helpers
|
|
46
47
|
- Struct types: `item`, `list`, `dict`, `table`, `flat`
|
|
47
48
|
- LLM-friendly: system prompt + linter loop for AI-assisted schema generation
|
|
48
49
|
|
|
@@ -10,7 +10,8 @@ Code generator for web scraping parsers. Describe HTML extraction rules in a dec
|
|
|
10
10
|
|
|
11
11
|
- Declarative DSL based on KDL 2.0 syntax
|
|
12
12
|
- Static type checking and linting before code generation
|
|
13
|
-
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API)
|
|
13
|
+
- Multiple output targets: Python (bs4, lxml, parsel, selectolax), JavaScript (DOM API), Go (goquery)
|
|
14
|
+
- Typed user extensions with lazy target imports and runtime helpers
|
|
14
15
|
- Struct types: `item`, `list`, `dict`, `table`, `flat`
|
|
15
16
|
- LLM-friendly: system prompt + linter loop for AI-assisted schema generation
|
|
16
17
|
|
|
@@ -20,7 +20,16 @@ from .module import (
|
|
|
20
20
|
Utilities,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
-
from .function import FunctionDef
|
|
23
|
+
from .function import FunctionDef
|
|
24
|
+
|
|
25
|
+
from .extension import (
|
|
26
|
+
ExtensionCall,
|
|
27
|
+
ExtensionDef,
|
|
28
|
+
ExtensionHelper,
|
|
29
|
+
ExtensionImport,
|
|
30
|
+
ExtensionTarget,
|
|
31
|
+
ExtensionType,
|
|
32
|
+
)
|
|
24
33
|
|
|
25
34
|
from .typedef import TypeDef, TypeDefField
|
|
26
35
|
|
|
@@ -150,7 +159,14 @@ __all__ = [
|
|
|
150
159
|
"Docstring",
|
|
151
160
|
"Utilities",
|
|
152
161
|
# function
|
|
153
|
-
"FunctionDef",
|
|
162
|
+
"FunctionDef",
|
|
163
|
+
# extensions
|
|
164
|
+
"ExtensionCall",
|
|
165
|
+
"ExtensionDef",
|
|
166
|
+
"ExtensionHelper",
|
|
167
|
+
"ExtensionImport",
|
|
168
|
+
"ExtensionTarget",
|
|
169
|
+
"ExtensionType",
|
|
154
170
|
# typedef
|
|
155
171
|
"TypeDef",
|
|
156
172
|
"TypeDefField",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .base import Node
|
|
6
|
+
from .types import TypeInfo, VariableType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ExtensionType:
|
|
11
|
+
"""Type pattern used by an extension operation signature."""
|
|
12
|
+
|
|
13
|
+
base: VariableType | None = None
|
|
14
|
+
generic: str | None = None
|
|
15
|
+
is_array: bool = False
|
|
16
|
+
is_optional: bool = False
|
|
17
|
+
|
|
18
|
+
def resolve(self, binding: TypeInfo | None = None) -> TypeInfo:
|
|
19
|
+
if self.generic:
|
|
20
|
+
if binding is None:
|
|
21
|
+
return TypeInfo(base=VariableType.AUTO)
|
|
22
|
+
return binding
|
|
23
|
+
return TypeInfo(
|
|
24
|
+
base=self.base or VariableType.AUTO,
|
|
25
|
+
is_array=self.is_array,
|
|
26
|
+
is_optional=self.is_optional,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def accepts(self, value: TypeInfo) -> bool:
|
|
30
|
+
if self.generic:
|
|
31
|
+
return True
|
|
32
|
+
return (
|
|
33
|
+
value.base in (self.base, VariableType.AUTO)
|
|
34
|
+
and value.is_array == self.is_array
|
|
35
|
+
and value.is_optional == self.is_optional
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ExtensionImport:
|
|
41
|
+
value: str
|
|
42
|
+
alias: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ExtensionHelper:
|
|
47
|
+
name: str
|
|
48
|
+
source: str
|
|
49
|
+
imports: tuple[ExtensionImport, ...] = ()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class ExtensionTarget:
|
|
54
|
+
language: str
|
|
55
|
+
emit: str
|
|
56
|
+
imports: tuple[ExtensionImport, ...] = ()
|
|
57
|
+
helpers: tuple[ExtensionHelper, ...] = ()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ExtensionDef:
|
|
62
|
+
namespace: str
|
|
63
|
+
name: str
|
|
64
|
+
accept: ExtensionType
|
|
65
|
+
ret: ExtensionType
|
|
66
|
+
targets: dict[str, ExtensionTarget] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def qualified_name(self) -> str:
|
|
70
|
+
return f"{self.namespace}.{self.name}"
|
|
71
|
+
|
|
72
|
+
def resolve_return(self, input_type: TypeInfo) -> TypeInfo:
|
|
73
|
+
binding = input_type if self.accept.generic else None
|
|
74
|
+
return self.ret.resolve(binding)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ExtensionCall(Node):
|
|
79
|
+
qualified_name: str = ""
|
|
80
|
+
definition: ExtensionDef | None = field(default=None, repr=False)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import warnings
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
4
5
|
|
|
5
6
|
from .base import Node
|
|
6
7
|
|
|
@@ -28,6 +29,7 @@ class Module(Node):
|
|
|
28
29
|
|
|
29
30
|
doc: str = ""
|
|
30
31
|
source_file: str = ""
|
|
32
|
+
extensions: dict[str, "ExtensionDef"] = field(default_factory=dict)
|
|
31
33
|
|
|
32
34
|
def __post_init__(self):
|
|
33
35
|
self.body.extend(
|
|
@@ -112,3 +114,6 @@ class Utilities(Node):
|
|
|
112
114
|
"""
|
|
113
115
|
|
|
114
116
|
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
from .extension import ExtensionDef # noqa: E402
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from enum import Enum, auto
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from ssc_codegen.ast import StructBase, VariableType
|
|
9
|
+
from ssc_codegen.ast import ExtensionDef, StructBase, VariableType
|
|
10
10
|
from kdlquery import KdlNode, ReadDiagnostic, Severity
|
|
11
11
|
|
|
12
12
|
|
|
@@ -49,6 +49,7 @@ class ParseContext:
|
|
|
49
49
|
children_defines: dict[str, list[KdlNode]] = field(default_factory=dict)
|
|
50
50
|
structs: dict[str, StructBase] = field(default_factory=dict)
|
|
51
51
|
json_defs: dict[str, JsonDef] = field(default_factory=dict)
|
|
52
|
+
extensions: dict[str, ExtensionDef] = field(default_factory=dict)
|
|
52
53
|
source_path: Path | None = None
|
|
53
54
|
node_source_paths: dict[int, Path] = field(default_factory=dict)
|
|
54
55
|
|
|
@@ -58,6 +59,7 @@ class ParseContext:
|
|
|
58
59
|
| set(self.children_defines)
|
|
59
60
|
| set(self.structs)
|
|
60
61
|
| set(self.json_defs)
|
|
62
|
+
| set(self.extensions)
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
|
|
@@ -16,6 +16,7 @@ from ssc_codegen.ast import (
|
|
|
16
16
|
CssSelect,
|
|
17
17
|
CssSelectAll,
|
|
18
18
|
Fallback,
|
|
19
|
+
ExtensionCall,
|
|
19
20
|
Field,
|
|
20
21
|
Filter,
|
|
21
22
|
Fmt,
|
|
@@ -80,6 +81,7 @@ from ssc_codegen.regex_utils import normalize_regex_pattern
|
|
|
80
81
|
from typing import cast
|
|
81
82
|
|
|
82
83
|
from ssc_codegen.core.contexts import LintContext, ParseContext, WalkCtx
|
|
84
|
+
from ssc_codegen.core.extensions import resolve_extension_call
|
|
83
85
|
from ssc_codegen.core.linter import (
|
|
84
86
|
lint_pipeline_op,
|
|
85
87
|
lint_validate_css,
|
|
@@ -106,6 +108,7 @@ FieldLikeNode: TypeAlias = (
|
|
|
106
108
|
| Field
|
|
107
109
|
| InitField
|
|
108
110
|
| FunctionDef
|
|
111
|
+
| CheckMethod
|
|
109
112
|
)
|
|
110
113
|
|
|
111
114
|
|
|
@@ -343,6 +346,9 @@ def _build_expression(
|
|
|
343
346
|
f"'self {ref_name}' syntax is no longer supported; use '@{ref_name}' instead"
|
|
344
347
|
)
|
|
345
348
|
|
|
349
|
+
if name.startswith("!"):
|
|
350
|
+
return _expr_extension(node, parent, ctx, lint)
|
|
351
|
+
|
|
346
352
|
# dispatch
|
|
347
353
|
handler = _EXPRESSION_HANDLERS.get(name)
|
|
348
354
|
if handler is None:
|
|
@@ -417,6 +423,13 @@ def parse_expressions(
|
|
|
417
423
|
raise BuildTimeError(
|
|
418
424
|
f"'self {ref_name}' syntax is no longer supported; use '@{ref_name}' instead"
|
|
419
425
|
)
|
|
426
|
+
if node.name.startswith("!"):
|
|
427
|
+
lint.push(node.name)
|
|
428
|
+
lint_pipeline_op(node, lint)
|
|
429
|
+
extension_expr = _expr_extension(node, parent, ctx, lint)
|
|
430
|
+
parent.body.append(extension_expr)
|
|
431
|
+
lint.pop()
|
|
432
|
+
continue
|
|
420
433
|
handler = _EXPRESSION_HANDLERS.get(node.name)
|
|
421
434
|
if handler is None:
|
|
422
435
|
lint_wildcard_op(node, ctx, lint)
|
|
@@ -501,7 +514,10 @@ def parse_expressions(
|
|
|
501
514
|
break
|
|
502
515
|
parent.is_array = is_arr
|
|
503
516
|
parent.ret_type_info = TypeInfo(
|
|
504
|
-
base=last_ret,
|
|
517
|
+
base=last_ret,
|
|
518
|
+
is_array=is_arr,
|
|
519
|
+
is_optional=last_ti.is_optional,
|
|
520
|
+
ref=ref or last_ti.ref,
|
|
505
521
|
)
|
|
506
522
|
|
|
507
523
|
lint.walk_context = prev_ctx
|
|
@@ -537,6 +553,59 @@ def _prev_type_info(
|
|
|
537
553
|
)
|
|
538
554
|
|
|
539
555
|
|
|
556
|
+
def _expr_extension(
|
|
557
|
+
node: KdlNode,
|
|
558
|
+
parent: FieldLikeNode,
|
|
559
|
+
ctx: ParseContext,
|
|
560
|
+
lint: LintContext,
|
|
561
|
+
) -> ExtensionCall:
|
|
562
|
+
ref = node.name[1:]
|
|
563
|
+
if node.args or node.properties or node.children:
|
|
564
|
+
lint.error(
|
|
565
|
+
node,
|
|
566
|
+
message=f"custom operation '!{ref}' does not accept entries",
|
|
567
|
+
code="E001",
|
|
568
|
+
)
|
|
569
|
+
definition = ctx.extensions.get(ref)
|
|
570
|
+
prev = _prev_type_info(parent)
|
|
571
|
+
if definition is None:
|
|
572
|
+
lint.error(
|
|
573
|
+
node,
|
|
574
|
+
message=f"unknown extension operation '{ref}'",
|
|
575
|
+
code="E300",
|
|
576
|
+
hint="import its extension namespace and check the operation name",
|
|
577
|
+
)
|
|
578
|
+
return ExtensionCall(
|
|
579
|
+
parent=parent,
|
|
580
|
+
qualified_name=ref,
|
|
581
|
+
accept_type_info=prev.ret_type_info,
|
|
582
|
+
ret_type_info=TypeInfo(base=VariableType.AUTO),
|
|
583
|
+
)
|
|
584
|
+
if not definition.accept.accepts(prev.ret_type_info):
|
|
585
|
+
expected = (
|
|
586
|
+
definition.accept.generic
|
|
587
|
+
or (definition.accept.base or VariableType.AUTO).name
|
|
588
|
+
)
|
|
589
|
+
lint.error(
|
|
590
|
+
node,
|
|
591
|
+
message=(
|
|
592
|
+
f"custom operation '{ref}' does not accept "
|
|
593
|
+
f"{prev.ret_type_info.base.name}; expected {expected}"
|
|
594
|
+
),
|
|
595
|
+
code="E100",
|
|
596
|
+
)
|
|
597
|
+
ret_type = resolve_extension_call(definition, prev.ret_type_info)
|
|
598
|
+
return ExtensionCall(
|
|
599
|
+
parent=parent,
|
|
600
|
+
qualified_name=ref,
|
|
601
|
+
definition=definition,
|
|
602
|
+
accept_type_info=prev.ret_type_info,
|
|
603
|
+
ret_type_info=ret_type,
|
|
604
|
+
is_array=ret_type.is_array,
|
|
605
|
+
span=node.span,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
540
609
|
# Ops whose handlers access ``parent.body[-1]`` unconditionally and therefore
|
|
541
610
|
# require a preceding node. Used by ``parse_expressions`` to emit a clean
|
|
542
611
|
# E100 diagnostic instead of crashing with IndexError on an empty pipeline.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Parsing and type resolution for user-defined pipeline extensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from kdlquery import KdlNode
|
|
6
|
+
|
|
7
|
+
from ssc_codegen.ast import (
|
|
8
|
+
ExtensionDef,
|
|
9
|
+
ExtensionHelper,
|
|
10
|
+
ExtensionImport,
|
|
11
|
+
ExtensionTarget,
|
|
12
|
+
ExtensionType,
|
|
13
|
+
Module,
|
|
14
|
+
TypeInfo,
|
|
15
|
+
VariableType,
|
|
16
|
+
)
|
|
17
|
+
from ssc_codegen.core.contexts import LintContext, ParseContext
|
|
18
|
+
from ssc_codegen.exceptions import BuildTimeError
|
|
19
|
+
|
|
20
|
+
_TYPE_NAMES: dict[str, VariableType] = {
|
|
21
|
+
"bool": VariableType.BOOL,
|
|
22
|
+
"doc": VariableType.DOCUMENT,
|
|
23
|
+
"float": VariableType.FLOAT,
|
|
24
|
+
"int": VariableType.INT,
|
|
25
|
+
"str": VariableType.STRING,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_extension_type(value) -> ExtensionType:
|
|
30
|
+
name = str(value.value)
|
|
31
|
+
is_optional = name.endswith("?")
|
|
32
|
+
if is_optional:
|
|
33
|
+
name = name[:-1]
|
|
34
|
+
is_array = value.type_annotation == "(array)"
|
|
35
|
+
if name == "T":
|
|
36
|
+
if is_array or is_optional:
|
|
37
|
+
raise BuildTimeError("generic type T cannot have modifiers")
|
|
38
|
+
return ExtensionType(generic="T")
|
|
39
|
+
base = _TYPE_NAMES.get(name)
|
|
40
|
+
if base is None:
|
|
41
|
+
raise BuildTimeError(f"unknown extension signature type: {name}")
|
|
42
|
+
return ExtensionType(base=base, is_array=is_array, is_optional=is_optional)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_extension_call(
|
|
46
|
+
definition: ExtensionDef, input_type: TypeInfo
|
|
47
|
+
) -> TypeInfo:
|
|
48
|
+
if not definition.accept.accepts(input_type):
|
|
49
|
+
return TypeInfo(base=VariableType.AUTO)
|
|
50
|
+
return definition.resolve_return(input_type)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_import(node: KdlNode) -> ExtensionImport:
|
|
54
|
+
return ExtensionImport(
|
|
55
|
+
value=str(node.args[0].value),
|
|
56
|
+
alias=str(node.get_prop("alias") or ""),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_helper(node: KdlNode) -> ExtensionHelper:
|
|
61
|
+
name = str(node.args[0].value)
|
|
62
|
+
imports = tuple(
|
|
63
|
+
_parse_import(child)
|
|
64
|
+
for child in node.children
|
|
65
|
+
if child.name == "import" and child.args
|
|
66
|
+
)
|
|
67
|
+
source_node = next(
|
|
68
|
+
(child for child in node.children if child.name == "source"), None
|
|
69
|
+
)
|
|
70
|
+
source = (
|
|
71
|
+
str(source_node.args[0].value)
|
|
72
|
+
if source_node is not None and source_node.args
|
|
73
|
+
else ""
|
|
74
|
+
)
|
|
75
|
+
return ExtensionHelper(name=name, source=source, imports=imports)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_target(node: KdlNode) -> ExtensionTarget:
|
|
79
|
+
imports = tuple(
|
|
80
|
+
_parse_import(child)
|
|
81
|
+
for child in node.children
|
|
82
|
+
if child.name == "import" and child.args
|
|
83
|
+
)
|
|
84
|
+
helpers = tuple(
|
|
85
|
+
_parse_helper(child)
|
|
86
|
+
for child in node.children
|
|
87
|
+
if child.name == "helper" and child.args
|
|
88
|
+
)
|
|
89
|
+
emit_node = next(
|
|
90
|
+
(child for child in node.children if child.name == "emit"), None
|
|
91
|
+
)
|
|
92
|
+
emit = (
|
|
93
|
+
str(emit_node.args[0].value)
|
|
94
|
+
if emit_node is not None and emit_node.args
|
|
95
|
+
else ""
|
|
96
|
+
)
|
|
97
|
+
return ExtensionTarget(
|
|
98
|
+
language=node.name,
|
|
99
|
+
emit=emit,
|
|
100
|
+
imports=imports,
|
|
101
|
+
helpers=helpers,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def handle_extension(
|
|
106
|
+
node: KdlNode,
|
|
107
|
+
module: Module,
|
|
108
|
+
ctx: ParseContext,
|
|
109
|
+
lint: LintContext,
|
|
110
|
+
) -> list[ExtensionDef]:
|
|
111
|
+
if not node.args:
|
|
112
|
+
raise BuildTimeError("'extension' requires a namespace")
|
|
113
|
+
namespace = str(node.args[0].value)
|
|
114
|
+
definitions: list[ExtensionDef] = []
|
|
115
|
+
for operation in node.children:
|
|
116
|
+
sig = next(
|
|
117
|
+
(child for child in operation.children if child.name == "sig"),
|
|
118
|
+
None,
|
|
119
|
+
)
|
|
120
|
+
if sig is None or len(sig.args) != 2:
|
|
121
|
+
continue
|
|
122
|
+
definition = ExtensionDef(
|
|
123
|
+
namespace=namespace,
|
|
124
|
+
name=operation.name,
|
|
125
|
+
accept=parse_extension_type(sig.args[0]),
|
|
126
|
+
ret=parse_extension_type(sig.args[1]),
|
|
127
|
+
targets={
|
|
128
|
+
child.name: _parse_target(child)
|
|
129
|
+
for child in operation.children
|
|
130
|
+
if child.name in ("py", "js", "go")
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
qualified_name = definition.qualified_name
|
|
134
|
+
if qualified_name in ctx.extensions:
|
|
135
|
+
lint.error(
|
|
136
|
+
operation,
|
|
137
|
+
message=f"duplicate extension operation '{qualified_name}'",
|
|
138
|
+
code="E402",
|
|
139
|
+
)
|
|
140
|
+
continue
|
|
141
|
+
ctx.extensions[qualified_name] = definition
|
|
142
|
+
module.extensions[qualified_name] = definition
|
|
143
|
+
definitions.append(definition)
|
|
144
|
+
return definitions
|