Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6f69556
move exec logic out of MethodMaker into GeneratedCode
DavidCEllis Apr 29, 2026
5bafe58
first pass at an eq generator that caches
DavidCEllis Apr 29, 2026
d58539f
pre-write some methods
DavidCEllis Apr 29, 2026
899518d
remove and replace the old eq generator
DavidCEllis Apr 29, 2026
abe610d
groundwork for repr generator
DavidCEllis Apr 30, 2026
3a30043
Reduce complexity of repr function, always recursive save, no special…
DavidCEllis Apr 30, 2026
d052ee2
remove redundant recursive_repr options
DavidCEllis Apr 30, 2026
33f8a42
remove text indicating recusive repr protection was still optional
DavidCEllis Apr 30, 2026
ae6a928
cached __repr__ method
DavidCEllis Apr 30, 2026
767d675
Switch to generating repr from individual arguments
DavidCEllis May 1, 2026
2fb8ba8
handle string placeholders as constants
DavidCEllis May 1, 2026
2c630b6
cached methods for comparisons
DavidCEllis May 1, 2026
9cd8f8f
generic -> counter
DavidCEllis May 1, 2026
270be55
Fix types, make counter generators private
DavidCEllis May 1, 2026
09a1ea0
use the private methods in the generator
DavidCEllis May 1, 2026
30664e6
reduce pregenerated cache count, add replace
DavidCEllis May 1, 2026
b812849
Update source in readme and outputs
DavidCEllis May 1, 2026
bfd71ed
update readme for new performance numbers
DavidCEllis May 1, 2026
c57cc85
defer recursive repr import until someone uses a repr
DavidCEllis May 1, 2026
2b1a186
fix missing field_getter
DavidCEllis May 1, 2026
d6e7546
use pre-generated replace methods
DavidCEllis May 1, 2026
af614a3
reuse the generic replace generator for Field
DavidCEllis May 1, 2026
50378bf
Fixes for python prior to 3.14
DavidCEllis May 1, 2026
69f92f8
Check inst is None, add custom to .pyi
DavidCEllis May 4, 2026
102a5b4
Fix missing `NotImplemented` from eq method and other linter fixes
DavidCEllis May 4, 2026
067768b
update perf test
DavidCEllis May 5, 2026
4bc7b40
autoformat markdown
DavidCEllis May 5, 2026
064ecb6
fully autoformat
DavidCEllis May 5, 2026
382ee86
Credit `dataklasses` too
DavidCEllis May 5, 2026
3e50b88
Simplify `counter_to_class_generator` by removing parameters unnecessary
DavidCEllis May 5, 2026
74ed0ec
remove parameters from the type stub too
DavidCEllis May 5, 2026
51f7d49
Add stats to the cached method makers to track cache hits.
DavidCEllis May 7, 2026
d3d25e3
Move the cache creator to the main module to test more easily.
DavidCEllis May 7, 2026
f67c960
Add stats and a clear cache and make the simple cache keyword only to
DavidCEllis May 7, 2026
1c4956a
Add tests for the generated code and cache hits.
DavidCEllis May 7, 2026
6421237
Ignore internal use of private functions
DavidCEllis May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 230 additions & 168 deletions README.md

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions docs/code_examples/docs_ex08_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
add_methods,
get_fields,
make_unified_gatherer,
print_generated_code,
get_generated_code,
GeneratedCode,
MethodMaker,
)
Expand Down Expand Up @@ -56,4 +56,8 @@ class ConverterEx(ConverterClass):

ex = ConverterEx("42", "42")
print(ex)
print_generated_code(ConverterEx)
print()
code = get_generated_code(ConverterEx)

for k in sorted(code):
print(code[k].source_code)
1 change: 0 additions & 1 deletion docs/code_examples/outputs/docs_ex01_basic.output
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ class Slotted(ducktools.classbuilder.prefab.Prefab)
| (This does not prevent the modification of mutable attributes such as lists)
| :param replace: generate a __replace__ method
| :param dict_method: Include an as_dict method for faster dictionary creation
| :param recursive_repr: Safely handle repr in case of recursion
| :param ignore_annotations: Ignore type annotations when gathering fields, only look for
| slots or attribute(...) values
| :param slots: automatically generate slots for this class's attributes
Expand Down
51 changes: 24 additions & 27 deletions docs/code_examples/outputs/docs_ex08_converters.output
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
ConverterEx(unconverted='42', converted=42)
Source:
def __eq__(self, other):
if self is other:
return True
return (
self.unconverted == other.unconverted
and self.converted == other.converted
) if self.__class__ is other.__class__ else NotImplemented

