Skip to content

Commit

Permalink
uprev dependencies and use pydantic v2, fix #72
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Nov 14, 2023
1 parent d7d5563 commit 0e553f1
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 196 deletions.
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ repos:

- repo: local
hooks:
- id: format
name: Format
entry: make format
types: [python]
language: system
pass_filenames: false
- id: lint
name: Lint
entry: make lint
Expand Down
24 changes: 20 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,34 @@ sources = dirty_equals tests

.PHONY: install
install:
pip install -U pip pre-commit pip-tools
pip install -r requirements/all.txt
pre-commit install

.PHONY: refresh-lockfiles
refresh-lockfiles:
@echo "Replacing requirements/*.txt files using pip-compile"
find requirements/ -name '*.txt' ! -name 'all.txt' -type f -delete
make update-lockfiles

.PHONY: update-lockfiles
update-lockfiles:
@echo "Updating requirements/*.txt files using pip-compile"
pip-compile -q -o requirements/linting.txt requirements/linting.in
pip-compile -q -o requirements/tests.txt requirements/tests.in
pip-compile -q -o requirements/docs.txt requirements/docs.in
pip-compile -q -o requirements/pyproject.txt pyproject.toml
pip install --dry-run -r requirements/all.txt

.PHONY: format
format:
black $(sources)
ruff --fix $(sources)
ruff check --fix-only $(sources)
ruff format $(sources)

.PHONY: lint
lint:
ruff $(sources)
black $(sources) --check --diff
ruff check $(sources)
ruff format --check $(sources)

.PHONY: test
test:
Expand Down
136 changes: 68 additions & 68 deletions dirty_equals/_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
from dataclasses import asdict, is_dataclass
from enum import Enum
from functools import lru_cache
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network
from typing import Any, Callable, TypeVar, Union, overload
from uuid import UUID
Expand All @@ -17,6 +18,21 @@
except ImportError:
from typing_extensions import Literal # type: ignore[assignment]

try:
from pydantic import (
AmqpDsn,
AnyHttpUrl,
AnyUrl,
FileUrl,
HttpUrl,
PostgresDsn,
RedisDsn,
TypeAdapter,
ValidationError,
)
except ImportError as e:
raise ImportError('Pydantic V2 is not installed, run `pip install dirty-equals[pydantic]`') from e


class IsUUID(DirtyEquals[UUID]):
"""
Expand Down Expand Up @@ -155,25 +171,33 @@ def equals(self, other: Any) -> bool:
return self.func(other)


class IsUrl(DirtyEquals[str]):
T = TypeVar('T')


@lru_cache()
def _build_type_adapter(schema: T) -> TypeAdapter[T]:
return TypeAdapter(schema)


_allowed_url_attribute_checks: set[str] = {
'scheme',
'host',
'host_type',
'user',
'password',
'port',
'path',
'query',
'fragment',
}


class IsUrl(DirtyEquals[Any]):
"""
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,
any_url: bool = False,
Expand All @@ -187,94 +211,70 @@ def __init__(
):
"""
Args:
any_url: any scheme allowed, TLD not required, host required
any_http_url: scheme http or https, TLD not required, host required
http_url: scheme http or https, TLD required, host required, max length 2083
any_url: any scheme allowed, host required
any_http_url: scheme http or https, host required
http_url: scheme http or https, host required, max length 2083
file_url: scheme file, host not required
postgres_dsn: user info required, TLD not required
ampqp_dsn: schema amqp or amqps, user info not required, TLD not required, host not required
redis_dsn: scheme redis or rediss, user info not required, tld not required, host not required
postgres_dsn: user info required
ampqp_dsn: schema amqp or amqps, user info not required, host not required
redis_dsn: scheme redis or rediss, user info not required, host not required
**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(host='example.com')
assert 'https://example.com' == IsUrl(scheme='https')
assert 'https://example.com' != IsUrl(scheme='http')
assert 'postgres://user:pass@localhost:5432/app' == IsUrl(postgres_dsn=True)
assert 'postgres://user:pass@localhost:5432/app' != IsUrl(http_url=True)
```
"""
try:
from pydantic import (
AmqpDsn,
AnyHttpUrl,
AnyUrl,
FileUrl,
HttpUrl,
PostgresDsn,
RedisDsn,
ValidationError,
parse_obj_as,
version,
)

