{"id":18296973,"url":"https://github.com/davidcellis/ducktools-classbuilder","last_synced_at":"2025-04-12T06:29:48.540Z","repository":{"id":232197500,"uuid":"783729207","full_name":"DavidCEllis/ducktools-classbuilder","owner":"DavidCEllis","description":"Toolkit for creating Python class boilerplate generators","archived":false,"fork":false,"pushed_at":"2025-04-11T14:30:25.000Z","size":390,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-11T15:46:45.801Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://ducktools-classbuilder.readthedocs.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DavidCEllis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-08T13:08:24.000Z","updated_at":"2025-01-10T13:26:25.000Z","dependencies_parsed_at":"2024-09-08T09:54:08.053Z","dependency_job_id":"7f2b5d26-4f20-4a02-aea2-7dea947a288c","html_url":"https://github.com/DavidCEllis/ducktools-classbuilder","commit_stats":null,"previous_names":["davidcellis/ducktools-classbuilder"],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-classbuilder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-classbuilder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-classbuilder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidCEllis%2Fducktools-classbuilder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DavidCEllis","download_url":"https://codeload.github.com/DavidCEllis/ducktools-classbuilder/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248437537,"owners_count":21103404,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T14:45:06.796Z","updated_at":"2025-04-12T06:29:48.513Z","avatar_url":"https://github.com/DavidCEllis.png","language":"Python","readme":"# Ducktools: Class Builder #\n\n`ducktools-classbuilder` is *the* Python package that will bring you the **joy**\nof writing... functions... that will bring back the **joy** of writing classes.\n\nMaybe.\n\nWhile `attrs` and `dataclasses` are class boilerplate generators, \n`ducktools.classbuilder` is intended to provide the tools to help make a customized\nversion of the same concept.\n\nInstall from PyPI with:\n`python -m pip install ducktools-classbuilder`\n\n## Included Implementations ##\n\nThere are 2 different implementations provided with the module each of which offers\na subclass based and decorator based option.\n\n\u003e [!TIP]\n\u003e For more information on using these tools to create your own implementations \n\u003e using the builder see\n\u003e [the tutorial](https://ducktools-classbuilder.readthedocs.io/en/latest/tutorial.html)\n\u003e for a full tutorial and \n\u003e [extension_examples](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)\n\u003e for other customizations.\n\n### Core ###\n\nThese tools are available from the main `ducktools.classbuilder` module.\n\n* `@slotclass`\n  * A decorator based implementation that uses a special dict subclass assigned\n     to `__slots__` to describe the fields for method generation.\n* `AnnotationClass`\n  * A subclass based implementation that works with `__slots__`, type annotations\n    or `Field(...)` attributes to describe the fields for method generation.\n  * If `__slots__` isn't used to declare fields, it will be generated by a metaclass.\n\nEach of these forms of class generation will result in the same methods being \nattached to the class after the field information has been obtained.\n\n```python\nfrom ducktools.classbuilder import Field, SlotFields, slotclass\n\n@slotclass\nclass SlottedDC:\n    __slots__ = SlotFields(\n        the_answer=42,\n        the_question=Field(\n            default=\"What do you get if you multiply six by nine?\",\n            doc=\"Life, the Universe, and Everything\",\n        ),\n    )\n    \nex = SlottedDC()\nprint(ex)\n```\n\n### Prefab ###\n\nThis is available from the `ducktools.classbuilder.prefab` submodule.\n\nThis includes more customization including `__prefab_pre_init__` and `__prefab_post_init__`\nfunctions for subclass customization.\n\nA `@prefab` decorator and `Prefab` base class are provided. \nSimilar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.\nHowever decorated classes with `@prefab` that do not declare fields using `__slots__`\nwill **not** be slotted and there is no `slots` argument to apply this.\n\nHere is an example of applying a conversion in `__post_init__`:\n```python\nfrom pathlib import Path\nfrom ducktools.classbuilder.prefab import Prefab\n\nclass AppDetails(Prefab, frozen=True):\n    app_name: str\n    app_path: Path\n\n    def __prefab_post_init__(self, app_path: str | Path):\n        # frozen in `Prefab` is implemented as a 'set-once' __setattr__ function.\n        # So we do not need to use `object.__setattr__` here\n        self.app_path = Path(app_path)\n\nsteam = AppDetails(\n    \"Steam\",\n    r\"C:\\Program Files (x86)\\Steam\\steam.exe\"\n)\n\nprint(steam)\n```\n\n\n## What is the issue with generating `__slots__` with a decorator ##\n\nIf you want to use `__slots__` in order to save memory you have to declare\nthem when the class is originally created as you can't add them later.\n\nWhen you use `@dataclass(slots=True)`[^2] with `dataclasses`, the function \nhas to make a new class and attempt to copy over everything from the original.\n\nThis is because decorators operate on classes *after they have been created* \nwhile slots need to be declared beforehand. \nWhile you can change the value of `__slots__` after a class has been created, \nthis will have no effect on the internal structure of the class.\n\nBy using a metaclass or by declaring fields using `__slots__` however,\nthe fields can be set *before* the class is constructed, so the class\nwill work correctly without needing to be rebuilt.\n\nFor example these two classes would be roughly equivalent, except that\n`@dataclass` has had to recreate the class from scratch while `AnnotationClass`\nhas created `__slots__` and added the methods on to the original class. \nThis means that any references stored to the original class *before*\n`@dataclass` has rebuilt the class will not be pointing towards the \ncorrect class.\n\nHere's a demonstration of the issue using a registry for serialization \nfunctions.\n\n\u003e This example requires Python 3.10 or later as earlier versions of \n\u003e `dataclasses` did not support the `slots` argument.\n\n```python\nimport json\nfrom dataclasses import dataclass\nfrom ducktools.classbuilder import AnnotationClass, Field\n\n\nclass _RegisterDescriptor:\n    def __init__(self, func, registry):\n        self.func = func\n        self.registry = registry\n\n    def __set_name__(self, owner, name):\n        self.registry.register(owner, self.func)\n        setattr(owner, name, self.func)\n\n\nclass SerializeRegister:\n    def __init__(self):\n        self.serializers = {}\n\n    def register(self, cls, func):\n        self.serializers[cls] = func\n\n    def register_method(self, method):\n        return _RegisterDescriptor(method, self)\n\n    def default(self, o):\n        try:\n            return self.serializers[type(o)](o)\n        except KeyError:\n            raise TypeError(f\"Object of type {type(o).__name__} is not JSON serializable\")\n\n\nregister = SerializeRegister()\n\n\n@dataclass(slots=True)\nclass DataCoords:\n    x: float = 0.0\n    y: float = 0.0\n\n    @register.register_method\n    def to_json(self):\n        return {\"x\": self.x, \"y\": self.y}\n\n\n# slots=True is the default for AnnotationClass\nclass BuilderCoords(AnnotationClass, slots=True):\n    x: float = 0.0\n    y: float = Field(default=0.0, doc=\"y coordinate\")\n\n    @register.register_method\n    def to_json(self):\n        return {\"x\": self.x, \"y\": self.y}\n\n\n# In both cases __slots__ have been defined\nprint(f\"{DataCoords.__slots__ = }\")\nprint(f\"{BuilderCoords.__slots__ = }\\n\")\n\ndata_ex = DataCoords()\nbuilder_ex = BuilderCoords()\n\nobjs = [data_ex, builder_ex]\n\nprint(data_ex)\nprint(builder_ex)\nprint()\n\n# Demonstrate you can not set values not defined in slots\nfor obj in objs:\n    try:\n        obj.z = 1.0\n    except AttributeError as e:\n        print(e)\nprint()\n\nprint(\"Attempt to serialize:\")\nfor obj in objs:\n    try:\n        print(f\"{type(obj).__name__}: {json.dumps(obj, default=register.default)}\")\n    except TypeError as e:\n        print(f\"{type(obj).__name__}: {e!r}\")\n```\n\nOutput (Python 3.12):\n```\nDataCoords.__slots__ = ('x', 'y')\nBuilderCoords.__slots__ = {'x': None, 'y': 'y coordinate'}\n\nDataCoords(x=0.0, y=0.0)\nBuilderCoords(x=0.0, y=0.0)\n\n'DataCoords' object has no attribute 'z'\n'BuilderCoords' object has no attribute 'z'\n\nAttempt to serialize:\nDataCoords: TypeError('Object of type DataCoords is not JSON serializable')\nBuilderCoords: {\"x\": 0.0, \"y\": 0.0}\n```\n\n## What features does this have? ##\n\nIncluded as an example implementation, the `slotclass` generator supports \n`default_factory` for creating mutable defaults like lists, dicts etc.\nIt also supports default values that are not builtins (try this on \n[Cluegen](https://github.com/dabeaz/cluegen)).\n\nIt will copy values provided as the `type` to `Field` into the \n`__annotations__` dictionary of the class. \nValues provided to `doc` will be placed in the final `__slots__` \nfield so they are present on the class if `help(...)` is called.\n\n`AnnotationClass` offers the same features with additional methods of gathering\nfields.\n\nIf you want something with more features you can look at the `prefab`\nsubmodule which provides more specific features that differ further from the \nbehaviour of `dataclasses`.\n\n## Will you add \\\u003cfeature\\\u003e to `classbuilder.prefab`? ##\n\nNo. Not unless it's something I need or find interesting.\n\nThe original version of `prefab_classes` was intended to have every feature\nanybody could possibly require, but this is no longer the case with this\nrebuilt version.\n\nI will fix bugs (assuming they're not actually intended behaviour).\n\nHowever the whole goal of this module is if you want to have a class generator\nwith a specific feature, you can create or add it yourself.\n\n## Credit ##\n\nHeavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)\n\n[^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`\n      works with dictionaries using the values of the keys, while fields are normally\n      used for documentation.\n\n[^2]: or `@attrs.define`.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidcellis%2Fducktools-classbuilder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidcellis%2Fducktools-classbuilder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidcellis%2Fducktools-classbuilder/lists"}