Skip to content

Commit

Permalink
Allow sentinel values to be used as types rather than instances.
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewwardrop committed Nov 30, 2024
1 parent 8230d6e commit 3d7bd48
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 42 deletions.
28 changes: 14 additions & 14 deletions formulaic/materializers/types/factor_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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)
19 changes: 13 additions & 6 deletions formulaic/parser/types/structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
Union,
)

from formulaic.utils.sentinels import MISSING

ItemType = TypeVar("ItemType")
_MISSING = object()


class Structured(Generic[ItemType]):
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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__(
**{
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 14 additions & 20 deletions formulaic/utils/sentinels.py
Original file line number Diff line number Diff line change
@@ -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.
"""
4 changes: 2 additions & 2 deletions tests/utils/test_sentinels.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 3d7bd48

Please sign in to comment.