Edit on GitHub

hexdoc.model

 1__all__ = [
 2    "DEFAULT_CONFIG",
 3    "Color",
 4    "HexdocModel",
 5    "HexdocSettings",
 6    "HexdocTypeAdapter",
 7    "IDModel",
 8    "InlineItemModel",
 9    "InlineModel",
10    "InternallyTaggedUnion",
11    "NoValue",
12    "NoValueType",
13    "ResourceModel",
14    "StripHiddenModel",
15    "TagValue",
16    "TypeTaggedTemplate",
17    "TypeTaggedUnion",
18    "ValidationContextModel",
19    "init_context",
20]
21
22from .base import (
23    DEFAULT_CONFIG,
24    HexdocModel,
25    HexdocSettings,
26    HexdocTypeAdapter,
27    ValidationContextModel,
28    init_context,
29)
30from .id import IDModel, ResourceModel
31from .inline import InlineItemModel, InlineModel
32from .strip_hidden import StripHiddenModel
33from .tagged_union import (
34    InternallyTaggedUnion,
35    NoValue,
36    NoValueType,
37    TagValue,
38    TypeTaggedTemplate,
39    TypeTaggedUnion,
40)
41from .types import Color
DEFAULT_CONFIG = {'extra': 'forbid', 'validate_default': True, 'ignored_types': (<class 'hexdoc.utils.classproperties.ClassPropertyDescriptor'>, <class 'yarl.URL'>)}
@dataclass(frozen=True, config=DEFAULT_CONFIG | json_schema_extra_config(type_str, inherited, pattern='(#|0x)?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})'))
class Color:
13@dataclass(
14    frozen=True,
15    config=DEFAULT_CONFIG
16    | json_schema_extra_config(
17        type_str,
18        inherited,
19        pattern=r"(#|0x)?([0-9a-fA-F]{6}|[0-9a-fA-F]{3})",
20    ),
21)
22class Color:
23    """Represents a hexadecimal color.
24
25    Inputs are coerced to lowercase `rrggbb`. Raises ValueError on invalid input.
26
27    Valid formats, all of which would be converted to `0099ff`:
28    - `"#0099FF"`
29    - `"#0099ff"`
30    - `"#09F"`
31    - `"#09f"`
32    - `"0099FF"`
33    - `"0099ff"`
34    - `"09F"`
35    - `"09f"`
36    - `0x0099ff`
37    """
38
39    value: str
40
41    @model_validator(mode="before")
42    def _pre_root(cls, value: Any):
43        if isinstance(value, (str, int)):
44            return {"value": value}
45        return value
46
47    @field_validator("value", mode="before")
48    def _check_value(cls, value: Any) -> str:
49        # type check
50        match value:
51            case str():
52                value = value.removeprefix("#").lower()
53            case int():
54                # int to hex string
55                value = f"{value:0>6x}"
56            case _:
57                raise TypeError(f"Expected str or int, got {type(value)}")
58
59        # 012 -> 001122
60        if len(value) == 3:
61            value = "".join(c + c for c in value)
62
63        # length and character check
64        if len(value) != 6 or any(c not in string.hexdigits for c in value):
65            raise ValueError(f"invalid color code: {value}")
66
67        return value

Represents a hexadecimal color.

Inputs are coerced to lowercase rrggbb. Raises ValueError on invalid input.

Valid formats, all of which would be converted to 0099ff:

  • "#0099FF"
  • "#0099ff"
  • "#09F"
  • "#09f"
  • "0099FF"
  • "0099ff"
  • "09F"
  • "09f"
  • 0x0099ff
Color(*args: Any, **kwargs: Any)
118    def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
119        __tracebackhide__ = True
120        s = __dataclass_self__
121        s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
value: str
@dataclass_transform()
class HexdocModel(pydantic.main.BaseModel):
41@dataclass_transform()
42class HexdocModel(BaseModel):
43    """Base class for all Pydantic models in hexdoc.
44
45    Sets the default model config, and overrides __init__ to allow using the
46    `init_context` context manager to set validation context for constructors.
47    """
48
49    model_config = DEFAULT_CONFIG
50    """@private"""
51
52    __hexdoc_before_validator__: ClassVar[ModelBeforeValidator | None] = None
53
54    def __init__(__pydantic_self__, **data: Any) -> None:  # type: ignore
55        __tracebackhide__ = True
56        __pydantic_self__.__pydantic_validator__.validate_python(
57            data,
58            self_instance=__pydantic_self__,
59            context=_init_context_var.get(),
60        )
61
62    __init__.__pydantic_base_init__ = True  # type: ignore
63
64    @model_validator(mode="before")
65    @classmethod
66    def _call_hexdoc_before_validator(cls, value: Any, info: ValidationInfo):
67        # allow json schema field in all models
68        if isinstance(value, dict):
69            value = cast(dict[Any, Any], value)
70            value.pop("$schema", None)
71        if cls.__hexdoc_before_validator__:
72            return cls.__hexdoc_before_validator__(cls, value, info)
73        return value

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

