Skip to content

Commit 5a2896d

Browse files
authored
Merge pull request #72 from DavidCEllis/add_order_comparisons
Add order comparisons
2 parents f156c3d + 9a996f1 commit 5a2896d

File tree

9 files changed

+353
-130
lines changed

9 files changed

+353
-130
lines changed

README.md

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,35 @@ Install from PyPI with:
1717
The classbuilder tools make up the core of this module and there is an implementation
1818
using these tools in the `prefab` submodule.
1919

20-
There is also a minimal `@slotclass` example that can construct classes from a special
21-
mapping used in `__slots__`.
20+
The implementation provides both a base class `Prefab` that will also generate `__slots__`
21+
and a decorator `@prefab` which does not support `__slots__`.
2222

2323
```python
24-
from ducktools.classbuilder import Field, SlotFields, slotclass
25-
26-
@slotclass
27-
class SlottedDC:
28-
__slots__ = SlotFields(
29-
the_answer=42,
30-
the_question=Field(
31-
default="What do you get if you multiply six by nine?",
32-
doc="Life, the Universe, and Everything",
33-
),
24+
from ducktools.classbuilder.prefab import Prefab, attribute
25+
26+
class Slotted(Prefab):
27+
the_answer: int = 42
28+
the_question: str = attribute(
29+
default="What do you get if you multiply six by nine?",
30+
doc="Life the universe and everything",
3431
)
3532

36-
ex = SlottedDC()
33+
ex = Slotted()
3734
print(ex)
35+
print(ex.__slots__)
36+
```
37+
38+
The generated source code for the methods can be viewed using the `print_generated_code` helper function.
39+
40+
```python
41+
from ducktools.classbuilder import print_generated_code
42+
43+
print_generated_code(SlottedDC)
3844
```
3945

4046
### Core ###
4147

42-
The core of the module provides tools for creating a customized version of the `dataclass` concept.
48+
The base `ducktools.classbuilder` module provides tools for creating a customized version of the `dataclass` concept.
4349

4450
* `MethodMaker`
4551
* This tool takes a function that generates source code and converts it into a descriptor
@@ -75,12 +81,6 @@ This prebuilt implementation is available from the `ducktools.classbuilder.prefa
7581
This includes more customization including `__prefab_pre_init__` and `__prefab_post_init__`
7682
functions for subclass customization.
7783

78-
A `@prefab` decorator and `Prefab` base class are provided.
79-
80-
`Prefab` will generate `__slots__` by default.
81-
decorated classes with `@prefab` that do not declare fields using `__slots__`
82-
will **not** be slotted and there is no `slots` argument to apply this.
83-
8484
Here is an example of applying a conversion in `__post_init__`:
8585
```python
8686
from pathlib import Path
@@ -106,7 +106,7 @@ print(steam)
106106
#### Features ####
107107

108108
`Prefab` and `@prefab` support many standard dataclass features along with
109-
a few extras.
109+
some extra features and some intentional differences in design.
110110

111111
* All standard methods are generated on-demand
112112
* This makes the construction of classes much faster in general
@@ -132,20 +132,20 @@ a few extras.
132132
* `iter=True` will include the attribute in the iterable if `__iter__` is generated
133133
* `serialize=True` decides if the attribute is include in `as_dict`
134134
* `exclude_field` is short for `repr=False`, `compare=False`, `iter=False`, `serialize=False`
135-
* `private` is short for `exclude_field=True` and `init=False` and requires a default/factory
135+
* `private` is short for `exclude_field=True` and `init=False` and requires a default or factory
136136
* `doc` will add this string as the value in slotted classes, which appears in `help()`
137137
* `build_prefab` can be used to dynamically create classes and *does* support a slots argument
138138
* Unlike dataclasses, this does not create the class twice in order to provide slots
139139

140140
There are also some intentionally missing features:
141141

