hexdoc.core.compat
1from __future__ import annotations 2 3from dataclasses import dataclass, field 4from typing import Annotated, Any, ClassVar, Generic, Protocol, TypeVar 5 6from packaging.specifiers import SpecifierSet 7from pydantic import GetCoreSchemaHandler, ValidationInfo 8from pydantic_core import core_schema 9from typing_extensions import override 10 11from hexdoc.model.base import HexdocModel 12 13_T = TypeVar("_T") 14 15_T_ModelType = TypeVar("_T_ModelType", bound=type[HexdocModel]) 16 17_If = TypeVar("_If") 18_Else = TypeVar("_Else") 19 20 21class VersionSource(Protocol): 22 @classmethod 23 def get(cls) -> str | None: 24 """Returns the current version.""" 25 ... 26 27 @classmethod 28 def matches(cls, specifier: str | SpecifierSet) -> bool: 29 """Returns True if the current version matches the version_spec.""" 30 ... 31 32 33class MinecraftVersion(VersionSource): 34 MINECRAFT_VERSION: ClassVar[str | None] = None 35 36 @override 37 @classmethod 38 def get(cls) -> str | None: 39 return cls.MINECRAFT_VERSION 40 41 @override 42 @classmethod 43 def matches(cls, specifier: str | SpecifierSet) -> bool: 44 if isinstance(specifier, str): 45 specifier = SpecifierSet(specifier) 46 if (version := cls.get()) is None: 47 return True 48 return version in specifier 49 50 51@dataclass(frozen=True) 52class Versioned: 53 """Base class for types which can behave differently based on a version source, 54 which defaults to MinecraftVersion.""" 55 56 version_spec: str 57 version_source: VersionSource = field(default=MinecraftVersion, kw_only=True) 58 59 @property 60 def is_current(self): 61 return self.version_source.matches(self.version_spec) 62 63 64@dataclass(frozen=True) 65class IsVersion(Versioned): 66 """Instances of this class are truthy if version_spec matches version_source, which 67 defaults to MinecraftVersion. 68 69 Can be used as a Pydantic validator annotation, which raises ValueError if 70 version_spec doesn't match the current version. Use it like this: 71 72 `Annotated[str, IsVersion(">=1.20")] | Annotated[None, IsVersion("<1.20")]` 73 74 Can also be used as a class decorator for Pydantic models, which raises ValueError 75 when validating the model if version_spec doesn't match the current version. 76 Decorated classes must subclass HexdocModel (or HexdocBaseModel). 77 """ 78 79 def __bool__(self): 80 return self.is_current 81 82 def __call__(self, cls: _T_ModelType) -> _T_ModelType: 83 cls.__hexdoc_before_validator__ = self._model_validator 84 return cls 85 86 def __get_pydantic_core_schema__( 87 self, 88 source_type: type[Any], 89 handler: GetCoreSchemaHandler, 90 ) -> core_schema.CoreSchema: 91 return core_schema.no_info_before_validator_function( 92 self._schema_validator, 93 schema=handler(source_type), 94 ) 95 96 def _schema_validator(self, value: Any): 97 if self.is_current: 98 return value 99 raise ValueError( 100 f"Expected version {self.version_spec}, got {self.version_source.get()}" 101 ) 102 103 def _model_validator(self, cls: Any, value: Any, info: ValidationInfo): 104 return self._schema_validator(value) 105 106 107Before_1_19 = Annotated[_T, IsVersion("<1.19")] 108"""Alias for `Annotated[_T, IsVersion("<1.19")]`.""" 109AtLeast_1_19 = Annotated[_T, IsVersion(">=1.19")] 110"""Alias for `Annotated[_T, IsVersion(">=1.19")]`.""" 111After_1_19 = Annotated[_T, IsVersion(">1.19")] 112"""Alias for `Annotated[_T, IsVersion("<1.19")]`.""" 113 114Before_1_20 = Annotated[_T, IsVersion("<1.20")] 115"""Alias for `Annotated[_T, IsVersion("<1.20")]`.""" 116AtLeast_1_20 = Annotated[_T, IsVersion(">=1.20")] 117"""Alias for `Annotated[_T, IsVersion(">=1.20")]`.""" 118After_1_20 = Annotated[_T, IsVersion(">1.20")] 119"""Alias for `Annotated[_T, IsVersion("<1.20")]`.""" 120 121 122@dataclass(frozen=True) 123class ValueIfVersion(Versioned, Generic[_If, _Else]): 124 value_if: _If 125 value_else: _Else 126 127 def __call__(self) -> _If | _Else: 128 if self.is_current: 129 return self.value_if 130 return self.value_else
22class VersionSource(Protocol): 23 @classmethod 24 def get(cls) -> str | None: 25 """Returns the current version.""" 26 ... 27 28 @classmethod 29 def matches(cls, specifier: str | SpecifierSet) -> bool: 30 """Returns True if the current version matches the version_spec.""" 31 ...
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto(Protocol[T]):
def meth(self) -> T:
...
1953def _no_init_or_replace_init(self, *args, **kwargs): 1954 cls = type(self) 1955 1956 if cls._is_protocol: 1957 raise TypeError('Protocols cannot be instantiated') 1958 1959 # Already using a custom `__init__`. No need to calculate correct 1960 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1961 if cls.__init__ is not _no_init_or_replace_init: 1962 return 1963 1964 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1965 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1966 # searches for a proper new `__init__` in the MRO. The new `__init__` 1967 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1968 # instantiation of the protocol subclass will thus use the new 1969 # `__init__` and no longer call `_no_init_or_replace_init`. 1970 for base in cls.__mro__: 1971 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1972 if init is not _no_init_or_replace_init: 1973 cls.__init__ = init 1974 break 1975 else: 1976 # should not happen 1977 cls.__init__ = object.__init__ 1978 1979 cls.__init__(self, *args, **kwargs)
28 @classmethod 29 def matches(cls, specifier: str | SpecifierSet) -> bool: 30 """Returns True if the current version matches the version_spec.""" 31 ...
Returns True if the current version matches the version_spec.
34class MinecraftVersion(VersionSource): 35 MINECRAFT_VERSION: ClassVar[str | None] = None 36 37 @override 38 @classmethod 39 def get(cls) -> str | None: 40 return cls.MINECRAFT_VERSION 41 42 @override 43 @classmethod 44 def matches(cls, specifier: str | SpecifierSet) -> bool: 45 if isinstance(specifier, str): 46 specifier = SpecifierSet(specifier) 47 if (version := cls.get()) is None: 48 return True 49 return version in specifier
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto(Protocol[T]):
def meth(self) -> T:
...
42 @override 43 @classmethod 44 def matches(cls, specifier: str | SpecifierSet) -> bool: 45 if isinstance(specifier, str): 46 specifier = SpecifierSet(specifier) 47 if (version := cls.get()) is None: 48 return True 49 return version in specifier
Returns True if the current version matches the version_spec.
52@dataclass(frozen=True) 53class Versioned: 54 """Base class for types which can behave differently based on a version source, 55 which defaults to MinecraftVersion.""" 56 57 version_spec: str 58 version_source: VersionSource = field(default=MinecraftVersion, kw_only=True) 59 60 @property 61 def is_current(self): 62 return self.version_source.matches(self.version_spec)
Base class for types which can behave differently based on a version source, which defaults to MinecraftVersion.
65@dataclass(frozen=True) 66class IsVersion(Versioned): 67 """Instances of this class are truthy if version_spec matches version_source, which 68 defaults to MinecraftVersion. 69 70 Can be used as a Pydantic validator annotation, which raises ValueError if 71 version_spec doesn't match the current version. Use it like this: 72 73 `Annotated[str, IsVersion(">=1.20")] | Annotated[None, IsVersion("<1.20")]` 74 75 Can also be used as a class decorator for Pydantic models, which raises ValueError 76 when validating the model if version_spec doesn't match the current version. 77 Decorated classes must subclass HexdocModel (or HexdocBaseModel). 78 """ 79 80 def __bool__(self): 81 return self.is_current 82 83 def __call__(self, cls: _T_ModelType) -> _T_ModelType: 84 cls.__hexdoc_before_validator__ = self._model_validator 85 return cls 86 87 def __get_pydantic_core_schema__( 88 self, 89 source_type: type[Any], 90 handler: GetCoreSchemaHandler, 91 ) -> core_schema.CoreSchema: 92 return core_schema.no_info_before_validator_function( 93 self._schema_validator, 94 schema=handler(source_type), 95 ) 96 97 def _schema_validator(self, value: Any): 98 if self.is_current: 99 return value 100 raise ValueError( 101 f"Expected version {self.version_spec}, got {self.version_source.get()}" 102 ) 103 104 def _model_validator(self, cls: Any, value: Any, info: ValidationInfo): 105 return self._schema_validator(value)
Instances of this class are truthy if version_spec matches version_source, which defaults to MinecraftVersion.
Can be used as a Pydantic validator annotation, which raises ValueError if version_spec doesn't match the current version. Use it like this:
Annotated[str, IsVersion(">=1.20")] | Annotated[None, IsVersion("<1.20")]
Can also be used as a class decorator for Pydantic models, which raises ValueError when validating the model if version_spec doesn't match the current version. Decorated classes must subclass HexdocModel (or HexdocBaseModel).
Alias for Annotated[_T, IsVersion("<1.19")]
.
Alias for Annotated[_T, IsVersion(">=1.19")]
.
Alias for Annotated[_T, IsVersion("<1.19")]
.
Alias for Annotated[_T, IsVersion("<1.20")]
.
Alias for Annotated[_T, IsVersion(">=1.20")]
.
Alias for Annotated[_T, IsVersion("<1.20")]
.