Skip to content

Commit

Permalink
Merge pull request #27 from DavidCEllis/frozen_attribute_example
Browse files Browse the repository at this point in the history
Add example to documentation / Change default to removing default values for gatherers
  • Loading branch information
DavidCEllis authored Aug 6, 2024
2 parents 3e58cb2 + 875b86e commit cd39f4c
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 61 deletions.
132 changes: 132 additions & 0 deletions docs/extension_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,138 @@ if __name__ == "__main__":
print(e)
```

#### Frozen Attributes ####

Here's an implementation that allows freezing of individual attributes.

```python
import ducktools.classbuilder as dtbuild


class FreezableField(dtbuild.Field):
frozen: bool = False


def setattr_generator(cls, funcname="__setattr__"):
globs = {}

flags = dtbuild.get_flags(cls)
fields = dtbuild.get_fields(cls)

frozen_fields = set(
name for name, field in fields.items()
if getattr(field, "frozen", False)
)

globs["__frozen_fields"] = frozen_fields

if flags.get("slotted", True):
globs["__setattr_func"] = object.__setattr__
setattr_method = "__setattr_func(self, name, value)"
attrib_check = "hasattr(self, name)"
else:
setattr_method = "self.__dict__[name] = value"
attrib_check = "name in self.__dict__"

code = (
f"def {funcname}(self, name, value):\n"
f" if name in __frozen_fields and {attrib_check}:\n"
f" raise AttributeError(\n"
f" f'Attribute {{name!r}} does not support assignment'\n"
f" )\n"
f" else:\n"
f" {setattr_method}\n"
)

return dtbuild.GeneratedCode(code, globs)


def delattr_generator(cls, funcname="__delattr__"):
globs = {}

flags = dtbuild.get_flags(cls)
fields = dtbuild.get_fields(cls)

frozen_fields = set(
name for name, field in fields.items()
if getattr(field, "frozen", False)
)

globs["__frozen_fields"] = frozen_fields

if flags.get("slotted", True):
globs["__delattr_func"] = object.__delattr__
delattr_method = "__delattr_func(self, name)"
else:
delattr_method = "del self.__dict__[name]"

code = (
f"def {funcname}(self, name):\n"
f" if name in __frozen_fields:"
f" raise AttributeError(\n"
f" f'Attribute {{name!r}} is frozen and can not be deleted'\n"
f" )\n"
f" else:\n"
f" {delattr_method}\n"
)

return dtbuild.GeneratedCode(code, globs)


frozen_setattr_field_maker = dtbuild.MethodMaker("__setattr__", setattr_generator)
frozen_delattr_field_maker = dtbuild.MethodMaker("__delattr__", delattr_generator)
gatherer = dtbuild.make_unified_gatherer(FreezableField)


def freezable(cls=None, /, *, frozen=False):
if cls is None:
return lambda cls_: freezable(cls_, frozen=frozen)

# To make a slotted class use a base class with metaclass
flags = {"frozen": frozen, "slotted": False}

cls = dtbuild.builder(
cls,
gatherer=gatherer,
methods=dtbuild.default_methods,
flags=flags,
)

# Frozen attributes need to be added afterwards
# Due to the need to know if frozen fields exist
if frozen:
setattr(cls, "__setattr__", dtbuild.frozen_setattr_maker)
setattr(cls, "__delattr__", dtbuild.frozen_delattr_maker)
else:
fields = dtbuild.get_fields(cls)
has_frozen_fields = False
for f in fields.values():
if getattr(f, "frozen", False):
has_frozen_fields = True
break

if has_frozen_fields:
setattr(cls, "__setattr__", frozen_setattr_field_maker)
setattr(cls, "__delattr__", frozen_delattr_field_maker)

return cls


@freezable
class X:
a: int = 2
b: int = FreezableField(default=12, frozen=True)


x = X()
x.a = 21

