Skip to content

Commit

Permalink
Merge pull request #5 from DavidCEllis/rework_internals
Browse files Browse the repository at this point in the history
Rework the internals to avoid the need to access the dict directly.
Add a `flags` argument to the builder for setting per-class flags needed for method generation.
  • Loading branch information
DavidCEllis authored Apr 17, 2024
2 parents 13211a3 + e6a5aea commit 2ad7efe
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 62 deletions.
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
```

```{eval-rst}
.. autofunction:: ducktools.classbuilder::get_internals
.. autofunction:: ducktools.classbuilder::get_flags
```

```{eval-rst}
Expand Down
49 changes: 31 additions & 18 deletions docs/extension_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,31 @@ create `__init__` and other magic methods are added to the class.
This function is the core class generator which takes your decorated class and
analyses and collects valid fields and then attaches the method makers.

The field information is stored in the `INTERNALS_DICT` attribute and can be
accessed using the `get_internals` function provided. This returns a dictionary
with 2 keys: `local_fields` and `fields`.
The field information is stored in the `INTERNALS_DICT` attribute which should generally
not need to be accessed directly. `get_fields` and `get_flags` functions are to be
used to access the important keys.

`"local_fields"` contains the field information obtained from **this class only**.
`get_fields(cls)` will return the resolved information obtained from this class and subclasses.

`"fields"` contains the resolved information obtained from this class and subclasses.
This can be obtained directly using the `get_fields` function.
`get_fields(cls, local=True)` will return the field information obtained from **this class only**.

Now let's look at what the two keyword arguments need to be.
Now let's look at what the keyword arguments to `builder` need to be.

#### Flags ####

Flags are information that defines how the entire class should be generated, for use by
method generators when operating on the class.

The default makers in `ducktools.classbuilder` make use of one flag - `"kw_only"` -
which indicates that a class `__init__` function should only take keyword arguments.

Prefabs also make use of a `"slotted"` flag to indicate if the class has been generated
with `__slots__` (checking for the existence of `__slots__` could find that a user has
manually placed slots in the class).

Flags are set using a dictionary with these keys and boolean values, for example:

`cls = builder(cls, gatherer=..., methods=..., flags={"kw_only": True, "slotted": True})`

#### Gatherers ####

Expand Down Expand Up @@ -686,7 +701,7 @@ from ducktools.classbuilder import (
builder,
fieldclass,
get_fields,
get_internals,
get_flags,
Field,
MethodMaker,
SlotFields,
Expand Down Expand Up @@ -770,8 +785,8 @@ def annotated_gatherer(cls: type) -> dict[str, Any]:

def init_maker(cls):

internals = get_internals(cls)
fields = get_fields(cls)
flags = get_flags(cls)

arglist = []
kw_only_arglist = []
Expand All @@ -780,9 +795,7 @@ def init_maker(cls):
globs = {}

# Whole class kw_only
kw_only = internals.get("kw_only", False)
if kw_only:
arglist.append("*")
kw_only = flags.get("kw_only", False)

for k, v in fields.items():
if getattr(v, "init", True):
Expand Down Expand Up @@ -875,12 +888,12 @@ def annotationsclass(cls=None, *, kw_only=False):
if not cls:
return lambda cls_: annotationsclass(cls_, kw_only=kw_only)

cls = builder(cls, gatherer=annotated_gatherer, methods=methods)

internals = get_internals(cls)
internals["kw_only"] = kw_only

return cls
return builder(
cls,
gatherer=annotated_gatherer,
methods=methods,
flags={"slotted": False, "kw_only": kw_only}
)


@annotationsclass
Expand Down
20 changes: 9 additions & 11 deletions docs_code/docs_ex9_annotated.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
builder,
fieldclass,
get_fields,
get_internals,
get_flags,
Field,
MethodMaker,
SlotFields,
Expand Down Expand Up @@ -90,8 +90,8 @@ def annotated_gatherer(cls: type) -> dict[str, Any]:

def init_maker(cls):

internals = get_internals(cls)
fields = get_fields(cls)
flags = get_flags(cls)

arglist = []
kw_only_arglist = []
Expand All @@ -100,9 +100,7 @@ def init_maker(cls):
globs = {}

# Whole class kw_only
kw_only = internals.get("kw_only", False)
if kw_only:
arglist.append("*")
kw_only = flags.get("kw_only", False)

for k, v in fields.items():
if getattr(v, "init", True):
Expand Down Expand Up @@ -195,12 +193,12 @@ def annotationsclass(cls=None, *, kw_only=False):
if not cls:
return lambda cls_: annotationsclass(cls_, kw_only=kw_only)

cls = builder(cls, gatherer=annotated_gatherer, methods=methods)

internals = get_internals(cls)
internals["kw_only"] = kw_only

return cls
return builder(
cls,
gatherer=annotated_gatherer,
methods=methods,
flags={"slotted": False, "kw_only": kw_only}
)


@annotationsclass
Expand Down
45 changes: 34 additions & 11 deletions src/ducktools/classbuilder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
__version__ = "v0.1.1"
__version__ = "v0.2.0"

# Change this name if you make heavy modifications
INTERNALS_DICT = "__classbuilder_internals__"
Expand All @@ -34,6 +34,9 @@ def get_internals(cls):
and 'local_fields' attributes this will always
evaluate as 'truthy' if this is a generated class.
Generally you should use the helper get_flags and
get_fields methods.
Usage:
if internals := get_internals(cls):
...
Expand All @@ -44,15 +47,28 @@ def get_internals(cls):
return getattr(cls, INTERNALS_DICT, None)


def get_fields(cls):
def get_fields(cls, *, local=False):
"""
Utility function to gather the fields dictionary
from the class internals.
:param cls: generated class
:param local: get only fields that were not inherited
:return: dictionary of keys and Field attribute info
"""
return getattr(cls, INTERNALS_DICT)["fields"]
key = "local_fields" if local else "fields"
return getattr(cls, INTERNALS_DICT)[key]


def get_flags(cls):
"""
Utility function to gather the flags dictionary
from the class internals.
:param cls: generated class
:return: dictionary of keys and flag values
"""
return getattr(cls, INTERNALS_DICT)["flags"]


def get_inst_fields(inst):
Expand Down Expand Up @@ -106,14 +122,15 @@ def __get__(self, instance, cls):
return method.__get__(instance, cls)


def init_maker(cls, *, null=NOTHING, kw_only=False):
def init_maker(cls, *, null=NOTHING):
fields = get_fields(cls)
flags = get_flags(cls)

arglist = []
assignments = []
globs = {}

if kw_only:
if flags.get("kw_only", False):
arglist.append("*")

for k, v in fields.items():
Expand Down Expand Up @@ -180,7 +197,7 @@ def eq_maker(cls):
default_methods = frozenset({init_desc, repr_desc, eq_desc})


def builder(cls=None, /, *, gatherer, methods):
def builder(cls=None, /, *, gatherer, methods, flags=None):
"""
The main builder for class generation
Expand All @@ -189,6 +206,8 @@ def builder(cls=None, /, *, gatherer, methods):
:type gatherer: Callable[[type], dict[str, Field]]
:param methods: MethodMakers to add to the class
:type methods: set[MethodMaker]
:param flags: additional flags to store in the internals dictionary
for use by method generators.
:return: The modified class (the class itself is modified, but this is expected).
"""
# Handle `None` to make wrapping with a decorator easier.
Expand All @@ -197,6 +216,7 @@ def builder(cls=None, /, *, gatherer, methods):
cls_,
gatherer=gatherer,
methods=methods,
flags=flags,
)

