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.
Files changed (33) hide show
  1. {pycodify-0.2.0 → pycodify-0.2.1}/PKG-INFO +1 -1
  2. {pycodify-0.2.0 → pycodify-0.2.1}/pyproject.toml +1 -1
  3. {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/__init__.py +3 -1
  4. pycodify-0.2.1/src/pycodify/documents.py +65 -0
  5. pycodify-0.2.1/tests/test_documents.py +45 -0
  6. {pycodify-0.2.0 → pycodify-0.2.1}/.github/workflows/ci.yml +0 -0
  7. {pycodify-0.2.0 → pycodify-0.2.1}/.github/workflows/publish.yml +0 -0
  8. {pycodify-0.2.0 → pycodify-0.2.1}/.gitignore +0 -0
  9. {pycodify-0.2.0 → pycodify-0.2.1}/.readthedocs.yaml +0 -0
  10. {pycodify-0.2.0 → pycodify-0.2.1}/CONTRIBUTING.md +0 -0
  11. {pycodify-0.2.0 → pycodify-0.2.1}/LICENSE +0 -0
  12. {pycodify-0.2.0 → pycodify-0.2.1}/README.md +0 -0
  13. {pycodify-0.2.0 → pycodify-0.2.1}/docs/Makefile +0 -0
  14. {pycodify-0.2.0 → pycodify-0.2.1}/docs/api.rst +0 -0
  15. {pycodify-0.2.0 → pycodify-0.2.1}/docs/architecture.rst +0 -0
  16. {pycodify-0.2.0 → pycodify-0.2.1}/docs/conf.py +0 -0
  17. {pycodify-0.2.0 → pycodify-0.2.1}/docs/index.rst +0 -0
  18. {pycodify-0.2.0 → pycodify-0.2.1}/docs/usage.rst +0 -0
  19. {pycodify-0.2.0 → pycodify-0.2.1}/paper.bib +0 -0
  20. {pycodify-0.2.0 → pycodify-0.2.1}/paper.md +0 -0
  21. {pycodify-0.2.0 → pycodify-0.2.1}/scripts/release.py +0 -0
  22. {pycodify-0.2.0 → pycodify-0.2.1}/scripts/update_and_release.py +0 -0
  23. {pycodify-0.2.0 → pycodify-0.2.1}/scripts/update_version.py +0 -0
  24. {pycodify-0.2.0 → pycodify-0.2.1}/scripts/verify_release_ready.py +0 -0
  25. {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/core.py +0 -0
  26. {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/formatters.py +0 -0
  27. {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/imports.py +0 -0
  28. {pycodify-0.2.0 → pycodify-0.2.1}/src/pycodify/paths.py +0 -0
  29. {pycodify-0.2.0 → pycodify-0.2.1}/tests/__init__.py +0 -0
  30. {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_core.py +0 -0
  31. {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_imports.py +0 -0
  32. {pycodify-0.2.0 → pycodify-0.2.1}/tests/test_paths.py +0 -0
  33. {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.0
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.0"
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.0"
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