blob: d10612cf3c736c8b73cab7786047e97b2fa60390 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Public helpers for describing dataclass-style defaults on FFI proxies."""
from __future__ import annotations
from dataclasses import _MISSING_TYPE, MISSING
from typing import Any, Callable, TypeVar, cast
_FieldValue = TypeVar("_FieldValue")
class Field:
"""(Experimental) Descriptor placeholder returned by :func:`tvm_ffi.dataclasses.field`.
A ``Field`` mirrors the object returned by :func:`dataclasses.field`, but it
is understood by :func:`tvm_ffi.dataclasses.c_class`. The decorator inspects
the ``Field`` instances, records the ``default_factory`` and later replaces
the field with a property that forwards to the underlying C++ attribute.
Users should not instantiate ``Field`` directly - use :func:`field` instead,
which guarantees that ``name`` and ``default_factory`` are populated in a
way the decorator understands.
"""
__slots__ = ("default_factory", "init", "name")
def __init__(
self,
*,
name: str | None = None,
default_factory: Callable[[], _FieldValue] | _MISSING_TYPE = MISSING,
init: bool = True,
) -> None:
"""Do not call directly; use :func:`field` instead."""
self.name = name
self.default_factory = default_factory
self.init = init
def field(
*,
default: _FieldValue | _MISSING_TYPE = MISSING, # type: ignore[assignment]
default_factory: Callable[[], _FieldValue] | _MISSING_TYPE = MISSING, # type: ignore[assignment]
init: bool = True,
) -> _FieldValue:
"""(Experimental) Declare a dataclass-style field on a :func:`c_class` proxy.
Use this helper exactly like :func:`dataclasses.field` when defining the
Python side of a C++ class. When :func:`c_class` processes the class body it
replaces the placeholder with a property and arranges for ``default`` or
``default_factory`` to be respected by the synthesized ``__init__``.
Parameters
----------
default
A literal default value that populates the field when no argument
is given. At most one of ``default`` or ``default_factory`` may be
given.
default_factory
A zero-argument callable that produces the default. This matches the
semantics of :func:`dataclasses.field` and is useful for mutable
defaults such as ``list`` or ``dict``.
init
If ``True`` the field is included in the generated ``__init__``.
If ``False`` the field is omitted from input arguments of ``__init__``.
Note
----
The decision to forward a field to the C++ ``__ffi_init__`` constructor
depends on its configuration:
* If ``init=True``, the field's value (from user input or defaults)
is forwarded.
* If ``init=False``:
- With a ``default`` or ``default_factory``, its computed value is
forwarded. The user cannot provide this value via Python ``__init__``.
- Without a ``default`` or ``default_factory``, the field is *not*
forwarded to C++ ``__ffi_init__`` and must be initialized by the
C++ constructor.
Returns
-------
Field
A placeholder object that :func:`c_class` will consume during class
registration.
Examples
--------
``field`` integrates with :func:`c_class` to express defaults the same way a
Python ``dataclass`` would:
.. code-block:: python
@c_class("testing.TestCxxClassBase")
class PyBase:
v_i64: int
v_i32: int = field(default=16)
obj = PyBase(v_i64=4)
obj.v_i32 # -> 16
"""
if default is not MISSING and default_factory is not MISSING:
raise ValueError("Cannot specify both `default` and `default_factory`")
if not isinstance(init, bool):
raise TypeError("`init` must be a bool")
if default is not MISSING:
default_factory = _make_default_factory(default)
ret = Field(default_factory=default_factory, init=init)
return cast(_FieldValue, ret)
def _make_default_factory(value: Any) -> Callable[[], Any]:
"""Make a default factory that returns the given value."""
def factory() -> Any:
return value
return factory