Source code for statham.schema.property

from typing import Any, Dict, Generic, Optional, TypeVar, TYPE_CHECKING

from statham.schema.constants import NotPassed
from statham.schema.exceptions import SchemaDefinitionError
from statham.schema.helpers import custom_repr_args

if TYPE_CHECKING:
    from statham.schema.elements.base import Element  # pragma: no cover


PropType = TypeVar("PropType")  # pylint: disable=invalid-name


class _Property(Generic[PropType]):
    """Descriptor for a property on an object.

    Used to bind information about the enclosing object to a target Element,
    e.g. "required". Should be instantiated via `Property` to enable
    type inference on instances.
    """

    required: bool
    parent: Any
    element: "Element"
    source: Optional[str]
    name: Optional[str]

    def __init__(
        self,
        element: "Element[PropType]",
        *,
        required: bool = False,
        source: str = None,
    ):
        self.element = element
        self.required = required
        self.name: Optional[str] = None
        self.source: Optional[str] = source
        self.parent = None

    def __eq__(self, other):
        if not isinstance(other, _Property):
            return False
        return (
            self.element == other.element
            and self.required == other.required
            and self.source == other.source
        )

    def clone(self):
        return _Property(
            self.element, required=self.required, source=self.source
        )

    def evolve(self, name: str) -> "_Property":
        """Generate renamed property object to pass into nested elements."""
        property_: _Property[PropType] = _Property(
            element=self.element, required=self.required, source=self.source
        )
        property_.bind(name=name, parent=self.parent)
        return property_

    def bind(self, name: str = None, parent: "Element" = None) -> None:
        if parent:
            self.parent = parent
        if not name:
            return
        if not self.source:
            self.source = name
        self.name = name

    def __call__(self, value):
        return self.element(value, self)

    def __repr__(self):
        repr_args = custom_repr_args(self)
        if self.source == self.name:
            _ = repr_args.kwargs.pop("source", None)
        return f"{self.__class__.__name__.lstrip('_')}{repr(repr_args)}"

    @property
    def annotation(self):
        if self.required or not isinstance(
            getattr(self.element, "default", NotPassed()), NotPassed
        ):
            return self.element.annotation
        return f"Maybe[{self.element.annotation}]"

    def python(self) -> str:
        prop_def = repr(self)
        return (
            (f"{self.name}: {self.annotation} = {prop_def}")
            if self.name
            else prop_def
        )


# Behaves as a wrapper for the `_Property` class.
# pylint: disable=invalid-name
[docs]def Property(element: "Element", *, required: bool = False, source: str = None): """Descriptor for adding a property when declaring an object schema model. Return value is typed to inform instance-level interface (see type stubs). :param element: The JSON Schema Element object accepted by this property. :param required: Whether this property is required. If false, then this field may be omitted when data is passed to the outer object's constructor. :param source: The source name of this property. Only necessary if it must differ from that of the attribute. :return: The property descriptor for this element. To hand property name conflicts, use the :paramref:`Property.source` option. For example, to express a property called `class`, one could do the following: .. code:: python class MyObject(Object): # Property called class class_: str = Property(String(), source="class") """ return _Property(element, required=required, source=source)
class _PropertyDict(Dict[str, _Property[Any]]): """Container for properties. Used internally to bind properties to the enclosing element, and attribute name. """ _parent: "Element" def __init__(self, *args, **kwargs): self._parent = None super().__init__(*args, **kwargs) bad_values = { name: prop for name, prop in self.items() # pylint: disable=no-member if not isinstance(prop, _Property) } if bad_values: raise SchemaDefinitionError(f"Got bad property types: {bad_values}") def __setitem__(self, key, value): if not isinstance(value, _Property): raise SchemaDefinitionError( f"{key} must be a `Property`, got {value}" ) super().__setitem__(key, value) # pylint: disable=no-member value.bind(name=key, parent=self.parent) @property def parent(self) -> "Element": return self._parent @parent.setter def parent(self, value: "Element"): self._parent = value for key, prop in self.items(): # pylint: disable=no-member prop.bind(name=key, parent=value) @property def required(self): # pylint: disable=no-member return [ prop.source or name for name, prop in self.items() if prop.required and isinstance(prop.element.default, NotPassed) ]