From 3d7bd48cc68d064e137027194b6693c719dad44f Mon Sep 17 00:00:00 2001 From: Matthew Wardrop Date: Fri, 29 Nov 2024 20:34:16 -0800 Subject: [PATCH] Allow sentinel values to be used as types rather than instances. --- .../materializers/types/factor_values.py | 28 +++++++-------- formulaic/parser/types/structured.py | 19 +++++++---- formulaic/utils/sentinels.py | 34 ++++++++----------- tests/utils/test_sentinels.py | 4 +-- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/formulaic/materializers/types/factor_values.py b/formulaic/materializers/types/factor_values.py index 3c2c1fe5..2c45acba 100644 --- a/formulaic/materializers/types/factor_values.py +++ b/formulaic/materializers/types/factor_values.py @@ -24,7 +24,7 @@ import wrapt from formulaic.parser.types import Factor -from formulaic.utils.sentinels import MISSING, _MissingType +from formulaic.utils.sentinels import MISSING if TYPE_CHECKING: # pragma: no cover from formulaic.model_spec import ModelSpec @@ -103,21 +103,21 @@ class FactorValues(Generic[T], wrapt.ObjectProxy): def __init__( self, values: Any, - metadata: Union[FactorValuesMetadata, _MissingType] = MISSING, + metadata: Union[FactorValuesMetadata, MISSING] = MISSING, *, - kind: Union[str, Factor.Kind, _MissingType] = MISSING, - column_names: Union[Tuple[Hashable, ...], _MissingType] = MISSING, - format: Union[str, _MissingType] = MISSING, # pylint: disable=redefined-builtin - encoded: Union[bool, _MissingType] = MISSING, + kind: Union[str, Factor.Kind, MISSING] = MISSING, + column_names: Union[Tuple[Hashable, ...], MISSING] = MISSING, + format: Union[str, MISSING] = MISSING, # pylint: disable=redefined-builtin + encoded: Union[bool, MISSING] = MISSING, encoder: Union[ None, Callable[[Any, bool, List[int], Dict[str, Any], ModelSpec], Any], - _MissingType, + MISSING, ] = MISSING, - spans_intercept: Union[bool, _MissingType] = MISSING, - drop_field: Union[None, Hashable, _MissingType] = MISSING, - reduced: Union[bool, _MissingType] = MISSING, - format_reduced: Union[str, _MissingType] = MISSING, + spans_intercept: Union[bool, MISSING] = MISSING, + drop_field: Union[None, Hashable, MISSING] = MISSING, + reduced: Union[bool, MISSING] = MISSING, + format_reduced: Union[str, MISSING] = MISSING, ): metadata_constructor: Callable = FactorValuesMetadata metadata_kwargs = dict( @@ -140,7 +140,7 @@ def __init__( if isinstance(values, FactorValues): values = values.__wrapped__ - if metadata and not isinstance(metadata, _MissingType): + if metadata and not isinstance(metadata, MISSING): metadata_constructor = metadata.replace wrapt.ObjectProxy.__init__(self, values) @@ -169,7 +169,7 @@ def __deepcopy__(self, memo: Any = None) -> FactorValues[T]: def __reduce_ex__( self, protocol: SupportsIndex ) -> Tuple[ - Callable[[Any, Union[FactorValuesMetadata, _MissingType]], FactorValues], - Tuple[Any, Union[FactorValuesMetadata, _MissingType]], + Callable[[Any, Union[FactorValuesMetadata, MISSING]], FactorValues], + Tuple[Any, Union[FactorValuesMetadata, MISSING]], ]: return FactorValues, (self.__wrapped__, self._self_metadata) diff --git a/formulaic/parser/types/structured.py b/formulaic/parser/types/structured.py index 3398d1c7..64b9a009 100644 --- a/formulaic/parser/types/structured.py +++ b/formulaic/parser/types/structured.py @@ -18,8 +18,9 @@ Union, ) +from formulaic.utils.sentinels import MISSING + ItemType = TypeVar("ItemType") -_MISSING = object() class Structured(Generic[ItemType]): @@ -87,7 +88,7 @@ class Structured(Generic[ItemType]): def __init__( self, - root: Any = _MISSING, + root: Any = MISSING, *, _metadata: Optional[Dict[str, Any]] = None, **structure: Any, @@ -97,7 +98,7 @@ def __init__( "Substructure keys cannot start with an underscore. " f"The invalid keys are: {set(key for key in structure if key.startswith('_'))}." ) - if root is not _MISSING: + if root is not MISSING: structure["root"] = root self._metadata = _metadata @@ -306,7 +307,7 @@ def simplify_obj(obj: Any) -> Tuple[Any, bool]: self._structure = structure return self - def _update(self, root: Any = _MISSING, **structure: Any) -> Structured[ItemType]: + def _update(self, root: Any = MISSING, **structure: Any) -> Structured[ItemType]: """ Return a new `Structured` instance that is identical to this one but the root and/or keys replaced with the nominated values. @@ -315,7 +316,7 @@ def _update(self, root: Any = _MISSING, **structure: Any) -> Structured[ItemType root: The (optional) replacement of the root node. structure: Any additional key/values to update in the structure. """ - if root is not _MISSING: + if root is not MISSING: structure["root"] = root return self.__class__( **{ @@ -480,7 +481,13 @@ def __setitem__(self, key: Any, value: Any) -> Any: self._structure[key] = self.__prepare_item(key, value) def __iter__(self) -> Generator[Any, None, None]: - if self._has_root and not self._has_keys and isinstance(self.root, Iterable): # pylint: disable=isinstance-second-argument-not-valid-type + if ( + self._has_root + and not self._has_keys + and isinstance( # pylint: disable=isinstance-second-argument-not-valid-type + self.root, Iterable + ) + ): yield from self.root else: if self._has_root: # Always yield root first. diff --git a/formulaic/utils/sentinels.py b/formulaic/utils/sentinels.py index 7d254134..3992bdff 100644 --- a/formulaic/utils/sentinels.py +++ b/formulaic/utils/sentinels.py @@ -1,29 +1,23 @@ from __future__ import annotations -from typing import Dict -from typing_extensions import Self +class _MissingType(type): + """ + This metaclass is used to create singleton falsey classes for use as missing + and/or sentinel placeholder values. + """ + def __repr__(cls): + return cls.__name__ -class _MissingType: - __instance__ = None - - def __new__(cls) -> _MissingType: - if cls.__instance__ is None: - cls.__instance__ = super(_MissingType, cls).__new__(cls) - return cls.__instance__ - - def __bool__(self) -> bool: + def __bool__(cls): return False - def __repr__(self) -> str: - return "MISSING" - - def __copy__(self) -> Self: - return self - - def __deepcopy__(self, memo: Dict) -> Self: - return self + def __call__(cls): + return cls -MISSING = _MissingType() +class MISSING(metaclass=_MissingType): + """ + Used to represent attributes that have not yet been assigned a value. + """ diff --git a/tests/utils/test_sentinels.py b/tests/utils/test_sentinels.py index b21479dc..c3e7c502 100644 --- a/tests/utils/test_sentinels.py +++ b/tests/utils/test_sentinels.py @@ -1,10 +1,10 @@ import copy -from formulaic.utils.sentinels import MISSING, _MissingType +from formulaic.utils.sentinels import MISSING def test_missing(): - assert MISSING is _MissingType() + assert MISSING is MISSING() assert bool(MISSING) is False assert repr(MISSING) == "MISSING" assert copy.copy(MISSING) is MISSING