internals = {}
Expand All @@ -212,11 +232,12 @@ def builder(cls=None, /, *, gatherer, methods):
fields = {}
for c in reversed(mro):
try:
fields.update(get_internals(c)["local_fields"])
fields.update(get_fields(c, local=True))
except AttributeError:
pass

internals["fields"] = fields
internals["flags"] = flags if flags is not None else {}

# Assign all of the method generators
for method in methods:
Expand Down Expand Up @@ -296,7 +317,8 @@ def from_field(cls, fld, /, **kwargs):
builder(
Field,
gatherer=lambda cls_: _field_internal,
methods=frozenset({repr_desc, eq_desc})
methods=frozenset({repr_desc, eq_desc}),
flags={"slotted": True, "kw_only": True},
)


Expand Down Expand Up @@ -368,7 +390,7 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
if not cls:
return lambda cls_: slotclass(cls_, methods=methods, syntax_check=syntax_check)

cls = builder(cls, gatherer=slot_gatherer, methods=methods)
cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})

if syntax_check:
fields = get_fields(cls)
Expand Down Expand Up @@ -398,7 +420,7 @@ def fieldclass(cls):
# Fields need a way to call their validate method
# So append it to the code from __init__.
def field_init_func(cls_):
code, globs = init_maker(cls_, null=field_nothing, kw_only=True)
code, globs = init_maker(cls_, null=field_nothing)
code += " self.validate_field()\n"
return code, globs

Expand All @@ -412,7 +434,8 @@ def field_init_func(cls_):
cls = builder(
cls,
gatherer=slot_gatherer,
methods=field_methods
methods=field_methods,
flags={"slotted": True, "kw_only": True}
)

return cls
11 changes: 7 additions & 4 deletions src/ducktools/classbuilder/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ INTERNALS_DICT: str

def get_internals(cls) -> dict[str, typing.Any] | None: ...

def get_fields(cls: type) -> dict[str, Field]: ...
def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...

def get_flags(cls:type) -> dict[str, bool]: ...

def get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...

Expand All @@ -30,7 +32,6 @@ def init_maker(
cls: type,
*,
null: _NothingType = NOTHING,
kw_only: bool = False
) -> tuple[str, dict[str, typing.Any]]: ...
def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
Expand All @@ -48,7 +49,8 @@ def builder(
/,
*,
gatherer: Callable[[type], dict[str, Field]],
methods: frozenset[MethodMaker] | set[MethodMaker]
methods: frozenset[MethodMaker] | set[MethodMaker],
flags: dict[str, bool] | None = None,
) -> type[_T]: ...

@typing.overload
Expand All @@ -57,7 +59,8 @@ def builder(
/,
*,
gatherer: Callable[[type], dict[str, Field]],
methods: frozenset[MethodMaker] | set[MethodMaker]
methods: frozenset[MethodMaker] | set[MethodMaker],
flags: dict[str, bool] | None = None,
) -> Callable[[type[_T]], type[_T]]: ...


Expand Down
Loading

0 comments on commit 2ad7efe

Please sign in to comment.