Skip to content

Commit 9d09ae7

Browse files
authored
Merge pull request #34 from DavidCEllis/dont_evaluate_annotations
Don't evaluate annotations in `get_ns_annotations`, remove AnnotationClass
2 parents 6b8ca17 + 0158a5d commit 9d09ae7

17 files changed

+199
-319
lines changed

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ These tools are available from the main `ducktools.classbuilder` module.
3232
* `@slotclass`
3333
* A decorator based implementation that uses a special dict subclass assigned
3434
to `__slots__` to describe the fields for method generation.
35-
* `AnnotationClass`
36-
* A subclass based implementation that works with `__slots__`, type annotations
37-
or `Field(...)` attributes to describe the fields for method generation.
38-
* If `__slots__` isn't used to declare fields, it will be generated by a metaclass.
35+
* `SlotMakerMeta`
36+
* A metaclass for creating other implementations using annotations, fields or slots.
37+
* This metaclass will allow for creating `__slots__` correctly in subclasses.
38+
* `builder`
39+
* This is the main tool used for constructing decorators and base classes to provide
40+
generated methods.
3941

4042
Each of these forms of class generation will result in the same methods being
4143
attached to the class after the field information has been obtained.
@@ -65,8 +67,9 @@ This includes more customization including `__prefab_pre_init__` and `__prefab_p
6567
functions for subclass customization.
6668

6769
A `@prefab` decorator and `Prefab` base class are provided.
68-
Similar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.
69-
However decorated classes with `@prefab` that do not declare fields using `__slots__`
70+
71+
`Prefab` will generate `__slots__` by default.
72+
decorated classes with `@prefab` that do not declare fields using `__slots__`
7073
will **not** be slotted and there is no `slots` argument to apply this.
7174

7275
Here is an example of applying a conversion in `__post_init__`:
@@ -110,7 +113,7 @@ the fields can be set *before* the class is constructed, so the class
110113
will work correctly without needing to be rebuilt.
111114

112115
For example these two classes would be roughly equivalent, except that
113-
`@dataclass` has had to recreate the class from scratch while `AnnotationClass`
116+
`@dataclass` has had to recreate the class from scratch while `Prefab`
114117
has created `__slots__` and added the methods on to the original class.
115118
This means that any references stored to the original class *before*
116119
`@dataclass` has rebuilt the class will not be pointing towards the
@@ -125,7 +128,7 @@ functions.
125128
```python
126129
import json
127130
from dataclasses import dataclass
128-
from ducktools.classbuilder import AnnotationClass, Field
131+
from ducktools.classbuilder.prefab import Prefab, attribute
129132

130133

131134
class _RegisterDescriptor:
@@ -168,10 +171,10 @@ class DataCoords:
168171
return {"x": self.x, "y": self.y}
169172

170173

171-
# slots=True is the default for AnnotationClass
172-
class BuilderCoords(AnnotationClass, slots=True):
174+
# slots=True is the default for Prefab
175+
class BuilderCoords(Prefab):
173176
x: float = 0.0
174-
y: float = Field(default=0.0, doc="y coordinate")
177+
y: float = attribute(default=0.0, doc="y coordinate")
175178

176179
@register.register_method
177180
def to_json(self):
@@ -235,9 +238,6 @@ It will copy values provided as the `type` to `Field` into the
235238
Values provided to `doc` will be placed in the final `__slots__`
236239
field so they are present on the class if `help(...)` is called.
237240

238-
`AnnotationClass` offers the same features with additional methods of gathering
239-
fields.
240-
241241
If you want something with more features you can look at the `prefab`
242242
submodule which provides more specific features that differ further from the
243243
behaviour of `dataclasses`.

docs/api.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,3 @@
4040
```{eval-rst}
4141
.. autofunction:: ducktools.classbuilder.annotations::make_annotation_gatherer
4242
```
43-
44-
```{eval-rst}
45-
.. autoclass:: ducktools.classbuilder.annotations::AnnotationClass
46-
```

docs/extension_examples.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,8 @@ To implement this you need to create a new annotated_gatherer function.
469469
> If you need to change the value of a field use Field.from_field(...) to make a new instance.
470470
471471
```python
472-
from __future__ import annotations
472+
# Don't use __future__ annotations with get_ns_annotations in this case
473+
# as it doesn't evaluate string annotations.
473474

474475
import types
475476
from typing import Annotated, Any, ClassVar, get_origin

docs/generated_code.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ There is a helper function `get_methods` that can be used to obtain the names an
77
methods that have been attached to the class by the builder.
88

99
```python
10-
from ducktools.classbuilder import AnnotationClass, get_methods
10+
from ducktools.classbuilder import SlotFields, get_methods, slotclass
1111

12-
class Example(AnnotationClass):
13-
a: str = "a"
14-
b: str = "b"
12+
@slotclass
13+
class Example:
14+
__slots__ = SlotFields(
15+
a='a',
16+
b='b'
17+
)
1518

1619
print(get_methods(Example))
1720
```
@@ -28,12 +31,15 @@ These can then be used to examine the generated source code and global variables
2831
to the `exec` function. This can be useful for debugging code generators.
2932

3033
```python
31-
from ducktools.classbuilder import AnnotationClass, get_methods
32-
33-
class Example(AnnotationClass):
34-
a: str = "a"
35-
b: str = "b"
36-
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+
3743
methods = get_methods(Example)
3844

3945
for method in methods.values():

docs/index.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,44 @@ ex = SlottedDC()
6565
print(ex)
6666
```
6767

68-
## Annotation Class Usage ##
68+
## Using Annotations ##
6969

70-
There is an additional AnnotationClass base class that allows creating slotted classes
71-
using annotations. This has to be a base class with a specific metaclass in order to
72-
create the `__slots__` field *before* the class has been generated in order to work
73-
correctly.
70+
It is possible to create slotted classes using Annotations.
71+
There is a `Prefab` base class in the `prefab` submodule that does this,
72+
but it also easy to implement using the provided tools.
73+
74+
In order to correctly implement `__slots__` this needs to be done
75+
using a metaclass as `__slots__` must be defined before the **class**
76+
is created.
7477

7578
```python
76-
from ducktools.classbuilder import AnnotationClass
79+
from ducktools.classbuilder import (
80+
SlotMakerMeta,
81+
annotation_gatherer,
82+
builder,
83+
check_argument_order,
84+
default_methods,
85+
)
86+
87+
88+
class AnnotationClass(metaclass=SlotMakerMeta):
89+
__slots__ = {}
90+
91+
def __init_subclass__(
92+
cls,
93+
methods=default_methods,
94+
gatherer=annotation_gatherer,
95+
**kwargs
96+
):
97+
# Check class dict otherwise this will always be True as this base
98+
# class uses slots.
99+
slots = "__slots__" in cls.__dict__
100+
101+
builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
102+
check_argument_order(cls)
103+
super().__init_subclass__(**kwargs)
104+
105+
77106

78107
class AnnotatedDC(AnnotationClass):
79108
the_answer: int = 42

docs/tutorial.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,15 @@ def report_generator(cls, funcname="report"):
146146
report_maker = dtbuild.MethodMaker("report", report_generator)
147147
```
148148

149-
We can take a quick look at what this generates by applying it to an `AnnotationClass`:
149+
We can take a quick look at what this generates by applying it to a `slotclass`:
150150
```python
151-
class CodegenDemo(dtbuild.AnnotationClass):
151+
@dtbuild.slotclass
152+
class CodegenDemo:
153+
__slots__ = dtbuild.SlotFields(
154+
field_1="Field one",
155+
field_2="Field two",
156+
field_3="Field three",
157+
)
152158
field_1: str = "Field one"
153159
field_2: str = "Field two"
154160
field_3: str = "Field three"

docs_code/docs_ex9_annotated.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
import types
42
from typing import Annotated, Any, ClassVar, get_origin
53

docs_code/index_example.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from ducktools.classbuilder import (
2+
SlotMakerMeta,
3+
builder,
4+
check_argument_order,
5+
default_methods,
6+
unified_gatherer,
7+
)
8+
9+
10+
class AnnotationClass(metaclass=SlotMakerMeta):
11+
__slots__ = {}
12+
13+
def __init_subclass__(
14+
cls,
15+
methods=default_methods,
16+
gatherer=unified_gatherer,
17+
**kwargs
18+
):
19+
# Check class dict otherwise this will always be True as this base
20+
# class uses slots.
21+
slots = "__slots__" in cls.__dict__
22+
23+
builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
24+
check_argument_order(cls)
25+
super().__init_subclass__(**kwargs)
26+
27+
28+
class AnnotatedDC(AnnotationClass):
29+
the_answer: int = 42
30+
the_question: str = "What do you get if you multiply six by nine?"
31+
32+
33+
ex = AnnotatedDC()
34+
print(ex)

docs_code/tutorial_code.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,13 @@ def report_generator(cls, funcname="report"):
8181

8282

8383
# View the generated code by testing on a demo class
84-
class CodegenDemo(dtbuild.AnnotationClass):
85-
field_1: str = "Field one"
86-
field_2: str = "Field two"
87-
field_3: str = "Field three"
84+
@dtbuild.slotclass
85+
class CodegenDemo:
86+
__slots__ = dtbuild.SlotFields(
87+
field_1="Field one",
88+
field_2="Field two",
89+
field_3="Field three",
90+
)
8891

8992

9093
print(report_generator(CodegenDemo).source_code)

src/ducktools/classbuilder/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ def field_annotation_gatherer(cls_or_ns):
769769
if is_classvar(v):
770770
continue
771771

772-
if v is KW_ONLY:
772+
if v is KW_ONLY or (isinstance(v, str) and v == "KW_ONLY"):
773773
if kw_flag:
774774
raise SyntaxError("KW_ONLY sentinel may only appear once.")
775775
kw_flag = True
@@ -868,7 +868,7 @@ def field_unified_gatherer(cls_or_ns):
868868
# To choose between annotation and attribute gatherers
869869
# compare sets of names.
870870
# Don't bother evaluating string annotations, as we only need names
871-
cls_annotations = get_ns_annotations(cls_dict, eval_str=False)
871+
cls_annotations = get_ns_annotations(cls_dict)
872872
cls_attributes = {
873873
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
874874
}

0 commit comments

Comments
 (0)