Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add IsUrl #47

Merged
merged 12 commits into from
Sep 27, 2022
3 changes: 2 additions & 1 deletion dirty_equals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
IsPositiveFloat,
IsPositiveInt,
)
from ._other import FunctionCheck, IsJson, IsUUID
from ._other import FunctionCheck, IsJson, IsUrl, IsUUID
from ._sequence import Contains, HasLen, IsList, IsListOrTuple, IsTuple
from ._strings import IsAnyStr, IsBytes, IsStr

Expand Down Expand Up @@ -69,6 +69,7 @@
'FunctionCheck',
'IsJson',
'IsUUID',
'IsUrl',
# strings
'IsStr',
'IsBytes',
Expand Down
86 changes: 85 additions & 1 deletion dirty_equals/_other.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
from typing import Any, Callable, TypeVar, overload
from typing import Any, Callable, Set, TypeVar, overload
from uuid import UUID

from pydantic import AmqpDsn, AnyHttpUrl, AnyUrl, BaseModel, FileUrl, HttpUrl, PostgresDsn, RedisDsn
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved

from ._base import DirtyEquals
from ._utils import plain_repr

Expand Down Expand Up @@ -145,3 +147,85 @@ def is_even(x):

def equals(self, other: Any) -> bool:
return self.func(other)


class IsUrl(DirtyEquals[AnyUrl]):
"""
A class that checks if a value is a valid URL, optionally checking different URL types and attributes with
[Pydantic](https://pydantic-docs.helpmanual.io/usage/types/#urls).
"""

allowed_attribute_checks: Set[str] = {
'scheme',
'host',
'host_type',
'user',
'password',
'tld',
'port',
'path',
'query',
'fragment',
}

def __init__(
self,
url_type: Literal[
'AnyUrl', 'AnyHttpUrl', 'HttpUrl', 'FileUrl', 'PostgresDsn', 'AmpqpDsn', 'RedisDsn'
] = 'AnyUrl',
**expected_attributes: Any,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some duplication, but much easier for users if you list each kwarg individually here - so IDEs can provide auto-complete.

):
"""
Args:
url_type: A Pydantic url type to check the url against
**expected_attributes: Expected values for url attributes
```py title="IsUrl"
from dirty_equals import IsUrl

assert 'https://example.com' == IsUrl
assert 'https://example.com' == IsUrl(tld='com')
assert 'https://example.com' == IsUrl(scheme='https')
assert 'https://example.com' != IsUrl(scheme='http')
assert 'postgres://user:pass@localhost:5432/app' == IsUrl(url_type='PostgresDsn')
assert 'postgres://user:pass@localhost:5432/app' != IsUrl(url_type='HttpUrl')
```
"""
for item in expected_attributes:
if item not in self.allowed_attribute_checks:
raise TypeError(
'IsURL only checks these attributes: scheme, host, host_type, user, password, tld, '
'port, path, query, fragment'
)

self.attribute_checks = expected_attributes
try:
self.url_type = {
'AnyUrl': AnyUrl,
'AnyHttpUrl': AnyHttpUrl,
'HttpUrl': HttpUrl,
'FileUrl': FileUrl,
'PostgresDsn': PostgresDsn,
'AmpqpDsn': AmqpDsn,
'RedisDsn': RedisDsn,
}[url_type]
except KeyError:
raise TypeError(
'IsUrl only checks these Pydantic URL types: AnyUrl, AnyHttpUrl, HttpUrl, FileUrl, '
'PostgresDsn, AmpqDsn, RedisDsn'
)
super().__init__(url_type)

def equals(self, other: Any) -> bool:

url_type = self.url_type

class UserModel(BaseModel):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to use parse_obj_as.

url: url_type # type: ignore[valid-type]

if not self.attribute_checks:
return UserModel(url=other).url == other

for attribute, expected in self.attribute_checks.items():
if getattr(UserModel(url=other).url, attribute) != expected:
return False
return UserModel(url=other).url == other
2 changes: 2 additions & 0 deletions docs/types/other.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
::: dirty_equals.AnyThing

::: dirty_equals.IsOneOf

::: dirty_equals.IsUrl
64 changes: 58 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ classifiers = [
python = "^3.7.0"
typing-extensions = {version = "^4.0.1", python = "<3.8"}
pytz = ">=2021.3"
pydantic = ">=1.9.1"

[build-system]
requires = ["poetry-core"]
Expand Down
1 change: 1 addition & 0 deletions tests/requirements-linting.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ isort[colors]==5.10.1
mypy==0.942
pre-commit==2.17.0
pycodestyle==2.8.0
pydantic==1.9.1
pyflakes==2.4.0
types-pytz==2021.3.6
45 changes: 44 additions & 1 deletion tests/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from dirty_equals import FunctionCheck, IsJson, IsUUID
from dirty_equals import FunctionCheck, IsJson, IsUrl, IsUUID


@pytest.mark.parametrize(
Expand Down Expand Up @@ -128,3 +128,46 @@ def foobar(v):
def test_json_both():
with pytest.raises(TypeError, match='IsJson requires either an argument or kwargs, not both'):
IsJson(1, a=2)


@pytest.mark.parametrize(
'other,dirty',
[
('https://example.com', IsUrl),
('https://example.com', IsUrl(scheme='https')),
('postgres://user:pass@localhost:5432/app', IsUrl(url_type='PostgresDsn')),
],
)
def test_is_url_true(other, dirty):
assert other == dirty


@pytest.mark.parametrize(
'other,dirty',
[
('https://example.com', IsUrl(url_type='PostgresDsn')),
('https://example.com', IsUrl(scheme='http')),
('definitely not a url', IsUrl),
(42, IsUrl),
],
)
def test_is_url_false(other, dirty):
assert other != dirty


def test_is_url_invalid_kwargs():
with pytest.raises(
TypeError,
match='IsURL only checks these attributes: scheme, host, host_type, user, password, tld, port, path, query, '
'fragment',
):
IsUrl(https=True)


def test_is_url_invalid_url_type():
with pytest.raises(
TypeError,
match='IsUrl only checks these Pydantic URL types: AnyUrl, AnyHttpUrl, HttpUrl, '
'FileUrl, PostgresDsn, AmpqDsn, RedisDsn',
):
IsUrl(url_type='HttpsUrl')