Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/code_examples/outputs/docs_ex09_annotated.output
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<generated class X; x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42>

{'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={}),
'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={}),
'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={}),
'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={}),
'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={}),
'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={}),
'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={})}
{'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={}),
'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={}),
'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={}),
'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={}),
'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={}),
'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={}),
'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={})}


<generated class Y; x='Value of x', a='Not In __init__ signature', c=[], e='Value of e', f=42>
Expand Down
6 changes: 3 additions & 3 deletions docs/code_examples/outputs/tutorial_code.output
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
({'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),
'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),
'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)},
({'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),
'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),
'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)},
{'__fields__': None})
@property
def report(self):
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ exclude_also = [
# A combination of types in stubs and tests using dataclass syntax
# means that there are a number of annotations in otherwise unannotated areas
disable_error_code = ["annotation-unchecked"]

[tool.uv]
exclude-newer = "1 week"
exclude-newer-package = { reannotate = "1 hour" }
20 changes: 16 additions & 4 deletions src/ducktools/classbuilder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,24 @@ def build_completed(cls):
# As 'None' can be a meaningful value we need a sentinel value
# to use to show no value has been provided.
class _NothingType:
def __init__(self, custom=None):
self.custom = custom
# Repeated calls to the same nothing type should
# return the same object
_registry = {} # type: ignore

def __new__(cls, custom=None):
# Instances with the same custom name
# should be the same object
inst = cls._registry.get(custom)
if not inst:
inst = super().__new__(cls)
inst.custom = custom
cls._registry[custom] = inst
return inst

def __repr__(self):
if self.custom:
return f"<{self.custom} NOTHING OBJECT>"
return "<NOTHING OBJECT>"
return f"<{self.custom} NOTHING Sentinel>"
return "<NOTHING Sentinel>"


NOTHING = _NothingType()
Expand Down
2 changes: 1 addition & 1 deletion src/ducktools/classbuilder/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def build_completed(cls: type) -> bool: ...
def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...

class _NothingType:
def __init__(self, custom: str | None = ...) -> None: ...
def __new__(cls, custom: str | None = ...) -> typing.Self: ...
def __repr__(self) -> str: ...
NOTHING: _NothingType
FIELD_NOTHING: _NothingType
Expand Down
18 changes: 16 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Tests for the core 'builder'
import inspect
import pytest
import pickle

from ducktools.classbuilder import (
_NothingType,

INTERNALS_DICT,
NOTHING,
FIELD_NOTHING,

add_methods,
builder,
Expand Down Expand Up @@ -124,6 +128,16 @@ def test_construct_field():
Field(default=None, default_factory=list)


def test_nothing_type():
# Test that creating new sentinels actually
# gives the original sentinel
assert _NothingType() is NOTHING
assert _NothingType("FIELD") is FIELD_NOTHING

dumps, loads = pickle.dumps, pickle.loads
assert loads(dumps(NOTHING)) is NOTHING


def test_eq_field():
f1 = Field(default=True)
f2 = Field(default=False)
Expand Down Expand Up @@ -537,10 +551,10 @@ class Ex:
assert repr(flds).endswith(
"GatheredFields("
"fields={'x': Field("
"default=1, default_factory=<NOTHING OBJECT>, type=<NOTHING OBJECT>, doc=None, "
"default=1, default_factory=<NOTHING Sentinel>, type=<NOTHING Sentinel>, doc=None, "
"init=True, repr=True, compare=True, kw_only=False"
")}, "
"modifications={'x': <NOTHING OBJECT>}"
"modifications={'x': <NOTHING Sentinel>}"
")"
)

Expand Down
Loading