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

1"""Python source for lazily resolved dataclasses, through pycodify. 

2 

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. 

6 

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. 

13 

14Full mode writes every field. 

15""" 

16 

17from __future__ import annotations 

18 

19from dataclasses import MISSING, Field, fields, is_dataclass 

20 

21from pycodify import ( 

22 FormatContext, 

23 SourceFormatter, 

24 SourceFragment, 

25 imported_name, 

26 to_source, 

27) 

28 

29from objectstate.field_access import DataclassFieldAccess 

30from objectstate.lazy_factory import LazyDataclass, LazyResolutionDataclass 

31 

32 

33def _set_field_names(value: LazyDataclass) -> frozenset[str]: 

34 return frozenset(object.__getattribute__(value, "_explicitly_set_fields")) 

35 

36 

37def lazy_dataclass_carries_values(value: LazyDataclass) -> bool: 

38 """Whether a lazy dataclass has a constructor-set field or a concrete value.""" 

39 

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 ) 

44 

45 

46def _is_instance_of_dataclass(value: object) -> bool: 

47 return is_dataclass(value) and not isinstance(value, type) 

48 

49 

50def raw_values_equal(left: object, right: object) -> bool: 

51 """Whether two values are equal, comparing dataclasses by raw field values.""" 

52 

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 

65 

66 

67def field_needs_source(current: object, default: object, context: FormatContext) -> bool: 

68 """Whether an owner's field holding ``current`` must be written. 

69 

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 """ 

75 

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 

83 

84 

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 

91 

92 

93class LazyResolutionFormatter(SourceFormatter): 

94 priority = 36 

95 formats: type = LazyResolutionDataclass 

96 

97 def can_format(self, value: object) -> bool: 

98 return isinstance(value, self.formats) 

99 

100 def clean_field_needs_source(self, value: object, field: Field, current: object) -> bool: 

101 return not raw_values_equal(current, _field_default(field)) 

102 

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}") 

118 

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)) 

124 

125 

126class LazyDataclassFormatter(LazyResolutionFormatter): 

127 priority = 37 

128 formats = LazyDataclass 

129 

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