pycodify 0.2.0__tar.gz → 0.2.1__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 → pycodify-0.2.1}/PKG-INFO +1 -1
- {pycodify-0.2.0 → pycodify-0.2.1}/pyproject.toml +1 -1
- {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/__init__.py +3 -1
- pycodify-0.2.1/src/pycodify/documents.py +65 -0
- pycodify-0.2.1/tests/test_documents.py +45 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/.github/workflows/ci.yml +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/.github/workflows/publish.yml +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/.gitignore +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/.readthedocs.yaml +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/CONTRIBUTING.md +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/LICENSE +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/README.md +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/docs/Makefile +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/docs/api.rst +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/docs/architecture.rst +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/docs/conf.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/docs/index.rst +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/docs/usage.rst +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/paper.bib +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/paper.md +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/scripts/release.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/scripts/update_and_release.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/scripts/update_version.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/scripts/verify_release_ready.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/core.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/formatters.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/imports.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/paths.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/tests/__init__.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_core.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_imports.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_paths.py +0 -0
- {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_round_trip.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycodify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycodify"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
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"}
|
|
@@ -14,10 +14,11 @@ from .core import (
|
|
|
14
14
|
imported_name,
|
|
15
15
|
to_source,
|
|
16
16
|
)
|
|
17
|
+
from .documents import SourceDocument
|
|
17
18
|
from .imports import resolve_imports
|
|
18
19
|
from .paths import PathCollector, PathConstructor, PathFactoring
|
|
19
20
|
|
|
20
|
-
__version__ = "0.2.
|
|
21
|
+
__version__ = "0.2.1"
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
23
24
|
"Assignment",
|
|
@@ -30,6 +31,7 @@ __all__ = [
|
|
|
30
31
|
"PathFactoring",
|
|
31
32
|
"PathRendering",
|
|
32
33
|
"PythonSourceLiteral",
|
|
34
|
+
"SourceDocument",
|
|
33
35
|
"SourceFormatter",
|
|
34
36
|
"SourceFragment",
|
|
35
37
|
"generate_python_source",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""A dataclass saved as one Python module of assignments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import MISSING, Field, fields
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
from .core import Assignment, BlankLine, CodeBlock, generate_python_source
|
|
10
|
+
|
|
11
|
+
DocumentT = TypeVar("DocumentT", bound="SourceDocument")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SourceDocument:
|
|
15
|
+
"""A dataclass whose constructor fields are a module's public assignments.
|
|
16
|
+
|
|
17
|
+
Each ``init=True`` field is written as one module-level assignment named
|
|
18
|
+
after the field, in declaration order, and read back by that name. The
|
|
19
|
+
field declaration is the only place the name is spelled. A field with a
|
|
20
|
+
default may be absent from a loaded module; a field without one must be
|
|
21
|
+
assigned. ``init=False`` fields are not part of the saved module.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__slots__ = ()
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def assignment_fields(cls) -> tuple[Field[Any], ...]:
|
|
28
|
+
"""Return the fields saved as assignments, in declaration order."""
|
|
29
|
+
return tuple(field for field in fields(cls) if field.init) # type: ignore[arg-type]
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def assignment_names(cls) -> tuple[str, ...]:
|
|
33
|
+
"""Return the names this document assigns at module level."""
|
|
34
|
+
return tuple(field.name for field in cls.assignment_fields())
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_namespace(cls: type[DocumentT], namespace: Mapping[str, object]) -> DocumentT:
|
|
38
|
+
"""Build the document from the names an executed module defines."""
|
|
39
|
+
values: dict[str, object] = {}
|
|
40
|
+
for field in cls.assignment_fields():
|
|
41
|
+
if field.name in namespace:
|
|
42
|
+
values[field.name] = namespace[field.name]
|
|
43
|
+
elif field.default is MISSING and field.default_factory is MISSING:
|
|
44
|
+
raise ValueError(f"{cls.__name__} source must define {field.name!r}.")
|
|
45
|
+
return cls(**values)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_source(cls: type[DocumentT], source: str) -> DocumentT:
|
|
49
|
+
"""Execute Python source and build the document from its assignments."""
|
|
50
|
+
namespace: dict[str, object] = {}
|
|
51
|
+
exec(compile(source, f"<{cls.__name__}>", "exec"), namespace) # noqa: S102
|
|
52
|
+
return cls.from_namespace(namespace)
|
|
53
|
+
|
|
54
|
+
def body(self) -> CodeBlock:
|
|
55
|
+
"""Return one assignment per saved field, separated by blank lines."""
|
|
56
|
+
items: list[object] = []
|
|
57
|
+
for name in self.assignment_names():
|
|
58
|
+
if items:
|
|
59
|
+
items.append(BlankLine())
|
|
60
|
+
items.append(Assignment(name, getattr(self, name)))
|
|
61
|
+
return CodeBlock.from_items(items)
|
|
62
|
+
|
|
63
|
+
def to_source(self, *, header: str = "", clean_mode: bool = False) -> str:
|
|
64
|
+
"""Render the document as a complete Python module."""
|
|
65
|
+
return generate_python_source(self.body(), header=header, clean_mode=clean_mode)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""A SourceDocument dataclass is saved as, and loaded from, its field names."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from pycodify import SourceDocument
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Recipe(SourceDocument):
|
|
14
|
+
title: str
|
|
15
|
+
steps: list[str] = field(default_factory=list)
|
|
16
|
+
loaded_from: str | None = field(default=None, init=False, compare=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_assignment_names_are_the_constructor_fields() -> None:
|
|
20
|
+
assert Recipe.assignment_names() == ("title", "steps")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_source_assigns_each_field_once_and_loads_back_equal() -> None:
|
|
24
|
+
recipe = Recipe(title="tea", steps=["boil", "steep"])
|
|
25
|
+
|
|
26
|
+
source = recipe.to_source(header="# Recipe")
|
|
27
|
+
|
|
28
|
+
assert source.splitlines()[0] == "# Recipe"
|
|
29
|
+
namespace: dict[str, object] = {}
|
|
30
|
+
exec(source, namespace)
|
|
31
|
+
assert {name for name in namespace if not name.startswith("__")} == {"title", "steps"}
|
|
32
|
+
assert Recipe.from_source(source) == recipe
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_absent_field_takes_its_declared_default() -> None:
|
|
36
|
+
assert Recipe.from_namespace({"title": "tea"}) == Recipe(title="tea")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_absent_required_field_is_an_error() -> None:
|
|
40
|
+
with pytest.raises(ValueError, match="Recipe source must define 'title'"):
|
|
41
|
+
Recipe.from_namespace({"steps": []})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_names_outside_the_declaration_are_ignored() -> None:
|
|
45
|
+
assert Recipe.from_namespace({"title": "tea", "other": 1}) == Recipe(title="tea")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|