Skip to content

viewable_tag_model.py

ofrak.model.viewable_tag_model

AttributesType (ResourceAttributes, Generic)

A Generic type for ViewableResourceTag to get the auto-generated ResourceAttributes class associated with that view type. The returned class is a dataclass which encapsulates the fields defined in one specific ViewableResourceTag.

For example if B inherits from A and A defines one or more new fields, AttributesType[B] has only fields defined in B, and none of the fields defined in A.

__getattr__(self, item) special

Type stub that 'tricks' MyPy into not trying to typecheck attribute accesses of an AttributesType instance. This stub solves the following case:

x: AttributesType[X] = func()
x.any_field  <--- Always is a type error because MyPy thinks AttributesType has no fields!

Without this stub, each instance of x.any_field would need to be marked with # type: ignore

Source code in ofrak/model/viewable_tag_model.py
def __getattr__(self, item):  # pragma: no cover
    """
    Type stub that 'tricks' MyPy into not trying to typecheck attribute accesses of an
    `AttributesType` instance. This stub solves the following case:

    ```
    x: AttributesType[X] = func()
    x.any_field  <--- Always is a type error because MyPy thinks AttributesType has no fields!
    ```

    Without this stub, each instance of x.any_field would need to be marked with # type: ignore
    """
    ...

ResourceViewInterface dataclass

ResourceViewInterface()

set_deleted(self)

Mark that the underlying resource has been deleted.

Returns:

Type Description
Source code in ofrak/model/viewable_tag_model.py
def set_deleted(self):  # pragma: no cover
    """
    Mark that the underlying resource has been deleted.
    :return:
    """
    raise NotImplementedError()

ViewableResourceTag (ResourceTag)

attributes_type: Type[ofrak.model.resource_model.ResourceAttributes] property readonly

Get the auto-generated ResourceAttributes subclass for this ViewableResourceTag. The returned class is a dataclass which encapsulates the fields defined in one specific ViewableResourceTag. For example if B inherits from A and A defines several fields, B.attributes_type has only fields defined in B, and none of the fields defined in A.

Returns:

Type Description
Type[ofrak.model.resource_model.ResourceAttributes]

The auto-generated ResourceAttributes subclass for this ViewableResourceTag class, in no particular order.

composed_attributes_types: Iterable[Type[ofrak.model.resource_model.ResourceAttributes]] property readonly

Get all of the ResourceAttributes subclasses which this class is composed of. This means walking back through the class hierarchy and getting the AttributesType[base] for every base class of this class.

Returns:

Type Description
Iterable[Type[ofrak.model.resource_model.ResourceAttributes]]

The attributes_type of every ViewableResourceTag this class inherits from, in no particular order.

__new__(mcs, name, bases, namespace) special staticmethod

Create a new attributes type for the ViewableResourceTag which is about to be created. This type will inherit from ResourceAttributes and have fields corresponding to all the fields of unique to the new ViewableResourceTag. This type, as well as all attributes types inherited from the bases, are added to the namespace of the new class under the dunders _VIEW_ATTRIBUTES_TYPE and _COMPOSED_ATTRIBUTES_TYPE.

This method also checks for possible attempted polymorphism, raising warnings if any method overrides are detected.

The __new__ method can inspect and edit the names, bases, and namespace (attributes and methods) of the class. It is called after the class definition has been parsed, and before the new class object has been created.

Parameters:

Name Type Description Default
name str

Name of the new class

required
bases Tuple[Type, ...]

Base classes of the new class

required
namespace Dict[str, Any]

Namespace of attributes for the new class, in the form of a dictionary mapping attribute names to objects

required

Returns:

Type Description
Source code in ofrak/model/viewable_tag_model.py
def __new__(mcs, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any]):
    """
    Create a new attributes type for the `ViewableResourceTag` which is about to be created.
    This type will inherit from `ResourceAttributes` and have fields corresponding to all the
    fields of unique to the new `ViewableResourceTag`. This type, as well as all attributes
    types inherited from the bases, are added to the namespace of the new class under the
    dunders `_VIEW_ATTRIBUTES_TYPE` and `_COMPOSED_ATTRIBUTES_TYPE`.

    This method also checks for possible attempted polymorphism, raising warnings if any
    method overrides are detected.

    The `__new__` method can inspect and edit the names, bases, and namespace (attributes and
    methods) of the class. It is called after the class definition has been parsed, and before
    the new class object has been created.

    :param name: Name of the new class
    :param bases: Base classes of the new class
    :param namespace: Namespace of attributes for the new class, in the form of a dictionary
    mapping attribute names to objects
    :return:
    """
    _check_for_polymorphism(name, bases, namespace)
    attributes_type = mcs._create_attributes_type(name, bases, namespace)
    composed_attributes_types = [attributes_type]
    for base_cls in bases:
        composed_attributes_types.extend(_get_attributes_types_recursively(base_cls))
    composed_attributes_types = _filter_attributes_types(composed_attributes_types)

    namespace[_VIEW_ATTRIBUTES_TYPE] = attributes_type
    namespace[_COMPOSED_ATTRIBUTES_TYPE] = composed_attributes_types
    return super().__new__(mcs, name, bases, namespace)

