pycodify 0.1.2__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.2 → pycodify-0.2.0}/PKG-INFO +16 -5
- {pycodify-0.1.2 → pycodify-0.2.0}/README.md +14 -3
- pycodify-0.2.0/docs/api.rst +41 -0
- pycodify-0.2.0/docs/architecture.rst +124 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/docs/conf.py +6 -6
- pycodify-0.2.0/docs/index.rst +38 -0
- pycodify-0.2.0/docs/usage.rst +201 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/pyproject.toml +3 -3
- {pycodify-0.1.2 → pycodify-0.2.0}/src/pycodify/__init__.py +13 -4
- pycodify-0.2.0/src/pycodify/core.py +210 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/src/pycodify/formatters.py +113 -98
- pycodify-0.2.0/src/pycodify/imports.py +64 -0
- pycodify-0.2.0/src/pycodify/paths.py +148 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/tests/test_core.py +36 -3
- pycodify-0.2.0/tests/test_imports.py +53 -0
- pycodify-0.2.0/tests/test_paths.py +115 -0
- pycodify-0.2.0/tests/test_round_trip.py +147 -0
- pycodify-0.1.2/docs/api.rst +0 -106
- pycodify-0.1.2/docs/architecture.rst +0 -89
- pycodify-0.1.2/docs/index.rst +0 -72
- pycodify-0.1.2/docs/usage.rst +0 -129
- pycodify-0.1.2/src/pycodify/core.py +0 -145
- pycodify-0.1.2/src/pycodify/imports.py +0 -36
- {pycodify-0.1.2 → pycodify-0.2.0}/.github/workflows/ci.yml +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/.github/workflows/publish.yml +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/.gitignore +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/CONTRIBUTING.md +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/LICENSE +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/docs/Makefile +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/paper.bib +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/paper.md +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/scripts/release.py +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/scripts/update_and_release.py +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/scripts/update_version.py +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/scripts/verify_release_ready.py +0 -0
- {pycodify-0.1.2 → pycodify-0.2.0}/tests/__init__.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
|
|
@@ -49,7 +49,7 @@ class Config:
|
|
|
49
49
|
value: int = 42
|
|
50
50
|
|
|
51
51
|
config = Config(name="production", value=100)
|
|
52
|
-
code = generate_python_source(Assignment("config", config)
|
|
52
|
+
code = generate_python_source(Assignment("config", config))
|
|
53
53
|
print(code)
|
|
54
54
|
# Output:
|
|
55
55
|
# from __main__ import Config
|
|
@@ -62,9 +62,12 @@ print(code)
|
|
|
62
62
|
|--------|:--------:|:-----------:|:--------:|:---------------:|:-------------:|
|
|
63
63
|
| pickle | ✗ | ✗ | ✗ | ✓ | ✗ |
|
|
64
64
|
| JSON/YAML | ✓ | ✓ | ✓ | ✗ | ✓ |
|
|
65
|
-
| Python source | ✓ | ✓ | ✓ | ✓ |
|
|
65
|
+
| Python source | ✓ | ✓ | ✓ | ✓ | Depends on imported APIs |
|
|
66
66
|
|
|
67
|
-
Binary formats like `pickle` cannot be diffed, inspected, or edited. Text
|
|
67
|
+
Binary formats like `pickle` cannot be diffed, inspected, or edited. Text
|
|
68
|
+
formats like JSON lose type information. Python source is diffable, inspectable,
|
|
69
|
+
editable, and type-preserving, but replay still depends on the imported APIs and
|
|
70
|
+
constructor signatures remaining compatible.
|
|
68
71
|
|
|
69
72
|
## Features
|
|
70
73
|
|
|
@@ -76,6 +79,14 @@ Binary formats like `pickle` cannot be diffed, inspected, or edited. Text format
|
|
|
76
79
|
|
|
77
80
|
## Documentation
|
|
78
81
|
|
|
82
|
+
Explicit mode is the default (``clean_mode=False``), preserving every field for
|
|
83
|
+
reproducibility. Pass ``clean_mode=True`` only when concise, default-eliding
|
|
84
|
+
source is desired.
|
|
85
|
+
|
|
86
|
+
Generated output is executable Python, not a sandboxed data format. Review it
|
|
87
|
+
before execution and never call `exec` on source generated from untrusted
|
|
88
|
+
objects, custom formatters, headers, or edited files.
|
|
89
|
+
|
|
79
90
|
Full documentation available at [pycodify.readthedocs.io](https://pycodify.readthedocs.io)
|
|
80
91
|
|
|
81
92
|
## Installation
|
|
@@ -14,7 +14,7 @@ class Config:
|
|
|
14
14
|
value: int = 42
|
|
15
15
|
|
|
16
16
|
config = Config(name="production", value=100)
|
|
17
|
-
code = generate_python_source(Assignment("config", config)
|
|
17
|
+
code = generate_python_source(Assignment("config", config))
|
|
18
18
|
print(code)
|
|
19
19
|
# Output:
|
|
20
20
|
# from __main__ import Config
|
|
@@ -27,9 +27,12 @@ print(code)
|
|
|
27
27
|
|--------|:--------:|:-----------:|:--------:|:---------------:|:-------------:|
|
|
28
28
|
| pickle | ✗ | ✗ | ✗ | ✓ | ✗ |
|
|
29
29
|
| JSON/YAML | ✓ | ✓ | ✓ | ✗ | ✓ |
|
|
30
|
-
| Python source | ✓ | ✓ | ✓ | ✓ |
|
|
30
|
+
| Python source | ✓ | ✓ | ✓ | ✓ | Depends on imported APIs |
|
|
31
31
|
|
|
32
|
-
Binary formats like `pickle` cannot be diffed, inspected, or edited. Text
|
|
32
|
+
Binary formats like `pickle` cannot be diffed, inspected, or edited. Text
|
|
33
|
+
formats like JSON lose type information. Python source is diffable, inspectable,
|
|
34
|
+
editable, and type-preserving, but replay still depends on the imported APIs and
|
|
35
|
+
constructor signatures remaining compatible.
|
|
33
36
|
|
|
34
37
|
## Features
|
|
35
38
|
|
|
@@ -41,6 +44,14 @@ Binary formats like `pickle` cannot be diffed, inspected, or edited. Text format
|
|
|
41
44
|
|
|
42
45
|
## Documentation
|
|
43
46
|
|
|
47
|
+
Explicit mode is the default (``clean_mode=False``), preserving every field for
|
|
48
|
+
reproducibility. Pass ``clean_mode=True`` only when concise, default-eliding
|
|
49
|
+
source is desired.
|
|
50
|
+
|
|
51
|
+
Generated output is executable Python, not a sandboxed data format. Review it
|
|
52
|
+
before execution and never call `exec` on source generated from untrusted
|
|
53
|
+
objects, custom formatters, headers, or edited files.
|
|
54
|
+
|
|
44
55
|
Full documentation available at [pycodify.readthedocs.io](https://pycodify.readthedocs.io)
|
|
45
56
|
|
|
46
57
|
## Installation
|
|
@@ -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
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Architecture
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
Two-Pass Algorithm
|
|
5
|
+
-------------------
|
|
6
|
+
|
|
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
|
+
|
|
9
|
+
pycodify solves this with two passes, preceded by a path-discovery pass
|
|
10
|
+
(see *Path factoring* below):
|
|
11
|
+
|
|
12
|
+
1. **Collection pass**: Traverse the object, emit code fragments, and collect
|
|
13
|
+
``(module, name)`` import pairs.
|
|
14
|
+
2. **Resolution**: ``resolve_imports`` sorts the pairs, detects names exported by
|
|
15
|
+
more than one module, and derives aliases from each module path.
|
|
16
|
+
3. **Regeneration pass**: Re-traverse with the resolved ``name_mappings`` and
|
|
17
|
+
emit final code using those local names.
|
|
18
|
+
|
|
19
|
+
This is not an optimization—it is structurally necessary. A single-pass algorithm cannot know whether ``Config`` needs aliasing until it has seen all types that might also be named ``Config``.
|
|
20
|
+
|
|
21
|
+
Extensible Formatter Registry
|
|
22
|
+
------------------------------
|
|
23
|
+
|
|
24
|
+
Each type maps to a ``SourceFormatter`` that emits a ``SourceFragment(code, imports)``. Formatters register via ``__init_subclass__``—defining a formatter class automatically adds it to the registry:
|
|
25
|
+
|
|
26
|
+
.. code-block:: python
|
|
27
|
+
|
|
28
|
+
class EnumFormatter(SourceFormatter):
|
|
29
|
+
priority = 70
|
|
30
|
+
|
|
31
|
+
def can_format(self, value: Any) -> bool:
|
|
32
|
+
return isinstance(value, Enum)
|
|
33
|
+
|
|
34
|
+
def format(self, value: Enum, context: FormatContext) -> SourceFragment:
|
|
35
|
+
enum_type = imported_name(type(value).__module__, type(value).__qualname__, context)
|
|
36
|
+
return SourceFragment(f"{enum_type.code}.{value.name}", enum_type.imports)
|
|
37
|
+
|
|
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.
|
|
55
|
+
|
|
56
|
+
Core Components
|
|
57
|
+
---------------
|
|
58
|
+
|
|
59
|
+
**SourceFormatter**
|
|
60
|
+
Base class for all formatters. Subclasses implement ``can_format()`` and ``format()`` methods.
|
|
61
|
+
|
|
62
|
+
**SourceFragment**
|
|
63
|
+
Represents a piece of generated code with its required imports.
|
|
64
|
+
|
|
65
|
+
**FormatContext**
|
|
66
|
+
Carries indentation depth, ``clean_mode``, the ``(module, name)`` to
|
|
67
|
+
local-name mapping, and the pass's ``PathRendering``. ``indented()`` returns
|
|
68
|
+
a copied context and does not mutate its caller.
|
|
69
|
+
|
|
70
|
+
**resolve_imports**
|
|
71
|
+
Detects collisions and returns import lines plus the name mapping used by the
|
|
72
|
+
regeneration pass.
|
|
73
|
+
|
|
74
|
+
**generate_python_source**
|
|
75
|
+
Orchestrates collection, resolution, and regeneration to produce a complete
|
|
76
|
+
source string.
|
|
77
|
+
|
|
78
|
+
Path factoring
|
|
79
|
+
--------------
|
|
80
|
+
|
|
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('...')``.
|
|
89
|
+
|
|
90
|
+
Clean Mode
|
|
91
|
+
----------
|
|
92
|
+
|
|
93
|
+
Clean mode omits fields matching their default values. This requires:
|
|
94
|
+
|
|
95
|
+
1. Accessing the dataclass field defaults
|
|
96
|
+
2. Comparing current values to defaults
|
|
97
|
+
3. Omitting fields where ``current_value == default_value``
|
|
98
|
+
|
|
99
|
+
Explicit mode includes all fields for complete reproducibility.
|
|
100
|
+
|
|
101
|
+
Stability and trust boundary
|
|
102
|
+
----------------------------
|
|
103
|
+
|
|
104
|
+
Import ordering and collision aliases are deterministic for a fixed set of
|
|
105
|
+
``(module, name)`` pairs. The library does not promise that arbitrary object
|
|
106
|
+
graphs produce byte-identical text across runs: container iteration, object
|
|
107
|
+
``repr`` implementations, custom formatters, and dependency versions can all
|
|
108
|
+
affect output or replay.
|
|
109
|
+
|
|
110
|
+
The output is executable Python. pycodify owns formatting and import resolution,
|
|
111
|
+
not sandboxing or validation of generated programs. Hosts must restrict inputs
|
|
112
|
+
and custom formatters to trusted sources, review persisted code before running
|
|
113
|
+
it, and define their own compatibility policy for imported domain APIs.
|
|
114
|
+
|
|
115
|
+
Module Structure
|
|
116
|
+
----------------
|
|
117
|
+
|
|
118
|
+
- **core.py**: Main API (``Assignment``, ``generate_python_source``)
|
|
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.
|
|
124
|
+
- **imports.py**: Import resolution and collision handling
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""Sphinx configuration for pycodify."""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import sys
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
# Add source to path
|
|
6
|
-
sys.path.insert(0,
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
7
9
|
|
|
8
10
|
# Project information
|
|
9
11
|
project = "pycodify"
|
|
10
12
|
copyright = "2024, Tristan Simas"
|
|
11
13
|
author = "Tristan Simas"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
release = import_module("pycodify").__version__
|
|
15
|
+
version = ".".join(release.split(".")[:2])
|
|
14
16
|
|
|
15
17
|
# General configuration
|
|
16
18
|
extensions = [
|
|
@@ -54,7 +56,6 @@ autosummary_generate = True
|
|
|
54
56
|
html_theme = "sphinx_rtd_theme"
|
|
55
57
|
html_theme_options = {
|
|
56
58
|
"logo_only": False,
|
|
57
|
-
"display_version": True,
|
|
58
59
|
"prev_next_buttons_location": "bottom",
|
|
59
60
|
"style_external_links": False,
|
|
60
61
|
"vcs_pageview_mode": "",
|
|
@@ -70,4 +71,3 @@ html_favicon = None
|
|
|
70
71
|
intersphinx_mapping = {
|
|
71
72
|
"python": ("https://docs.python.org/3", None),
|
|
72
73
|
}
|
|
73
|
-
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
pycodify
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
pycodify serializes Python objects to executable Python source while resolving
|
|
5
|
+
imports and name collisions.
|
|
6
|
+
|
|
7
|
+
Quick start
|
|
8
|
+
-----------
|
|
9
|
+
|
|
10
|
+
.. code-block:: python
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pycodify import Assignment, generate_python_source
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Config:
|
|
17
|
+
name: str = "default"
|
|
18
|
+
value: int = 42
|
|
19
|
+
|
|
20
|
+
config = Config(name="production", value=100)
|
|
21
|
+
code = generate_python_source(Assignment("config", config))
|
|
22
|
+
|
|
23
|
+
Explicit mode is the default (``clean_mode=False``) and includes default-valued
|
|
24
|
+
fields. Pass ``clean_mode=True`` only when default elision is desired.
|
|
25
|
+
|
|
26
|
+
.. toctree::
|
|
27
|
+
:maxdepth: 2
|
|
28
|
+
|
|
29
|
+
usage
|
|
30
|
+
api
|
|
31
|
+
architecture
|
|
32
|
+
|
|
33
|
+
Ownership
|
|
34
|
+
---------
|
|
35
|
+
|
|
36
|
+
pycodify owns source fragments, imports, formatter registration, collision
|
|
37
|
+
handling, and generic Python formatting. Host packages own their domain
|
|
38
|
+
formatters and round-trip policies.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Usage Guide
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
Basic Usage
|
|
5
|
+
-----------
|
|
6
|
+
|
|
7
|
+
The simplest way to use pycodify is to generate source code for an object:
|
|
8
|
+
|
|
9
|
+
.. code-block:: python
|
|
10
|
+
|
|
11
|
+
from pycodify import Assignment, generate_python_source
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ProcessingConfig:
|
|
16
|
+
input_path: str = "/data/input"
|
|
17
|
+
output_path: str = "/data/output"
|
|
18
|
+
num_workers: int = 4
|
|
19
|
+
|
|
20
|
+
config = ProcessingConfig(
|
|
21
|
+
input_path="/data/production",
|
|
22
|
+
num_workers=8
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
code = generate_python_source(
|
|
26
|
+
Assignment("config", config),
|
|
27
|
+
clean_mode=True
|
|
28
|
+
)
|
|
29
|
+
print(code)
|
|
30
|
+
|
|
31
|
+
Clean Mode vs Explicit Mode
|
|
32
|
+
----------------------------
|
|
33
|
+
|
|
34
|
+
**Clean mode** omits fields that match their default values and must be enabled
|
|
35
|
+
explicitly:
|
|
36
|
+
|
|
37
|
+
.. code-block:: python
|
|
38
|
+
|
|
39
|
+
# Clean mode - concise
|
|
40
|
+
code = generate_python_source(assignment, clean_mode=True)
|
|
41
|
+
# Output: config = ProcessingConfig(input_path='/data/production', num_workers=8)
|
|
42
|
+
|
|
43
|
+
**Explicit mode** includes all fields for complete reproducibility and is the
|
|
44
|
+
default:
|
|
45
|
+
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
# Explicit mode - complete
|
|
49
|
+
code = generate_python_source(assignment, clean_mode=False)
|
|
50
|
+
# Output: config = ProcessingConfig(
|
|
51
|
+
# input_path='/data/production',
|
|
52
|
+
# output_path='/data/output',
|
|
53
|
+
# num_workers=8
|
|
54
|
+
# )
|
|
55
|
+
|
|
56
|
+
Working with Enums
|
|
57
|
+
------------------
|
|
58
|
+
|
|
59
|
+
Enums are serialized with their full qualified names:
|
|
60
|
+
|
|
61
|
+
.. code-block:: python
|
|
62
|
+
|
|
63
|
+
from enum import Enum
|
|
64
|
+
|
|
65
|
+
class ImageFormat(Enum):
|
|
66
|
+
JPEG = "jpeg"
|
|
67
|
+
PNG = "png"
|
|
68
|
+
TIFF = "tiff"
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ImageConfig:
|
|
72
|
+
format: ImageFormat = ImageFormat.JPEG
|
|
73
|
+
|
|
74
|
+
config = ImageConfig(format=ImageFormat.PNG)
|
|
75
|
+
code = generate_python_source(Assignment("config", config))
|
|
76
|
+
# Output includes: from __main__ import ImageFormat
|
|
77
|
+
# config = ImageConfig(format=ImageFormat.PNG)
|
|
78
|
+
|
|
79
|
+
Handling Import Collisions
|
|
80
|
+
---------------------------
|
|
81
|
+
|
|
82
|
+
When multiple modules export the same name, pycodify aliases every colliding
|
|
83
|
+
import. Aliases are derived from the complete module path, with dots replaced
|
|
84
|
+
by underscores:
|
|
85
|
+
|
|
86
|
+
.. code-block:: python
|
|
87
|
+
|
|
88
|
+
# pycodify generates:
|
|
89
|
+
# from package.module_a import Config as Config_package_module_a
|
|
90
|
+
# from package.module_b import Config as Config_package_module_b
|
|
91
|
+
# left = Config_package_module_a(...)
|
|
92
|
+
# right = Config_package_module_b(...)
|
|
93
|
+
|
|
94
|
+
``resolve_imports`` sorts modules and imported names before emitting them, so a
|
|
95
|
+
given set of import requirements has stable ordering and aliases. Whole-file
|
|
96
|
+
byte-for-byte stability additionally depends on value iteration order, ``repr``
|
|
97
|
+
implementations, and any custom formatters.
|
|
98
|
+
|
|
99
|
+
Formatting one value
|
|
100
|
+
--------------------
|
|
101
|
+
|
|
102
|
+
``to_source`` returns a ``SourceFragment`` rather than a complete file. Pass a
|
|
103
|
+
``FormatContext`` when indentation, clean mode, or already-resolved names must
|
|
104
|
+
be controlled explicitly:
|
|
105
|
+
|
|
106
|
+
.. code-block:: python
|
|
107
|
+
|
|
108
|
+
from pycodify import FormatContext, to_source
|
|
109
|
+
|
|
110
|
+
context = FormatContext(indent=1, clean_mode=True)
|
|
111
|
+
fragment = to_source([1, 2], context)
|
|
112
|
+
print(fragment.code)
|
|
113
|
+
print(fragment.imports)
|
|
114
|
+
|
|
115
|
+
``FormatContext.indented()`` returns a copy with one additional indentation
|
|
116
|
+
level. ``name_mappings`` maps ``(module, imported_name)`` pairs to the local
|
|
117
|
+
names chosen by ``resolve_imports``; callers normally let
|
|
118
|
+
``generate_python_source`` populate it during the second pass.
|
|
119
|
+
|
|
120
|
+
Values that own their source
|
|
121
|
+
----------------------------
|
|
122
|
+
|
|
123
|
+
A value whose source is not its dataclass constructor implements
|
|
124
|
+
``PythonSourceLiteral``:
|
|
125
|
+
|
|
126
|
+
.. code-block:: python
|
|
127
|
+
|
|
128
|
+
from dataclasses import dataclass
|
|
129
|
+
|
|
130
|
+
from pycodify import (
|
|
131
|
+
Assignment,
|
|
132
|
+
FormatContext,
|
|
133
|
+
PythonSourceLiteral,
|
|
134
|
+
SourceFragment,
|
|
135
|
+
generate_python_source,
|
|
136
|
+
imported_name,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
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)
|
|
146
|
+
|
|
147
|
+
source = generate_python_source(Assignment("func", RegistryLookup("gauss")))
|
|
148
|
+
# from myapp.registry import get_function
|
|
149
|
+
#
|
|
150
|
+
# func = get_function('gauss')
|
|
151
|
+
|
|
152
|
+
Nested Dataclasses
|
|
153
|
+
-------------------
|
|
154
|
+
|
|
155
|
+
Nested dataclasses are properly serialized with all necessary imports:
|
|
156
|
+
|
|
157
|
+
.. code-block:: python
|
|
158
|
+
|
|
159
|
+
from dataclasses import dataclass, field
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class DatabaseConfig:
|
|
163
|
+
host: str = "localhost"
|
|
164
|
+
port: int = 5432
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class AppConfig:
|
|
168
|
+
database: DatabaseConfig = field(default_factory=DatabaseConfig)
|
|
169
|
+
debug: bool = False
|
|
170
|
+
|
|
171
|
+
config = AppConfig(
|
|
172
|
+
database=DatabaseConfig(host="prod.db.internal"),
|
|
173
|
+
debug=True
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
code = generate_python_source(Assignment("config", config))
|
|
177
|
+
# Generates imports for both DatabaseConfig and AppConfig
|
|
178
|
+
|
|
179
|
+
Executing Generated Code
|
|
180
|
+
------------------------
|
|
181
|
+
|
|
182
|
+
The generated code is executable Python:
|
|
183
|
+
|
|
184
|
+
.. code-block:: python
|
|
185
|
+
|
|
186
|
+
code = generate_python_source(assignment)
|
|
187
|
+
namespace = {}
|
|
188
|
+
exec(code, namespace)
|
|
189
|
+
recreated_config = namespace["config"]
|
|
190
|
+
assert recreated_config == original_config
|
|
191
|
+
|
|
192
|
+
.. warning::
|
|
193
|
+
|
|
194
|
+
Generated output is executable Python, not a sandboxed data format. Execute
|
|
195
|
+
it only when the input objects, custom formatters, header, and resulting file
|
|
196
|
+
are trusted and have been reviewed. pycodify does not make ``exec`` safe for
|
|
197
|
+
untrusted input.
|
|
198
|
+
|
|
199
|
+
Replay compatibility is owned jointly by the generated imports and the APIs
|
|
200
|
+
they call. Explicit mode records default-valued fields, but neither mode can
|
|
201
|
+
guarantee replay across incompatible module moves or constructor changes.
|
|
@@ -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"}
|
|
@@ -62,11 +62,11 @@ target-version = ["py310", "py311", "py312", "py313"]
|
|
|
62
62
|
|
|
63
63
|
[tool.ruff]
|
|
64
64
|
line-length = 100
|
|
65
|
-
target-version = "
|
|
65
|
+
target-version = "py310"
|
|
66
66
|
select = ["E", "F", "I", "N", "W", "UP"]
|
|
67
67
|
|
|
68
68
|
[tool.mypy]
|
|
69
|
-
python_version = "
|
|
69
|
+
python_version = "3.10"
|
|
70
70
|
warn_return_any = true
|
|
71
71
|
warn_unused_configs = true
|
|
72
72
|
disallow_untyped_defs = true
|
|
@@ -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.2"
|
|
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
|
]
|