Esempio n. 1
0
    def test_any_mapping_requires_items(self):
        class TestObject(list):
            def items(self):
                yield from self  # pragma: no cover

        assert TestObject() == AnyMapping()
        assert dict() == AnyMapping()
Esempio n. 2
0
class AnyURLCore(Matcher):
    """Matches any URL."""

    APPLY_DEFAULT = object()
    STRING_OR_NONE = NamedMatcher("<AnyStringOrNone>", AnyOf([None, AnyString()]))
    MAP_OR_NONE = NamedMatcher("<AnyMappingOrNone>", AnyOf([None, AnyMapping()]))

    DEFAULTS = {
        "scheme": STRING_OR_NONE,
        "host": STRING_OR_NONE,
        "path": STRING_OR_NONE,
        "params": STRING_OR_NONE,
        "query": MAP_OR_NONE,
        "fragment": STRING_OR_NONE,
    }

    # pylint: disable=too-many-arguments
    # I can't see a way around it. We could use kwargs, but then auto complete
    # would be hard
    def __init__(
        self,
        base_url=None,
        scheme=APPLY_DEFAULT,
        host=APPLY_DEFAULT,
        path=APPLY_DEFAULT,
        params=APPLY_DEFAULT,
        query=APPLY_DEFAULT,
        fragment=APPLY_DEFAULT,
    ):
        """Initialize a new URL matcher.

        :param base_url: URL (with scheme) to base the matcher on
        :param scheme: Scheme to match (e.g. http)
        :param host: Hostname to match
        :param path: URL path to match
        :param params: URL path params to match
        :param query: Query to match (string, dict or matcher)
        :param fragment: Anchor fragment to match (e.g. "name" for "#name")
        """
        self.parts = {
            # https://tools.ietf.org/html/rfc7230#section-2.7.3
            # scheme and host are case-insensitive
            "scheme": self._lower_if_string(scheme),
            "host": self._lower_if_string(host),
            # `path`, `query` and `fragment` are case-sensitive
            "path": self._get_path_matcher(path, scheme, host),
            "params": params,
            "fragment": fragment,
        }

        self._set_query(query)

        if base_url:
            self._set_base_url(base_url)
        else:
            # Apply default matchers for everything not provided
            self._apply_defaults(self.parts, self.DEFAULTS)

        super().__init__("dummy", self.assert_equal_to)

    def __str__(self):
        contraints = {
            key: value
            for key, value in self.parts.items()
            if value is not self.DEFAULTS[key]
        }

        if not contraints:
            return "* any URL *"

        return f"* any URL matching {contraints} *"

    @classmethod
    def parse_url(cls, url_string):
        """Parse a URL into a dict for comparison.

        Parses the given URL allowing you to see how AnyURL will understand it.
        This can be useful when debugging why a particular URL does or does
        not match.

        :param url_string: URL to parse
        :raise ValueError: If scheme is mandatory and not provided
        :return: A normalised string of comparison values
        """
        url = urlparse(url_string)

        if not url.scheme and not url.netloc:
            # Without a scheme `urlparse()` assumes that the hostname is part
            # of the path, so we have to try and guess what it really was

            host, path = cls._guess_hostname_and_path(url.path)
            url = url._replace(netloc=host, path=path)

        return {
            "scheme": url.scheme.lower() if url.scheme else None,
            "host": url.netloc.lower() if url.netloc else None,
            "path": url.path or None,
            "params": url.params or None,
            "query": MultiValueQuery.normalise(url.query),
            "fragment": url.fragment or None,
        }

    @staticmethod
    def _get_path_matcher(path, scheme, host):
        # If we are anything other than a plain string or None, use it directly
        if path is not None and not isinstance(path, str):
            return path

        # If we are matching paths alone, just return whatever we were given
        # so we match exactly. This lets the user distinguish between /path
        # and path which may be important
        if scheme is None and host is None:
            return path

        # If we got None, we need to allow ourselves to match either slash, ''
        # or None
        if path in (None, "/", ""):
            return NamedMatcher("<Path '/'>", AnyOf([None, AnyStringMatching(r"^/?$")]))

        # Otherwise construct a matcher which doesn't care about leading
        # slashes
        return NamedMatcher(
            f"'<Path '{path}'>", AnyStringMatching(f"^/?{re.escape(path)}$")
        )

    def _set_query(self, query, exact_match=True):
        if query is not self.APPLY_DEFAULT:
            query = MultiValueQuery.normalise(query)
            if query and not isinstance(query, Matcher):
                # MultiValueQuery is guaranteed to return something we can
                # provide to AnyMapping for comparison
                query = AnyMapping.containing(query)
                if exact_match:
                    query = query.only()

        self.parts["query"] = query

    def _set_base_url(self, base_url):
        # If we have a base URL, we'll take everything from there if it
        # wasn't explicitly provided in the constructor

        overlay = self.parse_url(base_url)

        path_matcher = self._get_path_matcher(
            overlay["path"], overlay["scheme"], overlay["host"]
        )
        overlay["path"] = path_matcher
        self._apply_defaults(self.parts, overlay)

    @staticmethod
    def _lower_if_string(value):
        if isinstance(value, str):
            return value.lower()

        return value

    @staticmethod
    def _apply_defaults(values, defaults, default_key=APPLY_DEFAULT):
        for key, default_value in defaults.items():
            if values[key] is default_key:
                values[key] = default_value

    @classmethod
    def _is_hostname(cls, host):
        if not host:
            return False

        if "." in host:
            return True

        return host.lower() == "localhost"

    @classmethod
    def _guess_hostname_and_path(cls, path):
        if "/" in path:
            head, tail = path.split("/", 1)
            if cls._is_hostname(head):
                return head, f"/{tail}"

        elif cls._is_hostname(path):
            return path, None

        return None, path

    def assert_equal_to(self, other):
        """Assert that the URL object is equal to another object.

        :raise AssertionError: If no match is found with details of why
        :return: True if equal
        """

        if not isinstance(other, str):
            raise AssertionError("Other URL is not a string")

        comparison = self.parse_url(other)

        for key, self_value in self.parts.items():
            other_value = comparison.get(key)

            if self_value != other_value:
                raise AssertionError(f"Other '{key}' {other_value} != {self_value}")

        return True