HexdocModel(**data: Any)
54    def __init__(__pydantic_self__, **data: Any) -> None:  # type: ignore
55        __tracebackhide__ = True
56        __pydantic_self__.__pydantic_validator__.validate_python(
57            data,
58            self_instance=__pydantic_self__,
59            context=_init_context_var.get(),
60        )

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

class HexdocSettings(pydantic_settings.main.BaseSettings):
76class HexdocSettings(BaseSettings):
77    model_config = SettingsConfigDict(
78        env_file=".env",
79        extra="allow",
80    )
81
82    @classmethod
83    def model_getenv(cls, defaults: Any = None):
84        return cls.model_validate(defaults or {})

Base class for settings, allowing values to be overridden by environment variables.

This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose), Heroku and any 12 factor app design.

All the below attributes can be set via model_config.

Args: _case_sensitive: Whether environment and CLI variable names should be read with case-sensitivity. Defaults to None. _nested_model_default_partial_update: Whether to allow partial updates on nested model default object fields. Defaults to False. _env_prefix: Prefix for all environment variables. Defaults to None. _env_file: The env file(s) to load settings values from. Defaults to Path(''), which means that the value from model_config['env_file'] should be used. You can also pass None to indicate that environment variables should not be loaded from an env file. _env_file_encoding: The env file encoding, e.g. 'latin-1'. Defaults to None. _env_ignore_empty: Ignore environment variables where the value is an empty string. Default to False. _env_nested_delimiter: The nested env values delimiter. Defaults to None. _env_nested_max_split: The nested env values maximum nesting. Defaults to None, which means no limit. _env_parse_none_str: The env string value that should be parsed (e.g. "null", "void", "None", etc.) into None type(None). Defaults to None type(None), which means no parsing should occur. _env_parse_enums: Parse enum field names to values. Defaults to None., which means no parsing should occur. _cli_prog_name: The CLI program name to display in help text. Defaults to None if _cli_parse_args is None. Otherwse, defaults to sys.argv[0]. _cli_parse_args: The list of CLI arguments to parse. Defaults to None. If set to True, defaults to sys.argv[1:]. _cli_settings_source: Override the default CLI settings source with a user defined instance. Defaults to None. _cli_parse_none_str: The CLI string value that should be parsed (e.g. "null", "void", "None", etc.) into None type(None). Defaults to _env_parse_none_str value if set. Otherwise, defaults to "null" if _cli_avoid_json is False, and "None" if _cli_avoid_json is True. _cli_hide_none_type: Hide None values in CLI help text. Defaults to False. _cli_avoid_json: Avoid complex JSON objects in CLI help text. Defaults to False. _cli_enforce_required: Enforce required fields at the CLI. Defaults to False. _cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions. Defaults to False. _cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs. Defaults to True. _cli_prefix: The root parser command line arguments prefix. Defaults to "". _cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. _cli_implicit_flags: Whether bool fields should be implicitly converted into CLI boolean flags. (e.g. --flag, --no-flag). Defaults to False. _cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to False. _cli_kebab_case: CLI args use kebab case. Defaults to False. _secrets_dir: The secret files directory or a sequence of directories. Defaults to None.

