Skip to content

Commit d65ff84

Browse files
authored
Merge pull request #98 from DavidCEllis/probably-nothing
Make the sentinels restore the original object after pickling
2 parents 225ede0 + e04a795 commit d65ff84

7 files changed

Lines changed: 341 additions & 306 deletions

File tree

docs/code_examples/outputs/docs_ex09_annotated.output

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<generated class X; x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42>
22

3-
{'x': Attribute(default=<NOTHING OBJECT>, default_factory=<NOTHING OBJECT>, type=<class 'str'>, doc=None, init=True, repr=True, compare=True, kw_only=False, iter=True, serialize=True, metadata={}),
4-
'a': Attribute(default='Not In __init__ signature', default_factory=<NOTHING OBJECT>, type=<class 'int'>, doc=None, init=False, repr=True, compare=True, kw_only=False, iter=True, serialize=True, metadata={}),
5-
'b': Attribute(default='Not In Repr', default_factory=<NOTHING OBJECT>, type=<class 'str'>, doc=None, init=True, repr=False, compare=True, kw_only=False, iter=True, serialize=True, metadata={}),
6-
'c': Attribute(default=<NOTHING OBJECT>, default_factory=<class 'list'>, type=list[str], doc=None, init=True, repr=True, compare=False, kw_only=False, iter=True, serialize=True, metadata={}),
7-
'd': Attribute(default='Not Anywhere', default_factory=<NOTHING OBJECT>, type=<class 'str'>, doc=None, init=False, repr=False, compare=False, kw_only=False, iter=True, serialize=True, metadata={}),
8-
'e': Attribute(default=<NOTHING OBJECT>, default_factory=<NOTHING OBJECT>, type=<class 'str'>, doc=None, init=True, repr=True, compare=False, kw_only=True, iter=True, serialize=True, metadata={}),
9-
'f': Attribute(default=42, default_factory=<NOTHING OBJECT>, type=ForwardRef('unknown'), doc=None, init=True, repr=True, compare=False, kw_only=False, iter=True, serialize=False, metadata={})}
3+
{'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={}),
4+
'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={}),
5+
'b': Attribute(default='Not In Repr', default_factory=<NOTHING Sentinel>, type=<class 'str'>, doc=None, init=True, repr=False, compare=True, kw_only=False, iter=True, serialize=True, metadata={}),
6+
'c': Attribute(default=<NOTHING Sentinel>, default_factory=<class 'list'>, type=list[str], doc=None, init=True, repr=True, compare=False, kw_only=False, iter=True, serialize=True, metadata={}),
7+
'd': Attribute(default='Not Anywhere', default_factory=<NOTHING Sentinel>, type=<class 'str'>, doc=None, init=False, repr=False, compare=False, kw_only=False, iter=True, serialize=True, metadata={}),
8+
'e': Attribute(default=<NOTHING Sentinel>, default_factory=<NOTHING Sentinel>, type=<class 'str'>, doc=None, init=True, repr=True, compare=False, kw_only=True, iter=True, serialize=True, metadata={}),
9+
'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={})}
1010

1111

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

docs/code_examples/outputs/tutorial_code.output

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
({'field_1': CustomField(default='First Field', default_factory=<NOTHING OBJECT>, type=<NOTHING OBJECT>, doc=None, init=True, repr=True, compare=True, kw_only=False, report=True),
2-
'field_2': CustomField(default='Second Field', default_factory=<NOTHING OBJECT>, type=<NOTHING OBJECT>, doc=None, init=True, repr=True, compare=True, kw_only=False, report=True),
3-
'field_3': CustomField(default='Third Field', default_factory=<NOTHING OBJECT>, type=<NOTHING OBJECT>, doc=None, init=True, repr=True, compare=True, kw_only=False, report=False)},
1+
({'field_1': CustomField(default='First Field', default_factory=<NOTHING Sentinel>, type=<NOTHING Sentinel>, doc=None, init=True, repr=True, compare=True, kw_only=False, report=True),
2+
'field_2': CustomField(default='Second Field', default_factory=<NOTHING Sentinel>, type=<NOTHING Sentinel>, doc=None, init=True, repr=True, compare=True, kw_only=False, report=True),
3+
'field_3': CustomField(default='Third Field', default_factory=<NOTHING Sentinel>, type=<NOTHING Sentinel>, doc=None, init=True, repr=True, compare=True, kw_only=False, report=False)},
44
{'__fields__': None})
55
@property
66
def report(self):

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,7 @@ exclude_also = [
8383
# A combination of types in stubs and tests using dataclass syntax
8484
# means that there are a number of annotations in otherwise unannotated areas
8585
disable_error_code = ["annotation-unchecked"]
86+
87+
[tool.uv]
88+
exclude-newer = "1 week"
89+
exclude-newer-package = { reannotate = "1 hour" }

src/ducktools/classbuilder/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,24 @@ def build_completed(cls):
175175
# As 'None' can be a meaningful value we need a sentinel value
176176
# to use to show no value has been provided.
177177
class _NothingType:
178-
def __init__(self, custom=None):
179-
self.custom = custom
178+
# Repeated calls to the same nothing type should
179+
# return the same object
180+
_registry = {} # type: ignore
181+
182+
def __new__(cls, custom=None):
183+
# Instances with the same custom name
184+
# should be the same object
185+
inst = cls._registry.get(custom)
186+
if not inst:
187+
inst = super().__new__(cls)
188+
inst.custom = custom
189+
cls._registry[custom] = inst
190+
return inst
191+
180192
def __repr__(self):
181193
if self.custom:
182-
return f"<{self.custom} NOTHING OBJECT>"
183-
return "<NOTHING OBJECT>"
194+
return f"<{self.custom} NOTHING Sentinel>"
195+
return "<NOTHING Sentinel>"
184196

185197

186198
NOTHING = _NothingType()

src/ducktools/classbuilder/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def build_completed(cls: type) -> bool: ...
4747
def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
4848

4949
class _NothingType:
50-
def __init__(self, custom: str | None = ...) -> None: ...
50+
def __new__(cls, custom: str | None = ...) -> typing.Self: ...
5151
def __repr__(self) -> str: ...
5252
NOTHING: _NothingType
5353
FIELD_NOTHING: _NothingType

tests/test_core.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# Tests for the core 'builder'
22
import inspect
33
import pytest
4+
import pickle
45

56
from ducktools.classbuilder import (
7+
_NothingType,
8+
69
INTERNALS_DICT,
710
NOTHING,
11+
FIELD_NOTHING,
812

913
add_methods,
1014
builder,
@@ -124,6 +128,16 @@ def test_construct_field():
124128
Field(default=None, default_factory=list)
125129

126130

131+
def test_nothing_type():
132+
# Test that creating new sentinels actually
133+
# gives the original sentinel
134+
assert _NothingType() is NOTHING
135+
assert _NothingType("FIELD") is FIELD_NOTHING
136+
137+
dumps, loads = pickle.dumps, pickle.loads
138+
assert loads(dumps(NOTHING)) is NOTHING
139+
140+
127141
def test_eq_field():
128142
f1 = Field(default=True)
129143
f2 = Field(default=False)
@@ -537,10 +551,10 @@ class Ex:
537551
assert repr(flds).endswith(
538552
"GatheredFields("
539553
"fields={'x': Field("
540-
"default=1, default_factory=<NOTHING OBJECT>, type=<NOTHING OBJECT>, doc=None, "
554+
"default=1, default_factory=<NOTHING Sentinel>, type=<NOTHING Sentinel>, doc=None, "
541555
"init=True, repr=True, compare=True, kw_only=False"
542556
")}, "
543-
"modifications={'x': <NOTHING OBJECT>}"
557+
"modifications={'x': <NOTHING Sentinel>}"
544558
")"
545559
)
546560

0 commit comments

Comments
 (0)