def __init__(self, unconverted, converted):
self.unconverted = unconverted
self.converted = converted
def __eq__(self, other):
if self is other:
return True
return (
self.unconverted == other.unconverted
and self.converted == other.converted
) if self.__class__ is other.__class__ else NotImplemented

def __replace__(self, /, **changes):
new_kwargs = {'unconverted': self.unconverted, 'converted': self.converted}
new_kwargs |= changes
return self.__class__(**new_kwargs)
def __init__(self, unconverted, converted):
self.unconverted = unconverted
self.converted = converted

def __repr__(self):
return f'{type(self).__qualname__}(unconverted={self.unconverted!r}, converted={self.converted!r})'
def __replace__(self, /, **changes):
new_kwargs = {
'unconverted': self.unconverted,
'converted': self.converted,
}
new_kwargs |= changes
return self.__class__(**new_kwargs)

def __setattr__(self, name, value):
if conv := _converters.get(name):
_object_setattr(self, name, conv(value))
else:
_object_setattr(self, name, value)
def __repr__(self):
return f'{type(self).__qualname__}(unconverted={self.unconverted!r}, converted={self.converted!r})'

def __setattr__(self, name, value):
if conv := _converters.get(name):
_object_setattr(self, name, conv(value))
else:
_object_setattr(self, name, value)

Globals:
__setattr__: {'_converters': {'converted': <class 'int'>}, '_object_setattr': <slot wrapper '__setattr__' of 'object' objects>}

Annotations:
__init__: {'unconverted': <class 'str'>, 'converted': <class 'int'>, 'return': None}
14 changes: 10 additions & 4 deletions docs/code_examples/outputs/docs_ex09_annotated.output
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<generated class X; x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42>
X(x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42)

{'x': Attribute(default=<NOTHING Sentinel>, default_factory=<NOTHING Sentinel>, type=<class 'str'>, doc=None, init=True, repr=True, compare=True, kw_only=False, iter=True, serialize=True, metadata={}),
'a': Attribute(default='Not In __init__ signature', default_factory=<NOTHING Sentinel>, type=<class 'int'>, doc=None, init=False, repr=True, compare=True, kw_only=False, iter=True, serialize=True, metadata={}),
Expand All @@ -9,7 +9,7 @@
'f': Attribute(default=42, default_factory=<NOTHING Sentinel>, type=ForwardRef('unknown'), doc=None, init=True, repr=True, compare=False, kw_only=False, iter=True, serialize=False, metadata={})}


<generated class Y; x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42>
Y(x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42)

Slots: {'x': None, 'a': None, 'b': None, 'c': None, 'd': None, 'e': None, 'f': None}

Expand All @@ -33,10 +33,16 @@ def __init__(self, x, b='Not In Repr', c=None, f=42, *, e):
self.f = f

def __replace__(self, /, **changes):
new_kwargs = {'x': self.x, 'b': self.b, 'c': self.c, 'e': self.e, 'f': self.f}
new_kwargs = {
'x': self.x,
'b': self.b,
'c': self.c,
'e': self.e,
'f': self.f,
}
new_kwargs |= changes
return self.__class__(**new_kwargs)

def __repr__(self):
return f'<generated class {type(self).__qualname__}; x={self.x!r}, a={self.a!r}, c={self.c!r}, e={self.e!r}, f={self.f!r}>'
return f'{type(self).__qualname__}(x={self.x!r}, a={self.a!r}, c={self.c!r}, e={self.e!r}, f={self.f!r})'

1 change: 0 additions & 1 deletion docs/prefab/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Including:
* Declaration by type hints, slots or `attribute(...)` assignment on the class
* `__prefab_pre_init__` and `__prefab_post_init__` detection to allow for validation/conversion
* Optional `as_dict` method generation to convert to a dictionary
* Optional recursive `__repr__` handling (off by default)

## Usage ##

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ testpaths = [

[tool.coverage.run]
plugins = ["coverage_simple_excludes"]
omit = ["_cached_methods.py"]

[tool.coverage.report]
fail_under = 90
Expand Down
4 changes: 4 additions & 0 deletions scripts/generate_precached_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ducktools.classbuilder._create_precached_methods import write_precached_methods

if __name__ == "__main__":
write_precached_methods()
Loading