pycodify 0.1.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.
@@ -0,0 +1,102 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master, claude/**]
6
+ pull_request:
7
+ branches: [main, master]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ # Test across Python versions and OSes
12
+ test:
13
+ runs-on: ${{ matrix.os }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
18
+ os: [ubuntu-latest, windows-latest, macos-latest]
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Setup Python ${{ matrix.python-version }}
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install -e ".[dev]"
33
+
34
+ - name: Run tests with coverage
35
+ run: |
36
+ pytest --cov=src/pycodify --cov-report=xml --cov-report=html --cov-report=term-missing -v
37
+
38
+ - name: Upload coverage to Codecov
39
+ if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
40
+ uses: codecov/codecov-action@v3
41
+ with:
42
+ file: ./coverage.xml
43
+ fail_ci_if_error: false
44
+
45
+ # Code quality checks
46
+ code-quality:
47
+ runs-on: ubuntu-latest
48
+
49
+ steps:
50
+ - name: Checkout
51
+ uses: actions/checkout@v4
52
+
53
+ - name: Setup Python
54
+ uses: actions/setup-python@v5
55
+ with:
56
+ python-version: "3.12"
57
+
58
+ - name: Install dependencies
59
+ run: |
60
+ python -m pip install --upgrade pip
61
+ pip install ruff black mypy
62
+
63
+ - name: Run ruff (linting)
64
+ run: |
65
+ ruff check src/ --output-format=github
66
+
67
+ - name: Run black (formatting check)
68
+ run: |
69
+ black --check src/
70
+
71
+ - name: Run mypy (type checking)
72
+ run: |
73
+ mypy src/ --ignore-missing-imports
74
+
75
+ # Documentation build check
76
+ docs:
77
+ runs-on: ubuntu-latest
78
+
79
+ steps:
80
+ - name: Checkout
81
+ uses: actions/checkout@v4
82
+
83
+ - name: Setup Python
84
+ uses: actions/setup-python@v5
85
+ with:
86
+ python-version: "3.12"
87
+
88
+ - name: Install dependencies
89
+ run: |
90
+ python -m pip install --upgrade pip
91
+ pip install -e ".[docs]"
92
+
93
+ - name: Build documentation
94
+ run: |
95
+ cd docs
96
+ make html
97
+
98
+ - name: Check for build warnings
99
+ run: |
100
+ cd docs
101
+ make html SPHINXOPTS="-W --keep-going"
102
+
@@ -0,0 +1,52 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write # Required for trusted publishing
11
+
12
+ jobs:
13
+ build-and-publish:
14
+ name: Build and publish to PyPI
15
+ runs-on: ubuntu-latest
16
+ environment:
17
+ name: pypi
18
+ url: https://pypi.org/p/pycodify
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Set up Python
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install build dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install build
33
+
34
+ - name: Build package
35
+ run: python -m build
36
+
37
+ - name: Check distribution
38
+ run: |
39
+ pip install twine
40
+ twine check dist/*
41
+
42
+ - name: Create GitHub Release
43
+ uses: softprops/action-gh-release@v2
44
+ with:
45
+ files: dist/*
46
+ generate_release_notes: true
47
+ env:
48
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
+
50
+ - name: Publish to PyPI
51
+ uses: pypa/gh-action-pypi-publish@release/v1
52
+
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .pytest_cache/
6
+ .coverage
7
+ .dist-info/
8
+ .build/
9
+ .eggs/
@@ -0,0 +1,134 @@
1
+ # Contributing to pycodify
2
+
3
+ Thank you for your interest in contributing to pycodify! This document provides guidelines for development, testing, and submission.
4
+
5
+ ## Development Setup
6
+
7
+ ### Prerequisites
8
+ - Python 3.10+
9
+ - Git
10
+
11
+ ### Installation
12
+
13
+ ```bash
14
+ # Clone the repository
15
+ git clone https://github.com/OpenHCSDev/pycodify.git
16
+ cd pycodify
17
+
18
+ # Create virtual environment
19
+ python -m venv venv
20
+ source venv/bin/activate # On Windows: venv\Scripts\activate
21
+
22
+ # Install in development mode with all dependencies
23
+ pip install -e ".[dev,docs]"
24
+ ```
25
+
26
+ ## Code Style
27
+
28
+ pycodify follows strict code quality standards:
29
+
30
+ ### Formatting
31
+ - **Black**: Code formatting (line length: 100)
32
+ - **Ruff**: Linting and import sorting
33
+ - **MyPy**: Static type checking
34
+
35
+ ```bash
36
+ # Format code
37
+ black src/ tests/
38
+
39
+ # Lint
40
+ ruff check src/ tests/
41
+
42
+ # Type check
43
+ mypy src/
44
+ ```
45
+
46
+ ### Docstrings
47
+ - Use Google-style docstrings
48
+ - Include type hints in function signatures
49
+ - Document all public APIs
50
+
51
+ ## Testing
52
+
53
+ ### Running Tests
54
+
55
+ ```bash
56
+ # Run all tests
57
+ pytest tests/ -v
58
+
59
+ # Run with coverage
60
+ pytest tests/ --cov=src/pycodify --cov-report=html
61
+
62
+ # Run specific test file
63
+ pytest tests/test_core.py -v
64
+ ```
65
+
66
+ ### Test Requirements
67
+ - Minimum 80% code coverage
68
+ - All tests must pass before submission
69
+ - Use pytest fixtures for setup/teardown
70
+ - Test both collection and regeneration passes
71
+
72
+ ## Submitting Changes
73
+
74
+ ### Before Submitting
75
+ 1. Ensure all tests pass: `pytest tests/`
76
+ 2. Format code: `black src/ tests/`
77
+ 3. Run linter: `ruff check src/ tests/`
78
+ 4. Type check: `mypy src/`
79
+ 5. Update documentation if needed
80
+
81
+ ### Pull Request Process
82
+ 1. Create a feature branch: `git checkout -b feature/your-feature`
83
+ 2. Make your changes with clear commit messages
84
+ 3. Push to your fork and create a Pull Request
85
+ 4. Ensure CI/CD checks pass
86
+ 5. Request review from maintainers
87
+
88
+ ### Commit Message Format
89
+ ```
90
+ <type>: <subject>
91
+
92
+ <body>
93
+
94
+ <footer>
95
+ ```
96
+
97
+ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
98
+
99
+ ## Documentation
100
+
101
+ ### Building Docs Locally
102
+
103
+ ```bash
104
+ cd docs
105
+ make html
106
+ # Open build/html/index.html in browser
107
+ ```
108
+
109
+ ### Documentation Standards
110
+ - Update README.md for user-facing changes
111
+ - Update docstrings for API changes
112
+ - Add examples for new features
113
+ - Document the two-pass algorithm for custom formatters
114
+
115
+ ## Architecture Notes
116
+
117
+ pycodify uses a two-pass algorithm:
118
+ 1. **Collection Pass**: Traverse object, emit code fragments, collect imports
119
+ 2. **Resolution Pass**: Detect collisions, generate aliases, regenerate code
120
+
121
+ Custom formatters should inherit from `SourceFormatter` and implement:
122
+ - `can_format(value)`: Check if formatter handles this type
123
+ - `format(value, context)`: Return SourceFragment with code and imports
124
+
125
+ ## Questions?
126
+
127
+ - Open an issue for bugs or feature requests
128
+ - Check existing issues before creating new ones
129
+ - Use discussions for questions
130
+
131
+ ## License
132
+
133
+ By contributing, you agree that your contributions will be licensed under the MIT License.
134
+
pycodify-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenHCS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycodify
3
+ Version: 0.1.0
4
+ Summary: Python source code as a serialization format with automatic import resolution
5
+ Project-URL: Homepage, https://github.com/OpenHCSDev/pycodify
6
+ Project-URL: Documentation, https://pycodify.readthedocs.io
7
+ Project-URL: Repository, https://github.com/OpenHCSDev/pycodify
8
+ Project-URL: Issues, https://github.com/OpenHCSDev/pycodify/issues
9
+ Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: code-generation,configuration,python-source,reproducibility,serialization
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: black>=23.0; extra == 'dev'
26
+ Requires-Dist: mypy>=1.0; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
30
+ Provides-Extra: docs
31
+ Requires-Dist: sphinx-autodoc-typehints>=1.25; extra == 'docs'
32
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
33
+ Requires-Dist: sphinx>=7.0; extra == 'docs'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # pycodify
37
+
38
+ Python source code as a serialization format with automatic import resolution.
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from pycodify import Assignment, generate_python_source
44
+ from dataclasses import dataclass
45
+
46
+ @dataclass
47
+ class Config:
48
+ name: str = "default"
49
+ value: int = 42
50
+
51
+ config = Config(name="production", value=100)
52
+ code = generate_python_source(Assignment("config", config), clean_mode=True)
53
+ print(code)
54
+ # Output:
55
+ # from __main__ import Config
56
+ # config = Config(name='production', value=100)
57
+ ```
58
+
59
+ ## Why Python Source?
60
+
61
+ | Format | Diffable | Inspectable | Editable | Type-preserving | Cross-version |
62
+ |--------|:--------:|:-----------:|:--------:|:---------------:|:-------------:|
63
+ | pickle | ✗ | ✗ | ✗ | ✓ | ✗ |
64
+ | JSON/YAML | ✓ | ✓ | ✓ | ✗ | ✓ |
65
+ | Python source | ✓ | ✓ | ✓ | ✓ | ✓ |
66
+
67
+ Binary formats like `pickle` cannot be diffed, inspected, or edited. Text formats like JSON lose type information. Python source code has all desired properties: it is diffable, inspectable, editable, type-preserving, and cross-version stable.
68
+
69
+ ## Features
70
+
71
+ - **Complete Executable Source**: Generates imports + code, not just expressions
72
+ - **Type-Preserving**: Enums, Paths, callables serialize as themselves
73
+ - **Collision Handling**: Automatic aliasing for name collisions across modules
74
+ - **Clean Mode**: Omit fields matching defaults for concise output
75
+ - **Extensible**: Register custom formatters for domain-specific types
76
+
77
+ ## Documentation
78
+
79
+ Full documentation available at [pycodify.readthedocs.io](https://pycodify.readthedocs.io)
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ pip install pycodify
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,54 @@
1
+ # pycodify
2
+
3
+ Python source code as a serialization format with automatic import resolution.
4
+
5
+ ## Quick Start
6
+
7
+ ```python
8
+ from pycodify import Assignment, generate_python_source
9
+ from dataclasses import dataclass
10
+
11
+ @dataclass
12
+ class Config:
13
+ name: str = "default"
14
+ value: int = 42
15
+
16
+ config = Config(name="production", value=100)
17
+ code = generate_python_source(Assignment("config", config), clean_mode=True)
18
+ print(code)
19
+ # Output:
20
+ # from __main__ import Config
21
+ # config = Config(name='production', value=100)
22
+ ```
23
+
24
+ ## Why Python Source?
25
+
26
+ | Format | Diffable | Inspectable | Editable | Type-preserving | Cross-version |
27
+ |--------|:--------:|:-----------:|:--------:|:---------------:|:-------------:|
28
+ | pickle | ✗ | ✗ | ✗ | ✓ | ✗ |
29
+ | JSON/YAML | ✓ | ✓ | ✓ | ✗ | ✓ |
30
+ | Python source | ✓ | ✓ | ✓ | ✓ | ✓ |
31
+
32
+ Binary formats like `pickle` cannot be diffed, inspected, or edited. Text formats like JSON lose type information. Python source code has all desired properties: it is diffable, inspectable, editable, type-preserving, and cross-version stable.
33
+
34
+ ## Features
35
+
36
+ - **Complete Executable Source**: Generates imports + code, not just expressions
37
+ - **Type-Preserving**: Enums, Paths, callables serialize as themselves
38
+ - **Collision Handling**: Automatic aliasing for name collisions across modules
39
+ - **Clean Mode**: Omit fields matching defaults for concise output
40
+ - **Extensible**: Register custom formatters for domain-specific types
41
+
42
+ ## Documentation
43
+
44
+ Full documentation available at [pycodify.readthedocs.io](https://pycodify.readthedocs.io)
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install pycodify
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,18 @@
1
+ # Minimal makefile for Sphinx documentation
2
+
3
+ # You can set these variables from the command line.
4
+ SPHINXOPTS =
5
+ SPHINXBUILD = sphinx-build
6
+ SOURCEDIR = .
7
+ BUILDDIR = _build
8
+
9
+ # Put it first so that "make" without argument is like "make help".
10
+ help:
11
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
12
+
13
+ .PHONY: help Makefile
14
+
15
+ # Catch-all target: route all unknown targets to Sphinx using the new
16
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
17
+ %: Makefile
18
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@@ -0,0 +1,106 @@
1
+ API Reference
2
+ =============
3
+
4
+ Core API
5
+ --------
6
+
7
+ .. automodule:: pycodify.core
8
+ :members:
9
+ :undoc-members:
10
+ :show-inheritance:
11
+
12
+ Formatters
13
+ ----------
14
+
15
+ .. automodule:: pycodify.formatters
16
+ :members:
17
+ :undoc-members:
18
+ :show-inheritance:
19
+
20
+ Import Resolution
21
+ -----------------
22
+
23
+ .. automodule:: pycodify.imports
24
+ :members:
25
+ :undoc-members:
26
+ :show-inheritance:
27
+
28
+ Main Functions
29
+ --------------
30
+
31
+ .. py:function:: generate_python_source(assignment: Assignment, clean_mode: bool = True) -> str
32
+
33
+ Generate executable Python source code from an object.
34
+
35
+ :param assignment: Assignment object specifying variable name and value
36
+ :type assignment: Assignment
37
+ :param clean_mode: If True, omit fields matching defaults. If False, include all fields.
38
+ :type clean_mode: bool
39
+ :return: Executable Python source code with imports
40
+ :rtype: str
41
+
42
+ **Example:**
43
+
44
+ .. code-block:: python
45
+
46
+ from pycodify import Assignment, generate_python_source
47
+ from dataclasses import dataclass
48
+
49
+ @dataclass
50
+ class Config:
51
+ name: str = "default"
52
+ value: int = 42
53
+
54
+ config = Config(name="production", value=100)
55
+ code = generate_python_source(Assignment("config", config))
56
+ print(code)
57
+
58
+ .. py:class:: Assignment
59
+
60
+ Represents a variable assignment for code generation.
61
+
62
+ .. py:method:: __init__(name: str, value: Any)
63
+
64
+ Create an assignment.
65
+
66
+ :param name: Variable name
67
+ :type name: str
68
+ :param value: Value to serialize
69
+ :type value: Any
70
+
71
+ .. py:class:: SourceFormatter
72
+
73
+ Base class for custom formatters.
74
+
75
+ .. py:method:: can_format(value: Any) -> bool
76
+
77
+ Check if this formatter can handle the value.
78
+
79
+ :param value: Value to check
80
+ :type value: Any
81
+ :return: True if this formatter can format the value
82
+ :rtype: bool
83
+
84
+ .. py:method:: format(value: Any, context: FormatContext) -> SourceFragment
85
+
86
+ Format a value to source code.
87
+
88
+ :param value: Value to format
89
+ :type value: Any
90
+ :param context: Formatting context with state
91
+ :type context: FormatContext
92
+ :return: Source code fragment with imports
93
+ :rtype: SourceFragment
94
+
95
+ .. py:class:: SourceFragment
96
+
97
+ Represents generated source code with its imports.
98
+
99
+ .. py:attribute:: code
100
+
101
+ The generated Python source code.
102
+
103
+ .. py:attribute:: imports
104
+
105
+ Set of (module, name) tuples required for the code.
106
+
@@ -0,0 +1,89 @@
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:
10
+
11
+ 1. **Collection Pass**: Traverse the object, emit code fragments, collect ``(module, name)`` import pairs
12
+ 2. **Resolution**: Detect collisions (same name, different modules), generate deterministic aliases
13
+ 3. **Regeneration Pass**: Re-traverse with resolved ``name_mappings``, emit final code
14
+
15
+ 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``.
16
+
17
+ Extensible Formatter Registry
18
+ ------------------------------
19
+
20
+ 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:
21
+
22
+ .. code-block:: python
23
+
24
+ class EnumFormatter(SourceFormatter):
25
+ priority = 70
26
+
27
+ def can_format(self, value: Any) -> bool:
28
+ return isinstance(value, Enum)
29
+
30
+ def format(self, value: Enum, context: FormatContext) -> SourceFragment:
31
+ cls = value.__class__
32
+ import_pair = (cls.__module__, cls.__name__)
33
+ name = context.name_mappings.get(import_pair, cls.__name__)
34
+ return SourceFragment(f"{name}.{value.name}", frozenset([import_pair]))
35
+
36
+ Priority-based dispatch selects the most specific formatter. Domain extensions add formatters without modifying core code.
37
+
38
+ Core Components
39
+ ---------------
40
+
41
+ **SourceFormatter**
42
+ Base class for all formatters. Subclasses implement ``can_format()`` and ``format()`` methods.
43
+
44
+ **SourceFragment**
45
+ Represents a piece of generated code with its required imports.
46
+
47
+ **FormatContext**
48
+ Carries state during formatting: ``name_mappings``, ``visited_objects``, etc.
49
+
50
+ **ImportResolver**
51
+ Detects collisions and generates deterministic aliases for imports.
52
+
53
+ **SourceGenerator**
54
+ Orchestrates the two-pass algorithm and produces final executable code.
55
+
56
+ Clean Mode
57
+ ----------
58
+
59
+ Clean mode omits fields matching their default values. This requires:
60
+
61
+ 1. Accessing the dataclass field defaults
62
+ 2. Comparing current values to defaults
63
+ 3. Omitting fields where ``current_value == default_value``
64
+
65
+ Explicit mode includes all fields for complete reproducibility.
66
+
67
+ Lazy Dataclass Integration
68
+ ---------------------------
69
+
70
+ For dataclasses with ``__getattribute__`` interception (used for hierarchical config inheritance), formatters use ``object.__getattribute__`` to access raw field values without triggering lazy resolution:
71
+
72
+ .. code-block:: python
73
+
74
+ # In DataclassFormatter
75
+ if hasattr(instance, "_resolve_field_value"):
76
+ # Bypass __getattribute__ to get raw None vs concrete value
77
+ current_value = object.__getattribute__(instance, field_name)
78
+ else:
79
+ current_value = getattr(instance, field_name)
80
+
81
+ This distinguishes explicitly-set values from inherited ones during serialization.
82
+
83
+ Module Structure
84
+ ----------------
85
+
86
+ - **core.py**: Main API (``Assignment``, ``generate_python_source``)
87
+ - **formatters.py**: Built-in formatters (dataclass, enum, primitive types)
88
+ - **imports.py**: Import resolution and collision handling
89
+