From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 02B2345C79; Mon, 4 Nov 2024 18:34:40 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 95D2F4028F; Mon, 4 Nov 2024 18:34:40 +0100 (CET) Received: from mail-lf1-f49.google.com (mail-lf1-f49.google.com [209.85.167.49]) by mails.dpdk.org (Postfix) with ESMTP id 3BBDC40281 for ; Mon, 4 Nov 2024 18:34:39 +0100 (CET) Received: by mail-lf1-f49.google.com with SMTP id 2adb3069b0e04-539f0802bf1so579752e87.3 for ; Mon, 04 Nov 2024 09:34:39 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1730741678; x=1731346478; darn=dpdk.org; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=oLK9UPyyI7PxMnn6kVXi9lLtXat0E7m9U4N33W+U9oo=; b=Oacz1C2jczmMlCK8DtiEgyBkztcBhKMBy8r0PMilbI2B+FWLhUCCG35JuZXcWeaclK kTbGx+u2GE8Ds9fmjVa84t6SErd4X3vq2qVzHXMh0J4TqGcqO2FZVdNtS+JBBIXMMDcO Amkmy/HWOEftAmSLf8eVqdhv6kVehg8hy7rA0= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1730741678; x=1731346478; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=oLK9UPyyI7PxMnn6kVXi9lLtXat0E7m9U4N33W+U9oo=; b=FqNAQkDz6s6Ok7h5LuyV61plO02kTsv0sSVOT3SQhVMTgmh6k5H6d7MtU7jJWgy+u+ UqeINoQ3XsBUDHVPDmQKfVGxdWXMhZiEyV7PBdqsT0mf8UBJOuRrH3N3xBvDBs7aWx+L qA7bt8jc168ummpYKFgK8fm2SDkdTfRJwKpsIKlZ4RFIwyntqWjkGdKTBofzjv5vS93Q DsQooYnZeFjB9PbL3aWpYMLbaUBFnvC+MGz2YilWKH6tDQ8ze6owUsCKP3ftapjsL3RJ w6zsuxESAPgogW0R1gqbsBFU1qmoCVIu14QFLDmlc+SJ6RjCk+h0tSKwGimg/UXhnLGK lB9w== X-Gm-Message-State: AOJu0YxrtLRKbdigjj56jb9cAr9G4Q5bbg5l5f+f6keipoMI4acDzbzD N239oxHwgNbLozGhlfBmc6Ml1WUxYRvPirAMAugCRKAlCrRJEazd3liQFXqr2LshLYRH0S80SKl KR9bmcnbE07ZFW/ed0iE01C2bg8+Ps5gqaM5Pwg== X-Google-Smtp-Source: AGHT+IE35Own0gcdDJ8WhwZvoyZ/Lfjz5Z7rfvRgKj8aMKZvOWQOY1x804hEoiCRKc0bk1X18h7K1jcRQTMTv6Di05I= X-Received: by 2002:a2e:bc0b:0:b0:2f7:5c24:1cab with SMTP id 38308e7fff4ca-2fcbe088e92mr42178501fa.10.1730741678000; Mon, 04 Nov 2024 09:34:38 -0800 (PST) MIME-Version: 1.0 References: <20240822163941.1390326-1-luca.vizzarro@arm.com> <20241028174949.3283701-1-luca.vizzarro@arm.com> <20241028174949.3283701-8-luca.vizzarro@arm.com> In-Reply-To: <20241028174949.3283701-8-luca.vizzarro@arm.com> From: Nicholas Pratte Date: Mon, 4 Nov 2024 12:34:27 -0500 Message-ID: Subject: Re: [PATCH v4 7/8] dts: improve configuration API docs To: Luca Vizzarro Cc: dev@dpdk.org, Paul Szczepanek , Patrick Robb Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org Reviewed-by: Nicholas Pratte On Mon, Oct 28, 2024 at 1:51=E2=80=AFPM Luca Vizzarro wrote: > > Pydantic models are not treated the same way as dataclasses by autodoc. > As a consequence the docstrings need to be applied directly to each > field. Otherwise the generated API documentation page would present two > entries per each field with each their own differences. > > Signed-off-by: Luca Vizzarro > Reviewed-by: Paul Szczepanek > --- > doc/guides/tools/dts.rst | 5 +- > dts/framework/config/__init__.py | 253 +++++++++++-------------------- > 2 files changed, 88 insertions(+), 170 deletions(-) > > diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst > index 7ccca63ae8..ac12c5c4fa 100644 > --- a/doc/guides/tools/dts.rst > +++ b/doc/guides/tools/dts.rst > @@ -1,5 +1,6 @@ > .. SPDX-License-Identifier: BSD-3-Clause > Copyright(c) 2022-2023 PANTHEON.tech s.r.o. > + Copyright(c) 2024 Arm Limited > > DPDK Test Suite > =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > @@ -327,8 +328,8 @@ where we deviate or where some additional clarificati= on is helpful: > * The ``dataclass.dataclass`` decorator changes how the attributes ar= e processed. > The dataclass attributes which result in instance variables/attribu= tes > should also be recorded in the ``Attributes:`` section. > - * Class variables/attributes, on the other hand, should be documented= with ``#:`` > - above the type annotated line. > + * Class variables/attributes and Pydantic model fields, on the other = hand, should be documented > + with ``#:`` above the type annotated line. > The description may be omitted if the meaning is obvious. > * The ``Enum`` and ``TypedDict`` also process the attributes in parti= cular ways > and should be documented with ``#:`` as well. > diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__in= it__.py > index c86bfaaabf..d7d3907a33 100644 > --- a/dts/framework/config/__init__.py > +++ b/dts/framework/config/__init__.py > @@ -116,54 +116,34 @@ class TrafficGeneratorType(str, Enum): > > > class HugepageConfiguration(BaseModel, frozen=3DTrue, extra=3D"forbid"): > - r"""The hugepage configuration of :class:`~framework.testbed_model.n= ode.Node`\s. > - > - Attributes: > - number_of: The number of hugepages to allocate. > - force_first_numa: If :data:`True`, the hugepages will be configu= red on the first NUMA node. > - """ > + r"""The hugepage configuration of :class:`~framework.testbed_model.n= ode.Node`\s.""" > > + #: The number of hugepages to allocate. > number_of: int > + #: If :data:`True`, the hugepages will be configured on the first NU= MA node. > force_first_numa: bool > > > class PortConfig(BaseModel, frozen=3DTrue, extra=3D"forbid"): > - r"""The port configuration of :class:`~framework.testbed_model.node.= Node`\s. > - > - Attributes: > - pci: The PCI address of the port. > - os_driver_for_dpdk: The operating system driver name for use wit= h DPDK. > - os_driver: The operating system driver name when the operating s= ystem controls the port. > - peer_node: The :class:`~framework.testbed_model.node.Node` of th= e port > - connected to this port. > - peer_pci: The PCI address of the port connected to this port. > - """ > + r"""The port configuration of :class:`~framework.testbed_model.node.= Node`\s.""" > > - pci: str =3D Field( > - description=3D"The local PCI address of the port.", pattern=3DRE= GEX_FOR_PCI_ADDRESS > - ) > - os_driver_for_dpdk: str =3D Field( > - description=3D"The driver that the kernel should bind this devic= e to for DPDK to use it.", > - examples=3D["vfio-pci", "mlx5_core"], > - ) > - os_driver: str =3D Field( > - description=3D"The driver normally used by this port", examples= =3D["i40e", "ice", "mlx5_core"] > - ) > - peer_node: str =3D Field(description=3D"The name of the peer node th= is port is connected to.") > - peer_pci: str =3D Field( > - description=3D"The PCI address of the peer port this port is con= nected to.", > - pattern=3DREGEX_FOR_PCI_ADDRESS, > - ) > + #: The PCI address of the port. > + pci: str =3D Field(pattern=3DREGEX_FOR_PCI_ADDRESS) > + #: The driver that the kernel should bind this device to for DPDK to= use it. > + os_driver_for_dpdk: str =3D Field(examples=3D["vfio-pci", "mlx5_core= "]) > + #: The operating system driver name when the operating system contro= ls the port. > + os_driver: str =3D Field(examples=3D["i40e", "ice", "mlx5_core"]) > + #: The name of the peer node this port is connected to. > + peer_node: str > + #: The PCI address of the peer port connected to this port. > + peer_pci: str =3D Field(pattern=3DREGEX_FOR_PCI_ADDRESS) > > > class TrafficGeneratorConfig(BaseModel, frozen=3DTrue, extra=3D"forbid")= : > - """A protocol required to define traffic generator types. > - > - Attributes: > - type: The traffic generator type, the child class is required to= define to be distinguished > - among others. > - """ > + """A protocol required to define traffic generator types.""" > > + #: The traffic generator type the child class is required to define = to be distinguished among > + #: others. > type: TrafficGeneratorType > > > @@ -176,13 +156,10 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorC= onfig, frozen=3DTrue, extra=3D"fo > #: A union type discriminating traffic generators by the `type` field. > TrafficGeneratorConfigTypes =3D Annotated[ScapyTrafficGeneratorConfig, F= ield(discriminator=3D"type")] > > - > -#: A field representing logical core ranges. > +#: Comma-separated list of logical cores to use. An empty string means u= se all lcores. > LogicalCores =3D Annotated[ > str, > Field( > - description=3D"Comma-separated list of logical cores to use. " > - "An empty string means use all lcores.", > examples=3D["1,2,3,4,5,18-22", "10-15"], > pattern=3Dr"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+))= )*)?$", > ), > @@ -190,61 +167,41 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorC= onfig, frozen=3DTrue, extra=3D"fo > > > class NodeConfiguration(BaseModel, frozen=3DTrue, extra=3D"forbid"): > - r"""The configuration of :class:`~framework.testbed_model.node.Node`= \s. > - > - Attributes: > - name: The name of the :class:`~framework.testbed_model.node.Node= `. > - hostname: The hostname of the :class:`~framework.testbed_model.n= ode.Node`. > - Can be an IP or a domain name. > - user: The name of the user used to connect to > - the :class:`~framework.testbed_model.node.Node`. > - password: The password of the user. The use of passwords is heav= ily discouraged. > - Please use keys instead. > - arch: The architecture of the :class:`~framework.testbed_model.n= ode.Node`. > - os: The operating system of the :class:`~framework.testbed_model= .node.Node`. > - lcores: A comma delimited list of logical cores to use when runn= ing DPDK. > - use_first_core: If :data:`True`, the first logical core won't be= used. > - hugepages: An optional hugepage configuration. > - ports: The ports that can be used in testing. > - """ > - > - name: str =3D Field(description=3D"A unique identifier for this node= .") > - hostname: str =3D Field(description=3D"The hostname or IP address of= the node.") > - user: str =3D Field(description=3D"The login user to use to connect = to this node.") > - password: str | None =3D Field( > - default=3DNone, > - description=3D"The login password to use to connect to this node= . " > - "SSH keys are STRONGLY preferred, use only as last resort.", > - ) > + r"""The configuration of :class:`~framework.testbed_model.node.Node`= \s.""" > + > + #: The name of the :class:`~framework.testbed_model.node.Node`. > + name: str > + #: The hostname of the :class:`~framework.testbed_model.node.Node`. = Can also be an IP address. > + hostname: str > + #: The name of the user used to connect to the :class:`~framework.te= stbed_model.node.Node`. > + user: str > + #: The password of the user. The use of passwords is heavily discour= aged, please use SSH keys. > + password: str | None =3D None > + #: The architecture of the :class:`~framework.testbed_model.node.Nod= e`. > arch: Architecture > + #: The operating system of the :class:`~framework.testbed_model.node= .Node`. > os: OS > + #: A comma delimited list of logical cores to use when running DPDK. > lcores: LogicalCores =3D "1" > - use_first_core: bool =3D Field( > - default=3DFalse, description=3D"DPDK won't use the first physica= l core if set to False." > - ) > + #: If :data:`True`, the first logical core won't be used. > + use_first_core: bool =3D False > + #: An optional hugepage configuration. > hugepages: HugepageConfiguration | None =3D Field(None, alias=3D"hug= epages_2mb") > + #: The ports that can be used in testing. > ports: list[PortConfig] =3D Field(min_length=3D1) > > > class SutNodeConfiguration(NodeConfiguration, frozen=3DTrue, extra=3D"fo= rbid"): > - """:class:`~framework.testbed_model.sut_node.SutNode` specific confi= guration. > + """:class:`~framework.testbed_model.sut_node.SutNode` specific confi= guration.""" > > - Attributes: > - memory_channels: The number of memory channels to use when runni= ng DPDK. > - """ > - > - memory_channels: int =3D Field( > - default=3D1, description=3D"Number of memory channels to use whe= n running DPDK." > - ) > + #: The number of memory channels to use when running DPDK. > + memory_channels: int =3D 1 > > > class TGNodeConfiguration(NodeConfiguration, frozen=3DTrue, extra=3D"for= bid"): > - """:class:`~framework.testbed_model.tg_node.TGNode` specific configu= ration. > - > - Attributes: > - traffic_generator: The configuration of the traffic generator pr= esent on the TG node. > - """ > + """:class:`~framework.testbed_model.tg_node.TGNode` specific configu= ration.""" > > + #: The configuration of the traffic generator present on the TG node= . > traffic_generator: TrafficGeneratorConfigTypes > > > @@ -258,20 +215,18 @@ def resolve_path(path: Path) -> Path: > > > class BaseDPDKLocation(BaseModel, frozen=3DTrue, extra=3D"forbid"): > - """DPDK location. > + """DPDK location base class. > > - The path to the DPDK sources, build dir and type of location. > - > - Attributes: > - remote: Optional, defaults to :data:`False`. If :data:`True`, `d= pdk_tree` or `tarball` is > - located on the SUT node, instead of the execution host. > + The path to the DPDK sources and type of location. > """ > > + #: Specifies whether to find DPDK on the SUT node or on the local ho= st. Which are respectively > + #: represented by :class:`RemoteDPDKLocation` and :class:`LocalDPDKT= reeLocation`. > remote: bool =3D False > > > class LocalDPDKLocation(BaseDPDKLocation, frozen=3DTrue, extra=3D"forbid= "): > - """Local DPDK location parent class. > + """Local DPDK location base class. > > This class is meant to represent any location that is present only l= ocally. > """ > @@ -284,14 +239,12 @@ class LocalDPDKTreeLocation(LocalDPDKLocation, froz= en=3DTrue, extra=3D"forbid"): > > This class makes a distinction from :class:`RemoteDPDKTreeLocation` = by enforcing on the fly > validation. > - > - Attributes: > - dpdk_tree: The path to the DPDK source tree directory. > """ > > + #: The path to the DPDK source tree directory on the local host pass= ed as string. > dpdk_tree: Path > > - #: Resolve the local DPDK tree path > + #: Resolve the local DPDK tree path. > resolve_dpdk_tree_path =3D field_validator("dpdk_tree")(resolve_path= ) > > @model_validator(mode=3D"after") > @@ -307,14 +260,12 @@ class LocalDPDKTarballLocation(LocalDPDKLocation, f= rozen=3DTrue, extra=3D"forbid"): > > This class makes a distinction from :class:`RemoteDPDKTarballLocatio= n` by enforcing on the fly > validation. > - > - Attributes: > - tarball: The path to the DPDK tarball. > """ > > + #: The path to the DPDK tarball on the local host passed as string. > tarball: Path > > - #: Resolve the local tarball path > + #: Resolve the local tarball path. > resolve_tarball_path =3D field_validator("tarball")(resolve_path) > > @model_validator(mode=3D"after") > @@ -326,7 +277,7 @@ def validate_tarball_path(self) -> Self: > > > class RemoteDPDKLocation(BaseDPDKLocation, frozen=3DTrue, extra=3D"forbi= d"): > - """Remote DPDK location parent class. > + """Remote DPDK location base class. > > This class is meant to represent any location that is present only r= emotely. > """ > @@ -338,11 +289,9 @@ class RemoteDPDKTreeLocation(RemoteDPDKLocation, fro= zen=3DTrue, extra=3D"forbid"): > """Remote DPDK tree location. > > This class is distinct from :class:`LocalDPDKTreeLocation` which enf= orces on the fly validation. > - > - Attributes: > - dpdk_tree: The path to the DPDK source tree directory. > """ > > + #: The path to the DPDK source tree directory on the remote node pas= sed as string. > dpdk_tree: PurePath > > > @@ -351,11 +300,9 @@ class RemoteDPDKTarballLocation(LocalDPDKLocation, f= rozen=3DTrue, extra=3D"forbid"): > > This class is distinct from :class:`LocalDPDKTarballLocation` which = enforces on the fly > validation. > - > - Attributes: > - tarball: The path to the DPDK tarball. > """ > > + #: The path to the DPDK tarball on the remote node passed as string. > tarball: PurePath > > > @@ -372,23 +319,17 @@ class BaseDPDKBuildConfiguration(BaseModel, frozen= =3DTrue, extra=3D"forbid"): > """The base configuration for different types of build. > > The configuration contain the location of the DPDK and configuration= used for building it. > - > - Attributes: > - dpdk_location: The location of the DPDK tree. > """ > > + #: The location of the DPDK tree. > dpdk_location: DPDKLocation > > > class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration, froz= en=3DTrue, extra=3D"forbid"): > - """DPDK precompiled build configuration. > - > - Attributes: > - precompiled_build_dir: If it's defined, DPDK has been pre-compil= ed and the build directory > - is located in a subdirectory of `dpdk_tree` or `tarball` roo= t directory. Otherwise, will > - be using `dpdk_build_config` from configuration to build the= DPDK from source. > - """ > + """DPDK precompiled build configuration.""" > > + #: If it's defined, DPDK has been pre-compiled and the build directo= ry is located in a > + #: subdirectory of `~dpdk_location.dpdk_tree` or `~dpdk_location.tar= ball` root directory. > precompiled_build_dir: str =3D Field(min_length=3D1) > > > @@ -396,20 +337,18 @@ class DPDKBuildOptionsConfiguration(BaseModel, froz= en=3DTrue, extra=3D"forbid"): > """DPDK build options configuration. > > The build options used for building DPDK. > - > - Attributes: > - arch: The target architecture to build for. > - os: The target os to build for. > - cpu: The target CPU to build for. > - compiler: The compiler executable to use. > - compiler_wrapper: This string will be put in front of the compil= er when executing the build. > - Useful for adding wrapper commands, such as ``ccache``. > """ > > + #: The target architecture to build for. > arch: Architecture > + #: The target OS to build for. > os: OS > + #: The target CPU to build for. > cpu: CPUType > + #: The compiler executable to use. > compiler: Compiler > + #: This string will be put in front of the compiler when executing t= he build. Useful for adding > + #: wrapper commands, such as ``ccache``. > compiler_wrapper: str =3D "" > > @cached_property > @@ -419,12 +358,9 @@ def name(self) -> str: > > > class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration, froze= n=3DTrue, extra=3D"forbid"): > - """DPDK uncompiled build configuration. > - > - Attributes: > - build_options: The build options to compile DPDK. > - """ > + """DPDK uncompiled build configuration.""" > > + #: The build options to compiled DPDK with. > build_options: DPDKBuildOptionsConfiguration > > > @@ -448,24 +384,13 @@ class TestSuiteConfig(BaseModel, frozen=3DTrue, ext= ra=3D"forbid"): > # or as model fields: > - test_suite: hello_world > test_cases: [hello_world_single_core] # without this field= all test cases are run > - > - Attributes: > - test_suite_name: The name of the test suite module without the s= tarting ``TestSuite_``. > - test_cases_names: The names of test cases from this test suite t= o execute. > - If empty, all test cases will be executed. > """ > > - test_suite_name: str =3D Field( > - title=3D"Test suite name", > - description=3D"The identifying module name of the test suite wit= hout the prefix.", > - alias=3D"test_suite", > - ) > - test_cases_names: list[str] =3D Field( > - default_factory=3Dlist, > - title=3D"Test cases by name", > - description=3D"The identifying name of the test cases of the tes= t suite.", > - alias=3D"test_cases", > - ) > + #: The name of the test suite module without the starting ``TestSuit= e_``. > + test_suite_name: str =3D Field(alias=3D"test_suite") > + #: The names of test cases from this test suite to execute. If empty= , all test cases will be > + #: executed. > + test_cases_names: list[str] =3D Field(default_factory=3Dlist, alias= =3D"test_cases") > > @cached_property > def test_suite_spec(self) -> "TestSuiteSpec": > @@ -507,14 +432,11 @@ def validate_names(self) -> Self: > > > class TestRunSUTNodeConfiguration(BaseModel, frozen=3DTrue, extra=3D"for= bid"): > - """The SUT node configuration of a test run. > - > - Attributes: > - node_name: The SUT node to use in this test run. > - vdevs: The names of virtual devices to test. > - """ > + """The SUT node configuration of a test run.""" > > + #: The SUT node to use in this test run. > node_name: str > + #: The names of virtual devices to test. > vdevs: list[str] =3D Field(default_factory=3Dlist) > > > @@ -523,25 +445,23 @@ class TestRunConfiguration(BaseModel, frozen=3DTrue= , extra=3D"forbid"): > > The configuration contains testbed information, what tests to execut= e > and with what DPDK build. > - > - Attributes: > - dpdk_config: The DPDK configuration used to test. > - perf: Whether to run performance tests. > - func: Whether to run functional tests. > - skip_smoke_tests: Whether to skip smoke tests. > - test_suites: The names of test suites and/or test cases to execu= te. > - system_under_test_node: The SUT node configuration to use in thi= s test run. > - traffic_generator_node: The TG node name to use in this test run= . > - random_seed: The seed to use for pseudo-random generation. > """ > > + #: The DPDK configuration used to test. > dpdk_config: DPDKBuildConfiguration =3D Field(alias=3D"dpdk_build") > - perf: bool =3D Field(description=3D"Enable performance testing.") > - func: bool =3D Field(description=3D"Enable functional testing.") > + #: Whether to run performance tests. > + perf: bool > + #: Whether to run functional tests. > + func: bool > + #: Whether to skip smoke tests. > skip_smoke_tests: bool =3D False > + #: The names of test suites and/or test cases to execute. > test_suites: list[TestSuiteConfig] =3D Field(min_length=3D1) > + #: The SUT node configuration to use in this test run. > system_under_test_node: TestRunSUTNodeConfiguration > + #: The TG node name to use in this test run. > traffic_generator_node: str > + #: The seed to use for pseudo-random generation. > random_seed: int | None =3D None > > > @@ -557,14 +477,11 @@ class TestRunWithNodesConfiguration(NamedTuple): > > > class Configuration(BaseModel, extra=3D"forbid"): > - """DTS testbed and test configuration. > - > - Attributes: > - test_runs: Test run configurations. > - nodes: Node configurations. > - """ > + """DTS testbed and test configuration.""" > > + #: Test run configurations. > test_runs: list[TestRunConfiguration] =3D Field(min_length=3D1) > + #: Node configurations. > nodes: list[NodeConfigurationTypes] =3D Field(min_length=3D1) > > @cached_property > -- > 2.43.0 >