Skip to content

Commit 5acf791

Browse files
authored
Merge pull request #113 from DavidCEllis/fix-potential-race
Fix potential race
2 parents d7d483e + 37c7636 commit 5acf791

6 files changed

Lines changed: 53 additions & 30 deletions

File tree

docs/code_examples/docs_ex10_frozen_attributes.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ def freezable(cls=None, /, *, frozen=False):
9494
# Frozen attributes need to be added afterwards
9595
# Due to the need to know if frozen fields exist
9696
if frozen:
97-
setattr(cls, "__setattr__", dtmethods.frozen_setattr_maker)
98-
setattr(cls, "__delattr__", dtmethods.frozen_delattr_maker)
97+
dtmethods.add_methods(
98+
cls,
99+
[dtmethods.frozen_setattr_maker, dtmethods.frozen_delattr_maker]
100+
)
99101
else:
100102
fields = dtfuncs.get_fields(cls)
101103
has_frozen_fields = False
@@ -105,8 +107,10 @@ def freezable(cls=None, /, *, frozen=False):
105107
break
106108

107109
if has_frozen_fields:
108-
setattr(cls, "__setattr__", frozen_setattr_field_maker)
109-
setattr(cls, "__delattr__", frozen_delattr_field_maker)
110+
dtmethods.add_methods(
111+
cls,
112+
[frozen_setattr_field_maker, frozen_delattr_field_maker]
113+
)
110114

111115
return cls
112116

src/ducktools/classbuilder/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,8 @@ def __eq__(self, other):
463463
def __repr__(self):
464464
return f"{type(self).__name__}(fields={self.fields!r}, modifications={self.modifications!r})"
465465

466-
def __call__(self, cls_dict):
467-
# cls_dict will be provided, but isn't needed
466+
def __call__(self, cls_or_ns):
467+
# cls_or_ns will be provided, but isn't needed
468468
return self.fields, self.modifications
469469

470470

src/ducktools/classbuilder/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class GatheredFields:
139139
def __repr__(self) -> str: ...
140140
def __eq__(self, other) -> bool: ...
141141
def __call__(
142-
self, cls_dict: type | dict[str, typing.Any]
142+
self, cls_or_ns: type | dict[str, typing.Any]
143143
) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
144144

145145
# Only technically frozen under testing but we should *act* like they are frozen

src/ducktools/classbuilder/functions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def get_fields(cls, *, local=False):
4747
"""
4848
key = "local_fields" if local else "fields"
4949
try:
50-
return getattr(cls, INTERNALS_DICT)[key]
50+
return cls.__dict__[INTERNALS_DICT][key]
5151
except (AttributeError, KeyError):
5252
raise TypeError(f"{cls} is not a classbuilder generated class")
5353

@@ -61,7 +61,7 @@ def get_flags(cls):
6161
:return: dictionary of keys and flag values
6262
"""
6363
try:
64-
return getattr(cls, INTERNALS_DICT)["flags"]
64+
return cls.__dict__[INTERNALS_DICT]["flags"]
6565
except (AttributeError, KeyError):
6666
raise TypeError(f"{cls} is not a classbuilder generated class")
6767

@@ -75,7 +75,7 @@ def get_methods(cls):
7575
:return: dict of generated methods attached to the class by name
7676
"""
7777
try:
78-
return getattr(cls, INTERNALS_DICT)["methods"]
78+
return cls.__dict__[INTERNALS_DICT]["methods"]
7979
except (AttributeError, KeyError):
8080
raise TypeError(f"{cls} is not a classbuilder generated class")
8181

src/ducktools/classbuilder/methods.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
from .annotations import apply_annotations
4242
from .constants import INTERNALS_DICT, NOTHING, REPLACE_NAME
43-
from .functions import get_fields, get_flags
43+
from .functions import get_fields, get_flags, get_methods
4444

4545
try:
4646
from ._cached_methods import init_cache, setattr_cache
@@ -109,6 +109,15 @@ def generate(self):
109109
return method
110110

111111

112+
def _get_method(cls, name):
113+
try:
114+
methods = get_methods(cls)
115+
except TypeError:
116+
return None
117+
118+
return methods.get(name, None)
119+
120+
112121
class MethodMaker:
113122
"""
114123
The descriptor class to place where methods should be generated.
@@ -135,16 +144,18 @@ def __repr__(self):
135144
return f"<MethodMaker for {self.funcname!r} method>"
136145

137146
def __get__(self, inst, cls):
138-
# This can be called via super().funcname(...) in which case the class
147+
# This can be called via a subclass or through
148+
# super().funcname(...) in which case the class
139149
# may not be the correct one. If this is the correct class
140150
# it should have this descriptor in the class dict under
141151
# the correct funcname.
142152
# Otherwise is should be found in the MRO of the class.
143-
if cls.__dict__.get(self.funcname) is self:
153+
154+
if _get_method(cls, self.funcname) is self:
144155
gen_cls = cls
145156
else:
146157
for c in cls.__mro__[1:]: # skip 'cls' as special cased
147-
if c.__dict__.get(self.funcname) is self:
158+
if _get_method(c, self.funcname) is self:
148159
gen_cls = c
149160
break
150161
else:

tests/test_core.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,37 @@
44
import pickle
55

66
from ducktools.classbuilder import (
7+
builder,
8+
default_methods,
9+
make_unified_gatherer,
10+
slot_gatherer,
11+
slotclass,
12+
13+
Field,
14+
GatheredFields,
15+
SlotFields,
16+
)
17+
from ducktools.classbuilder.constants import (
718
_NothingType,
819

920
INTERNALS_DICT,
1021
NOTHING,
1122
FIELD_NOTHING,
23+
)
24+
from ducktools.classbuilder.functions import (
25+
get_fields,
26+
get_flags,
27+
get_methods,
28+
)
29+
from ducktools.classbuilder.methods import (
30+
GeneratedCode,
31+
MethodMaker,
1232

1333
add_methods,
14-
builder,
15-
default_methods,
1634
eq_maker,
1735
frozen_delattr_maker,
1836
frozen_setattr_maker,
19-
get_fields,
20-
get_flags,
21-
get_methods,
2237
init_maker,
23-
make_unified_gatherer,
24-
slot_gatherer,
25-
slotclass,
26-
27-
Field,
28-
GatheredFields,
29-
GeneratedCode,
30-
MethodMaker,
31-
SlotFields,
3238
)
3339
from ducktools.classbuilder.annotations import get_ns_annotations
3440

@@ -68,12 +74,14 @@ def generator(cls, funcname="demo"):
6874

6975
assert repr(method_desc) == "<MethodMaker for 'demo' method>"
7076

77+
gatherer = GatheredFields({}, {})
78+
@builder(gatherer=gatherer, methods={method_desc})
7179
class ValueX:
72-
demo = method_desc
73-
7480
def __init__(self):
7581
self.x = "Example Value"
7682

83+
add_methods(ValueX, [method_desc])
84+
7785
ex = ValueX()
7886

7987
assert ValueX.__dict__["demo"] == method_desc

0 commit comments

Comments
 (0)