diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c2cb24f..ea8402d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ v0.4.0 (2023-12-12) ------------------------------------------------------------ * **BREAKING** :mod:`accessors` is imported implicitly. User code does not need to import it any longer :pull:`45` +* Add :attr:`~selectors.All` and :attr:`~selectors.None_` completing selector group + :pull:`46` * Fix type hints of function :func:`~core.aggregatelevel` :pull:`44` * Switch from black to ruff for formatting and update pre-commit versions :pull:`43` diff --git a/src/pandas_indexing/__init__.py b/src/pandas_indexing/__init__.py index 669043c..b13b7ce 100644 --- a/src/pandas_indexing/__init__.py +++ b/src/pandas_indexing/__init__.py @@ -21,7 +21,7 @@ to_tidy, uniquelevel, ) -from .selectors import isin, ismatch +from .selectors import All, None_, isin, ismatch from .units import convert_unit, dequantify, quantify, set_openscm_registry_as_default diff --git a/src/pandas_indexing/selectors.py b/src/pandas_indexing/selectors.py index aad0800..a5b127f 100644 --- a/src/pandas_indexing/selectors.py +++ b/src/pandas_indexing/selectors.py @@ -26,9 +26,13 @@ def __invert__(self): return Not(self) def __and__(self, other): + if isinstance(other, Special): + return other & self return And(self, maybe_const(other)) def __or__(self, other): + if isinstance(other, Special): + return other | self return Or(self, maybe_const(other)) __rand__ = __and__ @@ -69,6 +73,48 @@ def __call__(self, df): return ~self.a.__call__(df) +class Special(Selector): + pass + + +@define +class AllSelector(Special): + def __invert__(self): + return NoneSelector() + + def __and__(self, other): + return other + + def __or__(self, other): + return self + + def __call__(self, df): + return Series(True, df.index) + + +@define +class NoneSelector(Special): + def __invert__(self): + return AllSelector() + + def __and__(self, other): + return self + + def __or__(self, other): + return other + + __rand__ = __and__ + __ror__ = __or__ + + def __call__(self, df): + return Series(False, df.index) + + +# Singletons for easy access +All = AllSelector() +None_ = NoneSelector() + + @define class Isin(Selector): filters: Mapping[str, Any] diff --git a/tests/test_selectors.py b/tests/test_selectors.py index e0e06fb..ac474d9 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -5,7 +5,16 @@ from pandas.testing import assert_frame_equal, assert_series_equal from pandas_indexing import isin, ismatch -from pandas_indexing.selectors import And, Const, Isin, Ismatch, Not, Or +from pandas_indexing.selectors import ( + AllSelector, + And, + Const, + Isin, + Ismatch, + NoneSelector, + Not, + Or, +) def test_isin_mseries(mseries: Series): @@ -57,3 +66,23 @@ def test_ismatch_explicitly_given(sdf): assert_series_equal( ismatch(sdf.columns, ["o*"]), Series([True, False], sdf.columns) ) + + +def test_all_none(sdf): + assert isinstance(~AllSelector(), NoneSelector) + assert isinstance(~NoneSelector(), AllSelector) + + sel = isin(str="bar") + + assert isinstance(sel | AllSelector(), AllSelector) + assert isinstance(AllSelector() | sel, AllSelector) + assert sel & AllSelector() == sel + assert AllSelector() & sel == sel + + assert sel | NoneSelector() == sel + assert NoneSelector() | sel == sel + assert isinstance(sel & NoneSelector(), NoneSelector) + assert isinstance(NoneSelector() & sel, NoneSelector) + + assert_frame_equal(sdf.loc[AllSelector()], sdf) + assert_frame_equal(sdf.loc[NoneSelector()], sdf.iloc[[]])