From a2f0e4890003463872982fbba5cfcb8bf050cbcd Mon Sep 17 00:00:00 2001 From: Jonas Lundberg Date: Thu, 15 Sep 2022 11:24:38 +0200 Subject: [PATCH] Introduce Noop to make pattern non optional --- respx/models.py | 2 +- respx/patterns.py | 45 +++++++++++++++++++++++++++++++++--------- tests/test_patterns.py | 13 ++++++++++++ 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/respx/models.py b/respx/models.py index d804296..39b680b 100644 --- a/respx/models.py +++ b/respx/models.py @@ -161,7 +161,7 @@ def name(self, name: str) -> None: raise NotImplementedError("Can't set name on route.") @property - def pattern(self) -> Optional[Pattern]: + def pattern(self) -> Pattern: return self._pattern @pattern.setter diff --git a/respx/patterns.py b/respx/patterns.py index 59a9e4d..106bf90 100644 --- a/respx/patterns.py +++ b/respx/patterns.py @@ -88,13 +88,26 @@ def __init__(self, value: Any, lookup: Optional[Lookup] = None) -> None: def __iter__(self): yield self + def __bool__(self): + return True + def __and__(self, other: "Pattern") -> "Pattern": + if not bool(other): + return self + elif not bool(self): + return other return _And((self, other)) def __or__(self, other: "Pattern") -> "Pattern": + if not bool(other): + return self + elif not bool(self): + return other return _Or((self, other)) def __invert__(self): + if not bool(self): + return self return _Invert(self) def __repr__(self): # pragma: nocover @@ -159,6 +172,22 @@ def _in(self, value: Any) -> Match: return Match(value in self.value) +class Noop(Pattern): + def __init__(self) -> None: + super().__init__(None) + + def __repr__(self): + return f"<{self.__class__.__name__}>" + + def __bool__(self) -> bool: + # Treat this pattern as non-existent, e.g. when filtering or conditioning + return False + + def match(self, request: httpx.Request) -> Match: + # If this pattern is part of a combined pattern, always be truthy, i.e. noop + return Match(True) + + class PathPattern(Pattern): path: Optional[str] @@ -500,7 +529,7 @@ def clean(self, value: Dict) -> bytes: return data -def M(*patterns: Pattern, **lookups: Any) -> Optional[Pattern]: +def M(*patterns: Pattern, **lookups: Any) -> Pattern: extras = None for pattern__lookup, value in lookups.items(): @@ -550,12 +579,10 @@ def get_scheme_port(scheme: Optional[str]) -> Optional[int]: return {"http": 80, "https": 443}.get(scheme or "") -def combine( - patterns: Sequence[Pattern], op: Callable = operator.and_ -) -> Optional[Pattern]: +def combine(patterns: Sequence[Pattern], op: Callable = operator.and_) -> Pattern: patterns = tuple(filter(None, patterns)) if not patterns: - return None + return Noop() return reduce(op, patterns) @@ -598,14 +625,14 @@ def parse_url_patterns( return bases -def merge_patterns(pattern: Optional[Pattern], **bases: Pattern) -> Optional[Pattern]: +def merge_patterns(pattern: Pattern, **bases: Pattern) -> Pattern: if not bases: return pattern - if pattern: - # Flatten pattern - patterns = list(iter(pattern)) + # Flatten pattern + patterns: List[Pattern] = list(filter(None, iter(pattern))) + if patterns: if "host" in (_pattern.key for _pattern in patterns): # Pattern is "absolute", skip merging bases = {} diff --git a/tests/test_patterns.py b/tests/test_patterns.py index f90c561..f846654 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -15,6 +15,7 @@ Lookup, M, Method, + Noop, Params, Path, Pattern, @@ -66,6 +67,18 @@ def test_match_context(): assert match.context == {"host": "foo.bar", "slug": "baz"} +def test_noop_pattern(): + assert bool(Noop()) is False + assert bool(Noop().match(httpx.Request("GET", "https://example.org"))) is True + assert list(filter(None, [Noop()])) == [] + assert repr(Noop()) == "" + assert isinstance(~Noop(), Noop) + assert Method("GET") & Noop() == Method("GET") + assert Noop() & Method("GET") == Method("GET") + assert Method("GET") | Noop() == Method("GET") + assert Noop() | Method("GET") == Method("GET") + + @pytest.mark.parametrize( "kwargs,url,expected", [