__init__(cls, name, bases, namespace) special

Fix up any @index defined in this ViewableResourceTag. Each index descriptor needs an owner which is a ResourceAttributes subclass, and an automatically populated owner will be set to the newly created cls, which is an instance of ViewableResourceTag and not a ResourceAttributes subclass. This is a little hacky, but it works just fine.

Can inspect and edit the names, bases, and namespace (attributes and methods) of the class.

Called after the new class object has been created.

Parameters:

Name Type Description Default
name str

Name of the new class

required
bases Tuple[Type, ...]

Base classes of the new class

required
namespace Dict[str, Any]

Namespace of attributes for the new class, in the form of a dictionary mapping attribute names to objects

required

Returns:

Type Description

Exceptions:

Type Description
TypeError

If the viewable tag has no attributes but does have an @index

Source code in ofrak/model/viewable_tag_model.py
def __init__(cls, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any]):
    """
    Fix up any `@index` defined in this `ViewableResourceTag`. Each index descriptor needs an
    owner which is a `ResourceAttributes` subclass, and an automatically populated owner will be
    set to the newly created `cls`, which is an instance of `ViewableResourceTag` and not a
    `ResourceAttributes` subclass. This is a little hacky, but it works just fine.

    Can inspect and edit the names, bases, and namespace (attributes and methods) of the class.

    Called after the new class object has been created.

    :param name: Name of the new class
    :param bases: Base classes of the new class
    :param namespace: Namespace of attributes for the new class, in the form of a dictionary
    mapping attribute names to objects

    :raises TypeError: If the viewable tag has no attributes but does have an @index

    :return:
    """
    # Change owner of the indexes to be the attributes type
    for name, index_descriptor in _get_indexes(namespace).items():
        if getattr(cls, _VIEW_ATTRIBUTES_TYPE) is None:
            raise TypeError(
                f"Cannot have an index in a ResourceView which has no attributes "
                f"- an index should only access one set of attributes, "
                f"so this index is likely illegal anyway."
            )
        index_descriptor.__set_name__(getattr(cls, _VIEW_ATTRIBUTES_TYPE), name)

    super().__init__(cls, name, bases)  # type: ignore

_create_attributes_type(name, bases, namespace) classmethod private

Get a type inheriting from ResourceAttributes which is unique to this tag (cls).

Returns:

Type Description
Type[ofrak.model.resource_model.ResourceAttributes]
Source code in ofrak/model/viewable_tag_model.py
@classmethod
def _create_attributes_type(
    mcs, name: str, bases: Tuple[Type, ...], namespace: Dict[str, Any]
) -> Type[ResourceAttributes]:
    """
    Get a type inheriting from `ResourceAttributes` which is unique to this tag (`cls`).
    :return:
    """

    # First make this a dataclass to easily get its fields
    # We can't depend on the class already being a dataclass because class decorators run
    # after metaclass __new__ and __init__ methods, so it is not yet a dataclass
    tmp_cls = super().__new__(mcs, name, bases, namespace)
    tmp_dataclass: object = dataclass(tmp_cls)  # type: ignore

    base_fields: Set[dataclasses.Field] = set()
    for base in bases:
        if type(base) is ViewableResourceTag:
            base_fields.update(dataclasses.fields(base))

    fields = [
        (_field.name, _field.type, _field)
        for _field in dataclasses.fields(tmp_dataclass)
        if _field not in base_fields and not _field.name.startswith("_")
    ]
    if len(fields) == 0:
        return _NO_RESOURCE_ATTRIBUTES_TYPE
    indexed_attributes_namespace = _get_indexes(namespace)
    # Creates a new class inheriting from ResourceAttributes, with the same fields as this
    # ViewableResourceTag, as well as the same indexed attribute descriptors
    attributes_type = dataclasses.make_dataclass(
        f"{AttributesType.__name__}[{name}]",
        fields,
        bases=(ResourceAttributes,),
        namespace=indexed_attributes_namespace,
        **ResourceAttributes.DATACLASS_PARAMS,
    )

    # By default, this new attributes_type is part of the "types" module.
    # Instead, we make it part of the module ofrak.model._auto_attributes.
    attributes_type.__module__ = "ofrak.model._auto_attributes"
    setattr(
        ofrak.model._auto_attributes,
        attributes_type.__name__,
        attributes_type,
    )

    return attributes_type

