Skip to content

hololinked.injection.SchemaValidators

Bases: Registry

A singleton registry that decides which schema validator class validates against a given schema.

All members are class attributes and settings are applied process-wide (python process). Which validator handles a schema is decided by the class of the payload type. Usually pydantic models invoke a pydantic validator and dictionaries invoke a JSON schema validator, but the mapping can be changed by registering a new validator. Use register() with a predicate function to activate a custom validator.

from hololinked import SchemaValidators

SchemaValidators.register_lazy(
    "my_package.validators",
    "MsgspecValidator",
    "msgspec",
    predicate=lambda schema: issubklass(schema, msgspec.Struct),
)

class MyThing(Thing):
    @action(input_schema=MyStruct)  # validated by MsgspecValidator
    def act(self, value): ...

A validator registered later is selected for a schema in preference to one registered earlier, so the built-in JSON schema and pydantic validators can be superseded for schemas they would otherwise handle.

Source code in repo/hololinked/hololinked/injection.py
class SchemaValidators(Registry):
    """
    A singleton registry that decides which schema validator class validates against a given schema.

    All members are class attributes and settings are applied process-wide (python process).
    Which validator handles a schema is decided by the class of the payload type.
    Usually pydantic models invoke a pydantic validator and dictionaries invoke a JSON schema validator,
    but the mapping can be changed by registering a new validator. Use `register()` with a `predicate` function to
    activate a custom validator.

    ```python
    from hololinked import SchemaValidators

    SchemaValidators.register_lazy(
        "my_package.validators",
        "MsgspecValidator",
        "msgspec",
        predicate=lambda schema: issubklass(schema, msgspec.Struct),
    )

    class MyThing(Thing):
        @action(input_schema=MyStruct)  # validated by MsgspecValidator
        def act(self, value): ...
    ```

    A validator registered later is selected for a schema in preference to one registered earlier, so the built-in
    JSON schema and pydantic validators can be superseded for schemas they would otherwise handle.
    """

    adapter_kind: ClassVar[str] = "schema validator"
    package: ClassVar[str] = "hololinked.schema_validators"
    tables: ClassVar[tuple[str, ...]] = ("modules", "predicates")

    modules: ClassVar[dict[str, str | tuple[str, str]]] = {
        "json_schema": "JSONSchemaValidator",
        "pydantic": "PydanticSchemaValidator",
    }

    json_schema: type[BaseSchemaValidator]
    """JSON Schema validator class that can be instantiated with a JSON schema to validate data against that schema."""
    pydantic: type[BaseSchemaValidator]
    """
    Pydantic validator class that can be instantiated with a Pydantic model to validate data against
    the schema defined by that model.
    """

    predicates: ClassVar[dict[str, Callable[[Any], bool]]] = {
        "json_schema": lambda schema: isinstance(schema, dict),
        "pydantic": lambda schema: issubklass(schema, BaseModel),  # RootModel is a subclass of BaseModel
    }
    """
    Name of a validator mapped to a predicate deciding whether it can validate against a given schema.

    Insertion ordered and consulted in reverse, so a validator registered later is selected for a schema in
    preference to one registered earlier. Predicates must be answerable without importing the adapter they
    belong to, since they are what decides which adapter to import in the first place.
    """

    @classmethod
    def register(
        cls,
        validator: type[BaseSchemaValidator],
        name: str,
        predicate: Callable[[Any], bool],
    ) -> None:
        """
        Register a schema validator class under a given name, overriding any validator already using that name.

        Parameters
        ----------
        validator: type[BaseSchemaValidator]
            the validator class to register, must be a subclass of `BaseSchemaValidator`
        name: str
            the name to register the validator under, for example 'json_schema' or 'pydantic'
        predicate: Callable[[Any], bool]
            predicate returning whether this validator can validate against a given schema, which is how a
            property or action picks a validator for the schema it was declared with.

        Raises
        ------
        TypeError
            if the validator is not a subclass of `BaseSchemaValidator`
        """
        if not issubklass(validator, BaseSchemaValidator):
            raise TypeError(f"validator must be a subclass of BaseSchemaValidator, given : {validator}")
        cls.install(name, validator)
        cls.predicates.pop(name, None)
        cls.predicates[name] = predicate

    @classmethod
    def name_for_schema(cls, schema: Any) -> str | None:
        """
        Get the name of the validator selected for the given schema.

        ```python
        print(SchemaValidators.name_for_schema({"type": "string"}))
        # prints 'json_schema'
        print(SchemaValidators.name_for_schema(MyPydanticModel))
        # prints 'pydantic'
        ```

        Parameters
        ----------
        schema: Any
            the schema to find a validator for

        Returns
        -------
        str | None
            the name the selected validator is registered under, None if no registered validator matches the schema
        """
        for name in reversed(cls.predicates):
            if cls.predicates[name](schema):
                return name
        return None

    @classmethod
    def is_supported(cls, schema: Any) -> bool:
        """
        Check whether any registered validator can validate against the given schema.

        Parameters
        ----------
        schema: Any
            the schema to check

        Returns
        -------
        bool
            True if a registered validator matches the schema
        """
        return cls.name_for_schema(schema) is not None

    @classmethod
    def for_schema(cls, schema: Any) -> type[BaseSchemaValidator]:
        """
        Get the validator class that validates against the given schema, importing it if necessary.

        Parameters
        ----------
        schema: Any
            the schema to find a validator for

        Returns
        -------
        type[BaseSchemaValidator]
            the validator class, to be instantiated with the schema

        Raises
        ------
        TypeError
            if no registered validator matches the schema
        """
        name = cls.name_for_schema(schema)
        if name is None:
            raise TypeError(
                f"no registered schema validator can validate against a schema of type {type(schema)}. "
                + "Register one with SchemaValidators.register() or SchemaValidators.register_lazy(), "
                + "supplying a 'predicate' that recognises it."
            )
        return getattr(cls, name)

    @classmethod
    def check_schema(cls, schema: Any) -> None:
        """
        Check that the given object is a well formed schema, using whichever validator is selected for it.

        Does nothing if the selected validator has no notion of checking a schema, as is the case for pydantic
        models.

        Parameters
        ----------
        schema: Any
            the schema to check

        Raises
        ------
        TypeError
            if no registered validator matches the schema
        Exception
            whatever the selected validator raises for a malformed schema
        """
        check = getattr(cls.for_schema(schema), "check_schema", None)
        if check is not None:
            check(schema)

    @classmethod
    def reset(cls) -> None:
        """Reset the schema validator registry."""
        cls.forget_adapters()

Attributes

json_schema instance-attribute

json_schema: type[BaseSchemaValidator]

JSON Schema validator class that can be instantiated with a JSON schema to validate data against that schema.

pydantic instance-attribute

pydantic: type[BaseSchemaValidator]

Pydantic validator class that can be instantiated with a Pydantic model to validate data against the schema defined by that model.

modules class-attribute

modules: dict[str, str | tuple[str, str]] = {'json_schema': 'JSONSchemaValidator', 'pydantic': 'PydanticSchemaValidator'}

predicates class-attribute

predicates: dict[str, Callable[[Any], bool]] = {'json_schema': lambda schema: isinstance(schema, dict), 'pydantic': lambda schema: issubklass(schema, BaseModel)}

Name of a validator mapped to a predicate deciding whether it can validate against a given schema.

Insertion ordered and consulted in reverse, so a validator registered later is selected for a schema in preference to one registered earlier. Predicates must be answerable without importing the adapter they belong to, since they are what decides which adapter to import in the first place.

Functions

for_schema classmethod

for_schema(schema: Any) -> type[BaseSchemaValidator]

Get the validator class that validates against the given schema, importing it if necessary.

Parameters:

Name Type Description Default

schema

Any

the schema to find a validator for

required

Returns:

Type Description
type[BaseSchemaValidator]

the validator class, to be instantiated with the schema

Raises:

Type Description
TypeError

if no registered validator matches the schema

Source code in repo/hololinked/hololinked/injection.py
@classmethod
def for_schema(cls, schema: Any) -> type[BaseSchemaValidator]:
    """
    Get the validator class that validates against the given schema, importing it if necessary.

    Parameters
    ----------
    schema: Any
        the schema to find a validator for

    Returns
    -------
    type[BaseSchemaValidator]
        the validator class, to be instantiated with the schema

    Raises
    ------
    TypeError
        if no registered validator matches the schema
    """
    name = cls.name_for_schema(schema)
    if name is None:
        raise TypeError(
            f"no registered schema validator can validate against a schema of type {type(schema)}. "
            + "Register one with SchemaValidators.register() or SchemaValidators.register_lazy(), "
            + "supplying a 'predicate' that recognises it."
        )
    return getattr(cls, name)

name_for_schema classmethod

name_for_schema(schema: Any) -> str | None

Get the name of the validator selected for the given schema.

print(SchemaValidators.name_for_schema({"type": "string"}))
# prints 'json_schema'
print(SchemaValidators.name_for_schema(MyPydanticModel))
# prints 'pydantic'

Parameters:

Name Type Description Default

schema

Any

the schema to find a validator for

required

Returns:

Type Description
str | None

the name the selected validator is registered under, None if no registered validator matches the schema

Source code in repo/hololinked/hololinked/injection.py
@classmethod
def name_for_schema(cls, schema: Any) -> str | None:
    """
    Get the name of the validator selected for the given schema.

    ```python
    print(SchemaValidators.name_for_schema({"type": "string"}))
    # prints 'json_schema'
    print(SchemaValidators.name_for_schema(MyPydanticModel))
    # prints 'pydantic'
    ```

    Parameters
    ----------
    schema: Any
        the schema to find a validator for

    Returns
    -------
    str | None
        the name the selected validator is registered under, None if no registered validator matches the schema
    """
    for name in reversed(cls.predicates):
        if cls.predicates[name](schema):
            return name
    return None

is_supported classmethod

is_supported(schema: Any) -> bool

Check whether any registered validator can validate against the given schema.

Parameters:

Name Type Description Default

schema

Any

the schema to check

required

Returns:

Type Description
bool

True if a registered validator matches the schema

Source code in repo/hololinked/hololinked/injection.py
@classmethod
def is_supported(cls, schema: Any) -> bool:
    """
    Check whether any registered validator can validate against the given schema.

    Parameters
    ----------
    schema: Any
        the schema to check

    Returns
    -------
    bool
        True if a registered validator matches the schema
    """
    return cls.name_for_schema(schema) is not None

check_schema classmethod

check_schema(schema: Any) -> None

Check that the given object is a well formed schema, using whichever validator is selected for it.

Does nothing if the selected validator has no notion of checking a schema, as is the case for pydantic models.

Parameters:

Name Type Description Default

schema

Any

the schema to check

required

Raises:

Type Description
TypeError

if no registered validator matches the schema

Exception

whatever the selected validator raises for a malformed schema

Source code in repo/hololinked/hololinked/injection.py
@classmethod
def check_schema(cls, schema: Any) -> None:
    """
    Check that the given object is a well formed schema, using whichever validator is selected for it.

    Does nothing if the selected validator has no notion of checking a schema, as is the case for pydantic
    models.

    Parameters
    ----------
    schema: Any
        the schema to check

    Raises
    ------
    TypeError
        if no registered validator matches the schema
    Exception
        whatever the selected validator raises for a malformed schema
    """
    check = getattr(cls.for_schema(schema), "check_schema", None)
    if check is not None:
        check(schema)

register classmethod

register(validator: type[BaseSchemaValidator], name: str, predicate: Callable[[Any], bool]) -> None

Register a schema validator class under a given name, overriding any validator already using that name.

Parameters:

Name Type Description Default

validator

type[BaseSchemaValidator]

the validator class to register, must be a subclass of BaseSchemaValidator

required

name

str

the name to register the validator under, for example 'json_schema' or 'pydantic'

required

predicate

Callable[[Any], bool]

predicate returning whether this validator can validate against a given schema, which is how a property or action picks a validator for the schema it was declared with.

required

Raises:

Type Description
TypeError

if the validator is not a subclass of BaseSchemaValidator

Source code in repo/hololinked/hololinked/injection.py
@classmethod
def register(
    cls,
    validator: type[BaseSchemaValidator],
    name: str,
    predicate: Callable[[Any], bool],
) -> None:
    """
    Register a schema validator class under a given name, overriding any validator already using that name.

    Parameters
    ----------
    validator: type[BaseSchemaValidator]
        the validator class to register, must be a subclass of `BaseSchemaValidator`
    name: str
        the name to register the validator under, for example 'json_schema' or 'pydantic'
    predicate: Callable[[Any], bool]
        predicate returning whether this validator can validate against a given schema, which is how a
        property or action picks a validator for the schema it was declared with.

    Raises
    ------
    TypeError
        if the validator is not a subclass of `BaseSchemaValidator`
    """
    if not issubklass(validator, BaseSchemaValidator):
        raise TypeError(f"validator must be a subclass of BaseSchemaValidator, given : {validator}")
    cls.install(name, validator)
    cls.predicates.pop(name, None)
    cls.predicates[name] = predicate

reset classmethod

reset() -> None

Reset the schema validator registry.

Source code in repo/hololinked/hololinked/injection.py
@classmethod
def reset(cls) -> None:
    """Reset the schema validator registry."""
    cls.forget_adapters()