Esempio n. 3
0
class AnyURL(AnyURLCore):
    """A URL matcher with a fluent style interface."""

    # pylint: disable=function-redefined

    PRESENT_DEFAULT = {
        "scheme": AnyString(),
        "host": AnyString(),
        "path": AnyString(),
        "params": AnyString(),
        "query": AnyMapping(),
        "fragment": AnyString(),
    }

    def _apply_field_default(self, field, value):
        if value is not AnyURLCore.APPLY_DEFAULT:
            return value

        return self.PRESENT_DEFAULT[field]

    @staticmethod
    def with_scheme(scheme=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_host(host=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_path(path=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_params(params=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def containing_query(query=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_query(query=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @staticmethod
    def with_fragment(fragment=AnyURLCore.APPLY_DEFAULT):
        """Confuse pylint so it doesn't complain about fluent-endpoints."""

    @classmethod
    def matching(cls, base_url):
        """Create a URL matcher based on the given URL.

        :param base_url: URL to base the matcher on
        :return: An instance of AnyURLFluent
        """
        return AnyURL(base_url)

    @fluent_entrypoint
    def with_scheme(self, scheme=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a scheme or None.

        If you pass None this will ensure the URL has no scheme.

        :param scheme: None, string or matcher for the scheme
        """

        self.parts["scheme"] = self._apply_field_default("scheme", scheme)

    @fluent_entrypoint
    def with_host(self, host=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a host or None.

        If you pass None this will ensure the URL has no host.

        :param host: None, string or matcher for the host
        """
        self.parts["host"] = self._apply_field_default("host", host)

    @fluent_entrypoint
    def with_path(self, path=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a path or None.

        If you pass None this will ensure the URL has no path.

        :param path: None, string or matcher for the path
        """
        if path is AnyURLCore.APPLY_DEFAULT:
            self.parts["path"] = AnyString()
        else:
            self.parts["path"] = self._get_path_matcher(
                path, self.parts["scheme"], self.parts["host"])

    @fluent_entrypoint
    def with_params(self, params=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a params or None.

        If you pass None this will ensure the URL has no params.

        :param params: None, string or matcher for the params
        """
        self.parts["params"] = self._apply_field_default("params", params)

    @fluent_entrypoint
    def containing_query(self, query):
        """Specify that the query must have at least the items specified.

        :param query: A mappable to check
        """
        self._set_query(query, exact_match=False)

    @fluent_entrypoint
    def with_query(self, query=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a query or None.

        If you pass None this will ensure the URL has no query.

        :param query: None, mapping or matcher for the query
        """
        query = self._apply_field_default("query", query)
        self._set_query(query)

    @fluent_entrypoint
    def with_fragment(self, fragment=AnyURLCore.APPLY_DEFAULT):
        """Specify that this URL must have a fragment or None.

        If you pass None this will ensure the URL has no fragment.

        :param fragment: None, string or matcher for the fragment
        """
        self.parts["fragment"] = self._apply_field_default(
            "fragment", fragment)
Esempio n. 4
0
 def test_non_matching_items(self, non_matching):
     assert non_matching != AnyMapping()
Esempio n. 5
0
    def test_it_stringifies_as_we_specify(self):
        matcher = NamedMatcher("string", AnyMapping())

        assert str(matcher) == "string"
        assert repr(matcher) == "string"
Esempio n. 6
0
    def test_it_matches_like_its_contents(self):
        matcher = NamedMatcher("string", AnyMapping())

        assert matcher == {}
        assert matcher != []