11# Ducktools: Class Builder #
22
3- ` ducktools-classbuilder ` is * the * Python package that will bring you the ** joy **
4- of writing... functions... that will bring back the ** joy ** of writing classes .
3+ ` ducktools-classbuilder ` is both an alternate implementation of the dataclasses concept
4+ along with a toolkit for creating your own customised implementation .
55
6- Maybe.
6+ Create classes using type annotations:
77
8- While ` attrs ` and ` dataclasses ` are class boilerplate generators,
9- ` ducktools.classbuilder ` is intended to provide the tools to help make a customized
10- version of the same concept.
8+ ``` python
9+ from ducktools.classbuilder.prefab import prefab
1110
12- Install from PyPI with:
13- ` python -m pip install ducktools-classbuilder `
11+ @prefab
12+ class Book :
13+ title: str = " The Hitchhikers Guide to the Galaxy"
14+ author: str = " Douglas Adams"
15+ year: int = 1979
16+ ```
1417
15- ## Included Implementations ##
18+ Using ` attribute() ` calls (this may look familiar to ` attrs ` users before Python added
19+ type annotations)
1620
17- The classbuilder tools make up the core of this module and there is an implementation
18- using these tools in the ` prefab ` submodule.
21+ ``` python
22+ from ducktools.classbuilder.prefab import attribute, prefab
23+
24+ @prefab
25+ class Book :
26+ title = attribute(default = " The Hitchhikers Guide to the Galaxy" )
27+ author = attribute(default = " Douglas Adams" )
28+ year = attribute(default = 1979 )
29+ ```
1930
20- The implementation provides both a base class ` Prefab ` that will also generate ` __slots__ `
21- and a decorator ` @prefab ` which does not support ` __slots__ ` .
31+ Or using a special mapping for slots:
2232
2333``` python
34+ from ducktools.classbuilder.prefab import SlotFields, prefab
35+
36+ @prefab
37+ class Book :
38+ __slots__ = SlotFields(
39+ title = " The Hitchhikers Guide to the Galaxy" ,
40+ author = " Douglas Adams" ,
41+ year = 1979 ,
42+ )
43+ ```
44+
45+ As with ` dataclasses ` or ` attrs ` , ` ducktools-classbuilder ` will handle writing the
46+ boilerplate ` __init__ ` , ` __eq__ ` and ` __repr__ ` functions for you.
47+
48+ Unlike ` dataclasses ` or ` attrs ` , ` ducktools-classbuilder ` generates and executes its
49+ templated functions lazily, so they are only executed if and when the methods are first
50+ used. This significantly reduces the time taken to create the classes as unused methods
51+ are never generated. Before generation occurs, the descriptors can be seen in the class
52+ ` __dict__ ` , after first use these are replaced.
53+
54+ ``` python
55+ >> > Book.__dict__ [" __init__" ]
56+ < MethodMaker for ' __init__' method>
57+ >> > Book()
58+ Book(title = ' The Hitchhikers Guide to the Galaxy' , author = ' Douglas Adams' , year = 1979 )
59+ >> > Book.__dict__ [" __init__" ]
60+ < function Book.__init__ at ... >
61+ ```
62+
63+ The gathering of field and class information is also separated from the build step
64+ so it is possible to change how this information is gathered without needing to rewrite
65+ the code generation tools.
66+
67+ # # The base class `Prefab` implementation ##
68+
69+ Alongside the `@ prefab` decorator there is also a `Prefab` base class that can be used.
70+
71+ The main differences in behaviour are that `Prefab` will generate `__slots__ ` by default
72+ using a metaclass, and any options given to `Prefab` will automatically be set on subclasses.
73+
74+ Unlike attrs' `@define` or dataclasses' `@ dataclass` , `@ prefab` does not and will not support
75+ `__slots__ ` (this is explained in a section below).
76+
77+ ```python
78+ from pathlib import Path
2479from ducktools.classbuilder.prefab import Prefab, attribute
2580
2681class Slotted (Prefab ):
@@ -29,40 +84,127 @@ class Slotted(Prefab):
2984 default = " What do you get if you multiply six by nine?" ,
3085 doc = " Life the universe and everything" ,
3186 )
87+ python_path: Path(" /usr/bin/python4" )
3288
3389ex = Slotted()
3490print (ex)
35- print (ex.__slots__ )
3691```
3792
38- The generated source code for the methods can be viewed using the ` print_generated_code ` helper function.
93+ The generated code for the methods can be viewed using the ` print_generated_code ` helper function.
94+
95+ <details >
96+
97+ <summary >Generated source code for the same example, but with all optional methods enabled</summary >
3998
4099``` python
41- from ducktools.classbuilder import print_generated_code
100+ Source:
101+ def __delattr__ (self , name ):
102+ raise TypeError (
103+ f " { type (self ).__name__ !r } object "
104+ f " does not support attribute deletion "
105+ )
106+
107+ def __eq__ (self , other ):
108+ return (
109+ self .the_answer == other.the_answer
110+ and self .the_question == other.the_question
111+ and self .python_path == other.python_path
112+ ) if self .__class__ is other.__class__ else NotImplemented
113+
114+ def __ge__ (self , other ):
115+ if self .__class__ is other.__class__ :
116+ return (self .the_answer, self .the_question, self .python_path) >= (other.the_answer, other.the_question, other.python_path)
117+ return NotImplemented
118+
119+ def __gt__ (self , other ):
120+ if self .__class__ is other.__class__ :
121+ return (self .the_answer, self .the_question, self .python_path) > (other.the_answer, other.the_question, other.python_path)
122+ return NotImplemented
123+
124+ def __hash__ (self ):
125+ return hash ((self .the_answer, self .the_question, self .python_path))
126+
127+ def __init__ (self , the_answer = 42 , the_question = ' What do you get if you multiply six by nine?' , python_path = _python_path_default):
128+ self .the_answer = the_answer
129+ self .the_question = the_question
130+ self .python_path = python_path
131+
132+ def __iter__ (self ):
133+ yield self .the_answer
134+ yield self .the_question
135+ yield self .python_path
136+
137+ def __le__ (self , other ):
138+ if self .__class__ is other.__class__ :
139+ return (self .the_answer, self .the_question, self .python_path) <= (other.the_answer, other.the_question, other.python_path)
140+ return NotImplemented
141+
142+ def __lt__ (self , other ):
143+ if self .__class__ is other.__class__ :
144+ return (self .the_answer, self .the_question, self .python_path) < (other.the_answer, other.the_question, other.python_path)
145+ return NotImplemented
146+
147+ def __replace__ (self , / , ** changes ):
148+ new_kwargs = {' the_answer' : self .the_answer, ' the_question' : self .the_question, ' python_path' : self .python_path}
149+ new_kwargs |= changes
150+ return self .__class__ (** new_kwargs)
151+
152+ @_recursive_repr
153+ def __repr__ (self ):
154+ return f ' { type (self ).__qualname__ } (the_answer= { self .the_answer!r } , the_question= { self .the_question!r } , python_path= { self .python_path!r } ) '
155+
156+ def __setattr__ (self , name , value ):
157+ if hasattr (self , name) or name not in __field_names:
158+ raise TypeError (
159+ f " { type (self ).__name__ !r } object does not support "
160+ f " attribute assignment "
161+ )
162+ else :
163+ __setattr_func(self , name, value)
164+
165+ def as_dict (self ):
166+ return {' the_answer' : self .the_answer, ' the_question' : self .the_question, ' python_path' : self .python_path}
167+
168+
169+ Globals:
170+ __init__ : {' _python_path_default' : PosixPath(' /usr/bin/python' )}
171+ __repr__ : {' _recursive_repr' : < function recursive_repr.< locals > .decorating_function at 0x 7367f9cddf30> }
172+ __setattr__ : {' __field_names' : {' the_question' , ' the_answer' , ' python_path' }, ' __setattr_func' : < slot wrapper ' __setattr__' of ' object' objects> }
173+
174+ Annotations:
175+ __init__ : {' the_answer' : < class ' int' > , ' the_question' : < class ' str' > , ' python_path' : < class ' pathlib.Path' > , ' return' : None }
42176
43- print_generated_code(Slotted)
44177```
45178
179+ </details >
180+
46181### Core ###
47182
48- The base ` ducktools.classbuilder ` module provides tools for creating a customized version of the ` dataclass ` concept.
183+ The main ` ducktools.classbuilder ` module provides tools for creating a customized version of the ` dataclass ` concept.
49184
50185* ` MethodMaker `
51186 * This tool takes a function that generates source code and converts it into a descriptor
52187 that will execute the source code and attach the gemerated method to a class on demand.
188+ * This is what you use if you need to write a customized ` __init__ ` method or add some other
189+ generated method.
53190* ` Field `
54191 * This defines a basic dataclass-like field with some basic arguments
55192 * This class itself is a dataclass-like of sorts
193+ (unfortunately it does not play well with ` @dataclass_transform ` and hence, typing)
56194 * Additional arguments can be added by subclassing and using annotations
57195 * See ` ducktools.classbuilder.prefab.Attribute ` for an example of this
58196* Gatherers
59197 * These collect field information and return both the gathered fields and any modifications
60198 that will need to be made to the class when built to support them.
199+ * This is what you would use if, for instance you wanted to use ` Annotated[...] ` to define
200+ how fields should act instead of arguments. The full documentation includes an example
201+ implementing this for a simple dataclass-like.
61202* ` builder `
62203 * This is the main tool used for constructing decorators and base classes to provide
63204 generated methods.
64205 * Other than the required changes to a class for ` __slots__ ` that are done by ` SlotMakerMeta `
65206 this is where all class mutations should be applied.
207+ * Once you have a gatherer and a set of ` MethodMaker ` s run this to add the methods to the class
66208* ` SlotMakerMeta `
67209 * When given a gatherer, this metaclass will create ` __slots__ ` automatically.
68210
@@ -76,12 +218,18 @@ The base `ducktools.classbuilder` module provides tools for creating a customize
76218
77219### Prefab ###
78220
79- This prebuilt implementation is available from the ` ducktools.classbuilder.prefab ` submodule.
221+ The prebuilt 'prefab' implementation includes additional customization including
222+ ` __prefab_pre_init__ ` and ` __prefab_post_init__ ` methods.
223+
224+ Both of these methods will take any field names as arguments. Those passed to ` __prefab_pre_init__ ` will still be set
225+ inside the main ` __init__ ` body, while those passed to ` __prefab_post_init__ ` will not.
226+
227+ ` __prefab_pre_init__ ` is intended as a place to perform validation checks before values are set in the main body.
228+ ` __prefab_post_init__ ` can be seen as a partial ` __init__ ` function, where you only need to write
229+ the ` __init__ ` function for arguments that need more than basic assignment.
80230
81- This includes more customization including ` __prefab_pre_init__ ` and ` __prefab_post_init__ `
82- functions for subclass customization.
231+ Here is an example using ` __prefab_post_init__ ` that converts a string or Path object into a path object:
83232
84- Here is an example of applying a conversion in ` __prefab_post_init__ ` :
85233``` python
86234from pathlib import Path
87235from ducktools.classbuilder.prefab import Prefab
@@ -103,14 +251,29 @@ steam = AppDetails(
103251print (steam)
104252```
105253
106- #### Features ####
254+ <details >
255+
256+ <summary >The generated code for the init method</summary >
257+
258+ ``` python
259+ def __init__ (self , app_name , app_path ):
260+ self .app_name = app_name
261+ self .__prefab_post_init__(app_path = app_path)
262+ ```
263+
264+ Note: annotations are attached as ` __annotations__ ` and so do not appear in generated
265+ source code.
266+
267+ </details >
268+
269+ #### Features and Differences ####
107270
108271` Prefab ` and ` @prefab ` support many standard dataclass features along with
109272some extra features and some intentional differences in design.
110273
111274* All standard methods are generated on-demand
112275 * This makes the construction of classes much faster in general
113- * Generation is done and then cached on first access
276+ * Generation is done and then cached on first access using non-data descriptors
114277* Standard ` __init__ ` , ` __eq__ ` and ` __repr__ ` methods are generated by default
115278 - The ` __repr__ ` implementation does not automatically protect against recursion,
116279 but there is a ` recursive_repr ` argument that will do so if needed
@@ -155,7 +318,9 @@ There are also some intentionally missing features:
155318 * ` VALUE ` annotations are used as they are faster in most cases
156319 * As the ` __init__ ` method gets ` __annotations__ ` these need to be either values or strings
157320 to match the behaviour of previous Python versions
158-
321+ * There is currently no equivalent to ` InitVar `
322+ * I'm not sure * how* I would want to implement this other than I don't _ really_ want to use
323+ annotations to decide behaviour (this is messy enough with ` ClassVar ` and ` KW_ONLY ` ).
159324
160325## What is the issue with generating ` __slots__ ` with a decorator ##
161326
0 commit comments