pycodify 0.1.3__tar.gz → 0.2.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.
- pycodify-0.2.0/.readthedocs.yaml +17 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/PKG-INFO +2 -2
- pycodify-0.2.0/docs/api.rst +41 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/docs/architecture.rst +37 -44
- {pycodify-0.1.3 → pycodify-0.2.0}/docs/conf.py +4 -3
- {pycodify-0.1.3 → pycodify-0.2.0}/docs/usage.rst +16 -41
- {pycodify-0.1.3 → pycodify-0.2.0}/pyproject.toml +1 -1
- {pycodify-0.1.3 → pycodify-0.2.0}/src/pycodify/__init__.py +13 -4
- {pycodify-0.1.3 → pycodify-0.2.0}/src/pycodify/core.py +75 -28
- {pycodify-0.1.3 → pycodify-0.2.0}/src/pycodify/formatters.py +106 -98
- {pycodify-0.1.3 → pycodify-0.2.0}/src/pycodify/imports.py +11 -19
- pycodify-0.2.0/src/pycodify/paths.py +148 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/tests/test_core.py +24 -26
- pycodify-0.2.0/tests/test_paths.py +115 -0
- pycodify-0.2.0/tests/test_round_trip.py +147 -0
- pycodify-0.1.3/docs/api.rst +0 -32
- {pycodify-0.1.3 → pycodify-0.2.0}/.github/workflows/ci.yml +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/.github/workflows/publish.yml +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/.gitignore +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/CONTRIBUTING.md +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/LICENSE +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/README.md +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/docs/Makefile +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/docs/index.rst +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/paper.bib +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/paper.md +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/scripts/release.py +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/scripts/update_and_release.py +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/scripts/update_version.py +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/scripts/verify_release_ready.py +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/tests/__init__.py +0 -0
- {pycodify-0.1.3 → pycodify-0.2.0}/tests/test_imports.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pycodify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python source code as a serialization format with automatic import resolution
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenHCSDev/pycodify
|
|
6
6
|
Project-URL: Documentation, https://pycodify.readthedocs.io
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
API orientation
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
``Assignment`` and ``CodeBlock``
|
|
5
|
+
Structured top-level source items.
|
|
6
|
+
|
|
7
|
+
``generate_python_source(obj, header="", clean_mode=False)``
|
|
8
|
+
Generate complete executable source and required imports, with shared and
|
|
9
|
+
repeated absolute paths bound once at the top.
|
|
10
|
+
|
|
11
|
+
``to_source(value, context=None)``
|
|
12
|
+
Format one value into a ``SourceFragment``.
|
|
13
|
+
|
|
14
|
+
``SourceFormatter``
|
|
15
|
+
Nominal extension point. Subclasses implement ``can_format`` and ``format``;
|
|
16
|
+
application-specific formatters register without changing pycodify core.
|
|
17
|
+
|
|
18
|
+
``resolve_imports``
|
|
19
|
+
Resolve import requirements and aliases for colliding names.
|
|
20
|
+
|
|
21
|
+
``FormatContext`` and ``SourceFragment``
|
|
22
|
+
Immutable formatting state and output records.
|
|
23
|
+
|
|
24
|
+
``imported_name(module, qualname, context)``
|
|
25
|
+
Spell an importable object by its qualified name, honouring collision aliases.
|
|
26
|
+
|
|
27
|
+
``PythonSourceLiteral``
|
|
28
|
+
Base for values that spell their own source through ``source_fragment``.
|
|
29
|
+
|
|
30
|
+
``PathRendering``, ``PathCollector``, ``PathFactoring``, ``PathConstructor``
|
|
31
|
+
How a render pass spells paths: plainly, while recording, or through the
|
|
32
|
+
document's path bindings.
|
|
33
|
+
|
|
34
|
+
The canonical import surface is ``pycodify.__all__``.
|
|
35
|
+
|
|
36
|
+
Public API
|
|
37
|
+
----------
|
|
38
|
+
|
|
39
|
+
.. automodule:: pycodify
|
|
40
|
+
:members:
|
|
41
|
+
:member-order: bysource
|
|
@@ -6,7 +6,8 @@ Two-Pass Algorithm
|
|
|
6
6
|
|
|
7
7
|
Generating executable source requires knowing import aliases before emitting code. But aliases depend on detecting collisions, which requires visiting all types first. This creates a dependency: code generation requires alias resolution, but alias resolution requires traversing the object graph.
|
|
8
8
|
|
|
9
|
-
pycodify solves this with two passes
|
|
9
|
+
pycodify solves this with two passes, preceded by a path-discovery pass
|
|
10
|
+
(see *Path factoring* below):
|
|
10
11
|
|
|
11
12
|
1. **Collection pass**: Traverse the object, emit code fragments, and collect
|
|
12
13
|
``(module, name)`` import pairs.
|
|
@@ -31,12 +32,26 @@ Each type maps to a ``SourceFormatter`` that emits a ``SourceFragment(code, impo
|
|
|
31
32
|
return isinstance(value, Enum)
|
|
32
33
|
|
|
33
34
|
def format(self, value: Enum, context: FormatContext) -> SourceFragment:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
name = context.name_mappings.get(import_pair, cls.__name__)
|
|
37
|
-
return SourceFragment(f"{name}.{value.name}", frozenset([import_pair]))
|
|
35
|
+
enum_type = imported_name(type(value).__module__, type(value).__qualname__, context)
|
|
36
|
+
return SourceFragment(f"{enum_type.code}.{value.name}", enum_type.imports)
|
|
38
37
|
|
|
39
|
-
Priority-based dispatch selects the most specific formatter. Domain extensions add formatters without modifying core code.
|
|
38
|
+
Priority-based dispatch selects the most specific formatter. Domain extensions add formatters without modifying core code. A value no formatter accepts raises ``TypeError``: pycodify never emits ``repr`` text it cannot load back.
|
|
39
|
+
|
|
40
|
+
Qualified names
|
|
41
|
+
---------------
|
|
42
|
+
|
|
43
|
+
``imported_name(module, qualname, context)`` is the one rule for spelling an
|
|
44
|
+
importable object. It imports the root of ``__qualname__`` and spells the rest as
|
|
45
|
+
attribute access, so ``Outer.Mode.FAST`` imports ``Outer``. Enum members,
|
|
46
|
+
classes, functions, class-bound methods, named tuples and dataclass constructors
|
|
47
|
+
all use it. Objects defined inside a function raise ``TypeError``.
|
|
48
|
+
|
|
49
|
+
Values that own their spelling
|
|
50
|
+
------------------------------
|
|
51
|
+
|
|
52
|
+
A value whose source is not its constructor subclasses ``PythonSourceLiteral``
|
|
53
|
+
and implements ``source_fragment(context)``, typically building on
|
|
54
|
+
``imported_name`` so collision aliases apply.
|
|
40
55
|
|
|
41
56
|
Core Components
|
|
42
57
|
---------------
|
|
@@ -49,7 +64,7 @@ Core Components
|
|
|
49
64
|
|
|
50
65
|
**FormatContext**
|
|
51
66
|
Carries indentation depth, ``clean_mode``, the ``(module, name)`` to
|
|
52
|
-
local-name mapping, and
|
|
67
|
+
local-name mapping, and the pass's ``PathRendering``. ``indented()`` returns
|
|
53
68
|
a copied context and does not mutate its caller.
|
|
54
69
|
|
|
55
70
|
**resolve_imports**
|
|
@@ -60,27 +75,17 @@ Core Components
|
|
|
60
75
|
Orchestrates collection, resolution, and regeneration to produce a complete
|
|
61
76
|
source string.
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
``FormatContext`` is frozen. Its default mappings are read-only mapping proxies,
|
|
67
|
-
and context evolution uses dataclass replacement rather than mutation. A host
|
|
68
|
-
may supply an ``extensions`` mapping keyed by the exact nominal type of each
|
|
69
|
-
rendering policy or state record. A formatter retrieves one value with
|
|
70
|
-
``context.extension(ExtensionType)``; pycodify does not search base classes,
|
|
71
|
-
string keys, or a fallback chain.
|
|
72
|
-
|
|
73
|
-
``generate_python_source(..., context=context)`` threads the caller's context
|
|
74
|
-
through collection and regeneration. Import resolution replaces only
|
|
75
|
-
``name_mappings`` for the second pass, so typed extensions and all other context
|
|
76
|
-
fields survive. When a context is supplied, its ``clean_mode`` is authoritative;
|
|
77
|
-
the separate ``clean_mode=`` argument is used only when pycodify constructs the
|
|
78
|
-
context.
|
|
78
|
+
Path factoring
|
|
79
|
+
--------------
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
``generate_python_source`` first renders the document with a ``PathCollector``
|
|
82
|
+
on ``FormatContext.paths`` to record every ``pathlib.Path`` value. A
|
|
83
|
+
``PathFactoring`` plan then binds each directory shared by two or more absolute
|
|
84
|
+
paths (``path_root = Path('/data/screen')``) and each repeated absolute path
|
|
85
|
+
(``path_1 = path_root / 'plate_A'``) once, at the top of the document, and the
|
|
86
|
+
two rendering passes spell those paths through the bindings. Relative paths and
|
|
87
|
+
strings are never factored. ``to_source`` alone uses the base ``PathRendering``,
|
|
88
|
+
which spells each path as ``Path('...')``.
|
|
84
89
|
|
|
85
90
|
Clean Mode
|
|
86
91
|
----------
|
|
@@ -107,25 +112,13 @@ not sandboxing or validation of generated programs. Hosts must restrict inputs
|
|
|
107
112
|
and custom formatters to trusted sources, review persisted code before running
|
|
108
113
|
it, and define their own compatibility policy for imported domain APIs.
|
|
109
114
|
|
|
110
|
-
Lazy Dataclass Integration
|
|
111
|
-
---------------------------
|
|
112
|
-
|
|
113
|
-
For dataclasses with ``__getattribute__`` interception (used for hierarchical config inheritance), formatters use ``object.__getattribute__`` to access raw field values without triggering lazy resolution:
|
|
114
|
-
|
|
115
|
-
.. code-block:: python
|
|
116
|
-
|
|
117
|
-
# In DataclassFormatter
|
|
118
|
-
if hasattr(instance, "_resolve_field_value"):
|
|
119
|
-
# Bypass __getattribute__ to get raw None vs concrete value
|
|
120
|
-
current_value = object.__getattribute__(instance, field_name)
|
|
121
|
-
else:
|
|
122
|
-
current_value = getattr(instance, field_name)
|
|
123
|
-
|
|
124
|
-
This distinguishes explicitly-set values from inherited ones during serialization.
|
|
125
|
-
|
|
126
115
|
Module Structure
|
|
127
116
|
----------------
|
|
128
117
|
|
|
129
118
|
- **core.py**: Main API (``Assignment``, ``generate_python_source``)
|
|
130
|
-
- **formatters.py**: Built-in formatters (dataclass, enum, primitive types)
|
|
119
|
+
- **formatters.py**: Built-in formatters (dataclass, enum, class, callable, primitive types)
|
|
120
|
+
- **paths.py**: Document-level path factoring
|
|
121
|
+
|
|
122
|
+
Lazy configuration dataclasses (ObjectState) are formatted by
|
|
123
|
+
``objectstate.codegen``, which registers its own formatter.
|
|
131
124
|
- **imports.py**: Import resolution and collision handling
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Sphinx configuration for pycodify."""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import sys
|
|
4
|
+
from importlib import import_module
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
# Add source to path
|
|
@@ -10,8 +11,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
10
11
|
project = "pycodify"
|
|
11
12
|
copyright = "2024, Tristan Simas"
|
|
12
13
|
author = "Tristan Simas"
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
release = import_module("pycodify").__version__
|
|
15
|
+
version = ".".join(release.split(".")[:2])
|
|
15
16
|
|
|
16
17
|
# General configuration
|
|
17
18
|
extensions = [
|
|
@@ -117,62 +117,37 @@ level. ``name_mappings`` maps ``(module, imported_name)`` pairs to the local
|
|
|
117
117
|
names chosen by ``resolve_imports``; callers normally let
|
|
118
118
|
``generate_python_source`` populate it during the second pass.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
Values that own their source
|
|
121
|
+
----------------------------
|
|
122
122
|
|
|
123
|
-
A
|
|
124
|
-
|
|
125
|
-
render passes:
|
|
123
|
+
A value whose source is not its dataclass constructor implements
|
|
124
|
+
``PythonSourceLiteral``:
|
|
126
125
|
|
|
127
126
|
.. code-block:: python
|
|
128
127
|
|
|
129
128
|
from dataclasses import dataclass
|
|
130
|
-
from types import MappingProxyType
|
|
131
129
|
|
|
132
130
|
from pycodify import (
|
|
133
131
|
Assignment,
|
|
134
132
|
FormatContext,
|
|
135
|
-
|
|
133
|
+
PythonSourceLiteral,
|
|
136
134
|
SourceFragment,
|
|
137
135
|
generate_python_source,
|
|
136
|
+
imported_name,
|
|
138
137
|
)
|
|
139
138
|
|
|
140
139
|
@dataclass(frozen=True)
|
|
141
|
-
class
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class PolicyValueFormatter(SourceFormatter):
|
|
148
|
-
priority = 200
|
|
149
|
-
|
|
150
|
-
def can_format(self, value):
|
|
151
|
-
return isinstance(value, PolicyValue)
|
|
152
|
-
|
|
153
|
-
def format(self, value, context):
|
|
154
|
-
del value
|
|
155
|
-
policy = context.extension(RenderPolicy)
|
|
156
|
-
if policy is None:
|
|
157
|
-
raise RuntimeError("RenderPolicy is required")
|
|
158
|
-
return SourceFragment(repr(policy.marker))
|
|
159
|
-
|
|
160
|
-
context = FormatContext(
|
|
161
|
-
clean_mode=True,
|
|
162
|
-
extensions=MappingProxyType(
|
|
163
|
-
{RenderPolicy: RenderPolicy(marker="preserved")}
|
|
164
|
-
),
|
|
165
|
-
)
|
|
166
|
-
source = generate_python_source(
|
|
167
|
-
Assignment("value", PolicyValue()),
|
|
168
|
-
context=context,
|
|
169
|
-
)
|
|
170
|
-
assert source == "value = 'preserved'"
|
|
140
|
+
class RegistryLookup(PythonSourceLiteral):
|
|
141
|
+
key: str
|
|
142
|
+
|
|
143
|
+
def source_fragment(self, context: FormatContext) -> SourceFragment:
|
|
144
|
+
lookup = imported_name("myapp.registry", "get_function", context)
|
|
145
|
+
return SourceFragment(f"{lookup.code}({self.key!r})", lookup.imports)
|
|
171
146
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
147
|
+
source = generate_python_source(Assignment("func", RegistryLookup("gauss")))
|
|
148
|
+
# from myapp.registry import get_function
|
|
149
|
+
#
|
|
150
|
+
# func = get_function('gauss')
|
|
176
151
|
|
|
177
152
|
Nested Dataclasses
|
|
178
153
|
-------------------
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycodify"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Python source code as a serialization format with automatic import resolution"
|
|
9
9
|
authors = [{name = "Tristan Simas", email = "tristan.simas@mail.mcgill.ca"}]
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
# Register default formatters
|
|
2
|
+
from . import formatters as _formatters # noqa: F401
|
|
1
3
|
from .core import (
|
|
2
4
|
Assignment,
|
|
3
5
|
BlankLine,
|
|
4
6
|
CodeBlock,
|
|
5
7
|
Comment,
|
|
6
8
|
FormatContext,
|
|
9
|
+
PathRendering,
|
|
10
|
+
PythonSourceLiteral,
|
|
7
11
|
SourceFormatter,
|
|
8
12
|
SourceFragment,
|
|
9
13
|
generate_python_source,
|
|
14
|
+
imported_name,
|
|
10
15
|
to_source,
|
|
11
16
|
)
|
|
12
17
|
from .imports import resolve_imports
|
|
18
|
+
from .paths import PathCollector, PathConstructor, PathFactoring
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
from . import formatters as _formatters # noqa: F401
|
|
16
|
-
|
|
17
|
-
__version__ = "0.1.3"
|
|
20
|
+
__version__ = "0.2.0"
|
|
18
21
|
|
|
19
22
|
__all__ = [
|
|
20
23
|
"Assignment",
|
|
@@ -22,9 +25,15 @@ __all__ = [
|
|
|
22
25
|
"CodeBlock",
|
|
23
26
|
"Comment",
|
|
24
27
|
"FormatContext",
|
|
28
|
+
"PathCollector",
|
|
29
|
+
"PathConstructor",
|
|
30
|
+
"PathFactoring",
|
|
31
|
+
"PathRendering",
|
|
32
|
+
"PythonSourceLiteral",
|
|
25
33
|
"SourceFormatter",
|
|
26
34
|
"SourceFragment",
|
|
27
35
|
"generate_python_source",
|
|
36
|
+
"imported_name",
|
|
28
37
|
"resolve_imports",
|
|
29
38
|
"to_source",
|
|
30
39
|
]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
4
5
|
from dataclasses import dataclass, field, replace
|
|
5
|
-
from
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from types import MappingProxyType
|
|
7
|
-
from typing import Any, ClassVar
|
|
8
|
+
from typing import Any, ClassVar
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclass(frozen=True)
|
|
@@ -12,39 +13,78 @@ class SourceFragment:
|
|
|
12
13
|
"""Result of serializing a value to Python source."""
|
|
13
14
|
|
|
14
15
|
code: str
|
|
15
|
-
imports:
|
|
16
|
+
imports: frozenset[tuple[str, str]] = frozenset()
|
|
16
17
|
|
|
17
|
-
def __add__(self, other:
|
|
18
|
+
def __add__(self, other: SourceFragment) -> SourceFragment:
|
|
18
19
|
return SourceFragment(
|
|
19
20
|
code=self.code + other.code,
|
|
20
21
|
imports=self.imports | other.imports,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
class PathRendering:
|
|
26
|
+
"""How one render pass spells :class:`pathlib.Path` values.
|
|
27
|
+
|
|
28
|
+
The base spells every path as its own constructor call. Subclasses in
|
|
29
|
+
:mod:`pycodify.paths` record or substitute paths for document-level
|
|
30
|
+
factoring.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def path_source(self, value: Path, context: FormatContext) -> SourceFragment:
|
|
34
|
+
constructor = imported_name("pathlib", "Path", context)
|
|
35
|
+
return SourceFragment(
|
|
36
|
+
f"{constructor.code}({str(value)!r})",
|
|
37
|
+
constructor.imports,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
24
41
|
@dataclass(frozen=True)
|
|
25
42
|
class FormatContext:
|
|
26
43
|
"""Context passed through serialization."""
|
|
27
44
|
|
|
28
45
|
indent: int = 0
|
|
29
46
|
clean_mode: bool = False
|
|
30
|
-
name_mappings: Mapping[
|
|
31
|
-
default_factory=lambda: MappingProxyType({})
|
|
32
|
-
)
|
|
33
|
-
extensions: Mapping[type[object], object] = field(
|
|
47
|
+
name_mappings: Mapping[tuple[str, str], str] = field(
|
|
34
48
|
default_factory=lambda: MappingProxyType({})
|
|
35
49
|
)
|
|
50
|
+
paths: PathRendering = field(default_factory=PathRendering)
|
|
36
51
|
|
|
37
|
-
def indented(self) ->
|
|
52
|
+
def indented(self) -> FormatContext:
|
|
38
53
|
return replace(self, indent=self.indent + 1)
|
|
39
54
|
|
|
40
55
|
@property
|
|
41
56
|
def indent_str(self) -> str:
|
|
42
57
|
return " " * self.indent
|
|
43
58
|
|
|
44
|
-
def extension(self, extension_type: type[Any]) -> Any | None:
|
|
45
|
-
"""Return one caller-owned rendering extension by exact nominal type."""
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
def imported_name(module: str, qualname: str, context: FormatContext) -> SourceFragment:
|
|
61
|
+
"""Spell an importable object by its module and ``__qualname__``.
|
|
62
|
+
|
|
63
|
+
The root of the qualified name is imported (under its collision alias, if
|
|
64
|
+
any) and the rest is spelled as attribute access, so nested classes, enums
|
|
65
|
+
and functions load back as themselves. Objects defined inside a function
|
|
66
|
+
have no import path and are rejected.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if "<locals>" in qualname:
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f"{module}.{qualname} is defined inside a function and cannot be "
|
|
72
|
+
"imported by generated source."
|
|
73
|
+
)
|
|
74
|
+
root, dot, nested = qualname.partition(".")
|
|
75
|
+
if module == "builtins":
|
|
76
|
+
return SourceFragment(qualname)
|
|
77
|
+
import_pair = (module, root)
|
|
78
|
+
name = context.name_mappings.get(import_pair, root)
|
|
79
|
+
return SourceFragment(f"{name}{dot}{nested}", frozenset({import_pair}))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PythonSourceLiteral(ABC):
|
|
83
|
+
"""A value that spells its own Python source expression."""
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def source_fragment(self, context: FormatContext) -> SourceFragment:
|
|
87
|
+
"""Return the expression that reconstructs this value, with its imports."""
|
|
48
88
|
|
|
49
89
|
|
|
50
90
|
@dataclass(frozen=True)
|
|
@@ -65,10 +105,10 @@ class Assignment:
|
|
|
65
105
|
|
|
66
106
|
@dataclass(frozen=True)
|
|
67
107
|
class CodeBlock:
|
|
68
|
-
items:
|
|
108
|
+
items: tuple[Any, ...]
|
|
69
109
|
|
|
70
110
|
@classmethod
|
|
71
|
-
def from_items(cls, items: Iterable[Any]) ->
|
|
111
|
+
def from_items(cls, items: Iterable[Any]) -> CodeBlock:
|
|
72
112
|
return cls(tuple(items))
|
|
73
113
|
|
|
74
114
|
|
|
@@ -77,11 +117,11 @@ class SourceFormatter(ABC):
|
|
|
77
117
|
|
|
78
118
|
priority: int = 0
|
|
79
119
|
|
|
80
|
-
_registry: ClassVar[
|
|
81
|
-
_sorted_registry: ClassVar[
|
|
120
|
+
_registry: ClassVar[list[SourceFormatter]] = []
|
|
121
|
+
_sorted_registry: ClassVar[list[SourceFormatter]] = []
|
|
82
122
|
_registry_dirty: ClassVar[bool] = False
|
|
83
123
|
|
|
84
|
-
def __init_subclass__(cls, **kwargs):
|
|
124
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
85
125
|
super().__init_subclass__(**kwargs)
|
|
86
126
|
if getattr(cls, "__abstract__", False):
|
|
87
127
|
return
|
|
@@ -99,12 +139,12 @@ class SourceFormatter(ABC):
|
|
|
99
139
|
raise NotImplementedError
|
|
100
140
|
|
|
101
141
|
@classmethod
|
|
102
|
-
def register(cls, formatter:
|
|
142
|
+
def register(cls, formatter: SourceFormatter) -> None:
|
|
103
143
|
cls._registry.append(formatter)
|
|
104
144
|
cls._registry_dirty = True
|
|
105
145
|
|
|
106
146
|
@classmethod
|
|
107
|
-
def _iter_registry(cls) ->
|
|
147
|
+
def _iter_registry(cls) -> list[SourceFormatter]:
|
|
108
148
|
if cls._registry_dirty:
|
|
109
149
|
cls._sorted_registry = sorted(
|
|
110
150
|
cls._registry,
|
|
@@ -115,7 +155,7 @@ class SourceFormatter(ABC):
|
|
|
115
155
|
return cls._sorted_registry
|
|
116
156
|
|
|
117
157
|
@classmethod
|
|
118
|
-
def get_formatter(cls, value: Any) ->
|
|
158
|
+
def get_formatter(cls, value: Any) -> SourceFormatter:
|
|
119
159
|
for formatter in cls._iter_registry():
|
|
120
160
|
if formatter.can_format(value):
|
|
121
161
|
return formatter
|
|
@@ -135,21 +175,28 @@ def generate_python_source(
|
|
|
135
175
|
obj: Any,
|
|
136
176
|
header: str = "",
|
|
137
177
|
clean_mode: bool = False,
|
|
138
|
-
*,
|
|
139
|
-
context: FormatContext | None = None,
|
|
140
178
|
) -> str:
|
|
141
|
-
"""Generate complete Python source with imports.
|
|
179
|
+
"""Generate complete Python source with imports.
|
|
180
|
+
|
|
181
|
+
Absolute paths that share a root or repeat are bound once at the top of the
|
|
182
|
+
document (see :mod:`pycodify.paths`).
|
|
183
|
+
"""
|
|
142
184
|
from .imports import resolve_imports
|
|
185
|
+
from .paths import PathCollector, PathFactoring
|
|
143
186
|
|
|
144
|
-
ctx =
|
|
145
|
-
|
|
187
|
+
ctx = FormatContext(clean_mode=clean_mode)
|
|
188
|
+
collector = PathCollector()
|
|
189
|
+
to_source(obj, replace(ctx, paths=collector))
|
|
190
|
+
factoring = PathFactoring.from_occurrences(collector.values)
|
|
191
|
+
body = factoring.document(obj)
|
|
192
|
+
ctx = replace(ctx, paths=factoring)
|
|
146
193
|
|
|
194
|
+
fragment = to_source(body, ctx)
|
|
147
195
|
import_lines, name_mappings = resolve_imports(fragment.imports)
|
|
148
|
-
|
|
149
196
|
ctx = replace(ctx, name_mappings=MappingProxyType(name_mappings))
|
|
150
|
-
fragment = to_source(
|
|
197
|
+
fragment = to_source(body, ctx)
|
|
151
198
|
|
|
152
|
-
code_lines:
|
|
199
|
+
code_lines: list[str] = []
|
|
153
200
|
if header:
|
|
154
201
|
code_lines.extend(header.splitlines())
|
|
155
202
|
code_lines.append("")
|