try:
x.b = 43
except AttributeError as e:
print(repr(e))
```

#### Converters ####

Here's an implementation of basic converters that always convert when
Expand Down
73 changes: 31 additions & 42 deletions docs_code/docs_ex10_frozen_attributes.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
from ducktools.classbuilder import (
builder,
default_methods,
get_fields,
get_flags,
frozen_setattr_maker,
frozen_delattr_maker,
make_unified_gatherer,
Field,
GeneratedCode,
MethodMaker,
)


class FreezableField(Field):
import ducktools.classbuilder as dtbuild


class FreezableField(dtbuild.Field):
frozen: bool = False


def setattr_generator(cls, funcname="__setattr__"):
globs = {}

flags = get_flags(cls)
fields = get_fields(cls)
flags = dtbuild.get_flags(cls)
fields = dtbuild.get_fields(cls)

frozen_fields = set(
name for name, field in fields.items()
Expand All @@ -47,14 +36,14 @@ def setattr_generator(cls, funcname="__setattr__"):
f" {setattr_method}\n"
)

return GeneratedCode(code, globs)
return dtbuild.GeneratedCode(code, globs)


def delattr_generator(cls, funcname="__delattr__"):
globs = {}

flags = get_flags(cls)
fields = get_fields(cls)
flags = dtbuild.get_flags(cls)
fields = dtbuild.get_fields(cls)

frozen_fields = set(
name for name, field in fields.items()
Expand All @@ -79,58 +68,58 @@ def delattr_generator(cls, funcname="__delattr__"):
f" {delattr_method}\n"
)

return GeneratedCode(code, globs)


frozen_setattr_field_maker = MethodMaker("__setattr__", setattr_generator)
frozen_delattr_field_maker = MethodMaker("__delattr__", delattr_generator)
return dtbuild.GeneratedCode(code, globs)


gatherer = make_unified_gatherer(FreezableField, leave_default_values=True)
frozen_setattr_field_maker = dtbuild.MethodMaker("__setattr__", setattr_generator)
frozen_delattr_field_maker = dtbuild.MethodMaker("__delattr__", delattr_generator)
gatherer = dtbuild.make_unified_gatherer(FreezableField)


def freezable(cls=None, /, *, frozen=False):
if cls is None:
return lambda cls_: freezable(cls_, frozen=frozen)

# To make a slotted class use a base class with metaclass
flags = {
"frozen": frozen,
"slotted": False,
}
flags = {"frozen": frozen, "slotted": False}

cls = builder(
cls = dtbuild.builder(
cls,
gatherer=gatherer,
methods=default_methods,
methods=dtbuild.default_methods,
flags=flags,
)

# Frozen attributes need to be added afterwards
# Due to the need to know if frozen fields exist
if frozen:
setattr(cls, "__setattr__", frozen_setattr_maker)
setattr(cls, "__delattr__", frozen_delattr_maker)
setattr(cls, "__setattr__", dtbuild.frozen_setattr_maker)
setattr(cls, "__delattr__", dtbuild.frozen_delattr_maker)
else:
fields = get_fields(cls)
frozen_fields = [
f for f in fields.values()
if getattr(f, "frozen", False)
]
if frozen_fields:
fields = dtbuild.get_fields(cls)
has_frozen_fields = False
for f in fields.values():
if getattr(f, "frozen", False):
has_frozen_fields = True
break

if has_frozen_fields:
setattr(cls, "__setattr__", frozen_setattr_field_maker)
setattr(cls, "__delattr__", frozen_delattr_field_maker)

return cls


@freezable(frozen=True)
@freezable
class X:
a: int = 2
b: int = FreezableField(default=12, frozen=True)


x = X()
x.a = 21
x.b = 43

try:
x.b = 43
except AttributeError as e:
print(repr(e))
10 changes: 5 additions & 5 deletions src/ducktools/classbuilder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ def field_slot_gatherer(cls_or_ns):

def make_annotation_gatherer(
field_type=Field,
leave_default_values=True,
leave_default_values=False,
):
"""
Create a new annotation gatherer that will work with `Field` instances
Expand Down Expand Up @@ -809,7 +809,7 @@ def field_annotation_gatherer(cls_or_ns):

def make_field_gatherer(
field_type=Field,
leave_default_values=True,
leave_default_values=False,
):
def field_attribute_gatherer(cls_or_ns):
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
Expand Down Expand Up @@ -842,7 +842,7 @@ def field_attribute_gatherer(cls_or_ns):

def make_unified_gatherer(
field_type=Field,
leave_default_values=True,
leave_default_values=False,
):
"""
Create a gatherer that will work via first slots, then
Expand Down Expand Up @@ -892,7 +892,7 @@ def field_unified_gatherer(cls_or_ns):

# The unified gatherer used for slot classes must remove default
# values for slots to work correctly.
unified_gatherer = make_unified_gatherer(leave_default_values=False)
unified_gatherer = make_unified_gatherer()


# Now the gatherers have been defined, add __repr__ and __eq__ to Field.
Expand Down Expand Up @@ -958,7 +958,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
def __init_subclass__(
cls,
methods=default_methods,
gatherer=make_unified_gatherer(leave_default_values=True),
gatherer=unified_gatherer,
**kwargs
):
# Check class dict otherwise this will always be True as this base
Expand Down
14 changes: 7 additions & 7 deletions src/ducktools/classbuilder/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -178,37 +178,37 @@ def make_slot_gatherer(
@typing.overload
def make_annotation_gatherer(
field_type: type[_FieldType],
leave_default_values: bool = True,
leave_default_values: bool = False,
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...

@typing.overload
def make_annotation_gatherer(
field_type: _ReturnsField = Field,
leave_default_values: bool = True,
leave_default_values: bool = False,
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...

@typing.overload
def make_field_gatherer(
field_type: type[_FieldType],
leave_default_values: bool = True,
leave_default_values: bool = False,
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...

@typing.overload
def make_field_gatherer(
field_type: _ReturnsField = Field,
leave_default_values: bool = True,
leave_default_values: bool = False,
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...

@typing.overload
def make_unified_gatherer(
field_type: type[_FieldType],
leave_default_values: bool = True,
leave_default_values: bool = False,
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...

@typing.overload
def make_unified_gatherer(
field_type: _ReturnsField = Field,
leave_default_values: bool = True,
leave_default_values: bool = False,
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...


Expand Down Expand Up @@ -249,7 +249,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
def __init_subclass__(
cls,
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
gatherer: _gatherer_type = make_unified_gatherer(leave_default_values=True),
gatherer: _gatherer_type = unified_gatherer,
**kwargs,
) -> None: ...

Expand Down
Loading

0 comments on commit cd39f4c

Please sign in to comment.