Source code for joythief.strings

"""Matchers for the `text sequence type`_ (:py:class:`str`).

.. _text sequence type: https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str
"""

import json
import re
import typing as tp
from collections.abc import Mapping, Sequence
from urllib.parse import parse_qs, urlparse

from joythief.core import Matcher, MaybeMatcher
from joythief.objects import InstanceOf


[docs] class JsonString(Matcher[str]): """Matches any :py:class:`str` instance representing JSON. :param expected: What the result of parsing the JSON should be. If omitted, any valid JSON string is matched. """ __ANYTHING = object() _expected: tp.Any def __init__(self, expected: tp.Any = __ANYTHING): self._expected = InstanceOf(object) if expected == self.__ANYTHING else expected def __eq__(self, other: tp.Any) -> bool: if not isinstance(other, str): return NotImplemented try: return tp.cast(bool, json.loads(other) == self._expected) except json.decoder.JSONDecodeError: return False def __repr__(self) -> str: return f"JsonString({self._expected!r})"
[docs] class StringMatching(Matcher[str]): """Matches any :py:class:`str` instance matching a regular expression. :param pattern: Regex pattern to match, as a string or compiled pattern. :param flags: Any `flags`_ to compile a :py:class:`str` pattern with :raises ValueError: if flags are provided with a pre-compiled pattern. .. _flags: https://docs.python.org/3/library/re.html#flags """ _pattern: re.Pattern[str] @tp.overload def __init__(self, pattern: re.Pattern[str]): ... @tp.overload def __init__(self, pattern: str, *, flags: int = 0): ... def __init__( self, pattern: tp.Union[str, re.Pattern[str]], *, flags: int = 0, ): self._pattern = re.compile(pattern, flags=flags) def __eq__(self, other: tp.Any) -> bool: if not isinstance(other, str): return NotImplemented return self._pattern.match(other) is not None def __repr__(self) -> str: return f"StringMatching({self._pattern!r})"
[docs] class UrlString(Matcher[str]): """Matches any :py:class:`str` instance representing a URL. The string is parsed with :py:func:`~urllib.parse.urlparse` and compared attribute-by-attribute. Any attributes not provided are ignored. :param scheme: the scheme (e.g. ``"https"``) :param hostname: the hostname (e.g. ``"example.com"``) :param path: the path (e.g. ``"/some/path"``) :param query: the result of parsing the query string with :py:func:`~urllib.parse.parse_qs` :raises TypeError: if no arguments are provided. """ _hostname: tp.Optional[MaybeMatcher[str]] _path: tp.Optional[MaybeMatcher[str]] _query: tp.Optional[Mapping[str, Sequence[str]]] _scheme: tp.Optional[MaybeMatcher[str]] def __init__( self, *, scheme: tp.Optional[MaybeMatcher[str]] = None, hostname: tp.Optional[MaybeMatcher[str]] = None, path: tp.Optional[MaybeMatcher[str]] = None, query: tp.Optional[Mapping[str, Sequence[str]]] = None, ): self._hostname = hostname self._path = path self._query = query self._scheme = scheme if all( getattr(self, attr) is None for attr in ["_hostname", "_path", "_query", "_scheme"] ): raise TypeError("A UrlString with no arguments matches any string") def __eq__(self, other: tp.Any) -> bool: if not isinstance(other, str): return NotImplemented parsed = urlparse(other) for attribute in ["hostname", "path", "scheme"]: if (expected := getattr(self, f"_{attribute}")) is not None and getattr( parsed, attribute ) != expected: return False if (expected := self._query) is not None and parse_qs( parsed.query, keep_blank_values=True ) != expected: return False return True def __repr__(self) -> str: parameters = [ f"{name}={value!r}" for name in ["scheme", "hostname", "path", "query"] if (value := getattr(self, f"_{name}")) is not None ] return f"UrlString({', '.join(parameters)})"