diff --git a/docs/api.md b/docs/api.md index 541acc5..c7200cf 100644 --- a/docs/api.md +++ b/docs/api.md @@ -25,7 +25,7 @@ ``` ```{eval-rst} -.. autofunction:: ducktools.classbuilder::get_internals +.. autofunction:: ducktools.classbuilder::get_flags ``` ```{eval-rst} diff --git a/docs/extension_examples.md b/docs/extension_examples.md index d02a635..1f90283 100644 --- a/docs/extension_examples.md +++ b/docs/extension_examples.md @@ -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 #### @@ -686,7 +701,7 @@ from ducktools.classbuilder import ( builder, fieldclass, get_fields, - get_internals, + get_flags, Field, MethodMaker, SlotFields, @@ -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 = [] @@ -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): @@ -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 diff --git a/docs_code/docs_ex9_annotated.py b/docs_code/docs_ex9_annotated.py index e259bf8..c176176 100644 --- a/docs_code/docs_ex9_annotated.py +++ b/docs_code/docs_ex9_annotated.py @@ -6,7 +6,7 @@ builder, fieldclass, get_fields, - get_internals, + get_flags, Field, MethodMaker, SlotFields, @@ -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 = [] @@ -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): @@ -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 diff --git a/src/ducktools/classbuilder/__init__.py b/src/ducktools/classbuilder/__init__.py index bb3cc80..18152a6 100644 --- a/src/ducktools/classbuilder/__init__.py +++ b/src/ducktools/classbuilder/__init__.py @@ -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__" @@ -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): ... @@ -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): @@ -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(): @@ -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 @@ -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. @@ -197,6 +216,7 @@ def builder(cls=None, /, *, gatherer, methods): cls_, gatherer=gatherer, methods=methods, + flags=flags, ) internals = {} @@ -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: @@ -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}, ) @@ -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) @@ -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 @@ -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 diff --git a/src/ducktools/classbuilder/__init__.pyi b/src/ducktools/classbuilder/__init__.pyi index 9c6af8a..13249cd 100644 --- a/src/ducktools/classbuilder/__init__.pyi +++ b/src/ducktools/classbuilder/__init__.pyi @@ -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]: ... @@ -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]]: ... @@ -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 @@ -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]]: ... diff --git a/src/ducktools/classbuilder/prefab.py b/src/ducktools/classbuilder/prefab.py index b9b34f0..a09f1a3 100644 --- a/src/ducktools/classbuilder/prefab.py +++ b/src/ducktools/classbuilder/prefab.py @@ -31,7 +31,7 @@ from . import ( INTERNALS_DICT, NOTHING, Field, MethodMaker, SlotFields, - builder, fieldclass, get_internals, slot_gatherer + builder, fieldclass, get_flags, get_fields, slot_gatherer ) PREFAB_FIELDS = "PREFAB_FIELDS" @@ -84,10 +84,11 @@ def get_attributes(cls): def get_init_maker(*, init_name="__init__"): def __init__(cls: "type") -> "tuple[str, dict]": globs = {} - internals = get_internals(cls) # Get the internals dictionary and prepare attributes - attributes = internals["fields"] - kw_only = internals["kw_only"] + attributes = get_attributes(cls) + flags = get_flags(cls) + + kw_only = flags.get("kw_only", False) # Handle pre/post init first - post_init can change types for __init__ # Get pre and post init arguments @@ -342,14 +343,16 @@ def __iter__(cls: "type") -> "tuple[str, dict]": def get_frozen_setattr_maker(): def __setattr__(cls: "type") -> "tuple[str, dict]": globs = {} - internals = get_internals(cls) - field_names = internals["fields"].keys() + attributes = get_attributes(cls) + flags = get_flags(cls) # Make the fields set literal - fields_delimited = ", ".join(f"{field!r}" for field in field_names) + fields_delimited = ", ".join(f"{field!r}" for field in attributes) field_set = f"{{ {fields_delimited} }}" - if internals["slotted"]: + # Better to be safe and use the method that works in both cases + # if somehow slotted has not been set. + if flags.get("slotted", True): globs["__prefab_setattr_func"] = object.__setattr__ setattr_method = "__prefab_setattr_func(self, name, value)" else: @@ -488,6 +491,14 @@ def attribute( ) +def slot_prefab_gatherer(cls): + # For prefabs it's easier if everything is an attribute + return { + name: Attribute.from_field(fld) + for name, fld in slot_gatherer(cls).items() + } + + # Gatherer for classes built on attributes or annotations def attribute_gatherer(cls): cls_annotations = cls.__dict__.get("__annotations__", {}) @@ -599,7 +610,7 @@ def _make_prefab( slots = cls_dict.get("__slots__") if isinstance(slots, SlotFields): - gatherer = slot_gatherer + gatherer = slot_prefab_gatherer slotted = True else: gatherer = attribute_gatherer @@ -627,18 +638,20 @@ def _make_prefab( if dict_method: methods.add(asdict_desc) + flags = { + "kw_only": kw_only, + "slotted": slotted, + } + cls = builder( cls, gatherer=gatherer, methods=methods, + flags=flags, ) - # Add fields not covered by builder - internals = get_internals(cls) - internals["slotted"] = slotted - internals["kw_only"] = kw_only - fields = internals["fields"] - local_fields = internals["local_fields"] + # Get fields now the class has been built + fields = get_fields(cls) # Check pre_init and post_init functions if they exist try: @@ -710,8 +723,6 @@ def _make_prefab( if not isinstance(attrib, Attribute): attrib = Attribute.from_field(attrib) fields[name] = attrib - if name in local_fields: - local_fields[name] = attrib # Excluded fields *MUST* be forwarded to post_init if attrib.exclude_field: diff --git a/src/ducktools/classbuilder/prefab.pyi b/src/ducktools/classbuilder/prefab.pyi index de46030..c562237 100644 --- a/src/ducktools/classbuilder/prefab.pyi +++ b/src/ducktools/classbuilder/prefab.pyi @@ -101,6 +101,8 @@ def attribute( exclude_field: bool = False, ) -> Attribute: ... +def slot_prefab_gatherer(cls: type) -> dict[str, Attribute]: ... + def attribute_gatherer(cls: type) -> dict[str, Attribute]: ... def _make_prefab(