142-
* The `@prefab` decorator does not and will not support a `slots` argument
142+
* The `@prefab` decorator does not and will never support a `slots` argument
143143
* Use `Prefab` for slots.
144144
* `as_dict` and the generated `.as_dict` method **do not** recurse or deep copy
145145
* `unsafe_hash` is not provided
146146
* `weakref_slot` is not available as an argument
147147
* `__weakref__` can be added to slots by declaring it as if it were an attribute
148-
* There is no check for mutable defaults
148+
* There is no safety check for mutable defaults
149149
* You should still use `default_factory` as you would for dataclasses, not doing so
150150
is still incorrect
151151
* `dataclasses` uses hashability as a proxy for mutability, but technically this is
@@ -162,7 +162,7 @@ There are also some intentionally missing features:
162162
If you want to use `__slots__` in order to save memory you have to declare
163163
them when the class is originally created as you can't add them later.
164164

165-
When you use `@dataclass(slots=True)`[^2] with `dataclasses`, the function
165+
When you use `@dataclass(slots=True)`[^1] with `dataclasses`, the function
166166
has to make a new class and attempt to copy over everything from the original.
167167

168168
This is because decorators operate on classes *after they have been created*
@@ -305,8 +305,4 @@ with a specific feature, you can create or add it yourself.
305305

306306
Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
307307

