pyscriptc 0.1.0__py3-none-any.whl
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.
- pyscriptc/__init__.py +3 -0
- pyscriptc/__main__.py +3 -0
- pyscriptc/_posthog_token.py +1 -0
- pyscriptc/builtins.py +156 -0
- pyscriptc/canonical.py +167 -0
- pyscriptc/check.py +49 -0
- pyscriptc/cli.py +271 -0
- pyscriptc/coverage.py +388 -0
- pyscriptc/debug.py +120 -0
- pyscriptc/diagnostics.py +179 -0
- pyscriptc/errors.py +56 -0
- pyscriptc/ir.py +365 -0
- pyscriptc/llvm.py +2129 -0
- pyscriptc/log.py +127 -0
- pyscriptc/lower/__init__.py +1071 -0
- pyscriptc/lower/_calls.py +1499 -0
- pyscriptc/lower/_classes.py +969 -0
- pyscriptc/lower/_core.py +301 -0
- pyscriptc/lower/_expr.py +886 -0
- pyscriptc/lower/_stdlib.py +491 -0
- pyscriptc/lower/_stdlib_shared.py +93 -0
- pyscriptc/lower/_stmt.py +1173 -0
- pyscriptc/package.py +249 -0
- pyscriptc/restlower.py +1008 -0
- pyscriptc/runtime/ps_argv.c +28 -0
- pyscriptc/runtime/ps_b64.c +168 -0
- pyscriptc/runtime/ps_coll.c +133 -0
- pyscriptc/runtime/ps_dt.c +388 -0
- pyscriptc/runtime/ps_hash.c +411 -0
- pyscriptc/runtime/ps_help.c +219 -0
- pyscriptc/runtime/ps_hmac.c +120 -0
- pyscriptc/runtime/ps_io.c +198 -0
- pyscriptc/runtime/ps_island.c +272 -0
- pyscriptc/runtime/ps_json.c +402 -0
- pyscriptc/runtime/ps_mix.c +47 -0
- pyscriptc/runtime/ps_more.c +1089 -0
- pyscriptc/runtime/ps_osmore.c +447 -0
- pyscriptc/runtime/ps_path.c +547 -0
- pyscriptc/runtime/ps_re.c +1102 -0
- pyscriptc/runtime/ps_rt.c +1175 -0
- pyscriptc/runtime/ps_rt.h +189 -0
- pyscriptc/runtime/ps_struct.c +291 -0
- pyscriptc/runtime/ps_time.c +207 -0
- pyscriptc/runtime/ps_unicode.c +1521 -0
- pyscriptc/runtime/ps_url.c +327 -0
- pyscriptc/stdlib_queue.py +191 -0
- pyscriptc/stdmore.py +1596 -0
- pyscriptc/telemetry.py +219 -0
- pyscriptc-0.1.0.dist-info/METADATA +187 -0
- pyscriptc-0.1.0.dist-info/RECORD +54 -0
- pyscriptc-0.1.0.dist-info/WHEEL +5 -0
- pyscriptc-0.1.0.dist-info/entry_points.txt +2 -0
- pyscriptc-0.1.0.dist-info/licenses/LICENSE +201 -0
- pyscriptc-0.1.0.dist-info/top_level.txt +1 -0
pyscriptc/package.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Compile a directory as one module and resolve relative imports.
|
|
2
|
+
|
|
3
|
+
Each ``.py`` file becomes a dotted module under the directory name.
|
|
4
|
+
``from .util import add`` and ``from . import VERSION`` bind a function, a
|
|
5
|
+
module-level constant, or a class from another file in that tree. A class
|
|
6
|
+
export is the class the defining module already emits, including a subclass
|
|
7
|
+
of a class exported by another module in the tree. ``__main__.py`` is the
|
|
8
|
+
``python -m`` script and is not an importable module, so it is skipped and
|
|
9
|
+
the compile says so. A single file still refuses a relative import.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import copy
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pyscriptc.errors import IRError
|
|
20
|
+
from pyscriptc.ir import SUBSET, cpython_version
|
|
21
|
+
from pyscriptc.lower import Lowerer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def lower_directory(root: Path) -> tuple[dict[str, Any], str]:
|
|
25
|
+
units, skipped = _discover(root)
|
|
26
|
+
exports, failed = _catalog(units)
|
|
27
|
+
|
|
28
|
+
functions: list[dict[str, Any]] = []
|
|
29
|
+
classes: list[dict[str, Any]] = []
|
|
30
|
+
fences: list[dict[str, Any]] = []
|
|
31
|
+
class_names: dict[str, dict[str, Any]] = {}
|
|
32
|
+
exc_n = 8
|
|
33
|
+
for name, path, is_package in units:
|
|
34
|
+
text = path.read_text(encoding="utf-8")
|
|
35
|
+
tree = ast.parse(text, filename=str(path))
|
|
36
|
+
lowerer = Lowerer(str(path), text)
|
|
37
|
+
lowerer.module_name = name
|
|
38
|
+
lowerer.is_package = is_package
|
|
39
|
+
lowerer.package = exports
|
|
40
|
+
lowerer.exc_n = exc_n
|
|
41
|
+
module = lowerer.module(tree)
|
|
42
|
+
exc_n = lowerer.exc_n
|
|
43
|
+
for fn in module["functions"]:
|
|
44
|
+
functions.append(fn)
|
|
45
|
+
for cls in module["classes"]:
|
|
46
|
+
previous = class_names.get(cls["name"])
|
|
47
|
+
if previous is not None:
|
|
48
|
+
if previous["fields"] == cls["fields"] and previous["methods"] == cls["methods"]:
|
|
49
|
+
continue
|
|
50
|
+
fences.append(
|
|
51
|
+
{
|
|
52
|
+
"code": "PS2002",
|
|
53
|
+
"message": f"class {cls['name']} is defined in more than one module",
|
|
54
|
+
"span": cls["span"],
|
|
55
|
+
"function": None,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
continue
|
|
59
|
+
class_names[cls["name"]] = cls
|
|
60
|
+
classes.append(cls)
|
|
61
|
+
fences.extend(module["fences"])
|
|
62
|
+
fences = _promote_definition_fences(fences, failed)
|
|
63
|
+
merged = {
|
|
64
|
+
"irVersion": 1,
|
|
65
|
+
"subset": SUBSET,
|
|
66
|
+
"cpython": cpython_version(),
|
|
67
|
+
"sourceFile": str(root),
|
|
68
|
+
"entry": functions[0]["name"] if functions else None,
|
|
69
|
+
"functions": functions,
|
|
70
|
+
"classes": classes,
|
|
71
|
+
"fences": fences,
|
|
72
|
+
}
|
|
73
|
+
if skipped:
|
|
74
|
+
merged["_notes"] = [_skip_note(root, rel) for rel in skipped]
|
|
75
|
+
return merged, ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _catalog(
|
|
79
|
+
units: list[tuple[str, Path, bool]],
|
|
80
|
+
) -> tuple[dict[str, dict[str, dict[str, Any]]], dict[str, dict[str, dict[str, Any]]]]:
|
|
81
|
+
"""Repeat until a subclass can see a base exported by an earlier pass."""
|
|
82
|
+
|
|
83
|
+
exports: dict[str, dict[str, dict[str, Any]]] = {}
|
|
84
|
+
failed: dict[str, dict[str, dict[str, Any]]] = {}
|
|
85
|
+
for _ in range(len(units) + 1):
|
|
86
|
+
nxt, nxt_failed = _catalog_once(units, exports)
|
|
87
|
+
if nxt == exports:
|
|
88
|
+
return nxt, nxt_failed
|
|
89
|
+
exports, failed = nxt, nxt_failed
|
|
90
|
+
return exports, failed
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _catalog_once(
|
|
94
|
+
units: list[tuple[str, Path, bool]], exports: dict[str, dict[str, dict[str, Any]]]
|
|
95
|
+
) -> tuple[dict[str, dict[str, dict[str, Any]]], dict[str, dict[str, dict[str, Any]]]]:
|
|
96
|
+
found: dict[str, dict[str, dict[str, Any]]] = {}
|
|
97
|
+
failed: dict[str, dict[str, dict[str, Any]]] = {}
|
|
98
|
+
exc_n = 8
|
|
99
|
+
for name, path, is_package in units:
|
|
100
|
+
text = path.read_text(encoding="utf-8")
|
|
101
|
+
tree = ast.parse(text, filename=str(path))
|
|
102
|
+
lowerer = Lowerer(str(path), text)
|
|
103
|
+
lowerer.module_name = name
|
|
104
|
+
lowerer.is_package = is_package
|
|
105
|
+
lowerer.cataloguing = True
|
|
106
|
+
lowerer.package = exports
|
|
107
|
+
lowerer.exc_n = exc_n
|
|
108
|
+
module = lowerer.module(tree)
|
|
109
|
+
exc_n = lowerer.exc_n
|
|
110
|
+
found[name] = _exports_of(name, lowerer)
|
|
111
|
+
missed = _unexported_definitions(tree, lowerer, module["fences"])
|
|
112
|
+
if missed:
|
|
113
|
+
failed[name] = missed
|
|
114
|
+
return found, failed
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _exports_of(module_name: str, lowerer: Lowerer) -> dict[str, dict[str, Any]]:
|
|
118
|
+
bucket: dict[str, dict[str, Any]] = {}
|
|
119
|
+
for fn, (params, ret) in lowerer.sigs.items():
|
|
120
|
+
bucket[fn] = {
|
|
121
|
+
"kind": "func",
|
|
122
|
+
"ir_name": f"{module_name}.{fn}",
|
|
123
|
+
"params": copy.deepcopy(params),
|
|
124
|
+
"return": copy.deepcopy(ret),
|
|
125
|
+
}
|
|
126
|
+
for const_name, value in lowerer.constants.items():
|
|
127
|
+
bucket[const_name] = {"kind": "const", "value": copy.deepcopy(value)}
|
|
128
|
+
for class_name, info in lowerer.classes.items():
|
|
129
|
+
if info.get("synthetic") or info.get("imported"):
|
|
130
|
+
continue
|
|
131
|
+
bucket[class_name] = {"kind": "class", "name": class_name, "info": _class_export(info)}
|
|
132
|
+
return bucket
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _class_export(info: dict[str, Any]) -> dict[str, Any]:
|
|
136
|
+
"""Class shape without the AST node. The defining module still emits the class."""
|
|
137
|
+
|
|
138
|
+
exported: dict[str, Any] = {
|
|
139
|
+
"fields": copy.deepcopy(info.get("fields", [])),
|
|
140
|
+
"own_fields": copy.deepcopy(info.get("own_fields", [])),
|
|
141
|
+
"methods": copy.deepcopy(info.get("methods", {})),
|
|
142
|
+
"method_kind": dict(info.get("method_kind", {})),
|
|
143
|
+
"method_owner": dict(info.get("method_owner", {})),
|
|
144
|
+
"base": info.get("base"),
|
|
145
|
+
"imported": True,
|
|
146
|
+
}
|
|
147
|
+
if info.get("generated_init") is not None:
|
|
148
|
+
exported["generated_init"] = info["generated_init"]
|
|
149
|
+
if info.get("exception"):
|
|
150
|
+
exported["exception"] = True
|
|
151
|
+
exported["exc_code"] = info.get("exc_code")
|
|
152
|
+
if info.get("enum"):
|
|
153
|
+
exported["enum"] = True
|
|
154
|
+
exported["members"] = dict(info.get("members") or {})
|
|
155
|
+
return exported
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _discover(root: Path) -> tuple[list[tuple[str, Path, bool]], list[str]]:
|
|
159
|
+
root = root.resolve()
|
|
160
|
+
package = root.name
|
|
161
|
+
if not package.isidentifier():
|
|
162
|
+
raise IRError(f"package directory {package} is not an identifier")
|
|
163
|
+
found: list[tuple[str, Path, bool]] = []
|
|
164
|
+
skipped: list[str] = []
|
|
165
|
+
for path in sorted(root.rglob("*.py")):
|
|
166
|
+
relative = path.relative_to(root)
|
|
167
|
+
if any(part.startswith(".") or part == "__pycache__" for part in relative.parts):
|
|
168
|
+
continue
|
|
169
|
+
if path.name == "__main__.py":
|
|
170
|
+
skipped.append(relative.as_posix())
|
|
171
|
+
continue
|
|
172
|
+
if path.name == "__init__.py":
|
|
173
|
+
parts = relative.parts[:-1]
|
|
174
|
+
name = ".".join((package, *parts)) if parts else package
|
|
175
|
+
found.append((name, path, True))
|
|
176
|
+
continue
|
|
177
|
+
stem = relative.with_suffix("")
|
|
178
|
+
if not all(part.isidentifier() for part in stem.parts):
|
|
179
|
+
raise IRError(f"{relative} is not a module name")
|
|
180
|
+
found.append(((".".join((package, *stem.parts))), path, False))
|
|
181
|
+
if not found:
|
|
182
|
+
raise IRError(f"no Python files in {root}")
|
|
183
|
+
return found, skipped
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _skip_note(root: Path, relative: str) -> str:
|
|
187
|
+
return f"note: skipped {root.name}/{relative}; __main__.py is the python -m script, not an importable module"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_NOT_EXPORTED = " is not exported by this package"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _promote_definition_fences(
|
|
194
|
+
fences: list[dict[str, Any]], failed: dict[str, dict[str, dict[str, Any]]]
|
|
195
|
+
) -> list[dict[str, Any]]:
|
|
196
|
+
"""Report the fence inside a class or function before the import that missed it."""
|
|
197
|
+
|
|
198
|
+
by_qual = {f"{module}.{name}": fence for module, names in failed.items() for name, fence in names.items()}
|
|
199
|
+
if not by_qual:
|
|
200
|
+
return fences
|
|
201
|
+
|
|
202
|
+
def ident(fence: dict[str, Any]) -> tuple[Any, ...]:
|
|
203
|
+
span = fence.get("span") or {}
|
|
204
|
+
return (fence.get("code"), fence.get("message"), span.get("file"), span.get("line"), span.get("col"))
|
|
205
|
+
|
|
206
|
+
wanted = {ident(fence) for fence in by_qual.values()}
|
|
207
|
+
emitted: set[tuple[Any, ...]] = set()
|
|
208
|
+
out: list[dict[str, Any]] = []
|
|
209
|
+
for fence in fences:
|
|
210
|
+
message = fence.get("message") or ""
|
|
211
|
+
if message.endswith(_NOT_EXPORTED):
|
|
212
|
+
defined = by_qual.get(message[: -len(_NOT_EXPORTED)])
|
|
213
|
+
if defined is not None:
|
|
214
|
+
mark = ident(defined)
|
|
215
|
+
if mark not in emitted:
|
|
216
|
+
out.append(defined)
|
|
217
|
+
emitted.add(mark)
|
|
218
|
+
mark = ident(fence)
|
|
219
|
+
if mark in emitted and mark in wanted:
|
|
220
|
+
continue
|
|
221
|
+
out.append(fence)
|
|
222
|
+
if mark in wanted:
|
|
223
|
+
emitted.add(mark)
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _unexported_definitions(
|
|
228
|
+
tree: ast.Module, lowerer: Lowerer, fences: list[dict[str, Any]]
|
|
229
|
+
) -> dict[str, dict[str, Any]]:
|
|
230
|
+
exported = set(lowerer.sigs)
|
|
231
|
+
for class_name, info in lowerer.classes.items():
|
|
232
|
+
if info.get("synthetic") or info.get("imported"):
|
|
233
|
+
continue
|
|
234
|
+
exported.add(class_name)
|
|
235
|
+
found: dict[str, dict[str, Any]] = {}
|
|
236
|
+
for node in tree.body:
|
|
237
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
238
|
+
continue
|
|
239
|
+
if node.name in exported:
|
|
240
|
+
continue
|
|
241
|
+
start = node.lineno
|
|
242
|
+
decorators = getattr(node, "decorator_list", None) or []
|
|
243
|
+
if decorators:
|
|
244
|
+
start = min(start, min(item.lineno for item in decorators))
|
|
245
|
+
end = node.end_lineno or node.lineno
|
|
246
|
+
inside = [fence for fence in fences if start <= fence["span"]["line"] <= end]
|
|
247
|
+
if inside:
|
|
248
|
+
found[node.name] = inside[0]
|
|
249
|
+
return found
|