model_config = {'extra': 'allow', 'arbitrary_types_allowed': True, 'validate_default': True, 'case_sensitive': False, 'env_prefix': '', 'nested_model_default_partial_update': False, 'env_file': '.env', 'env_file_encoding': None, 'env_ignore_empty': False, 'env_nested_delimiter': None, 'env_nested_max_split': None, 'env_parse_none_str': None, 'env_parse_enums': None, 'cli_prog_name': None, 'cli_parse_args': None, 'cli_parse_none_str': None, 'cli_hide_none_type': False, 'cli_avoid_json': False, 'cli_enforce_required': False, 'cli_use_class_docs_for_groups': False, 'cli_exit_on_error': True, 'cli_prefix': '', 'cli_flag_prefix_char': '-', 'cli_implicit_flags': False, 'cli_ignore_unknown_args': False, 'cli_kebab_case': False, 'json_file': None, 'json_file_encoding': None, 'yaml_file': None, 'yaml_file_encoding': None, 'toml_file': None, 'secrets_dir': None, 'protected_namespaces': ('model_validate', 'model_dump', 'settings_customise_sources'), 'enable_decoding': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

@classmethod
def model_getenv(cls, defaults: Any = None):
82    @classmethod
83    def model_getenv(cls, defaults: Any = None):
84        return cls.model_validate(defaults or {})
HexdocTypeAdapter = <class 'pydantic.type_adapter.TypeAdapter'>
class IDModel(hexdoc.model.HexdocModel):
23class IDModel(HexdocModel):
24    id: SkipJsonSchema[ResourceLocation]
25    resource_dir: SkipJsonSchema[PathResourceDir]
26
27    @classmethod
28    def load(
29        cls,
30        resource_dir: PathResourceDir,
31        id: ResourceLocation,
32        data: JSONDict,
33        context: dict[str, Any],
34    ) -> Self:
35        logger.log(TRACE, f"Load {cls} at {id}")
36        return cls.model_validate(
37            data | {"id": id, "resource_dir": resource_dir},
38            context=context,
39        )

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

id: typing.Annotated[hexdoc.core.ResourceLocation, SkipJsonSchema()]
resource_dir: typing.Annotated[hexdoc.core.PathResourceDir, SkipJsonSchema()]
@classmethod
def load( cls, resource_dir: hexdoc.core.PathResourceDir, id: hexdoc.core.ResourceLocation, data: dict[str, JsonValue], context: dict[str, typing.Any]) -> Self:
27    @classmethod
28    def load(
29        cls,
30        resource_dir: PathResourceDir,
31        id: ResourceLocation,
32        data: JSONDict,
33        context: dict[str, Any],
34    ) -> Self:
35        logger.log(TRACE, f"Load {cls} at {id}")
36        return cls.model_validate(
37            data | {"id": id, "resource_dir": resource_dir},
38            context=context,
39        )
@dataclass_transform()
class InlineItemModel(hexdoc.model.inline.BaseInlineModel, abc.ABC):
 70@dataclass_transform()
 71class InlineItemModel(BaseInlineModel, ABC):
 72    @classmethod
 73    def _id_type(cls):
 74        return ItemStack
 75
 76    @classmethod
 77    @abstractmethod
 78    def load_id(cls, item: ItemStack, context: dict[str, Any]) -> Any: ...
 79
 80    @model_validator(mode="wrap")
 81    @classmethod
 82    def _wrap_root_load_from_id(
 83        cls,
 84        value: Any,
 85        handler: ModelWrapValidatorHandler[Self],
 86        info: ValidationInfo,
 87    ) -> Self:
 88        """Loads the recipe from json if the actual value is a resource location str.
 89
 90        Uses a wrap validator so we load the file *before* resolving the tagged union.
 91        """
 92        # if necessary, convert the id to a ItemStack
 93        match value:
 94            case str():
 95                item = ItemStack.from_str(value)
 96            case ItemStack() as item:
 97                pass
 98            case ResourceLocation() as id:
 99                item = ItemStack(namespace=id.namespace, path=id.path)
100            case _:
101                return handler(value)
102
103        # load the data
104        assert info.context is not None
105        return handler(cls.load_id(item, info.context))

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
@abstractmethod
def load_id( cls, item: hexdoc.core.ItemStack, context: dict[str, typing.Any]) -> Any:
76    @classmethod
77    @abstractmethod
78    def load_id(cls, item: ItemStack, context: dict[str, Any]) -> Any: ...
@dataclass_transform()
class InlineModel(hexdoc.model.inline.BaseInlineModel, abc.ABC):
34@dataclass_transform()
35class InlineModel(BaseInlineModel, ABC):
36    @classmethod
37    def _id_type(cls):
38        return ResourceLocation
39
40    @classmethod
41    @abstractmethod
42    def load_id(cls, id: ResourceLocation, context: dict[str, Any]) -> Any: ...
43
44    @model_validator(mode="wrap")
45    @classmethod
46    def _wrap_root_load_from_id(
47        cls,
48        value: Any,
49        handler: ModelWrapValidatorHandler[Self],
50        info: ValidationInfo,
51    ) -> Self:
52        """Loads the recipe from json if the actual value is a resource location str.
53
54        Uses a wrap validator so we load the file *before* resolving the tagged union.
55        """
56        # if necessary, convert the id to a ResourceLocation
57        match value:
58            case str():
59                id = ResourceLocation.from_str(value)
60            case ResourceLocation() as id:
61                pass
62            case _:
63                return handler(value)
64
65        # load the data
66        assert info.context is not None
67        return handler(cls.load_id(id, info.context))

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
@abstractmethod
def load_id( cls, id: hexdoc.core.ResourceLocation, context: dict[str, typing.Any]) -> Any:
40    @classmethod
41    @abstractmethod
42    def load_id(cls, id: ResourceLocation, context: dict[str, Any]) -> Any: ...
class InternallyTaggedUnion(hexdoc.model.HexdocModel):
 44class InternallyTaggedUnion(HexdocModel):
 45    """Implements [internally tagged unions](https://serde.rs/enum-representations.html#internally-tagged)
 46    using the [Registry pattern](https://charlesreid1.github.io/python-patterns-the-registry.html).
 47
 48    To ensure your subtypes are loaded even if they're not imported by any file, add a
 49    Pluggy hook implementation for `hexdoc_load_tagged_unions() -> list[Package]`.
 50
 51    Subclasses MUST NOT be generic unless they provide a default value for all
 52    `__init_subclass__` arguments. See pydantic/7171 for more info.
 53
 54    Args:
 55        key: The dict key for the internal tag. If None, the parent's value is used.
 56        value: The expected tag value for this class. Should be None for types which
 57            shouldn't be instantiated (eg. abstract classes).
 58    """
 59
 60    # inherited
 61    _tag_key: ClassVar[str | None] = None
 62    _tag_value: ClassVar[TagValue | None] = None
 63
 64    # per-class
 65    __all_subtypes: ClassVar[set[type[Self]]]
 66    __concrete_subtypes: ClassVar[defaultdict[TagValue, set[type[Self]]]]
 67
 68    def __init_subclass__(
 69        cls,
 70        *,
 71        key: str | InheritType | None = Inherit,
 72        value: TagValue | InheritType | None = Inherit,
 73        **kwargs: Unpack[ConfigDict],
 74    ):
 75        super().__init_subclass__(**kwargs)
 76
 77        # inherited data
 78        if key is not Inherit:
 79            cls._tag_key = key
 80        if value is not Inherit:
 81            cls._tag_value = value
 82
 83        # don't bother with rest of init if it's not part of a union
 84        if cls._tag_key is None:
 85            if cls._tag_value is None:
 86                return
 87            raise ValueError(
 88                f"Expected value=None for {cls} with key=None, got {value}"
 89            )
 90
 91        # per-class data and lookups
 92        cls.__all_subtypes = set()
 93        cls.__concrete_subtypes = defaultdict(set)
 94
 95        # add to all the parents
 96        for supertype in cls._supertypes():
 97            supertype.__all_subtypes.add(cls)
 98            if cls._tag_value is not None:
 99                supertype.__concrete_subtypes[cls._tag_value].add(cls)
100
101    @classmethod
102    def _tag_key_or_raise(cls) -> str:
103        if cls._tag_key is None:
104            raise NotImplementedError
105        return cls._tag_key
106
107    @classmethod
108    def _supertypes(cls) -> Generator[type[InternallyTaggedUnion], None, None]:
109        tag_key = cls._tag_key_or_raise()
110
111        # we consider a type to be its own supertype/subtype
112        yield cls
113
114        # recursively yield bases
115        # stop when we reach a non-union or a type with a different key (or no key)
116        for base in cls.__bases__:
117            if issubclass(base, InternallyTaggedUnion) and base._tag_key == tag_key:
118                yield from base._supertypes()
119
120    @model_validator(mode="wrap")
121    @classmethod
122    def _resolve_from_dict(
123        cls,
124        value: Any,
125        handler: ModelWrapValidatorHandler[Self],
126        info: ValidationInfo,
127    ) -> Self:
128        pm = PluginManager.of(info)
129
130        # load plugins from entry points
131        global _is_loaded
132        if not _is_loaded:
133            more_itertools.consume(pm.load_tagged_unions())
134            _is_loaded = True
135
136        # do this early so we know it's part of a union before returning anything
137        tag_key = cls._tag_key_or_raise()
138
139        # if it's already instantiated, just return it; otherwise ensure it's a dict
140        match value:
141            case InternallyTaggedUnion() if isinstance(value, cls):
142                return value
143            case dict() if _RESOLVED not in value:
144                data: dict[str, Any] = value
145                data[_RESOLVED] = True
146            case _:
147                return handler(value)
148
149        # tag value, eg. "minecraft:crafting_shaped"
150        tag_value = data.get(tag_key, NoValue)
151
152        # list of matching types, eg. [ShapedCraftingRecipe, ModConditionalShapedCraftingRecipe]
153        tag_types = cls.__concrete_subtypes.get(tag_value)
154        if tag_types is None:
155            raise TypeError(f"Unhandled tag: {tag_key}={tag_value} for {cls}: {data}")
156
157        # try all the types
158        exceptions: list[InitErrorDetails] = []
159        matches: dict[type[Self], Self] = {}
160
161        for inner_type in tag_types:
162            try:
163                matches[inner_type] = inner_type.model_validate(
164                    data, context=info.context
165                )
166            except Exception as e:
167                exceptions.append(
168                    InitErrorDetails(
169                        type=PydanticCustomError(
170                            "TaggedUnionMatchError",
171                            "{exception_class}: {exception}",
172                            {
173                                "exception_class": e.__class__.__name__,
174                                "exception": str(e),
175                            },
176                        ),
177                        loc=(
178                            cls.__name__,
179                            inner_type.__name__,
180                        ),
181                        input=data,
182                    )
183                )
184
185        # ensure we only matched one
186        match len(matches):
187            case 1:
188                return matches.popitem()[1]
189            case x if x > 1:
190                ambiguous_types = ", ".join(str(t) for t in matches.keys())
191                reason = f"Ambiguous union match: {ambiguous_types}"
192            case _:
193                reason = "No match found"
194
195        # something went wrong, raise an exception
196        error = PydanticCustomError(
197            "TaggedUnionMatchErrorGroup",
198            (
199                "Failed to match tagged union {class_name}: {reason}\n"
200                "  Tag: {tag_key}={tag_value}\n"
201                "  Types: {types}\n"
202                "  Data: {data}"
203            ),
204            {
205                "class_name": str(cls),
206                "reason": reason,
207                "tag_key": cls._tag_key,
208                "tag_value": tag_value,
209                "types": ", ".join(str(t) for t in tag_types),
210                "data": repr(data),
211            },
212        )
213
214        if exceptions:
215            if _RESOLVED in data:
216                data.pop(_RESOLVED)  # avoid interfering with other types
217            exceptions.insert(
218                0,
219                InitErrorDetails(
220                    type=error,
221                    loc=(cls.__name__,),
222                    input=data,
223                ),
224            )
225            raise ValidationError.from_exception_data(
226                "TaggedUnionMatchError", exceptions
227            )
228
229        raise RuntimeError(str(error))
230
231    @model_validator(mode="before")
232    def _pop_temporary_keys(cls, value: dict[Any, Any] | Any):
233        if isinstance(value, dict) and _RESOLVED in value:
234            # copy because this validator may be called multiple times
235            # eg. two types with the same key
236            value = value.copy()
237            value.pop(_RESOLVED)
238            assert value.pop(cls._tag_key, NoValue) == cls._tag_value
239        return value
240
241    @classmethod
242    @override
243    def __get_pydantic_json_schema__(
244        cls,
245        core_schema: cs.CoreSchema,
246        handler: GetJsonSchemaHandler,
247    ) -> JsonSchemaValue:
248        base_schema = handler.resolve_ref_schema(
249            super().__get_pydantic_json_schema__(core_schema, handler)
250        )
251
252        if cls._tag_key is None:
253            return base_schema
254
255        properties = base_schema.setdefault("properties", {})
256
257        properties[cls._tag_key] = handler(cs.literal_schema([cls._tag_value]))
258        base_schema.setdefault("required", []).append(cls._tag_key)
259
260        if cls._tag_value is not None:
261            return base_schema
262
263        subtypes = {
264            subtype
265            for subtypes in cls.__concrete_subtypes.values()
266            for subtype in subtypes
267            if subtype is not cls
268        }
269        if not subtypes:
270            return base_schema
271
272        union_schema = cs.union_schema(
273            [subtype.__pydantic_core_schema__ for subtype in subtypes],
274        )
275        json_schema = handler.resolve_ref_schema(handler(union_schema))
276
277        if any_of := json_schema.get("anyOf"):
278            other_schema = base_schema | {
279                "additionalProperties": True,
280                "properties": (
281                    properties
282                    | {
283                        cls._tag_key: {
284                            "allOf": [
285                                handler(cls._tag_value_schema),
286                                {
287                                    "not": handler(
288                                        cs.union_schema(
289                                            [
290                                                cs.literal_schema([subtype._tag_value])
291                                                for subtype in subtypes
292                                                if subtype._tag_value
293                                                not in {None, NoValue}
294                                            ]
295                                        )
296                                    )
297                                },
298                            ],
299                        },
300                    }
301                ),
302            }
303            any_of.append(other_schema)
304
305        return json_schema
306
307    @classproperty
308    @classmethod
309    def _tag_value_type(cls) -> type[Any]:
310        return str
311
312    @classproperty
313    @classmethod
314    def _tag_value_schema(cls):
315        return TypeAdapter(cls._tag_value_type).core_schema

Implements internally tagged unions using the Registry pattern.

To ensure your subtypes are loaded even if they're not imported by any file, add a Pluggy hook implementation for hexdoc_load_tagged_unions() -> list[Package].

Subclasses MUST NOT be generic unless they provide a default value for all __init_subclass__ arguments. See pydantic/7171 for more info.

Args: key: The dict key for the internal tag. If None, the parent's value is used. value: The expected tag value for this class. Should be None for types which shouldn't be instantiated (eg. abstract classes).

NoValue = <NoValueType._token: 0>
class NoValueType(enum.Enum):
 8class NoValueType(Enum):
 9    """Type of NoValue, a singleton representing the value of a nonexistent dict key."""
10
11    _token = 0
12
13    def __str__(self):
14        return "NoValue"
15
16    def __bool__(self) -> Literal[False]:
17        return False

Type of NoValue, a singleton representing the value of a nonexistent dict key.

class ResourceModel(hexdoc.model.IDModel, hexdoc.model.InlineModel, abc.ABC):
42class ResourceModel(IDModel, InlineModel, ABC):
43    @classmethod
44    def load_id(cls, id: ResourceLocation, context: dict[str, Any]):
45        loader = ModResourceLoader.of(context)
46        resource_dir, data = cls.load_resource(id, loader)
47        return cls.load(resource_dir, id, data, context)
48
49    @classmethod
50    @abstractmethod
51    def load_resource(
52        cls,
53        id: ResourceLocation,
54        loader: ModResourceLoader,
55    ) -> tuple[PathResourceDir, JSONDict]: ...

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

@classmethod
def load_id( cls, id: hexdoc.core.ResourceLocation, context: dict[str, typing.Any]):
43    @classmethod
44    def load_id(cls, id: ResourceLocation, context: dict[str, Any]):
45        loader = ModResourceLoader.of(context)
46        resource_dir, data = cls.load_resource(id, loader)
47        return cls.load(resource_dir, id, data, context)
@classmethod
@abstractmethod
def load_resource( cls, id: hexdoc.core.ResourceLocation, loader: hexdoc.core.ModResourceLoader) -> tuple[hexdoc.core.PathResourceDir, dict[str, JsonValue]]:
49    @classmethod
50    @abstractmethod
51    def load_resource(
52        cls,
53        id: ResourceLocation,
54        loader: ModResourceLoader,
55    ) -> tuple[PathResourceDir, JSONDict]: ...
@dataclass_transform()
class StripHiddenModel(hexdoc.model.HexdocModel):
20@dataclass_transform()
21class StripHiddenModel(HexdocModel):
22    """Base model which removes all keys starting with _ before validation."""
23
24    model_config = DEFAULT_CONFIG | ConfigDict(
25        json_schema_extra=_json_schema_extra,
26    )
27
28    @model_validator(mode="before")
29    def _pre_root_strip_hidden(cls, values: dict[Any, Any] | Any) -> Any:
30        if not isinstance(values, dict):
31            return values
32
33        return {
34            key: value
35            for key, value in values.items()
36            if not (isinstance(key, str) and (key.startswith("_") or key == "$schema"))
37        }

Base model which removes all keys starting with _ before validation.

TagValue = str | NoValueType
class TypeTaggedTemplate(hexdoc.model.TypeTaggedUnion, abc.ABC):
347class TypeTaggedTemplate(TypeTaggedUnion, ABC, type=None):
348    __template_id: ClassVar[ResourceLocation]
349
350    def __init_subclass__(
351        cls,
352        *,
353        type: str | InheritType | None = Inherit,
354        template_type: str | None = None,
355        **kwargs: Unpack[ConfigDict],
356    ) -> None:
357        super().__init_subclass__(type=type, **kwargs)
358
359        # jinja template path
360        if template_type is not None:
361            template_id = ResourceLocation.from_str(template_type)
362        else:
363            template_id = cls.type
364
365        if template_id:
366            cls.__template_id = template_id
367
368    @classproperty
369    @classmethod
370    @abstractmethod
371    def template(cls) -> str:
372        """Returns the Jinja template path for this class without any file extension.
373
374        For example, return `"pages/{path}"`, not `"pages/{path}.html.jinja"`.
375        """
376
377    @classproperty
378    @classmethod
379    def template_id(cls):
380        return cls.__template_id

Implements internally tagged unions using the Registry pattern.

To ensure your subtypes are loaded even if they're not imported by any file, add a Pluggy hook implementation for hexdoc_load_tagged_unions() -> list[Package].

Subclasses MUST NOT be generic unless they provide a default value for all __init_subclass__ arguments. See pydantic/7171 for more info.

Args: key: The dict key for the internal tag. If None, the parent's value is used. value: The expected tag value for this class. Should be None for types which shouldn't be instantiated (eg. abstract classes).

def template(unknown):

Equivalent of classmethod(property(...)).

Use @classproperty. Do not instantiate this class directly.

def template_id(unknown):

Equivalent of classmethod(property(...)).

Use @classproperty. Do not instantiate this class directly.

class TypeTaggedUnion(hexdoc.model.InternallyTaggedUnion):
318class TypeTaggedUnion(InternallyTaggedUnion, key="type", value=None):
319    _type: ClassVar[ResourceLocation | NoValueType | None] = None
320
321    def __init_subclass__(
322        cls,
323        *,
324        type: TagValue | InheritType | None = Inherit,
325        **kwargs: Unpack[ConfigDict],
326    ):
327        super().__init_subclass__(value=type, **kwargs)
328
329        match cls._tag_value:
330            case str(raw_value):
331                cls._type = ResourceLocation.from_str(raw_value)
332            case value:
333                cls._type = value
334
335    @classproperty
336    @classmethod
337    def type(cls):
338        return cls._type
339
340    @classproperty
341    @classmethod
342    @override
343    def _tag_value_type(cls) -> type[Any]:
344        return ResourceLocation

Implements internally tagged unions using the Registry pattern.

To ensure your subtypes are loaded even if they're not imported by any file, add a Pluggy hook implementation for hexdoc_load_tagged_unions() -> list[Package].

Subclasses MUST NOT be generic unless they provide a default value for all __init_subclass__ arguments. See pydantic/7171 for more info.

Args: key: The dict key for the internal tag. If None, the parent's value is used. value: The expected tag value for this class. Should be None for types which shouldn't be instantiated (eg. abstract classes).

def type(unknown):

Equivalent of classmethod(property(...)).

Use @classproperty. Do not instantiate this class directly.

class ValidationContextModel(hexdoc.model.HexdocModel, hexdoc.utils.context.ValidationContext):
87class ValidationContextModel(HexdocModel, ValidationContext):
88    pass

Base class for all Pydantic models in hexdoc.

Sets the default model config, and overrides __init__ to allow using the init_context context manager to set validation context for constructors.

def init_context(value: Any):
36def init_context(value: Any):
37    """https://docs.pydantic.dev/latest/usage/validators/#using-validation-context-with-basemodel-initialization"""
38    return set_contextvar(_init_context_var, value)