_NO_RESOURCE_ATTRIBUTES_TYPE (ResourceAttributes) dataclass private

Dummy class indicating a ViewableResourceTag has no attributes_type attribute.

_NoResourceAttributesType (ResourceAttributes) dataclass private

Dummy class indicating a ViewableResourceTag has no attributes_type attribute.

_check_for_polymorphism(name, bases, namespace) private

Check for any methods in a new class which override a parent's method. The behavior of view_as means that overriding methods might not work as users think it does. Calling something like a = resource.view_as(A) will always and only return instances of A. These resources may have tags B and/or C which inherit from A but the returned view is not an instance of B or C. Calling a method a.foo() on that resource will therefore ALWAYS dispatch to A.foo, never to B.foo or C.foo.

This would break a design in which a user expects to only program against a common viewable tag's interface and let individual instances dispatch to some unique behavior. For example, if one gets all descendants with a common tag as a view of that tag, then calls a virtual method of those views, expecting each view to possibly do some unique behavior. Such a class hierarchy could be made to work, but we raise a warning to ensure that developers are conscious of this restriction and program accordingly.

Parameters:

Name Type Description Default
name str

Name of the new class

required
bases Iterable[Type]

Base classes of the new class

required
namespace Dict[str, Any]

Namespace of attributes for the new class, in the form of a dictionary mapping attribute names to objects

required
Source code in ofrak/model/viewable_tag_model.py
def _check_for_polymorphism(name: str, bases: Iterable[Type], namespace: Dict[str, Any]):
    """
    Check for any methods in a new class which override a parent's method. The behavior of
    `view_as` means that overriding methods might not work as users think it does. Calling
    something like `a = resource.view_as(A)` will always and only return instances of `A`. These
    resources may have tags `B` and/or `C` which inherit from `A` but the returned view is not an
    instance of `B` or `C`. Calling a method `a.foo()` on that resource will therefore **ALWAYS**
    dispatch to `A.foo`, never to `B.foo` or `C.foo`.

    This would break a design in which a user expects to only program against a common viewable
    tag's interface and let individual instances dispatch to some unique behavior. For example, if
    one gets all descendants with a common tag as a view of that tag, then calls a virtual method
    of those views, expecting each view to possibly do some unique behavior. Such a class hierarchy
    could be made to work, but we raise a warning to ensure that developers are conscious of this
    restriction and program accordingly.

    :param name: Name of the new class
    :param bases: Base classes of the new class
    :param namespace: Namespace of attributes for the new class, in the form of a dictionary
    mapping attribute names to objects
    """
    for base_cls in bases:
        # TODO: Figure out cleaner way to make an exception for ResourceView
        if name == "ResourceView":
            continue
        if isinstance(base_cls, ViewableResourceTag):
            parent_cls = base_cls

            parent_class_namespace = dir(parent_cls)

            common_attrs = set(namespace.keys()).intersection(set(parent_class_namespace))
            common_attrs = common_attrs.difference(dir(ViewableResourceTag))

            common_methods = {
                attr_name: namespace[attr_name]
                for attr_name in common_attrs
                if callable(namespace[attr_name])
            }

            overwritten_methods = {
                attr_name: attr
                for attr_name, attr in common_methods.items()
                if namespace[attr_name] != getattr(parent_cls, attr_name)
            }

            for method_name, method in overwritten_methods.items():
                warn(
                    f"{name}.{method_name} overrides the parent's method "
                    f"{method_name}; OFRAK Resources do not support runtime polymorphism, "
                    f"and this function may depend on runtime polymorphism."
                )

_get_indexes(namespace) private

Extract the index descriptors from a namespaces.

Source code in ofrak/model/viewable_tag_model.py
def _get_indexes(namespace: Dict[str, Any]) -> Dict[str, ResourceIndexedAttribute]:
    """
    Extract the index descriptors from a namespaces.
    """
    return {
        name: item for name, item in namespace.items() if isinstance(item, ResourceIndexedAttribute)
    }