Coverage for src/objectstate/codegen.py: 91%
69 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 07:22 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 07:22 +0000
1"""Python source for lazily resolved dataclasses, through pycodify.
3Importing this module registers two formatters. Both write raw field values,
4never resolved ones: a ``None`` field inherits from context and stays ``None``
5in the source.
7- :class:`LazyResolutionFormatter` (any :class:`LazyResolutionDataclass`):
8 clean mode writes the fields whose raw value differs from the field default.
9- :class:`LazyDataclassFormatter` (factory-made :class:`LazyDataclass`): clean
10 mode writes a field when it was passed to the constructor or holds a concrete
11 value; an inherited ``None`` is omitted, and so is a nested lazy dataclass
12 that carries nothing.
14Full mode writes every field.
15"""
17from __future__ import annotations
19from dataclasses import MISSING, Field, fields, is_dataclass
21from pycodify import (
22 FormatContext,
23 SourceFormatter,
24 SourceFragment,
25 imported_name,
26 to_source,
27)
29from objectstate.field_access import DataclassFieldAccess
30from objectstate.lazy_factory import LazyDataclass, LazyResolutionDataclass
33def _set_field_names(value: LazyDataclass) -> frozenset[str]:
34 return frozenset(object.__getattribute__(value, "_explicitly_set_fields"))
37def lazy_dataclass_carries_values(value: LazyDataclass) -> bool:
38 """Whether a lazy dataclass has a constructor-set field or a concrete value."""
40 return bool(_set_field_names(value)) or any(
41 DataclassFieldAccess.raw_value(value, field.name) is not None
42 for field in fields(value)
43 )
46def _is_instance_of_dataclass(value: object) -> bool:
47 return is_dataclass(value) and not isinstance(value, type)
50def raw_values_equal(left: object, right: object) -> bool:
51 """Whether two values are equal, comparing dataclasses by raw field values."""
53 if _is_instance_of_dataclass(left) and _is_instance_of_dataclass(right):
54 if type(left) is not type(right):
55 return False
56 return all(
57 raw_values_equal(
58 DataclassFieldAccess.raw_value(left, field.name),
59 DataclassFieldAccess.raw_value(right, field.name),
60 )
61 for field in fields(left)
62 if field.init
63 )
64 return left == right
67def field_needs_source(current: object, default: object, context: FormatContext) -> bool:
68 """Whether an owner's field holding ``current`` must be written.
70 For owners (such as a step) that format their own constructor fields and
71 hold lazy dataclasses among them. Full mode writes every field; clean mode
72 writes a lazy dataclass that carries values different from the default,
73 and any other value that differs from the default.
74 """
76 if not context.clean_mode:
77 return True
78 if isinstance(current, LazyDataclass):
79 if not lazy_dataclass_carries_values(current):
80 return False
81 return not raw_values_equal(current, default)
82 return current != default
85def _field_default(field: Field) -> object:
86 if field.default is not MISSING:
87 return field.default
88 if field.default_factory is not MISSING:
89 return field.default_factory()
90 return MISSING
93class LazyResolutionFormatter(SourceFormatter):
94 priority = 36
95 formats: type = LazyResolutionDataclass
97 def can_format(self, value: object) -> bool:
98 return isinstance(value, self.formats)
100 def clean_field_needs_source(self, value: object, field: Field, current: object) -> bool:
101 return not raw_values_equal(current, _field_default(field))
103 def format(self, value: object, context: FormatContext) -> SourceFragment:
104 lazy_type = type(value)
105 class_ref = imported_name(lazy_type.__module__, lazy_type.__qualname__, context)
106 imports = set(class_ref.imports)
107 field_ctx = context.indented()
108 lines = []
109 for field in fields(value):
110 if not field.init:
111 continue
112 current = DataclassFieldAccess.raw_value(value, field.name)
113 if context.clean_mode and not self.clean_field_needs_source(value, field, current):
114 continue
115 fragment = to_source(current, field_ctx)
116 imports |= fragment.imports
117 lines.append(f"{field.name}={fragment.code}")
119 if not lines:
120 return SourceFragment(f"{class_ref.code}()", frozenset(imports))
121 inner = f",\n{field_ctx.indent_str}".join(lines)
122 code = f"{class_ref.code}(\n{field_ctx.indent_str}{inner}\n{context.indent_str})"
123 return SourceFragment(code, frozenset(imports))
126class LazyDataclassFormatter(LazyResolutionFormatter):
127 priority = 37
128 formats = LazyDataclass
130 def clean_field_needs_source(self, value: object, field: Field, current: object) -> bool:
131 if current is None:
132 return False
133 if field.name in _set_field_names(value):
134 return True
135 if isinstance(current, LazyDataclass):
136 return lazy_dataclass_carries_values(current)
137 return True