308-
[^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
309-
works with dictionaries using the values of the keys, while fields are normally
310-
used for documentation.
311-
312-
[^2]: or `@attrs.define`.
308+
[^1]: or `@attrs.define`.
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
from ducktools.classbuilder import slotclass, Field, SlotFields
1+
from ducktools.classbuilder.prefab import Prefab, attribute
22

3-
4-
@slotclass
5-
class SlottedDC:
6-
__slots__ = SlotFields(
7-
the_answer=42,
8-
the_question=Field(
9-
default="What do you get if you multiply six by nine?",
10-
doc="Life, the Universe, and Everything",
11-
),
3+
class Slotted(Prefab):
4+
the_answer: int = 42
5+
the_question: str = attribute(
6+
default="What do you get if you multiply six by nine?",
7+
doc="Life the universe and everything",
128
)
139

14-
15-
ex = SlottedDC()
10+
ex = Slotted()
1611
print(ex)
17-
help(SlottedDC)
12+
print(ex.__slots__)
13+
help(Slotted)
Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1-
SlottedDC(the_answer=42, the_question='What do you get if you multiply six by nine?')
2-
Help on class SlottedDC in module __main__:
1+
Slotted(the_answer=42, the_question='What do you get if you multiply six by nine?')
2+
{'the_answer': None, 'the_question': 'Life the universe and everything'}
3+
Help on class Slotted in module __main__:
34

4-
class SlottedDC(builtins.object)
5-
| SlottedDC(
6-
| the_answer=42,
7-
| the_question='What do you get if you multiply six by nine?'
8-
| )
5+
class Slotted(ducktools.classbuilder.prefab.Prefab)
6+
| Slotted(
7+
| the_answer: int = 42,
8+
| the_question: str = 'What do you get if you multiply six by nine?'
9+
| ) -> None
10+
|
11+
| Method resolution order:
12+
| Slotted
13+
| ducktools.classbuilder.prefab.Prefab
14+
| builtins.object
915
|
1016
| Methods defined here:
1117
|
12-
| __eq__(self, other) from SlottedDC
18+
| __eq__(self, other) from Slotted
1319
|
1420
| __init__(
1521
| self,
16-
| the_answer=42,
17-
| the_question='What do you get if you multiply six by nine?'
18-
| ) from SlottedDC
22+
| the_answer: int = 42,
23+
| the_question: str = 'What do you get if you multiply six by nine?'
24+
| ) -> None from Slotted
25+
|
26+
| __replace__(self, /, **changes) from Slotted
1927
|
20-
| __repr__(self) from SlottedDC
28+
| __repr__(self) from Slotted
2129
|
2230
| __signature__
2331
|
@@ -27,12 +35,41 @@ class SlottedDC(builtins.object)
2735
| the_answer
2836
|
2937
| the_question
30-
| Life, the Universe, and Everything
38+
| Life the universe and everything
3139
|
3240
| ----------------------------------------------------------------------
3341
| Data and other attributes defined here:
3442
|
43+
| PREFAB_FIELDS = ['the_answer', 'the_question']
44+
|
45+
| __classbuilder_gathered_fields__ = ({'the_answer': Attribute(default=4...
46+
|
3547
| __classbuilder_internals__ = {'build_complete': True, 'fields': {'the_...
3648
|
3749
| __hash__ = None
50+
|
51+
| __match_args__ = ('the_answer', 'the_question')
52+
|
53+
| ----------------------------------------------------------------------
54+
| Class methods inherited from ducktools.classbuilder.prefab.Prefab:
55+
|
56+
| __init_subclass__(**kwargs)
57+
| Generate boilerplate code for dunder methods in a class.
58+
|
59+
| Use as a base class, slotted by default
60+
|
61+
| :param init: generates __init__ if true or __prefab_init__ if false
62+
| :param repr: generate __repr__
63+
| :param eq: generate __eq__
64+
| :param iter: generate __iter__
65+
| :param match_args: generate __match_args__
66+
| :param kw_only: make all attributes keyword only
67+
| :param frozen: Prevent attribute values from being changed once defined
68+
| (This does not prevent the modification of mutable attributes such as lists)
69+
| :param replace: generate a __replace__ method
70+
| :param dict_method: Include an as_dict method for faster dictionary creation
71+
| :param recursive_repr: Safely handle repr in case of recursion
72+
| :param ignore_annotations: Ignore type annotations when gathering fields, only look for
73+
| slots or attribute(...) values
74+
| :param slots: automatically generate slots for this class's attributes
3875

docs/generated_code.md

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,75 @@
33
If you wish to see the source code generated by a constructed class you need to access the
44
`MethodMaker` instances on that class.
55

6-
There is a helper function `get_methods` that can be used to obtain the names and respective
7-
methods that have been attached to the class by the builder.
6+
There are helper functions that will do this for you and also retrieve the source code the
7+
`MethodMaker` objects would generate.
8+
9+
`print_generated_code` will print the source code for all methods along with any necessary globals
10+
and annotations associated with the method being generated.
11+
12+
`get_generated_code` will retrieve the `GeneratedCode` objects that will be used to create the
13+
class methods. These have `source_code`, `globs` and `annotations` attributes.
14+
15+
You can also directly access the MethodMakers with the `get_methods` function
816

917
```python
10-
from ducktools.classbuilder import SlotFields, get_methods, slotclass
18+
from pathlib import Path
19+
from pprint import pp
1120

12-
@slotclass
13-
class Example:
14-
__slots__ = SlotFields(
15-
a='a',
16-
b='b'
17-
)
21+
from ducktools.classbuilder import get_generated_code, print_generated_code
22+
from ducktools.classbuilder.prefab import attribute, Prefab
1823

19-
print(get_methods(Example))
20-
```
2124

25+
class Example(Prefab):
26+
a: int
27+
b: int = 42
28+
c: list = attribute(default_factory=list)
29+
p: Path = Path("/usr/bin/python")
30+
31+
generated_code = get_generated_code(Example)
32+
33+
pp(generated_code)
2234
```
23-
{
24-
'__init__': <MethodMaker for '__init__' method>,
25-
'__repr__': <MethodMaker for '__repr__' method>,
26-
'__eq__': <MethodMaker for '__eq__' method>
27-
}
35+
36+
Output:
37+
38+
```python
39+
{'__init__': GeneratorOutput(source_code='def __init__(self, a, b=42, c=None, p=_p_default): ...', globs={'_c_factory': <class 'list'>, '_p_default': PosixPath('/usr/bin/python')}, annotations={'a': <class 'int'>, 'b': <class 'int'>, 'c': <class 'list'>, 'p': <class 'pathlib.Path'>, 'return': None}),
40+
'__eq__': GeneratorOutput(source_code='def __eq__(self, other): ...', globs={}, annotations=None),
41+
'__repr__': GeneratorOutput(source_code='def __repr__(self): ...', globs={}, annotations=None),
42+
'__replace__': GeneratorOutput(source_code='def __replace__(self, /, **changes): ...', globs={}, annotations=None)}
2843
```
2944

30-
These can then be used to examine the generated source code and global variables provided
31-
to the `exec` function. This can be useful for debugging code generators.
45+
`print_generated_code(Example)` cleans up the output to be more presentable and readable
3246

3347
```python
34-
from ducktools.classbuilder import SlotFields, get_methods, slotclass
35-
36-
@slotclass
37-
class Example:
38-
__slots__ = SlotFields(
39-
a='a',
40-
b='b',
41-
)
42-
43-
methods = get_methods(Example)
44-
45-
for method in methods.values():
46-
print(method.code_generator(Example))
48+
Source:
49+
def __eq__(self, other):
50+
return (
51+
self.a == other.a
52+
and self.b == other.b
53+
and self.c == other.c
54+
and self.p == other.p
55+
) if self.__class__ is other.__class__ else NotImplemented
56+
57+
def __init__(self, a, b=42, c=None, p=_p_default):
58+
self.a = a
59+
self.b = b
60+
self.c = c if c is not None else _c_factory()
61+
self.p = p
62+
63+
def __replace__(self, /, **changes):
64+
new_kwargs = {'a': self.a, 'b': self.b, 'c': self.c, 'p': self.p}
65+
new_kwargs |= changes
66+
return self.__class__(**new_kwargs)
67+
68+
def __repr__(self):
69+
return f'{type(self).__qualname__}(a={self.a!r}, b={self.b!r}, c={self.c!r}, p={self.p!r})'
70+
71+
72+
Globals:
73+
__init__: {'_c_factory': <class 'list'>, '_p_default': PosixPath('/usr/bin/python')}
74+
75+
Annotations:
76+
__init__: {'a': <class 'int'>, 'b': <class 'int'>, 'c': <class 'list'>, 'p': <class 'pathlib.Path'>, 'return': None}
4777
```

src/ducktools/classbuilder/__init__.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def print_generated_code(cls):
136136
globs_list = []
137137
annotation_list = []
138138

139-
for name, method in source.items():
139+
for name, method in sorted(source.items()):
140140
source_list.append(method.source_code)
141141
if method.globs:
142142
globs_list.append(f"{name}: {method.globs}")
@@ -484,6 +484,38 @@ def eq_generator(cls, funcname="__eq__"):
484484
return GeneratedCode(code, globs)
485485

486486

487+
def get_order_generator(cls, funcname, *, operator):
488+
field_names = [
489+
name
490+
for name, attrib in get_fields(cls).items()
491+
if attrib.compare
492+
]
493+
494+
self_tuple = ", ".join(f"self.{name}" for name in field_names)
495+
other_tuple = self_tuple.replace("self.", "other.")
496+
497+
code = (
498+
f"def {funcname}(self, other):\n"
499+
f" if self.__class__ is other.__class__:\n"
500+
f" return ({self_tuple}) {operator} ({other_tuple})\n"
501+
f" return NotImplemented\n"
502+
)
503+
globs = {}
504+
return GeneratedCode(code, globs)
505+
506+
def lt_generator(cls, funcname="__lt__"):
507+
return get_order_generator(cls, funcname, operator="<")
508+
509+
def le_generator(cls, funcname="__le__"):
510+
return get_order_generator(cls, funcname, operator="<=")
511+
512+
def gt_generator(cls, funcname="__gt__"):
513+
return get_order_generator(cls, funcname, operator=">")
514+
515+
def ge_generator(cls, funcname="__ge__"):
516+
return get_order_generator(cls, funcname, operator=">=")
517+
518+
487519
def replace_generator(cls, funcname="__replace__"):
488520
# Generate the replace method for built classes
489521
# unlike the dataclasses implementation this is generated
@@ -556,6 +588,10 @@ def frozen_delattr_generator(cls, funcname="__delattr__"):
556588
init_maker = MethodMaker("__init__", init_generator)
557589
repr_maker = MethodMaker("__repr__", repr_generator)
558590
eq_maker = MethodMaker("__eq__", eq_generator)
591+
lt_maker = MethodMaker("__lt__", lt_generator)
592+
le_maker = MethodMaker("__le__", le_generator)
593+
gt_maker = MethodMaker("__gt__", gt_generator)
594+
ge_maker = MethodMaker("__ge__", ge_generator)
559595
replace_maker = MethodMaker("__replace__", replace_generator)
560596
frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
561597
frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)

0 commit comments

Comments
 (0)