blob: bde5fd00557dee5bd9b92f859cdb1a3be7ba038b [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.
"""``dataclasses``-style helpers unified over stdlib, ``@c_class``, and ``@py_class``."""
from __future__ import annotations
import copy
from collections.abc import Callable
from typing import Any
from ..container import Array, Dict, List, Map
from .field import Field
__all__ = ["asdict", "astuple", "fields", "is_dataclass", "replace"]
# Exact-type fast path for atomic (immutable, non-recursive) values. Mirrors
# :data:`dataclasses._ATOMIC_TYPES` from the standard library.
_ATOMIC_TYPES: frozenset[type] = frozenset(
{
bool,
bytes,
complex,
float,
int,
str,
type(None),
type(Ellipsis),
type(NotImplemented),
}
)
def is_dataclass(obj: Any) -> bool:
"""Return True if ``obj`` is a ``@c_class`` / ``@py_class`` type or instance.
Returns False for FFI container types (:class:`~tvm_ffi.Array`,
:class:`~tvm_ffi.List`, :class:`~tvm_ffi.Map`, :class:`~tvm_ffi.Dict`)
even though they also carry ``__tvm_ffi_type_info__``; those are
reflected through :func:`~tvm_ffi.register_object` directly, not
through the dataclass decorators.
"""
cls = obj if isinstance(obj, type) else type(obj)
return getattr(cls, "__tvm_ffi_is_dataclass__", False) is True
def fields(obj_or_cls: Any) -> tuple[Field, ...]:
"""Return the :class:`~tvm_ffi.dataclasses.Field` descriptors for a type.
Accepts a ``@c_class`` / ``@py_class`` type or instance and walks the
parent chain so inherited fields appear parent-first, matching the
order of the auto-generated ``__init__``.
Raises
------
TypeError
If ``obj_or_cls`` is not a ``@c_class`` / ``@py_class`` type or instance.
"""
if not is_dataclass(obj_or_cls):
raise TypeError(
f"fields() argument must be a c_class or py_class type or instance, "
f"got {type(obj_or_cls).__name__}"
)
cls = obj_or_cls if isinstance(obj_or_cls, type) else type(obj_or_cls)
ti = getattr(cls, "__tvm_ffi_type_info__", None)
chain = []
while ti is not None:
chain.append(ti)
ti = ti.parent_type_info
out: list[Field] = []
for ti in reversed(chain):
for tf in ti.fields or ():
if tf.dataclass_field is not None:
out.append(tf.dataclass_field)
return tuple(out)
def replace(obj: Any, /, **changes: Any) -> Any:
"""Return a copy of ``obj`` with selected fields replaced.
Drop-in for :func:`dataclasses.replace` for FFI-backed instances: the
call is forwarded to ``obj.__replace__`` (installed by the decorator),
which uses the ``FFIProperty.set()`` escape hatch so frozen fields are
still replaceable.
"""
return obj.__replace__(**changes)
def _is_ffi_dataclass_instance(obj: Any) -> bool:
"""Return True when *obj* is a ``@c_class`` / ``@py_class`` **instance** (not a type)."""
if isinstance(obj, type):
return False
return is_dataclass(obj)
def _asdict_inner( # noqa: PLR0911, PLR0912
obj: Any, dict_factory: Callable[..., Any]
) -> Any:
obj_type = type(obj)
if obj_type in _ATOMIC_TYPES:
return obj
# FFI containers are treated as their stdlib analogues so the result is
# plain Python data — handy for JSON serialisation, the main use case.
if isinstance(obj, (Array, List)):
return [_asdict_inner(v, dict_factory) for v in obj]
if isinstance(obj, (Map, Dict)):
return dict_factory(
[
(_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
for k, v in obj.items()
]
)
if _is_ffi_dataclass_instance(obj):
fs = fields(obj)
if dict_factory is dict:
return {f.name: _asdict_inner(getattr(obj, f.name), dict) for f in fs} # ty: ignore[invalid-argument-type]
return dict_factory(
[(f.name, _asdict_inner(getattr(obj, f.name), dict_factory)) for f in fs] # ty: ignore[invalid-argument-type]
)
if obj_type is list:
return [_asdict_inner(v, dict_factory) for v in obj]
if obj_type is dict:
return {
_asdict_inner(k, dict_factory): _asdict_inner(v, dict_factory) for k, v in obj.items()
}
if obj_type is tuple:
return tuple(_asdict_inner(v, dict_factory) for v in obj)
if issubclass(obj_type, tuple):
if hasattr(obj, "_fields"): # namedtuple
return obj_type(*[_asdict_inner(v, dict_factory) for v in obj])
return obj_type(_asdict_inner(v, dict_factory) for v in obj)
if issubclass(obj_type, dict):
if hasattr(obj_type, "default_factory"):
result = obj_type(obj.default_factory)
for k, v in obj.items():
result[_asdict_inner(k, dict_factory)] = _asdict_inner(v, dict_factory)
return result
return obj_type(
(_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) for k, v in obj.items()
)
if issubclass(obj_type, list):
return obj_type(_asdict_inner(v, dict_factory) for v in obj)
return copy.deepcopy(obj)
def _astuple_inner(obj: Any, tuple_factory: Callable[..., Any]) -> Any: # noqa: PLR0911
obj_type = type(obj)
if obj_type in _ATOMIC_TYPES:
return obj
if isinstance(obj, (Array, List)):
return [_astuple_inner(v, tuple_factory) for v in obj]
if isinstance(obj, (Map, Dict)):
return {
_astuple_inner(k, tuple_factory): _astuple_inner(v, tuple_factory)
for k, v in obj.items()
}
if _is_ffi_dataclass_instance(obj):
return tuple_factory(
[_astuple_inner(getattr(obj, f.name), tuple_factory) for f in fields(obj)] # ty: ignore[invalid-argument-type]
)
if isinstance(obj, tuple) and hasattr(obj, "_fields"): # namedtuple
return obj_type(*[_astuple_inner(v, tuple_factory) for v in obj])
if isinstance(obj, (list, tuple)):
return obj_type(_astuple_inner(v, tuple_factory) for v in obj)
if isinstance(obj, dict):
if hasattr(obj_type, "default_factory"):
result = obj_type(obj.default_factory)
for k, v in obj.items():
result[_astuple_inner(k, tuple_factory)] = _astuple_inner(v, tuple_factory)
return result
return obj_type(
(_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory))
for k, v in obj.items()
)
return copy.deepcopy(obj)
def asdict(obj: Any, *, dict_factory: Callable[..., Any] = dict) -> Any:
r"""Return the fields of a ``@c_class`` / ``@py_class`` instance as a new dict.
Mirrors :func:`dataclasses.asdict` from the standard library. The
function recurses into nested FFI dataclass instances, FFI containers
(:class:`~tvm_ffi.Array`, :class:`~tvm_ffi.List`,
:class:`~tvm_ffi.Map`, :class:`~tvm_ffi.Dict`), and the built-in
``list`` / ``tuple`` / ``dict``. FFI sequence containers are
converted to Python ``list``\ s and FFI mapping containers to
Python ``dict``\ s so the result is plain Python data, e.g. for
JSON serialisation. Any other value is copied with
:func:`copy.deepcopy`.
Parameters
----------
obj
A ``@c_class`` / ``@py_class`` instance. Passing a type raises
:class:`TypeError`.
dict_factory
Callable used to construct the outer mapping and any nested
mapping recursed from an FFI dataclass. Defaults to :class:`dict`.
Raises
------
TypeError
If ``obj`` is not a ``@c_class`` / ``@py_class`` instance.
"""
if not _is_ffi_dataclass_instance(obj):
raise TypeError("asdict() should be called on c_class / py_class instances")
return _asdict_inner(obj, dict_factory)
def astuple(obj: Any, *, tuple_factory: Callable[..., Any] = tuple) -> Any:
"""Return the fields of a ``@c_class`` / ``@py_class`` instance as a new tuple.
Mirrors :func:`dataclasses.astuple` from the standard library. The
function recurses into nested FFI dataclass instances, FFI containers
(:class:`~tvm_ffi.Array`, :class:`~tvm_ffi.List`,
:class:`~tvm_ffi.Map`, :class:`~tvm_ffi.Dict`), and the built-in
``list`` / ``tuple`` / ``dict``. Any other value is copied with
:func:`copy.deepcopy`.
Parameters
----------
obj
A ``@c_class`` / ``@py_class`` instance. Passing a type raises
:class:`TypeError`.
tuple_factory
Callable used to construct the outer tuple and any nested tuple
recursed from an FFI dataclass. Defaults to :class:`tuple`.
Raises
------
TypeError
If ``obj`` is not a ``@c_class`` / ``@py_class`` instance.
"""
if not _is_ffi_dataclass_instance(obj):
raise TypeError("astuple() should be called on c_class / py_class instances")
return _astuple_inner(obj, tuple_factory)