self.AmqpDsn = AmqpDsn
self.AnyHttpUrl = AnyHttpUrl
self.AnyUrl = AnyUrl
self.FileUrl = FileUrl
self.HttpUrl = HttpUrl
self.PostgresDsn = PostgresDsn
self.RedisDsn = RedisDsn
self.parse_obj_as = parse_obj_as
self.ValidationError = ValidationError
self.pydantic_version = tuple(map(int, version.VERSION.split('.')))

except ImportError as e:
raise ImportError('pydantic is not installed, run `pip install dirty-equals[pydantic]`') from e
url_type_mappings = {
self.AnyUrl: any_url,
self.AnyHttpUrl: any_http_url,
self.HttpUrl: http_url,
self.FileUrl: file_url,
self.PostgresDsn: postgres_dsn,
self.AmqpDsn: ampqp_dsn,
self.RedisDsn: redis_dsn,
AnyUrl: any_url,
AnyHttpUrl: any_http_url,
HttpUrl: http_url,
FileUrl: file_url,
PostgresDsn: postgres_dsn,
AmqpDsn: ampqp_dsn,
RedisDsn: redis_dsn,
}
url_types_sum = sum(url_type_mappings.values())
if url_types_sum > 1:
raise ValueError('You can only check against one Pydantic url type at a time')
for item in expected_attributes:
if item not in self.allowed_attribute_checks:
if item not in _allowed_url_attribute_checks:
raise TypeError(
'IsURL only checks these attributes: scheme, host, host_type, user, password, tld, '
'IsURL only checks these attributes: scheme, host, host_type, user, password, '
'port, path, query, fragment'
)
self.attribute_checks = expected_attributes
if url_types_sum == 0:
url_type = AnyUrl
self.url_type: Any = AnyUrl
else:
url_type = max(url_type_mappings, key=url_type_mappings.get) # type: ignore[arg-type]
self.url_type = url_type
super().__init__(url_type)
self.url_type = max(url_type_mappings, key=url_type_mappings.get) # type: ignore[arg-type]
super().__init__()

def equals(self, other: Any) -> bool:
type_adapter = _build_type_adapter(self.url_type)
try:
parsed = self.parse_obj_as(self.url_type, other)
except self.ValidationError:
other_url = type_adapter.validate_python(other)
except ValidationError:
raise ValueError('Invalid URL')

if self.pydantic_version[0] == 1: # checking major version
equal = parsed == other
else:
equal = parsed.unicode_string() == other
# we now check that str() of the parsed URL equals its original value
# so that invalid encodings fail
# we remove trailing slashes since they're added by pydantic's URL parsing, but don't mean `other` is invalid
other_url_str = str(other_url)
if not other.endswith('/') and other_url_str.endswith('/'):
other_url_str = other_url_str[:-1]
equal = other_url_str == other

if not self.attribute_checks:
return equal

for attribute, expected in self.attribute_checks.items():
if getattr(parsed, attribute) != expected:
if getattr(other_url, attribute) != expected:
return False
return equal

Expand Down
4 changes: 2 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ markdown_extensions:
- attr_list
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg

extra:
version:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I']
flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'}
mccabe = { max-complexity = 14 }
isort = { known-first-party = ['tests'] }
format.quote-style = 'single'
target-version = 'py37'

[tool.pytest.ini_options]
Expand Down
4 changes: 1 addition & 3 deletions requirements/docs.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
black
# waiting for https://github.com/jimporter/mike/issues/154
git+https://github.com/jimporter/mike.git
mike
mkdocs
mkdocs-material
mkdocs-simple-hooks
Expand Down

0 comments on commit 0e553f1

Please sign in to comment.