pycodify 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ from .core import (
2
+ Assignment,
3
+ BlankLine,
4
+ CodeBlock,
5
+ Comment,
6
+ FormatContext,
7
+ SourceFormatter,
8
+ SourceFragment,
9
+ generate_python_source,
10
+ to_source,
11
+ )
12
+ from .imports import resolve_imports
13
+
14
+ # Register default formatters
15
+ from . import formatters as _formatters # noqa: F401
16
+
17
+ __version__ = "0.1.1"
18
+
19
+ __all__ = [
20
+ "Assignment",
21
+ "BlankLine",
22
+ "CodeBlock",
23
+ "Comment",
24
+ "FormatContext",
25
+ "SourceFormatter",
26
+ "SourceFragment",
27
+ "generate_python_source",
28
+ "resolve_imports",
29
+ "to_source",
30
+ ]
pycodify/core.py ADDED
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field, replace
5
+ from typing import Any, ClassVar, Dict, FrozenSet, Iterable, List, Tuple
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class SourceFragment:
10
+ """Result of serializing a value to Python source."""
11
+
12
+ code: str
13
+ imports: FrozenSet[Tuple[str, str]] = frozenset()
14
+
15
+ def __add__(self, other: "SourceFragment") -> "SourceFragment":
16
+ return SourceFragment(
17
+ code=self.code + other.code,
18
+ imports=self.imports | other.imports,
19
+ )
20
+
21
+
22
+ @dataclass
23
+ class FormatContext:
24
+ """Context passed through serialization."""
25
+
26
+ indent: int = 0
27
+ clean_mode: bool = False
28
+ name_mappings: Dict[Tuple[str, str], str] = field(default_factory=dict)
29
+
30
+ def indented(self) -> "FormatContext":
31
+ return replace(self, indent=self.indent + 1)
32
+
33
+ @property
34
+ def indent_str(self) -> str:
35
+ return " " * self.indent
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Comment:
40
+ text: str
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class BlankLine:
45
+ pass
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Assignment:
50
+ name: str
51
+ value: Any
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class CodeBlock:
56
+ items: Tuple[Any, ...]
57
+
58
+ @classmethod
59
+ def from_items(cls, items: Iterable[Any]) -> "CodeBlock":
60
+ return cls(tuple(items))
61
+
62
+
63
+ class SourceFormatter(ABC):
64
+ """Formats a value to Python source code."""
65
+
66
+ priority: int = 0
67
+
68
+ _registry: ClassVar[List["SourceFormatter"]] = []
69
+ _sorted_registry: ClassVar[List["SourceFormatter"]] = []
70
+ _registry_dirty: ClassVar[bool] = False
71
+
72
+ def __init_subclass__(cls, **kwargs):
73
+ super().__init_subclass__(**kwargs)
74
+ if getattr(cls, "__abstract__", False):
75
+ return
76
+ SourceFormatter._registry.append(cls())
77
+ SourceFormatter._registry_dirty = True
78
+
79
+ @abstractmethod
80
+ def can_format(self, value: Any) -> bool:
81
+ """Return True if this formatter handles this value type."""
82
+ raise NotImplementedError
83
+
84
+ @abstractmethod
85
+ def format(self, value: Any, context: FormatContext) -> SourceFragment:
86
+ """Format value to Python source, returning code and required imports."""
87
+ raise NotImplementedError
88
+
89
+ @classmethod
90
+ def register(cls, formatter: "SourceFormatter") -> None:
91
+ cls._registry.append(formatter)
92
+ cls._registry_dirty = True
93
+
94
+ @classmethod
95
+ def _iter_registry(cls) -> List["SourceFormatter"]:
96
+ if cls._registry_dirty:
97
+ cls._sorted_registry = sorted(
98
+ cls._registry,
99
+ key=lambda fmt: fmt.priority,
100
+ reverse=True,
101
+ )
102
+ cls._registry_dirty = False
103
+ return cls._sorted_registry
104
+
105
+ @classmethod
106
+ def get_formatter(cls, value: Any) -> "SourceFormatter":
107
+ for formatter in cls._iter_registry():
108
+ if formatter.can_format(value):
109
+ return formatter
110
+ raise TypeError(f"No formatter for {type(value)}")
111
+
112
+
113
+ def to_source(value: Any, ctx: FormatContext | None = None) -> SourceFragment:
114
+ """Convert any value to Python source code."""
115
+ if isinstance(value, SourceFragment):
116
+ return value
117
+ ctx = ctx or FormatContext()
118
+ formatter = SourceFormatter.get_formatter(value)
119
+ return formatter.format(value, ctx)
120
+
121
+
122
+ def generate_python_source(obj: Any, header: str = "", clean_mode: bool = False) -> str:
123
+ """Generate complete Python source with imports."""
124
+ from .imports import resolve_imports
125
+
126
+ ctx = FormatContext(clean_mode=clean_mode)
127
+ fragment = to_source(obj, ctx)
128
+
129
+ import_lines, name_mappings = resolve_imports(fragment.imports)
130
+
131
+ ctx = FormatContext(clean_mode=clean_mode, name_mappings=name_mappings)
132
+ fragment = to_source(obj, ctx)
133
+
134
+ code_lines: List[str] = []
135
+ if header:
136
+ code_lines.extend(header.splitlines())
137
+ code_lines.append("")
138
+ if import_lines:
139
+ code_lines.extend(import_lines)
140
+ code_lines.append("")
141
+
142
+ if fragment.code:
143
+ code_lines.append(fragment.code)
144
+
145
+ return "\n".join(code_lines)
pycodify/formatters.py ADDED
@@ -0,0 +1,348 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+ from dataclasses import is_dataclass
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Any, Tuple
9
+
10
+ from .core import (
11
+ Assignment,
12
+ BlankLine,
13
+ CodeBlock,
14
+ Comment,
15
+ FormatContext,
16
+ SourceFormatter,
17
+ SourceFragment,
18
+ to_source,
19
+ )
20
+
21
+
22
+ class CommentFormatter(SourceFormatter):
23
+ priority = 100
24
+
25
+ def can_format(self, value: Any) -> bool:
26
+ return isinstance(value, Comment)
27
+
28
+ def format(self, value: Comment, context: FormatContext) -> SourceFragment:
29
+ text = value.text.strip()
30
+ if not text:
31
+ code = "#"
32
+ elif text.lstrip().startswith("#"):
33
+ code = text
34
+ else:
35
+ code = f"# {text}"
36
+ return SourceFragment(f"{context.indent_str}{code}", frozenset())
37
+
38
+
39
+ class BlankLineFormatter(SourceFormatter):
40
+ priority = 100
41
+
42
+ def can_format(self, value: Any) -> bool:
43
+ return isinstance(value, BlankLine)
44
+
45
+ def format(self, value: BlankLine, context: FormatContext) -> SourceFragment:
46
+ return SourceFragment("", frozenset())
47
+
48
+
49
+ class AssignmentFormatter(SourceFormatter):
50
+ priority = 90
51
+
52
+ def can_format(self, value: Any) -> bool:
53
+ return isinstance(value, Assignment)
54
+
55
+ def format(self, value: Assignment, context: FormatContext) -> SourceFragment:
56
+ fragment = to_source(value.value, context)
57
+ code = f"{context.indent_str}{value.name} = {fragment.code}"
58
+ return SourceFragment(code, fragment.imports)
59
+
60
+
61
+ class CodeBlockFormatter(SourceFormatter):
62
+ priority = 80
63
+
64
+ def can_format(self, value: Any) -> bool:
65
+ return isinstance(value, CodeBlock)
66
+
67
+ def format(self, value: CodeBlock, context: FormatContext) -> SourceFragment:
68
+ codes = []
69
+ imports = set()
70
+ for item in value.items:
71
+ fragment = to_source(item, context)
72
+ imports |= fragment.imports
73
+ codes.append(fragment.code)
74
+ return SourceFragment("\n".join(codes), frozenset(imports))
75
+
76
+
77
+ class EnumFormatter(SourceFormatter):
78
+ priority = 70
79
+
80
+ def can_format(self, value: Any) -> bool:
81
+ return isinstance(value, Enum)
82
+
83
+ def format(self, value: Enum, context: FormatContext) -> SourceFragment:
84
+ cls = value.__class__
85
+ import_pair = (cls.__module__, cls.__name__)
86
+ name = context.name_mappings.get(import_pair, cls.__name__)
87
+ return SourceFragment(f"{name}.{value.name}", frozenset([import_pair]))
88
+
89
+
90
+ class PathFormatter(SourceFormatter):
91
+ priority = 70
92
+
93
+ def can_format(self, value: Any) -> bool:
94
+ return isinstance(value, Path)
95
+
96
+ def format(self, value: Path, context: FormatContext) -> SourceFragment:
97
+ import_pair = ("pathlib", "Path")
98
+ name = context.name_mappings.get(import_pair, "Path")
99
+ return SourceFragment(f"{name}({repr(str(value))})", frozenset([import_pair]))
100
+
101
+
102
+ class StrFormatter(SourceFormatter):
103
+ priority = 60
104
+
105
+ def can_format(self, value: Any) -> bool:
106
+ return isinstance(value, str)
107
+
108
+ def format(self, value: str, context: FormatContext) -> SourceFragment:
109
+ return SourceFragment(repr(value), frozenset())
110
+
111
+
112
+ class LiteralFormatter(SourceFormatter):
113
+ priority = 50
114
+
115
+ def can_format(self, value: Any) -> bool:
116
+ return value is None or isinstance(value, (bool, int, float))
117
+
118
+ def format(self, value: Any, context: FormatContext) -> SourceFragment:
119
+ return SourceFragment(repr(value), frozenset())
120
+
121
+
122
+ class CallableFormatter(SourceFormatter):
123
+ priority = 40
124
+
125
+ def can_format(self, value: Any) -> bool:
126
+ return callable(value) and not isinstance(value, type)
127
+
128
+ def format(self, value: Any, context: FormatContext) -> SourceFragment:
129
+ import_pair = (value.__module__, value.__name__)
130
+ name = context.name_mappings.get(import_pair, value.__name__)
131
+ return SourceFragment(name, frozenset([import_pair]))
132
+
133
+
134
+ class ListFormatter(SourceFormatter):
135
+ priority = 30
136
+
137
+ def can_format(self, value: Any) -> bool:
138
+ return isinstance(value, list)
139
+
140
+ def format(self, value: list, context: FormatContext) -> SourceFragment:
141
+ if not value:
142
+ return SourceFragment("[]", frozenset())
143
+
144
+ item_ctx = context.indented()
145
+ item_frags = [to_source(item, item_ctx) for item in value]
146
+ imports = frozenset().union(*(frag.imports for frag in item_frags))
147
+ inner = f",\n{item_ctx.indent_str}".join(frag.code for frag in item_frags)
148
+ code = f"[\n{item_ctx.indent_str}{inner}\n{context.indent_str}]"
149
+ return SourceFragment(code, imports)
150
+
151
+
152
+ class TupleFormatter(SourceFormatter):
153
+ priority = 30
154
+
155
+ def can_format(self, value: Any) -> bool:
156
+ return isinstance(value, tuple)
157
+
158
+ def format(self, value: tuple, context: FormatContext) -> SourceFragment:
159
+ if not value:
160
+ return SourceFragment("()", frozenset())
161
+
162
+ item_ctx = context.indented()
163
+ item_frags = [to_source(item, item_ctx) for item in value]
164
+ imports = frozenset().union(*(frag.imports for frag in item_frags))
165
+ inner = f",\n{item_ctx.indent_str}".join(frag.code for frag in item_frags)
166
+ if len(value) == 1:
167
+ inner = f"{inner},"
168
+ code = f"(\n{item_ctx.indent_str}{inner}\n{context.indent_str})"
169
+ return SourceFragment(code, imports)
170
+
171
+
172
+ class DictFormatter(SourceFormatter):
173
+ priority = 30
174
+
175
+ def can_format(self, value: Any) -> bool:
176
+ return isinstance(value, dict)
177
+
178
+ def format(self, value: dict, context: FormatContext) -> SourceFragment:
179
+ if not value:
180
+ return SourceFragment("{}", frozenset())
181
+
182
+ item_ctx = context.indented()
183
+ entries = []
184
+ imports = set()
185
+ for key, val in value.items():
186
+ key_frag = to_source(key, item_ctx)
187
+ val_frag = to_source(val, item_ctx)
188
+ imports |= key_frag.imports
189
+ imports |= val_frag.imports
190
+ entries.append(f"{key_frag.code}: {val_frag.code}")
191
+
192
+ inner = f",\n{item_ctx.indent_str}".join(entries)
193
+ code = f"{{\n{item_ctx.indent_str}{inner}\n{context.indent_str}}}"
194
+ return SourceFragment(code, frozenset(imports))
195
+
196
+
197
+ class SetFormatter(SourceFormatter):
198
+ priority = 30
199
+
200
+ def can_format(self, value: Any) -> bool:
201
+ return isinstance(value, set)
202
+
203
+ def format(self, value: set, context: FormatContext) -> SourceFragment:
204
+ if not value:
205
+ return SourceFragment("set()", frozenset())
206
+
207
+ item_ctx = context.indented()
208
+ item_frags = [to_source(item, item_ctx) for item in value]
209
+ imports = frozenset().union(*(frag.imports for frag in item_frags))
210
+ inner = f",\n{item_ctx.indent_str}".join(frag.code for frag in item_frags)
211
+ code = f"{{\n{item_ctx.indent_str}{inner}\n{context.indent_str}}}"
212
+ return SourceFragment(code, imports)
213
+
214
+
215
+ class DataclassFormatter(SourceFormatter):
216
+ priority = 35
217
+
218
+ def can_format(self, value: Any) -> bool:
219
+ return is_dataclass(value) and not isinstance(value, type)
220
+
221
+ def format(self, value: Any, context: FormatContext) -> SourceFragment:
222
+ code, imports, _ = _format_dataclass_expression(value, context)
223
+ return SourceFragment(code, frozenset(imports))
224
+
225
+
226
+ def _resolve_dataclass_class_from_type(field_type: Any) -> Any | None:
227
+ if field_type is None:
228
+ return None
229
+
230
+ origin = typing.get_origin(field_type)
231
+ if origin is typing.Union:
232
+ for arg in typing.get_args(field_type):
233
+ if arg is type(None):
234
+ continue
235
+ resolved = _resolve_dataclass_class_from_type(arg)
236
+ if resolved:
237
+ return resolved
238
+ return None
239
+
240
+ if isinstance(field_type, type) and dataclasses.is_dataclass(field_type):
241
+ return field_type
242
+
243
+ return None
244
+
245
+
246
+ def _create_placeholder_dataclass_instance(dataclass_cls: type) -> Any:
247
+ instance = object.__new__(dataclass_cls)
248
+ for dc_field in dataclasses.fields(dataclass_cls):
249
+ object.__setattr__(instance, dc_field.name, None)
250
+
251
+ if hasattr(dataclass_cls, "_is_lazy_dataclass"):
252
+ object.__setattr__(instance, "_is_lazy_dataclass", True)
253
+
254
+ return instance
255
+
256
+
257
+ def _create_default_instance(instance: Any) -> Any:
258
+ if hasattr(instance, "_resolve_field_value"):
259
+ default_instance = object.__new__(instance.__class__)
260
+ for dc_field in dataclasses.fields(instance):
261
+ object.__setattr__(default_instance, dc_field.name, None)
262
+ if hasattr(instance.__class__, "_is_lazy_dataclass"):
263
+ object.__setattr__(default_instance, "_is_lazy_dataclass", True)
264
+ return default_instance
265
+
266
+ try:
267
+ return instance.__class__()
268
+ except Exception:
269
+ default_instance = object.__new__(instance.__class__)
270
+ for dc_field in dataclasses.fields(instance):
271
+ if dc_field.default is not dataclasses.MISSING:
272
+ value = dc_field.default
273
+ elif dc_field.default_factory is not dataclasses.MISSING: # type: ignore
274
+ value = dc_field.default_factory() # type: ignore
275
+ else:
276
+ value = None
277
+ object.__setattr__(default_instance, dc_field.name, value)
278
+ return default_instance
279
+
280
+
281
+ def _format_dataclass_expression(
282
+ instance: Any,
283
+ context: FormatContext,
284
+ ) -> Tuple[str, set[Tuple[str, str]], bool]:
285
+ cls = instance.__class__
286
+ import_pair = (cls.__module__, cls.__name__)
287
+ class_name = context.name_mappings.get(import_pair, cls.__name__)
288
+ imports: set[Tuple[str, str]] = {import_pair}
289
+
290
+ default_instance = _create_default_instance(instance)
291
+
292
+ try:
293
+ type_hints = typing.get_type_hints(instance.__class__)
294
+ except Exception:
295
+ type_hints = {}
296
+
297
+ lines: list[str] = []
298
+ field_ctx = context.indented()
299
+
300
+ for dc_field in dataclasses.fields(instance):
301
+ field_name = dc_field.name
302
+
303
+ if hasattr(instance, "_resolve_field_value"):
304
+ current_value = object.__getattribute__(instance, field_name)
305
+ default_value = object.__getattribute__(default_instance, field_name)
306
+ else:
307
+ current_value = getattr(instance, field_name)
308
+ default_value = getattr(default_instance, field_name)
309
+
310
+ if (not context.clean_mode) and current_value is None:
311
+ field_type = type_hints.get(field_name, dc_field.type)
312
+ dataclass_cls = _resolve_dataclass_class_from_type(field_type)
313
+ if dataclass_cls is not None:
314
+ current_value = _create_placeholder_dataclass_instance(dataclass_cls)
315
+
316
+ if context.clean_mode and current_value == default_value:
317
+ continue
318
+
319
+ if dataclasses.is_dataclass(current_value) and not isinstance(current_value, type):
320
+ nested_code, nested_imports, nested_has_fields = _format_dataclass_expression(
321
+ current_value,
322
+ field_ctx,
323
+ )
324
+ if context.clean_mode and not nested_has_fields:
325
+ continue
326
+ imports |= nested_imports
327
+ lines.append(f"{field_name}={nested_code}")
328
+ else:
329
+ frag = to_source(current_value, field_ctx)
330
+ imports |= frag.imports
331
+ lines.append(f"{field_name}={frag.code}")
332
+
333
+ if not lines:
334
+ return f"{class_name}()", imports, False
335
+
336
+ inner = f",\n{field_ctx.indent_str}".join(lines)
337
+ code = f"{class_name}(\n{field_ctx.indent_str}{inner}\n{context.indent_str})"
338
+ return code, imports, True
339
+
340
+
341
+ class FallbackFormatter(SourceFormatter):
342
+ priority = -100
343
+
344
+ def can_format(self, value: Any) -> bool:
345
+ return True
346
+
347
+ def format(self, value: Any, context: FormatContext) -> SourceFragment:
348
+ return SourceFragment(repr(value), frozenset())
pycodify/imports.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import Dict, FrozenSet, Iterable, List, Tuple
5
+
6
+
7
+ def resolve_imports(
8
+ imports: FrozenSet[Tuple[str, str]] | Iterable[Tuple[str, str]],
9
+ ) -> Tuple[List[str], Dict[Tuple[str, str], str]]:
10
+ """Resolve import collisions and generate import lines."""
11
+ import_pairs = {pair for pair in imports if pair and pair[0] and pair[1]}
12
+
13
+ # Filter out builtins and None modules
14
+ filtered = [
15
+ (module, name)
16
+ for module, name in import_pairs
17
+ if module not in (None, "builtins")
18
+ ]
19
+
20
+ name_to_modules = defaultdict(list)
21
+ for module, name in filtered:
22
+ name_to_modules[name].append(module)
23
+
24
+ import_lines: List[str] = []
25
+ name_mappings: Dict[Tuple[str, str], str] = {}
26
+
27
+ for module, name in sorted(filtered):
28
+ if len(name_to_modules[name]) > 1:
29
+ alias = f"{name}_{module.split('.')[-1]}"
30
+ import_lines.append(f"from {module} import {name} as {alias}")
31
+ name_mappings[(module, name)] = alias
32
+ else:
33
+ import_lines.append(f"from {module} import {name}")
34
+ name_mappings[(module, name)] = name
35
+
36
+ return import_lines, name_mappings
@@ -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,8 @@
1
+ pycodify/__init__.py,sha256=gHiMGKUAfA2OXU1gBtMPqIQR6rUoZRiH9hLhHjruhf0,547
2
+ pycodify/core.py,sha256=o4Fhpxx8QOFrXBSxhNLB6KweJbGeM1pR2oDxpYbCLuo,4065
3
+ pycodify/formatters.py,sha256=Zm50i71UXX1lfiGqRz97-zXRrYBT8NcNA9cqAlrlW2k,11774
4
+ pycodify/imports.py,sha256=QIClblhNdr8pTkBLjsVN0iFxVv39PYEwz3OkuggkwwI,1226
5
+ pycodify-0.1.0.dist-info/METADATA,sha256=Y0iGtSeVCmkWb5a-U-zcby97W7WLAmUeWrVGPXxcM4s,3182
6
+ pycodify-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ pycodify-0.1.0.dist-info/licenses/LICENSE,sha256=QaLlb9Yy_FmAXOzvvTABEDPQ5n_M3ck1KaGmz_A6p2Y,1064
8
+ pycodify-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.