mirror of
https://github.com/django-components/django-components.git
synced 2025-11-18 06:06:14 +00:00
refactor: make it optional having to specify parent class of Args, Kwargs, Slots, etc
This commit is contained in:
parent
c66bd21231
commit
0255782ee2
31 changed files with 620 additions and 294 deletions
|
|
@ -207,7 +207,7 @@ class CreateCommand(ComponentCommand):
|
|||
js_file = "{js_filename}"
|
||||
css_file = "{css_filename}"
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
param: str
|
||||
|
||||
class Defaults:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# ruff: noqa: ARG002, N804, N805
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from inspect import signature
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
|
|
@ -78,7 +78,14 @@ from django_components.template import cache_component_template_file, prepare_co
|
|||
from django_components.util.context import gen_context_processors_data, snapshot_context
|
||||
from django_components.util.exception import component_error_message
|
||||
from django_components.util.logger import trace_component_msg
|
||||
from django_components.util.misc import default, gen_id, hash_comp_cls, is_generator, to_dict
|
||||
from django_components.util.misc import (
|
||||
convert_class_to_namedtuple,
|
||||
default,
|
||||
gen_id,
|
||||
hash_comp_cls,
|
||||
is_generator,
|
||||
to_dict,
|
||||
)
|
||||
from django_components.util.template_tag import TagAttr
|
||||
from django_components.util.weakref import cached_ref
|
||||
|
||||
|
|
@ -311,7 +318,7 @@ class ComponentVars(NamedTuple):
|
|||
|
||||
@register("table")
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -363,7 +370,7 @@ class ComponentVars(NamedTuple):
|
|||
|
||||
@register("table")
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -415,7 +422,7 @@ class ComponentVars(NamedTuple):
|
|||
|
||||
@register("table")
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
template = '''
|
||||
|
|
@ -506,6 +513,34 @@ class ComponentMeta(ComponentMediaMeta):
|
|||
attrs["template_file"] = attrs.pop("template_name")
|
||||
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
||||
|
||||
# Allow to define data classes (`Args`, `Kwargs`, `Slots`, `TemplateData`, `JsData`, `CssData`)
|
||||
# without explicitly subclassing anything. In which case we make them into a subclass of `NamedTuple`.
|
||||
# In other words:
|
||||
# ```py
|
||||
# class MyTable(Component):
|
||||
# class Kwargs(NamedTuple):
|
||||
# ...
|
||||
# ```
|
||||
# Can be simplified to:
|
||||
# ```py
|
||||
# class MyTable(Component):
|
||||
# class Kwargs:
|
||||
# ...
|
||||
# ```
|
||||
for data_class_name in ["Args", "Kwargs", "Slots", "TemplateData", "JsData", "CssData"]:
|
||||
data_class = attrs.get(data_class_name)
|
||||
# Not a class
|
||||
if data_class is None or not isinstance(data_class, type):
|
||||
continue
|
||||
# Is dataclass
|
||||
if is_dataclass(data_class):
|
||||
continue
|
||||
# Has base class(es)
|
||||
has_parents = data_class.__bases__ != (object,)
|
||||
if has_parents:
|
||||
continue
|
||||
attrs[data_class_name] = convert_class_to_namedtuple(data_class)
|
||||
|
||||
cls = cast("Type[Component]", super().__new__(mcs, name, bases, attrs))
|
||||
|
||||
# If the component defined `template_file`, then associate this Component class
|
||||
|
|
@ -598,11 +633,10 @@ class Component(metaclass=ComponentMeta):
|
|||
will be the instance of this class:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -615,15 +649,6 @@ class Component(metaclass=ComponentMeta):
|
|||
}
|
||||
```
|
||||
|
||||
The constructor of this class MUST accept positional arguments:
|
||||
|
||||
```py
|
||||
Args(*args)
|
||||
```
|
||||
|
||||
As such, a good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
|
||||
|
||||
Use `Args` to:
|
||||
|
||||
- Validate the input at runtime.
|
||||
|
|
@ -640,6 +665,20 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
|
||||
If you do not specify any bases, the `Args` class will be automatically
|
||||
converted to a `NamedTuple`:
|
||||
|
||||
`class Args:` -> `class Args(NamedTuple):`
|
||||
|
||||
If you explicitly set bases, the constructor of this class MUST accept positional arguments:
|
||||
|
||||
```py
|
||||
Args(*args)
|
||||
```
|
||||
|
||||
As such, a good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
|
||||
|
||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
"""
|
||||
|
||||
|
|
@ -654,11 +693,10 @@ class Component(metaclass=ComponentMeta):
|
|||
will be the instance of this class:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -671,16 +709,6 @@ class Component(metaclass=ComponentMeta):
|
|||
}
|
||||
```
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
Kwargs(**kwargs)
|
||||
```
|
||||
|
||||
As such, a good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Use `Kwargs` to:
|
||||
|
||||
- Validate the input at runtime.
|
||||
|
|
@ -697,6 +725,21 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
|
||||
If you do not specify any bases, the `Kwargs` class will be automatically
|
||||
converted to a `NamedTuple`:
|
||||
|
||||
`class Kwargs:` -> `class Kwargs(NamedTuple):`
|
||||
|
||||
If you explicitly set bases, the constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
Kwargs(**kwargs)
|
||||
```
|
||||
|
||||
As such, a good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
"""
|
||||
|
||||
|
|
@ -711,11 +754,10 @@ class Component(metaclass=ComponentMeta):
|
|||
will be the instance of this class:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: SlotInput
|
||||
footer: Slot
|
||||
|
||||
|
|
@ -728,16 +770,6 @@ class Component(metaclass=ComponentMeta):
|
|||
}
|
||||
```
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
Slots(**slots)
|
||||
```
|
||||
|
||||
As such, a good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Use `Slots` to:
|
||||
|
||||
- Validate the input at runtime.
|
||||
|
|
@ -757,6 +789,21 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
|
||||
If you do not specify any bases, the `Slots` class will be automatically
|
||||
converted to a `NamedTuple`:
|
||||
|
||||
`class Slots:` -> `class Slots(NamedTuple):`
|
||||
|
||||
If you explicitly set bases, the constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
Slots(**slots)
|
||||
```
|
||||
|
||||
As such, a good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
|
||||
!!! info
|
||||
|
|
@ -1081,18 +1128,17 @@ class Component(metaclass=ComponentMeta):
|
|||
**Example:**
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django.template import Context
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class MyComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
size: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
|
|
@ -1123,7 +1169,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -1156,22 +1202,22 @@ class Component(metaclass=ComponentMeta):
|
|||
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
||||
[`get_template_data()`](../api#django_components.Component.get_template_data) to validate the data.
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
Use `TemplateData` to:
|
||||
|
||||
```py
|
||||
TemplateData(**template_data)
|
||||
```
|
||||
- Validate the data returned from
|
||||
[`get_template_data()`](../api#django_components.Component.get_template_data) at runtime.
|
||||
- Set type hints for this data.
|
||||
- Document the component data.
|
||||
|
||||
You can also return an instance of `TemplateData` directly from
|
||||
[`get_template_data()`](../api#django_components.Component.get_template_data)
|
||||
to get type hints:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -1182,17 +1228,16 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
TemplateData(**template_data)
|
||||
```
|
||||
|
||||
A good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Use `TemplateData` to:
|
||||
|
||||
- Validate the data returned from
|
||||
[`get_template_data()`](../api#django_components.Component.get_template_data) at runtime.
|
||||
- Set type hints for this data.
|
||||
- Document the component data.
|
||||
|
||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
|
||||
!!! info
|
||||
|
|
@ -1376,13 +1421,13 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component, SlotInput
|
||||
|
||||
class MyComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
size: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
|
|
@ -1413,7 +1458,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class JsData(NamedTuple):
|
||||
class JsData:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -1439,22 +1484,22 @@ class Component(metaclass=ComponentMeta):
|
|||
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
||||
[`get_js_data()`](../api#django_components.Component.get_js_data) to validate the data.
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
Use `JsData` to:
|
||||
|
||||
```py
|
||||
JsData(**js_data)
|
||||
```
|
||||
- Validate the data returned from
|
||||
[`get_js_data()`](../api#django_components.Component.get_js_data) at runtime.
|
||||
- Set type hints for this data.
|
||||
- Document the component data.
|
||||
|
||||
You can also return an instance of `JsData` directly from
|
||||
[`get_js_data()`](../api#django_components.Component.get_js_data)
|
||||
to get type hints:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class JsData(NamedTuple):
|
||||
class JsData(
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -1465,17 +1510,16 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
JsData(**js_data)
|
||||
```
|
||||
|
||||
A good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Use `JsData` to:
|
||||
|
||||
- Validate the data returned from
|
||||
[`get_js_data()`](../api#django_components.Component.get_js_data) at runtime.
|
||||
- Set type hints for this data.
|
||||
- Document the component data.
|
||||
|
||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
|
||||
!!! info
|
||||
|
|
@ -1664,18 +1708,17 @@ class Component(metaclass=ComponentMeta):
|
|||
**Example:**
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django.template import Context
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class MyComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
size: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
|
|
@ -1705,7 +1748,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -1731,22 +1774,22 @@ class Component(metaclass=ComponentMeta):
|
|||
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
||||
[`get_css_data()`](../api#django_components.Component.get_css_data) to validate the data.
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
Use `CssData` to:
|
||||
|
||||
```py
|
||||
CssData(**css_data)
|
||||
```
|
||||
- Validate the data returned from
|
||||
[`get_css_data()`](../api#django_components.Component.get_css_data) at runtime.
|
||||
- Set type hints for this data.
|
||||
- Document the component data.
|
||||
|
||||
You can also return an instance of `CssData` directly from
|
||||
[`get_css_data()`](../api#django_components.Component.get_css_data)
|
||||
to get type hints:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
|
@ -1757,17 +1800,16 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
|
||||
The constructor of this class MUST accept keyword arguments:
|
||||
|
||||
```py
|
||||
CssData(**css_data)
|
||||
```
|
||||
|
||||
A good starting point is to set this field to a subclass of
|
||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||
|
||||
Use `CssData` to:
|
||||
|
||||
- Validate the data returned from
|
||||
[`get_css_data()`](../api#django_components.Component.get_css_data) at runtime.
|
||||
- Set type hints for this data.
|
||||
- Document the component data.
|
||||
|
||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
|
||||
!!! info
|
||||
|
|
@ -2567,7 +2609,7 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -2635,7 +2677,7 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -2706,7 +2748,7 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: SlotInput
|
||||
footer: SlotInput
|
||||
|
||||
|
|
@ -3314,19 +3356,19 @@ class Component(metaclass=ComponentMeta):
|
|||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
footer: SlotInput
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import NamedTuple, Optional, cast
|
||||
from typing import Optional, cast
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
|
|
@ -115,10 +115,10 @@ class ErrorFallback(Component):
|
|||
Remember to define the `content` slot as function, so it's evaluated from inside of `ErrorFallback`.
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
fallback: Optional[str] = None
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
default: Optional[SlotInput] = None
|
||||
content: Optional[SlotInput] = None
|
||||
fallback: Optional[SlotInput] = None
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ class DependenciesExtension(ComponentExtension):
|
|||
|
||||
# Cache the component's JS and CSS scripts when the class is created, so that
|
||||
# components' JS/CSS files are accessible even before having to render the component first.
|
||||
#
|
||||
# This is important for the scenario when the web server may restart in a middle of user
|
||||
# session. In which case, if we did not cache the JS/CSS, then user may fail to retrieve
|
||||
# JS/CSS of some component.
|
||||
#
|
||||
# Component JS/CSS is then also cached after each time a component is rendered.
|
||||
# That way, if the component JS/CSS cache is smaller than the total number of
|
||||
# components/assets, we add back the most-recent entries.
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
cache_component_js(ctx.component_cls, force=True)
|
||||
cache_component_css(ctx.component_cls, force=True)
|
||||
|
|
|
|||
|
|
@ -394,7 +394,6 @@ slot content function.
|
|||
**Example:**
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from typing_extensions import TypedDict
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
|
|
@ -402,7 +401,7 @@ class TableFooterSlotData(TypedDict):
|
|||
page_number: int
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: SlotInput
|
||||
footer: SlotInput[TableFooterSlotData]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from hashlib import md5
|
||||
from importlib import import_module
|
||||
from inspect import getmembers
|
||||
from itertools import chain
|
||||
from types import ModuleType
|
||||
from typing import (
|
||||
|
|
@ -267,3 +269,50 @@ def format_as_ascii_table(
|
|||
def is_generator(obj: Any) -> bool:
|
||||
"""Check if an object is a generator with send method."""
|
||||
return hasattr(obj, "send")
|
||||
|
||||
|
||||
def convert_class_to_namedtuple(cls: Type[Any]) -> Type[Tuple[Any, ...]]:
|
||||
# Construct fields for a NamedTuple. Unfortunately one can't further subclass the subclass of `NamedTuple`,
|
||||
# so we need to construct a new class with the same fields.
|
||||
# NamedTuple has:
|
||||
# - Required fields, which are defined without values (annotations only)
|
||||
# - Optional fields with defaults
|
||||
# ```py
|
||||
# class Z:
|
||||
# b: str # Required, annotated
|
||||
# a: int = None # Optional, annotated
|
||||
# c = 1 # NOT A FIELD! Class var!
|
||||
# ```
|
||||
# Annotations are stored in `X.__annotations__`, while the defaults are regular class attributes
|
||||
# NOTE: We ignore dunder methods
|
||||
# NOTE 2: All fields with default values must come after fields without defaults.
|
||||
field_names = list(cls.__annotations__.keys())
|
||||
|
||||
# Get default values from the original class and set them on the new NamedTuple class
|
||||
field_names_set = set(field_names)
|
||||
defaults = {}
|
||||
class_attrs = {}
|
||||
for name, value in getmembers(cls):
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
# Field default
|
||||
if name in field_names_set:
|
||||
defaults[name] = value
|
||||
else:
|
||||
# Class attribute
|
||||
class_attrs[name] = value
|
||||
|
||||
# Figure out how many tuple fields have defaults. We need to know this
|
||||
# because NamedTuple functional syntax uses the pattern where defaults
|
||||
# are applied from the end.
|
||||
# Final call then looks like this:
|
||||
# `namedtuple("MyClass", ["a", "b", "c", "d"], defaults=[3, 4])`
|
||||
# with defaults c=3 and d=4
|
||||
num_fields_with_defaults = len(defaults)
|
||||
if num_fields_with_defaults:
|
||||
defaults_list = [defaults[name] for name in field_names[-num_fields_with_defaults:]]
|
||||
else:
|
||||
defaults_list = []
|
||||
tuple_cls = namedtuple(cls.__name__, field_names, defaults=defaults_list) # noqa: PYI024
|
||||
tuple_cls.__annotations__ = cls.__annotations__
|
||||
return tuple_cls
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue