diff --git a/pyaml/validation/schema_builder.py b/pyaml/validation/schema_builder.py index edd95d5f..4912b209 100644 --- a/pyaml/validation/schema_builder.py +++ b/pyaml/validation/schema_builder.py @@ -3,6 +3,7 @@ import inspect import logging import types +from abc import ABC from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum @@ -22,6 +23,15 @@ RESERVED_CONFIGURATION_FIELDS = {"class_path"} +# Types that should not be included in the registry when +# registing classes by walking a class's MRO. +EXCLUDED_TYPES: set[object] = { + object, + BaseModel, + ConfigurationSchema, + ABC, +} + SUPPORTED_TYPES = ( int, str, @@ -80,7 +90,7 @@ def _extract_source_bases(source: type) -> list[type]: bases: list[type] = [] for base in source.__mro__[1:]: - if base in (object, BaseModel, ConfigurationSchema): + if base in EXCLUDED_TYPES: continue bases.append(base) @@ -222,7 +232,7 @@ def _resolve_annotation(annotation: Any) -> Any: generic types are resolved recursively into equivalent type hints. """ - if annotation is inspect._empty: + if annotation is inspect._empty or annotation is Any: return Any if annotation is None: diff --git a/tests/validation/test_schema_builder.py b/tests/validation/test_schema_builder.py index 3f6abefe..796fdbfd 100644 --- a/tests/validation/test_schema_builder.py +++ b/tests/validation/test_schema_builder.py @@ -1,6 +1,7 @@ """Tests of the schema builder.""" import inspect +from abc import ABC from collections.abc import Generator from enum import Enum from typing import Annotated, Any, Literal, get_args, get_origin @@ -11,6 +12,7 @@ from pyaml.validation.configuration_models import ConfigurationSchema from pyaml.validation.registry import SchemaRegistry from pyaml.validation.schema_builder import ( + EXCLUDED_TYPES, _configuration_schema_from_basemodel, _field_definition_from_field_info, _fields_from_constructor_signature, @@ -189,6 +191,7 @@ def __init__(self, class_path: str): def test_resolve_annotation(): assert _resolve_annotation(inspect._empty) is Any + assert _resolve_annotation(Any) is Any assert _resolve_annotation(None) is type(None) assert _resolve_annotation(int) is int assert _resolve_annotation(Color) is Color @@ -206,6 +209,23 @@ def test_resolve_annotation(): assert get_args(annotated) == (int, "meta") +def test_excluded_framework_bases_are_not_registered(): + class FrameworkConfig(ABC): + pass + + class UserConfig(FrameworkConfig): + pass + + EXCLUDED_TYPES.add(FrameworkConfig) + try: + generate_configuration_schema(UserConfig) + registry = SchemaRegistry() + assert f"{FrameworkConfig.__module__}.{FrameworkConfig.__name__}" not in registry + assert f"{UserConfig.__module__}.{UserConfig.__name__}" in registry + finally: + EXCLUDED_TYPES.remove(FrameworkConfig) + + def test_resolve_annotation_rejects_forward_reference(): with pytest.raises(TypeError, match="Forward references"): _resolve_annotation("SomeClass")