diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8182f76..2910a62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v3 - # We use Python 3.8 here because it's the minimum Python version supported by this library. - - name: Setup Python 3.8 + # We use Python 3.9 here because it's the minimum Python version supported by this library. + - name: Setup Python 3.9 uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: pip install --upgrade pip build @@ -42,10 +42,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Setup Python 3.8 + - name: Setup Python 3.9 uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Download build artifacts uses: actions/download-artifact@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20512d5..0f4e0f7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,6 @@ jobs: fail-fast: false matrix: python-version: - - '3.8' - '3.9' - '3.10' - '3.11' diff --git a/Makefile b/Makefile index 285252c..789c618 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,7 @@ open-coverage: # Run complete tox test suite in a multi-python Docker container .PHONY: docker-tox +docker-tox: TOX_ARGS='-e clean,py312,py311,py310,py39,report,flake8,py312-mypy' docker-tox: docker run --rm --tty \ --user $(DOCKER_USER) \ @@ -66,7 +67,7 @@ docker-tox: tox run --workdir .tox_docker $(TOX_ARGS) # Run partial tox test suites in Docker -.PHONY: docker-tox-py312 docker-tox-py311 docker-tox-py310 docker-tox-py39 docker-tox-py38 +.PHONY: docker-tox-py312 docker-tox-py311 docker-tox-py310 docker-tox-py39 docker-test-py312: TOX_ARGS="-e clean,py312,py312-report" docker-test-py312: docker-tox docker-test-py311: TOX_ARGS="-e clean,py311,py311-report" @@ -75,21 +76,18 @@ docker-test-py310: TOX_ARGS="-e clean,py310,py310-report" docker-test-py310: docker-tox docker-test-py39: TOX_ARGS="-e clean,py39,py39-report" docker-test-py39: docker-tox -docker-test-py38: TOX_ARGS="-e clean,py38,py38-report" -docker-test-py38: docker-tox # Run all tox test suites, but separately to check code coverage individually .PHONY: docker-test-all docker-test-all: - make docker-test-py38 make docker-test-py39 make docker-test-py310 make docker-test-py311 make docker-test-py312 # Run mypy using all different (or specific) Python versions in Docker -.PHONY: docker-mypy-all docker-mypy-py312 docker-mypy-py311 docker-mypy-py310 docker-mypy-py39 docker-mypy-py38 -docker-mypy-all: TOX_ARGS="-e py312-mypy,py311-mypy,py310-mypy,py39-mypy,py38-mypy,py37-mypy" +.PHONY: docker-mypy-all docker-mypy-py312 docker-mypy-py311 docker-mypy-py310 docker-mypy-py39 +docker-mypy-all: TOX_ARGS="-e py312-mypy,py311-mypy,py310-mypy,py39-mypy" docker-mypy-all: docker-tox docker-mypy-py312: TOX_ARGS="-e py312-mypy" docker-mypy-py312: docker-tox @@ -99,8 +97,6 @@ docker-mypy-py310: TOX_ARGS="-e py310-mypy" docker-mypy-py310: docker-tox docker-mypy-py39: TOX_ARGS="-e py39-mypy" docker-mypy-py39: docker-tox -docker-mypy-py38: TOX_ARGS="-e py38-mypy" -docker-mypy-py38: docker-tox # Pull the latest image of the multi-python Docker image .PHONY: docker-pull diff --git a/docs/01-introduction.md b/docs/01-introduction.md index 26870b9..c37eda8 100644 --- a/docs/01-introduction.md +++ b/docs/01-introduction.md @@ -146,7 +146,6 @@ If you feel overwhelmed by this example: Don't worry! We will cover validation o ```python from datetime import datetime from enum import Enum -from typing import List from validataclass.dataclasses import validataclass from validataclass.validators import DataclassValidator, DateTimeValidator, EnumValidator, IntegerValidator, \ @@ -169,7 +168,7 @@ class Fruit: @validataclass class Order: id: int = IntegerValidator() - items: List[Fruit] = ListValidator(DataclassValidator(Fruit)) + items: list[Fruit] = ListValidator(DataclassValidator(Fruit)) ordered_at: datetime = DateTimeValidator() diff --git a/docs/03-basic-validators.md b/docs/03-basic-validators.md index 0021841..6d80fbe 100644 --- a/docs/03-basic-validators.md +++ b/docs/03-basic-validators.md @@ -730,9 +730,7 @@ requires the datetimes to already have a timezone, though, so if you are using a you need to specify `local_timezone` as well. See [`datetime.timezone`](https://docs.python.org/3/library/datetime.html#timezone-objects) and -[`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) (only supported as of Python 3.9) for information on -defining timezones. For older Python versions, libraries like [`pytz`](https://pythonhosted.org/pytz/) or -[`dateutil`](https://dateutil.readthedocs.io/en/stable/tz.html) can be used instead. +[`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) for information on defining timezones. Additionally, the parameter `datetime_range` can be used to specify a range of datetime values that are allowed (e.g. a minimum and a maximum datetime, which can be dynamically defined using callables). See the classes diff --git a/docs/05-dataclasses.md b/docs/05-dataclasses.md index 70a53ab..c672c45 100644 --- a/docs/05-dataclasses.md +++ b/docs/05-dataclasses.md @@ -1046,7 +1046,7 @@ In conclusion, let's take a look at a final example. This one is a bit more exte from datetime import datetime from decimal import Decimal from enum import Enum -from typing import Optional, List +from typing import Optional from validataclass.dataclasses import validataclass, Default from validataclass.exceptions import ValidationError, DataclassPostValidationError @@ -1068,7 +1068,7 @@ class OrderItem: @validataclass class Order: id: int = IntegerValidator() - items: List[OrderItem] = ListValidator(DataclassValidator(OrderItem)) + items: list[OrderItem] = ListValidator(DataclassValidator(OrderItem)) total_price: Decimal = DecimalValidator() ordered_at: datetime = DateTimeValidator() diff --git a/setup.cfg b/setup.cfg index 4101734..69a2d0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,6 @@ classifiers = License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 @@ -30,7 +29,7 @@ classifiers = package_dir = = src packages = find: -python_requires = >=3.8 +python_requires = ~=3.9 install_requires = typing-extensions ~= 4.3 @@ -45,5 +44,3 @@ testing = coverage-conditional-plugin ~= 0.5 flake8 ~= 7.0 mypy ~= 1.9 - python-dateutil - types-python-dateutil diff --git a/src/validataclass/dataclasses/defaults.py b/src/validataclass/dataclasses/defaults.py index 9e2c02b..6fab3b5 100644 --- a/src/validataclass/dataclasses/defaults.py +++ b/src/validataclass/dataclasses/defaults.py @@ -4,8 +4,9 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ +from collections.abc import Callable from copy import copy, deepcopy -from typing import Any, Callable, NoReturn +from typing import Any, NoReturn from typing_extensions import Self diff --git a/src/validataclass/dataclasses/validataclass.py b/src/validataclass/dataclasses/validataclass.py index d5f64b4..39317a8 100644 --- a/src/validataclass/dataclasses/validataclass.py +++ b/src/validataclass/dataclasses/validataclass.py @@ -7,7 +7,8 @@ import dataclasses import sys from collections import namedtuple -from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar, Union, overload +from collections.abc import Callable +from typing import Any, Optional, TypeVar, Union, overload from typing_extensions import dataclass_transform @@ -26,12 +27,12 @@ @overload -def validataclass(cls: Type[_T]) -> Type[_T]: +def validataclass(cls: type[_T]) -> type[_T]: ... @overload -def validataclass(cls: None = None, /, **kwargs: Any) -> Callable[[Type[_T]], Type[_T]]: +def validataclass(cls: None = None, /, **kwargs: Any) -> Callable[[type[_T]], type[_T]]: ... @@ -40,10 +41,10 @@ def validataclass(cls: None = None, /, **kwargs: Any) -> Callable[[Type[_T]], Ty field_specifiers=(dataclasses.field, dataclasses.Field, validataclass_field), ) def validataclass( - cls: Optional[Type[_T]] = None, + cls: Optional[type[_T]] = None, /, **kwargs: Any, -) -> Union[Type[_T], Callable[[Type[_T]], Type[_T]]]: +) -> Union[type[_T], Callable[[type[_T]], type[_T]]]: """ Decorator that turns a normal class into a `DataclassValidator`-compatible dataclass. @@ -87,7 +88,7 @@ class ExampleDataclass: parameters are necessary. In Python 3.10 and upwards, the argument `kw_only=True` will be used by default. """ - def decorator(_cls: Type[_T]) -> Type[_T]: + def decorator(_cls: type[_T]) -> type[_T]: # In Python 3.10 and higher, we use kw_only=True to allow both required and optional fields in any order. # In older Python versions, we use a workaround by setting default_factory to a function that raises an # exception for required fields. @@ -103,7 +104,7 @@ def decorator(_cls: Type[_T]) -> Type[_T]: return decorator if cls is None else decorator(cls) -def _prepare_dataclass_metadata(cls: Type[_T]) -> None: +def _prepare_dataclass_metadata(cls: type[_T]) -> None: """ Prepares a soon-to-be dataclass (before it is decorated with `@dataclass`) to be usable with `DataclassValidator` by checking it for `Validator` objects and setting dataclass metadata. @@ -168,7 +169,7 @@ def _prepare_dataclass_metadata(cls: Type[_T]) -> None: setattr(cls, name, validataclass_field(validator=field_validator, default=field_default, _name=name)) -def _get_existing_validator_fields(cls: Type[_T]) -> Dict[str, _ValidatorField]: +def _get_existing_validator_fields(cls: type[_T]) -> dict[str, _ValidatorField]: """ Returns a dictionary containing all fields (as `_ValidatorField` objects) of an existing validataclass that have a validator set in their metadata, or an empty dictionary if the class is not a dataclass (yet). @@ -196,7 +197,7 @@ def _get_existing_validator_fields(cls: Type[_T]) -> Dict[str, _ValidatorField]: return validator_fields -def _parse_validator_tuple(args: Union[Tuple[Any, ...], Validator, Default, None]) -> _ValidatorField: +def _parse_validator_tuple(args: Union[tuple[Any, ...], Validator, Default, None]) -> _ValidatorField: """ Parses field arguments (the value of a field in a dataclass that has not been parsed by `@dataclass` yet) to a tuple of a Validator and a Default object. diff --git a/src/validataclass/dataclasses/validataclass_field.py b/src/validataclass/dataclasses/validataclass_field.py index f0334b4..202ab76 100644 --- a/src/validataclass/dataclasses/validataclass_field.py +++ b/src/validataclass/dataclasses/validataclass_field.py @@ -6,7 +6,7 @@ import dataclasses import sys -from typing import Any, Dict, NoReturn, Optional +from typing import Any, NoReturn, Optional from validataclass.validators import Validator from .defaults import Default, NoDefault @@ -20,7 +20,7 @@ def validataclass_field( validator: Validator, default: Any = NoDefault, *, - metadata: Optional[Dict[str, Any]] = None, + metadata: Optional[dict[str, Any]] = None, _name: Optional[str] = None, # noqa (undocumented parameter, only used internally) **kwargs: Any, ) -> Any: diff --git a/src/validataclass/dataclasses/validataclass_mixin.py b/src/validataclass/dataclasses/validataclass_mixin.py index 661e7fb..cd5c086 100644 --- a/src/validataclass/dataclasses/validataclass_mixin.py +++ b/src/validataclass/dataclasses/validataclass_mixin.py @@ -6,7 +6,7 @@ import dataclasses import warnings -from typing import Any, Dict, cast +from typing import Any, cast from typing_extensions import Self @@ -30,7 +30,7 @@ class ExampleClass(ValidataclassMixin): ``` """ - def to_dict(self, *, keep_unset_values: bool = False) -> Dict[str, Any]: + def to_dict(self, *, keep_unset_values: bool = False) -> dict[str, Any]: """ Returns the data of the object as a dictionary (recursively resolving inner dataclasses as well). @@ -42,7 +42,7 @@ def to_dict(self, *, keep_unset_values: bool = False) -> Dict[str, Any]: # Technically, there is no guarantee that this class is used as a mixin in an actual dataclass. # However, if that's not the case, calling to_dict() doesn't make sense and will just fail with an exception. # For all intents and purposes, we can safely assume that `self` is a dataclass instance. - data = cast(Dict[str, Any], dataclasses.asdict(self)) # type: ignore[call-overload] # noqa + data = cast(dict[str, Any], dataclasses.asdict(self)) # type: ignore[call-overload] # noqa # Filter out all UnsetValues (unless said otherwise) if not keep_unset_values: diff --git a/src/validataclass/exceptions/base_exceptions.py b/src/validataclass/exceptions/base_exceptions.py index 1fa6401..7ad3b98 100644 --- a/src/validataclass/exceptions/base_exceptions.py +++ b/src/validataclass/exceptions/base_exceptions.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict, Optional +from typing import Any, Optional __all__ = [ 'ValidationError', @@ -29,7 +29,7 @@ class ValidationError(Exception): """ code: str = 'unknown_error' reason: Optional[str] = None - extra_data: Optional[Dict[str, Any]] = None + extra_data: Optional[dict[str, Any]] = None def __init__(self, *, code: Optional[str] = None, reason: Optional[str] = None, **kwargs: Any): if code is not None: @@ -45,7 +45,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.__repr__() - def _get_repr_dict(self) -> Dict[str, str]: + def _get_repr_dict(self) -> dict[str, str]: """ Returns a dictionary representing the error fields as strings (e.g. by applying `repr()` on the values). @@ -58,7 +58,7 @@ def _get_repr_dict(self) -> Dict[str, str]: key: repr(value) for key, value in self.to_dict().items() if value is not None } - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """ Generates a dictionary containing error information, suitable as response to the user. May be overridden by subclasses to extend the dictionary. diff --git a/src/validataclass/exceptions/common_exceptions.py b/src/validataclass/exceptions/common_exceptions.py index f0a75ff..e94b8ad 100644 --- a/src/validataclass/exceptions/common_exceptions.py +++ b/src/validataclass/exceptions/common_exceptions.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict, List, Union +from typing import Any, Union from .base_exceptions import ValidationError @@ -45,9 +45,9 @@ class InvalidTypeError(ValidationError): value, a different `ValidationError` will be raised. """ code = 'invalid_type' - expected_types: List[str] + expected_types: list[str] - def __init__(self, *, expected_types: Union[type, str, List[Union[type, str]]], **kwargs: Any): + def __init__(self, *, expected_types: Union[type, str, list[Union[type, str]]], **kwargs: Any): super().__init__(**kwargs) if not isinstance(expected_types, list): @@ -69,7 +69,7 @@ def add_expected_type(self, new_type: Union[type, str]) -> None: if new_type not in self.expected_types: self.expected_types.append(new_type) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: base_dict = super().to_dict() self.expected_types.sort() if len(self.expected_types) == 1: diff --git a/src/validataclass/exceptions/dataclass_exceptions.py b/src/validataclass/exceptions/dataclass_exceptions.py index cc83545..0e15f4e 100644 --- a/src/validataclass/exceptions/dataclass_exceptions.py +++ b/src/validataclass/exceptions/dataclass_exceptions.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict, Optional +from typing import Any, Optional from .base_exceptions import ValidationError @@ -53,13 +53,13 @@ class DataclassPostValidationError(ValidationError): """ code = 'post_validation_errors' wrapped_error: Optional[ValidationError] = None - field_errors: Optional[Dict[str, ValidationError]] = None + field_errors: Optional[dict[str, ValidationError]] = None def __init__( self, *, error: Optional[ValidationError] = None, - field_errors: Optional[Dict[str, ValidationError]] = None, + field_errors: Optional[dict[str, ValidationError]] = None, **kwargs: Any, ): super().__init__(**kwargs) @@ -74,7 +74,7 @@ def __init__( assert all(isinstance(error, ValidationError) for error in field_errors.values()) self.field_errors = field_errors - def _get_repr_dict(self) -> Dict[str, str]: + def _get_repr_dict(self) -> dict[str, str]: base_dict = super()._get_repr_dict() if self.wrapped_error is not None: @@ -84,7 +84,7 @@ def _get_repr_dict(self) -> Dict[str, str]: return base_dict - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: base_dict = super().to_dict() # Convert inner errors to dicts recursively diff --git a/src/validataclass/exceptions/dict_exceptions.py b/src/validataclass/exceptions/dict_exceptions.py index 091c6e8..9ba3466 100644 --- a/src/validataclass/exceptions/dict_exceptions.py +++ b/src/validataclass/exceptions/dict_exceptions.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict +from typing import Any from .base_exceptions import ValidationError @@ -26,21 +26,21 @@ class DictFieldsValidationError(ValidationError): The implementation of `to_dict()` recursively converts the field validation errors to dictionaries. """ code = 'field_errors' - field_errors: Dict[str, ValidationError] + field_errors: dict[str, ValidationError] - def __init__(self, *, field_errors: Dict[str, ValidationError], **kwargs: Any): + def __init__(self, *, field_errors: dict[str, ValidationError], **kwargs: Any): super().__init__(**kwargs) assert all(isinstance(error, ValidationError) for error in field_errors.values()) self.field_errors = field_errors - def _get_repr_dict(self) -> Dict[str, str]: + def _get_repr_dict(self) -> dict[str, str]: base_dict = super()._get_repr_dict() return { **base_dict, 'field_errors': repr(self.field_errors), } - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: base_dict = super().to_dict() return { **base_dict, diff --git a/src/validataclass/exceptions/list_exceptions.py b/src/validataclass/exceptions/list_exceptions.py index ac3ec7d..0d25396 100644 --- a/src/validataclass/exceptions/list_exceptions.py +++ b/src/validataclass/exceptions/list_exceptions.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict, Optional +from typing import Any, Optional from .base_exceptions import ValidationError @@ -25,21 +25,21 @@ class ListItemsValidationError(ValidationError): The implementation of `to_dict()` recursively converts the item validation errors to dictionaries. """ code = 'list_item_errors' - item_errors: Dict[int, ValidationError] + item_errors: dict[int, ValidationError] - def __init__(self, *, item_errors: Dict[int, ValidationError], **kwargs: Any): + def __init__(self, *, item_errors: dict[int, ValidationError], **kwargs: Any): super().__init__(**kwargs) assert all(isinstance(error, ValidationError) for error in item_errors.values()) self.item_errors = item_errors - def _get_repr_dict(self) -> Dict[str, str]: + def _get_repr_dict(self) -> dict[str, str]: base_dict = super()._get_repr_dict() return { **base_dict, 'item_errors': repr(self.item_errors), } - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: base_dict = super().to_dict() return { **base_dict, diff --git a/src/validataclass/exceptions/misc_exceptions.py b/src/validataclass/exceptions/misc_exceptions.py index f7b4bfa..6f40e7c 100644 --- a/src/validataclass/exceptions/misc_exceptions.py +++ b/src/validataclass/exceptions/misc_exceptions.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, List, Optional +from typing import Any, Optional from .base_exceptions import ValidationError @@ -22,5 +22,5 @@ class ValueNotAllowedError(ValidationError): """ code = 'value_not_allowed' - def __init__(self, *, allowed_values: Optional[List[Any]] = None, **kwargs: Any): + def __init__(self, *, allowed_values: Optional[list[Any]] = None, **kwargs: Any): super().__init__(allowed_values=allowed_values, **kwargs) diff --git a/src/validataclass/helpers/datetime_range.py b/src/validataclass/helpers/datetime_range.py index 02c0b92..3a637ba 100644 --- a/src/validataclass/helpers/datetime_range.py +++ b/src/validataclass/helpers/datetime_range.py @@ -5,8 +5,9 @@ """ from abc import ABC, abstractmethod +from collections.abc import Callable from datetime import datetime, timedelta, timezone, tzinfo -from typing import Callable, Dict, Optional, Tuple, Union +from typing import Optional, Union __all__ = [ 'BaseDateTimeRange', @@ -32,7 +33,7 @@ def contains_datetime(self, dt: datetime, local_timezone: Optional[tzinfo] = Non raise NotImplementedError() @abstractmethod - def to_dict(self, local_timezone: Optional[tzinfo] = None) -> Dict[str, str]: + def to_dict(self, local_timezone: Optional[tzinfo] = None) -> dict[str, str]: """ Abstract method to be implemented by subclasses. Should return a dictionary with string representations of the range boundaries, suitable for the `DateTimeRangeError` exception to generate JSON error responses. @@ -116,7 +117,7 @@ def contains_datetime(self, dt: datetime, local_timezone: Optional[tzinfo] = Non # Note: These comparisons will raise TypeErrors when mixing datetimes with and without timezones return (lower_datetime is None or dt >= lower_datetime) and (upper_datetime is None or dt <= upper_datetime) - def to_dict(self, local_timezone: Optional[tzinfo] = None) -> Dict[str, str]: + def to_dict(self, local_timezone: Optional[tzinfo] = None) -> dict[str, str]: """ Returns a dictionary with string representations of the range boundaries, suitable for the `DateTimeRangeError` exception to generate JSON error responses. @@ -222,7 +223,7 @@ def contains_datetime(self, dt: datetime, local_timezone: Optional[tzinfo] = Non # Note: These comparisons will raise TypeErrors when mixing datetimes with and without timezones return lower_datetime <= dt <= upper_datetime - def to_dict(self, local_timezone: Optional[tzinfo] = None) -> Dict[str, str]: + def to_dict(self, local_timezone: Optional[tzinfo] = None) -> dict[str, str]: """ Returns a dictionary with string representations of the range boundaries (calculating `lower_datetime` and `upper_datetime` from the pivot minus/plus the offsets), suitable for the `DateTimeRangeError` exception to @@ -248,7 +249,7 @@ def _get_pivot_datetime(self, local_timezone: Optional[tzinfo] = None) -> dateti else: return self._resolve_datetime_boundary(self.pivot, local_timezone) - def _get_boundaries(self, local_timezone: Optional[tzinfo] = None) -> Tuple[datetime, datetime]: + def _get_boundaries(self, local_timezone: Optional[tzinfo] = None) -> tuple[datetime, datetime]: """ Helper method to get the lower and upper boundaries as datetimes, resolving callables and applying `local_timezone` if necessary. diff --git a/src/validataclass/internal/internet_helpers.py b/src/validataclass/internal/internet_helpers.py index 4ad91de..a7797b2 100644 --- a/src/validataclass/internal/internet_helpers.py +++ b/src/validataclass/internal/internet_helpers.py @@ -7,8 +7,6 @@ import ipaddress import re -from typing import Pattern - __all__ = [ 'validate_hostname', 'validate_ip_address', @@ -19,15 +17,15 @@ _REGEX_DOMAIN_LABEL = r'([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)' # Precompiled regular expressions -_ip_charset_regex: Pattern[str] = re.compile( +_ip_charset_regex: re.Pattern[str] = re.compile( r'(\d+\.){3}\d+|\[[0-9a-f:]+]', re.ASCII | re.IGNORECASE, ) -_domain_optional_tld_regex: Pattern[str] = re.compile( +_domain_optional_tld_regex: re.Pattern[str] = re.compile( f'({_REGEX_DOMAIN_LABEL}\\.)*{_REGEX_DOMAIN_LABEL}', re.IGNORECASE, ) -_domain_required_tld_regex: Pattern[str] = re.compile( +_domain_required_tld_regex: re.Pattern[str] = re.compile( f'({_REGEX_DOMAIN_LABEL}\\.)+{_REGEX_DOMAIN_LABEL}', re.IGNORECASE, ) diff --git a/src/validataclass/validators/any_of_validator.py b/src/validataclass/validators/any_of_validator.py index 30d0ca6..289e9eb 100644 --- a/src/validataclass/validators/any_of_validator.py +++ b/src/validataclass/validators/any_of_validator.py @@ -5,7 +5,8 @@ """ import warnings -from typing import Any, Iterable, List, Optional, Union +from collections.abc import Iterable +from typing import Any, Optional, Union from validataclass.exceptions import ValueNotAllowedError, InvalidValidatorOptionException from .validator import Validator @@ -58,10 +59,10 @@ class AnyOfValidator(Validator): max_allowed_values_in_validation_error: int = 20 # Values allowed as input - allowed_values: List[Any] + allowed_values: list[Any] # Types allowed for input data (set by parameter or autodetermined from allowed_values) - allowed_types: List[type] + allowed_types: list[type] # If set, strings will be matched case-sensitively case_sensitive: bool = False diff --git a/src/validataclass/validators/anything_validator.py b/src/validataclass/validators/anything_validator.py index dd13b64..c040f38 100644 --- a/src/validataclass/validators/anything_validator.py +++ b/src/validataclass/validators/anything_validator.py @@ -4,7 +4,8 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, List, Iterable, Optional, Union +from collections.abc import Iterable +from typing import Any, Optional, Union from validataclass.exceptions import InvalidValidatorOptionException from .validator import Validator @@ -64,7 +65,7 @@ class AnythingValidator(Validator): allow_none: bool # Which input types to allow (None for anything) - allowed_types: Optional[List[type]] + allowed_types: Optional[list[type]] def __init__( self, @@ -110,7 +111,7 @@ def _normalize_allowed_types( *, allowed_types: Union[Iterable[Union[type, None]], type, None], allow_none: Optional[bool], - ) -> List[type]: + ) -> list[type]: """ Helper method to normalize the `allowed_types` parameter to a unique list that contains only types. """ diff --git a/src/validataclass/validators/dataclass_validator.py b/src/validataclass/validators/dataclass_validator.py index 8ff679c..247bc3e 100644 --- a/src/validataclass/validators/dataclass_validator.py +++ b/src/validataclass/validators/dataclass_validator.py @@ -7,7 +7,7 @@ import dataclasses import inspect import warnings -from typing import Any, Dict, Generic, Optional, Type, TypeVar, TYPE_CHECKING +from typing import Any, Generic, Optional, TypeVar from typing_extensions import TypeGuard @@ -30,15 +30,6 @@ # Type variable for an instance of a dataclass T_Dataclass = TypeVar('T_Dataclass', bound=object) -# Define type alias for dataclasses.Field -# NOTE: In Python >= 3.9, dataclasses.Field is a Generic, so mypy will complain if no type parameter is given. -# However, Field[Any] will raise a runtime error in Python 3.8 because there the type is not parametrized yet. -# TODO: Replace type alias with dataclasses.Field[Any] when removing Python 3.9 support. (#15) -if TYPE_CHECKING: - T_DataclassField = dataclasses.Field[Any] -else: - T_DataclassField = dataclasses.Field - class DataclassValidator(Generic[T_Dataclass], DictValidator): """ @@ -118,12 +109,12 @@ def __post_validate__(self, *, require_optional_field: bool = False): """ # Dataclass type that the validated dictionary will be converted to - dataclass_cls: Type[T_Dataclass] + dataclass_cls: type[T_Dataclass] # Field default values - field_defaults: Dict[str, Default] + field_defaults: dict[str, Default] - def __init__(self, dataclass_cls: Optional[Type[T_Dataclass]] = None) -> None: + def __init__(self, dataclass_cls: Optional[type[T_Dataclass]] = None) -> None: # For easier subclassing: If 'self.dataclass_cls' is already set (e.g. as class member in a subclass), use that # class as the default. if dataclass_cls is None: @@ -167,7 +158,7 @@ def __init__(self, dataclass_cls: Optional[Type[T_Dataclass]] = None) -> None: super().__init__(field_validators=field_validators, required_fields=required_fields) @staticmethod - def _get_field_validator(field: T_DataclassField) -> Validator: + def _get_field_validator(field: dataclasses.Field[Any]) -> Validator: # Parse field metadata to get Validator validator = field.metadata.get('validator') @@ -182,7 +173,7 @@ def _get_field_validator(field: T_DataclassField) -> Validator: return validator @staticmethod - def _get_field_default(field: T_DataclassField) -> Default: + def _get_field_default(field: dataclasses.Field[Any]) -> Default: # Parse field metadata to get Default object default = field.metadata.get('validator_default', NoDefault) @@ -198,7 +189,7 @@ def _get_field_default(field: T_DataclassField) -> Default: return default - def _pre_validate(self, input_data: Any, **kwargs: Any) -> Dict[str, Any]: + def _pre_validate(self, input_data: Any, **kwargs: Any) -> dict[str, Any]: """ Pre-validation steps: Validates the input as a dictionary and fills in the default values. """ @@ -296,7 +287,7 @@ def _post_validate(validated_object: T_Dataclass, **kwargs: Any) -> T_Dataclass: return validated_object -def _is_dataclass_type(obj: Any) -> TypeGuard[Type[T_Dataclass]]: +def _is_dataclass_type(obj: Any) -> TypeGuard[type[T_Dataclass]]: """ Type-safe helper function that checks if the given object is a dataclass (specifically a class, not an instance). """ diff --git a/src/validataclass/validators/datetime_validator.py b/src/validataclass/validators/datetime_validator.py index 235ecaf..ff0523d 100644 --- a/src/validataclass/validators/datetime_validator.py +++ b/src/validataclass/validators/datetime_validator.py @@ -7,7 +7,7 @@ import re from datetime import datetime, tzinfo from enum import Enum -from typing import Any, Optional, Pattern +from typing import Any, Optional from validataclass.exceptions import DateTimeRangeError, InvalidDateTimeError, InvalidValidatorOptionException from validataclass.helpers import BaseDateTimeRange @@ -113,9 +113,7 @@ class DateTimeValidator(StringValidator): you need to specify `local_timezone` as well. See [`datetime.timezone`](https://docs.python.org/3/library/datetime.html#timezone-objects) and - [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) (only supported as of Python 3.9) for information on - defining timezones. For older Python versions, libraries like [`pytz`](https://pythonhosted.org/pytz/) or - [`dateutil`](https://dateutil.readthedocs.io/en/stable/tz.html) can be used instead. + [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) for information on defining timezones. Additionally, the parameter `datetime_range` can be used to specify a range of datetime values that are allowed (e.g. a minimum and a maximum datetime, which can be dynamically defined using callables). See the classes @@ -282,7 +280,7 @@ class DateTimeValidator(StringValidator): datetime_format: DateTimeFormat # Precompiled regular expression for the specified datetime string format - datetime_format_regex: Pattern[str] + datetime_format_regex: re.Pattern[str] # Whether to discard milli- and microseconds in the output datetime discard_milliseconds: bool = False diff --git a/src/validataclass/validators/decimal_validator.py b/src/validataclass/validators/decimal_validator.py index b68b86b..980a346 100644 --- a/src/validataclass/validators/decimal_validator.py +++ b/src/validataclass/validators/decimal_validator.py @@ -7,7 +7,7 @@ import decimal import re from decimal import Decimal, InvalidOperation -from typing import Any, Optional, Pattern, Union +from typing import Any, Optional, Union from validataclass.exceptions import ( DecimalPlacesError, @@ -88,7 +88,7 @@ class DecimalValidator(StringValidator): rounding: Optional[str] = None # Precompiled regular expression for decimal values - decimal_regex: Pattern[str] = re.compile(r'[+-]?([0-9]+\.[0-9]*|\.?[0-9]+)') + decimal_regex: re.Pattern[str] = re.compile(r'[+-]?([0-9]+\.[0-9]*|\.?[0-9]+)') def __init__( self, diff --git a/src/validataclass/validators/dict_validator.py b/src/validataclass/validators/dict_validator.py index 38d3849..a77038d 100644 --- a/src/validataclass/validators/dict_validator.py +++ b/src/validataclass/validators/dict_validator.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict, List, Optional, Set +from typing import Any, Optional from validataclass.exceptions import ( DictFieldsValidationError, @@ -66,21 +66,21 @@ class DictValidator(Validator): """ # Dictionary to specify which validators are applied to which fields of the input dictionary - field_validators: Dict[str, Validator] + field_validators: dict[str, Validator] # Validator that is applied to all fields not specified in field_validators default_validator: Optional[Validator] # Set of required fields - required_fields: Set[str] + required_fields: set[str] def __init__( self, *, - field_validators: Optional[Dict[str, Validator]] = None, + field_validators: Optional[dict[str, Validator]] = None, default_validator: Optional[Validator] = None, - required_fields: Optional[List[str]] = None, - optional_fields: Optional[List[str]] = None + required_fields: Optional[list[str]] = None, + optional_fields: Optional[list[str]] = None ): """ Creates a `DictValidator`. @@ -132,7 +132,7 @@ def __init__( if optional_fields is not None: self.required_fields = self.required_fields - set(optional_fields) - def validate(self, input_data: Any, **kwargs: Any) -> Dict[str, Any]: + def validate(self, input_data: Any, **kwargs: Any) -> dict[str, Any]: """ Validates input data. Returns a validated dict. """ @@ -143,8 +143,8 @@ def validate(self, input_data: Any, **kwargs: Any) -> Dict[str, Any]: if type(key) is not str: raise DictInvalidKeyTypeError() - field_errors: Dict[str, ValidationError] = {} - validated_dict: Dict[str, Any] = {} + field_errors: dict[str, ValidationError] = {} + validated_dict: dict[str, Any] = {} # Check that required fields exist in input data for field_name in self.required_fields: diff --git a/src/validataclass/validators/email_validator.py b/src/validataclass/validators/email_validator.py index a9fad34..df1b881 100644 --- a/src/validataclass/validators/email_validator.py +++ b/src/validataclass/validators/email_validator.py @@ -5,7 +5,7 @@ """ import re -from typing import Any, Pattern +from typing import Any from validataclass.exceptions import InvalidEmailError from validataclass.internal import internet_helpers @@ -51,7 +51,7 @@ class EmailValidator(StringValidator): """ # Precompiled regular expression - email_regex: Pattern[str] = re.compile( + email_regex: re.Pattern[str] = re.compile( f'(?P {_REGEX_LOCAL_PART_CHARS}+ (?: \\.{_REGEX_LOCAL_PART_CHARS}+)* ) ' f'@ (?P [^@?]+ )', re.IGNORECASE | re.VERBOSE, diff --git a/src/validataclass/validators/enum_validator.py b/src/validataclass/validators/enum_validator.py index fb967fe..aaf33dd 100644 --- a/src/validataclass/validators/enum_validator.py +++ b/src/validataclass/validators/enum_validator.py @@ -4,8 +4,9 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ +from collections.abc import Iterable from enum import Enum -from typing import Any, Generic, Iterable, Optional, Type, TypeVar, Union +from typing import Any, Generic, Optional, TypeVar, Union from validataclass.exceptions import InvalidValidatorOptionException, ValueNotAllowedError from .any_of_validator import AnyOfValidator @@ -69,13 +70,13 @@ class EnumValidator(Generic[T_Enum], AnyOfValidator): """ # Enum class used to determine the list of allowed values - enum_cls: Type[T_Enum] + enum_cls: type[T_Enum] # TODO: For version 1.0, remove the old parameter "case_insensitive" completely and set a real default value for the # new "case_sensitive" parameter. (See base AnyOfValidator.) def __init__( self, - enum_cls: Type[T_Enum], + enum_cls: type[T_Enum], *, allowed_values: Optional[Iterable[Any]] = None, allowed_types: Optional[Union[type, Iterable[type]]] = None, diff --git a/src/validataclass/validators/float_to_decimal_validator.py b/src/validataclass/validators/float_to_decimal_validator.py index 48507c5..78d6e76 100644 --- a/src/validataclass/validators/float_to_decimal_validator.py +++ b/src/validataclass/validators/float_to_decimal_validator.py @@ -7,7 +7,7 @@ import decimal import math from decimal import Decimal -from typing import Any, List, Optional, Union +from typing import Any, Optional, Union from validataclass.exceptions import NonFiniteNumberError from .decimal_validator import DecimalValidator @@ -65,7 +65,7 @@ class FloatToDecimalValidator(DecimalValidator): allow_strings: bool = False # List of allowed input types - allowed_types: List[type] + allowed_types: list[type] def __init__( self, diff --git a/src/validataclass/validators/list_validator.py b/src/validataclass/validators/list_validator.py index 29c0d4d..9308a69 100644 --- a/src/validataclass/validators/list_validator.py +++ b/src/validataclass/validators/list_validator.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Generic, List, Optional, TypeVar +from typing import Any, Generic, Optional, TypeVar from validataclass.exceptions import ( InvalidValidatorOptionException, @@ -109,7 +109,7 @@ def __init__( self.max_length = max_length self.discard_invalid = discard_invalid - def validate(self, input_data: Any, **kwargs: Any) -> List[T_ListItem]: + def validate(self, input_data: Any, **kwargs: Any) -> list[T_ListItem]: """ Validates input data. Returns a validated list. """ diff --git a/src/validataclass/validators/regex_validator.py b/src/validataclass/validators/regex_validator.py index 301a751..00ff923 100644 --- a/src/validataclass/validators/regex_validator.py +++ b/src/validataclass/validators/regex_validator.py @@ -5,7 +5,7 @@ """ import re -from typing import Any, Optional, Pattern, Type, Union +from typing import Any, Optional, Union from validataclass.exceptions import RegexMatchError, ValidationError from .string_validator import StringValidator @@ -85,13 +85,13 @@ class InvalidHexNumberError(RegexMatchError): """ # Precompiled regex pattern - regex_pattern: Pattern[str] + regex_pattern: re.Pattern[str] # Output template output_template: Optional[str] # Exception class to use when regex matching fails - custom_error_class: Type[ValidationError] + custom_error_class: type[ValidationError] # Custom error code to use in the regex match exception (use default if None) custom_error_code: Optional[str] @@ -101,10 +101,10 @@ class InvalidHexNumberError(RegexMatchError): def __init__( self, - pattern: Union[Pattern[str], str], + pattern: Union[re.Pattern[str], str], output_template: Optional[str] = None, *, - custom_error_class: Type[ValidationError] = RegexMatchError, + custom_error_class: type[ValidationError] = RegexMatchError, custom_error_code: Optional[str] = None, allow_empty: bool = False, **kwargs: Any, diff --git a/src/validataclass/validators/reject_validator.py b/src/validataclass/validators/reject_validator.py index 6a50d02..cd456d2 100644 --- a/src/validataclass/validators/reject_validator.py +++ b/src/validataclass/validators/reject_validator.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Optional, Type +from typing import Any, Optional from validataclass.exceptions import ValidationError, FieldNotAllowedError from .validator import Validator @@ -62,7 +62,7 @@ class CustomValidationError(ValidationError): allow_none: bool # Validation error to raise when rejecting input - error_class: Type[ValidationError] + error_class: type[ValidationError] error_code: Optional[str] error_reason: Optional[str] @@ -70,7 +70,7 @@ def __init__( self, *, allow_none: bool = False, - error_class: Type[ValidationError] = FieldNotAllowedError, + error_class: type[ValidationError] = FieldNotAllowedError, error_code: Optional[str] = None, error_reason: Optional[str] = None, ): diff --git a/src/validataclass/validators/time_validator.py b/src/validataclass/validators/time_validator.py index b168a44..93aa4bc 100644 --- a/src/validataclass/validators/time_validator.py +++ b/src/validataclass/validators/time_validator.py @@ -7,7 +7,7 @@ import re from datetime import time from enum import Enum -from typing import Any, Pattern +from typing import Any from validataclass.exceptions import InvalidTimeError from .string_validator import StringValidator @@ -78,7 +78,7 @@ class TimeValidator(StringValidator): time_format: TimeFormat # Precompiled regular expression for the specified time string format - time_format_regex: Pattern[str] + time_format_regex: re.Pattern[str] def __init__(self, time_format: TimeFormat = TimeFormat.WITH_SECONDS): """ diff --git a/src/validataclass/validators/url_validator.py b/src/validataclass/validators/url_validator.py index 2721d59..c8747db 100644 --- a/src/validataclass/validators/url_validator.py +++ b/src/validataclass/validators/url_validator.py @@ -5,7 +5,7 @@ """ import re -from typing import Any, List, Optional, Pattern +from typing import Any, Optional from validataclass.exceptions import InvalidUrlError from validataclass.internal import internet_helpers @@ -69,7 +69,7 @@ class UrlValidator(StringValidator): """ # List of schemes allowed in URLs (empty list means any scheme is allowed) - allowed_schemes: List[str] + allowed_schemes: list[str] # Whether domain names must have a top-level domain (e.g. "myhost" or "localhost" would not be allowed) require_tld: bool @@ -84,7 +84,7 @@ class UrlValidator(StringValidator): allow_empty: bool # Precompiled regular expression - url_regex: Pattern[str] = re.compile( + url_regex: re.Pattern[str] = re.compile( r''' (?P [a-z][a-z0-9.+-]* ) :// @@ -99,7 +99,7 @@ class UrlValidator(StringValidator): def __init__( self, *, - allowed_schemes: Optional[List[str]] = None, + allowed_schemes: Optional[list[str]] = None, require_tld: bool = True, allow_ip: bool = True, allow_userinfo: bool = False, diff --git a/src/validataclass/validators/validator.py b/src/validataclass/validators/validator.py index 2930a07..ff31aae 100644 --- a/src/validataclass/validators/validator.py +++ b/src/validataclass/validators/validator.py @@ -7,7 +7,7 @@ import inspect import warnings from abc import ABC, abstractmethod -from typing import Any, List, Union +from typing import Any, Union from validataclass.exceptions import InvalidTypeError, RequiredValueError @@ -75,7 +75,7 @@ def _ensure_not_none(input_data: Any) -> None: if input_data is None: raise RequiredValueError() - def _ensure_type(self, input_data: Any, expected_types: Union[type, List[type]]) -> None: + def _ensure_type(self, input_data: Any, expected_types: Union[type, list[type]]) -> None: """ Checks if input data is not `None` and has the expected type (or one of multiple expected types). diff --git a/tests/dataclasses/_helpers.py b/tests/dataclasses/_helpers.py index 7aa1e13..1a24f39 100644 --- a/tests/dataclasses/_helpers.py +++ b/tests/dataclasses/_helpers.py @@ -6,23 +6,17 @@ import dataclasses import sys -from typing import Any, Dict, Type, TYPE_CHECKING +from typing import Any import pytest from validataclass.dataclasses import Default from validataclass.validators import T_Dataclass -# TODO: Replace type alias with dataclasses.Field[Any] when removing Python 3.9 support. (#15) -if TYPE_CHECKING: - T_DataclassField = dataclasses.Field[Any] -else: - T_DataclassField = dataclasses.Field - # Test helpers for dataclass tests -def assert_field_default(field: T_DataclassField, default_value: Any) -> None: +def assert_field_default(field: dataclasses.Field[Any], default_value: Any) -> None: """ Asserts that a given (vali-)dataclass field has a specified default value. """ @@ -42,7 +36,7 @@ def assert_field_default(field: T_DataclassField, default_value: Any) -> None: assert metadata_default.get_value() == default_value -def assert_field_no_default(field: T_DataclassField) -> None: +def assert_field_no_default(field: dataclasses.Field[Any]) -> None: """ Asserts that a given (vali-)dataclass field has no default value. """ @@ -61,7 +55,7 @@ def assert_field_no_default(field: T_DataclassField) -> None: assert field.default_factory is dataclasses.MISSING -def get_dataclass_fields(cls: Type[T_Dataclass]) -> Dict[str, T_DataclassField]: +def get_dataclass_fields(cls: type[T_Dataclass]) -> dict[str, dataclasses.Field[Any]]: """ Returns a dictionary containing all fields of a given dataclass. """ diff --git a/tests/dataclasses/defaults_test.py b/tests/dataclasses/defaults_test.py index 0416994..0a55201 100644 --- a/tests/dataclasses/defaults_test.py +++ b/tests/dataclasses/defaults_test.py @@ -5,7 +5,7 @@ """ from copy import copy -from typing import Any, List +from typing import Any import pytest @@ -43,7 +43,7 @@ def test_default_immutable_values(value, expected_repr): @staticmethod def test_default_list_deepcopied(): """ Test Default object with a list, make sure that it is deepcopied. """ - default_list: List[Any] = [] + default_list: list[Any] = [] default = Default(default_list) # Check string representation and value diff --git a/tests/dataclasses/validataclass_test.py b/tests/dataclasses/validataclass_test.py index bec424d..0e58d4c 100644 --- a/tests/dataclasses/validataclass_test.py +++ b/tests/dataclasses/validataclass_test.py @@ -7,7 +7,7 @@ # mypy: no-strict-equality import dataclasses -from typing import Dict, List, Optional, Union +from typing import Optional, Union import pytest @@ -215,8 +215,8 @@ def test_validataclass_with_mutable_defaults(): @validataclass class UnitTestDataclass: - field_list: List[int] = ListValidator(IntegerValidator()), Default([]) - field_dict: Dict[str, int] = ( + field_list: list[int] = ListValidator(IntegerValidator()), Default([]) + field_dict: dict[str, int] = ( DictValidator(field_validators={'foo': IntegerValidator()}), Default({'foo': 0}), ) diff --git a/tests/helpers/datetime_range_test.py b/tests/helpers/datetime_range_test.py index 136cec7..49ca446 100644 --- a/tests/helpers/datetime_range_test.py +++ b/tests/helpers/datetime_range_test.py @@ -5,9 +5,9 @@ """ from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo import pytest -from dateutil import tz from tests.test_utils import unpack_params from validataclass.helpers import DateTimeRange, DateTimeOffsetRange @@ -35,7 +35,7 @@ class DateTimeRangeTest: [ datetime(1900, 1, 1, 0, 0, 0), datetime(2021, 9, 7, 12, 34, 56), - datetime(2999, 12, 31, 12, 34, 56, tzinfo=tz.gettz('Europe/Berlin')), + datetime(2999, 12, 31, 12, 34, 56, tzinfo=ZoneInfo('Europe/Berlin')), ], ) def test_range_without_boundaries(input_datetime): @@ -237,21 +237,21 @@ def test_range_with_callable_boundaries(): [ # Boundaries have explicit timezone, same as local_timezone ( - datetime(2010, 2, 1, 13, 0, 0, tzinfo=tz.gettz('Europe/Berlin')), # No DST (+01:00), UTC: 12:00:00 - datetime(2010, 7, 1, 14, 0, 0, tzinfo=tz.gettz('Europe/Berlin')), # DST (+02:00), UTC: 12:00:00 - tz.gettz('Europe/Berlin'), + datetime(2010, 2, 1, 13, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')), # No DST (+01:00), UTC: 12:00:00 + datetime(2010, 7, 1, 14, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')), # DST (+02:00), UTC: 12:00:00 + ZoneInfo('Europe/Berlin'), ), # Boundaries have explicit timezone, but different from local_timezone ( datetime(2010, 2, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=-3))), # Fixed offset, UTC: 12:00:00 datetime(2010, 7, 1, 7, 0, 0, tzinfo=timezone(timedelta(hours=-5))), # Fixed offset, UTC: 12:00:00 - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), ), # Boundaries have no explicit timezone, local_timezone has DST ( datetime(2010, 2, 1, 13, 0, 0), # Should be interpreted as: No DST (+01:00), UTC: 12:00:00 datetime(2010, 7, 1, 14, 0, 0), # Should be interpreted as: DST (+02:00), UTC: 12:00:00 - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), ), # Boundaries have no explicit timezone, local_timezone is UTC ( @@ -271,9 +271,9 @@ def test_range_with_callable_boundaries(): # UTC: 12:00:00 (datetime(2010, 2, 1, 12, 0, 0, tzinfo=timezone.utc), True), # No DST, UTC: 11:59:59 - (datetime(2010, 2, 1, 12, 59, 59, tzinfo=tz.gettz('Europe/Berlin')), False), + (datetime(2010, 2, 1, 12, 59, 59, tzinfo=ZoneInfo('Europe/Berlin')), False), # No DST, UTC: 12:00:00 - (datetime(2010, 2, 1, 13, 0, 0, tzinfo=tz.gettz('Europe/Berlin')), True), + (datetime(2010, 2, 1, 13, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')), True), # Fixed offset, UTC: 11:59:59 (datetime(2010, 2, 1, 12, 59, 59, tzinfo=timezone(timedelta(hours=1))), False), # Fixed offset, UTC: 12:00:00 @@ -290,9 +290,9 @@ def test_range_with_callable_boundaries(): # UTC: 12:00:01 (datetime(2010, 7, 1, 12, 0, 1, tzinfo=timezone.utc), False), # DST, UTC: 12:00:00 - (datetime(2010, 7, 1, 14, 0, 0, tzinfo=tz.gettz('Europe/Berlin')), True), + (datetime(2010, 7, 1, 14, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')), True), # DST, UTC: 12:00:01 - (datetime(2010, 7, 1, 14, 0, 1, tzinfo=tz.gettz('Europe/Berlin')), False), + (datetime(2010, 7, 1, 14, 0, 1, tzinfo=ZoneInfo('Europe/Berlin')), False), # Fixed offset, UTC: 12:00:00 (datetime(2010, 7, 1, 14, 0, 0, tzinfo=timezone(timedelta(hours=2))), True), # Fixed offset, UTC: 12:00:01 @@ -583,13 +583,13 @@ def test_offset_range_with_default_pivot(): # Pivot with explicit timezone (but different from local_timezone) ( datetime(2021, 7, 1, 12, 0, 0, tzinfo=timezone.utc), - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), ), # Pivot without timezone, local_timezone is affected by DST (UTC+2) ( datetime(2021, 7, 1, 14, 0, 0), - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), ), # Pivot without timezone, local_timezone is UTC @@ -609,10 +609,10 @@ def test_offset_range_with_default_pivot(): (datetime(2021, 7, 1, 13, 0, 1, tzinfo=timezone.utc), False), # UTC: 13:00:01 # Input in timezone Europe/Berlin with DST (UTC+2) - (datetime(2021, 7, 1, 13, 59, 59, tzinfo=tz.gettz('Europe/Berlin')), False), # UTC: 11:59:59 - (datetime(2021, 7, 1, 14, 0, 0, tzinfo=tz.gettz('Europe/Berlin')), True), # UTC: 12:00:00 - (datetime(2021, 7, 1, 15, 0, 0, tzinfo=tz.gettz('Europe/Berlin')), True), # UTC: 13:00:00 - (datetime(2021, 7, 1, 15, 0, 1, tzinfo=tz.gettz('Europe/Berlin')), False), # UTC: 13:00:01 + (datetime(2021, 7, 1, 13, 59, 59, tzinfo=ZoneInfo('Europe/Berlin')), False), # UTC: 11:59:59 + (datetime(2021, 7, 1, 14, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')), True), # UTC: 12:00:00 + (datetime(2021, 7, 1, 15, 0, 0, tzinfo=ZoneInfo('Europe/Berlin')), True), # UTC: 13:00:00 + (datetime(2021, 7, 1, 15, 0, 1, tzinfo=ZoneInfo('Europe/Berlin')), False), # UTC: 13:00:01 # Input with fixed offset as timezone (datetime(2021, 7, 1, 8, 59, 59, tzinfo=timezone(timedelta(hours=-3))), False), # UTC: 11:59:59 diff --git a/tests/test_utils.py b/tests/test_utils.py index e20fd8c..0777ff1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,12 +5,12 @@ """ from decimal import Decimal -from typing import Any, List, Tuple, Union +from typing import Any, Union from validataclass.validators import Validator -def unpack_params(*args: Any) -> List[Tuple[Any, ...]]: +def unpack_params(*args: Any) -> list[tuple[Any, ...]]: """ Returns a list containing tuples build from the arguments. @@ -64,7 +64,7 @@ def unpack_params(*args: Any) -> List[Tuple[Any, ...]]: ] ``` """ - unpacked: List[Tuple[Any, ...]] = [tuple()] + unpacked: list[tuple[Any, ...]] = [tuple()] for arg in args: if type(arg) is list: diff --git a/tests/validators/dataclass_validator_test.py b/tests/validators/dataclass_validator_test.py index 2409528..b9990ed 100644 --- a/tests/validators/dataclass_validator_test.py +++ b/tests/validators/dataclass_validator_test.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from decimal import Decimal -from typing import Any, Dict, List, Optional +from typing import Any, Optional import pytest @@ -166,7 +166,7 @@ class UnitTestPreValidateStaticMethodDataclass: example_int: int = IntegerValidator() @staticmethod - def __pre_validate__(input_data: Dict[Any, Any]) -> Dict[Any, Any]: + def __pre_validate__(input_data: dict[Any, Any]) -> dict[Any, Any]: mapping = { 'exampleStr': 'example_str', 'exampleInt': 'example_int', @@ -193,7 +193,7 @@ class UnitTestPreValidateClassMethodDataclass: example_int: int = IntegerValidator() @classmethod - def __pre_validate__(cls, input_data: Dict[Any, Any]) -> Dict[Any, Any]: + def __pre_validate__(cls, input_data: dict[Any, Any]) -> dict[Any, Any]: for from_key, to_key in cls.__key_mapping.items(): if from_key in input_data: input_data[to_key] = input_data.pop(from_key) @@ -212,7 +212,7 @@ class UnitTestPreValidateContextSensitiveDataclass: target_field: int = IntegerValidator() @classmethod - def __pre_validate__(cls, input_data: Dict[Any, Any], *, source_field_name: str) -> Dict[Any, Any]: + def __pre_validate__(cls, input_data: dict[Any, Any], *, source_field_name: str) -> dict[Any, Any]: if source_field_name in input_data: return {'target_field': input_data[source_field_name]} else: @@ -236,7 +236,7 @@ class UnitTestPreValidateContextSensitiveVarKwargsDataclass: example_int: int = IntegerValidator() @classmethod - def __pre_validate__(cls, input_data: Dict[Any, Any], **kwargs: Any) -> Dict[Any, Any]: + def __pre_validate__(cls, input_data: dict[Any, Any], **kwargs: Any) -> dict[Any, Any]: # Fill input_data with default values based on kwargs for key, default_value in kwargs.items(): if key not in input_data: @@ -252,7 +252,7 @@ class UnitTestInvalidPreValidateDataclass1: """ Dataclass with invalid __pre_validate__ class method: Not enough arguments. """ @classmethod - def __pre_validate__(cls) -> Dict[Any, Any]: + def __pre_validate__(cls) -> dict[Any, Any]: return {} @@ -261,7 +261,7 @@ class UnitTestInvalidPreValidateDataclass2: """ Dataclass with invalid __pre_validate__ static method: Not enough arguments. """ @staticmethod - def __pre_validate__() -> Dict[Any, Any]: + def __pre_validate__() -> dict[Any, Any]: return {} @@ -270,7 +270,7 @@ class UnitTestInvalidPreValidateDataclass3: """ Dataclass with invalid __pre_validate__ class method: Too many positional arguments. """ @classmethod - def __pre_validate__(cls, input_data: Dict[Any, Any], _extra_pos_argument: Any) -> Dict[Any, Any]: + def __pre_validate__(cls, input_data: dict[Any, Any], _extra_pos_argument: Any) -> dict[Any, Any]: return input_data @@ -279,7 +279,7 @@ class UnitTestInvalidPreValidateDataclass4: """ Dataclass with invalid __pre_validate__ static method: Too many positional arguments. """ @staticmethod - def __pre_validate__(input_data: Dict[Any, Any], _extra_pos_argument: Any) -> Dict[Any, Any]: + def __pre_validate__(input_data: dict[Any, Any], _extra_pos_argument: Any) -> dict[Any, Any]: return input_data @@ -288,7 +288,7 @@ class UnitTestInvalidPreValidateDataclass5: """ Dataclass with invalid __pre_validate__ class method: Too many (variable) positional arguments. """ @classmethod - def __pre_validate__(cls, input_data: Dict[Any, Any], *_args: Any) -> Dict[Any, Any]: + def __pre_validate__(cls, input_data: dict[Any, Any], *_args: Any) -> dict[Any, Any]: return input_data @@ -297,7 +297,7 @@ class UnitTestInvalidPreValidateDataclass6: """ Dataclass with invalid __pre_validate__ static method: Too many (variable) positional arguments. """ @staticmethod - def __pre_validate__(input_data: Dict[Any, Any], *_args: Any) -> Dict[Any, Any]: + def __pre_validate__(input_data: dict[Any, Any], *_args: Any) -> dict[Any, Any]: return input_data @@ -399,7 +399,7 @@ def counter(): @validataclass class DataclassWithDefaults: default_str: str = StringValidator(), Default('example default') - default_list: List[int] = ListValidator(IntegerValidator()), Default([]) + default_list: list[int] = ListValidator(IntegerValidator()), Default([]) default_counter: int = IntegerValidator(), DefaultFactory(counter) default_unset: OptionalUnset[str] = StringValidator(), DefaultUnset diff --git a/tests/validators/datetime_validator_test.py b/tests/validators/datetime_validator_test.py index 6e5ad00..e6cadff 100644 --- a/tests/validators/datetime_validator_test.py +++ b/tests/validators/datetime_validator_test.py @@ -5,9 +5,9 @@ """ from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo import pytest -from dateutil import tz from validataclass.exceptions import ( DateTimeRangeError, @@ -478,22 +478,22 @@ def test_with_discard_milliseconds(input_string, expected_datetime): # Test with a timezone that has Daylight Saving Time ( '2021-02-01T01:02:03', - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), datetime(2021, 2, 1, 1, 2, 3, tzinfo=timezone(timedelta(hours=1))), ), ( '2021-02-01T01:02:03Z', - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), datetime(2021, 2, 1, 1, 2, 3, tzinfo=timezone.utc), ), ( '2021-07-01T01:02:03', - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), datetime(2021, 7, 1, 1, 2, 3, tzinfo=timezone(timedelta(hours=2))), ), ( '2021-07-01T01:02:03Z', - tz.gettz('Europe/Berlin'), + ZoneInfo('Europe/Berlin'), datetime(2021, 7, 1, 1, 2, 3, tzinfo=timezone.utc), ), ], @@ -536,25 +536,25 @@ def test_with_local_timezone_valid(input_string, local_timezone, expected_dateti # Convert local datetimes (Europe/Berlin) with and without DST to a different timezone that has DST ( '2021-02-01T12:34:56', - tz.gettz('Europe/Helsinki'), - datetime(2021, 2, 1, 13, 34, 56, tzinfo=tz.gettz('Europe/Helsinki')), + ZoneInfo('Europe/Helsinki'), + datetime(2021, 2, 1, 13, 34, 56, tzinfo=ZoneInfo('Europe/Helsinki')), ), ( '2021-07-01T12:34:56', - tz.gettz('Europe/Helsinki'), - datetime(2021, 7, 1, 13, 34, 56, tzinfo=tz.gettz('Europe/Helsinki')), + ZoneInfo('Europe/Helsinki'), + datetime(2021, 7, 1, 13, 34, 56, tzinfo=ZoneInfo('Europe/Helsinki')), ), # Convert UTC datetimes to a timezone with DST ( '2021-02-01T12:34:56Z', - tz.gettz('Europe/Helsinki'), - datetime(2021, 2, 1, 14, 34, 56, tzinfo=tz.gettz('Europe/Helsinki')), + ZoneInfo('Europe/Helsinki'), + datetime(2021, 2, 1, 14, 34, 56, tzinfo=ZoneInfo('Europe/Helsinki')), ), ( '2021-07-01T12:34:56Z', - tz.gettz('Europe/Helsinki'), - datetime(2021, 7, 1, 15, 34, 56, tzinfo=tz.gettz('Europe/Helsinki')), + ZoneInfo('Europe/Helsinki'), + datetime(2021, 7, 1, 15, 34, 56, tzinfo=ZoneInfo('Europe/Helsinki')), ), # Convert datetimes with timezone info to UTC @@ -572,20 +572,20 @@ def test_with_local_timezone_valid(input_string, local_timezone, expected_dateti # Convert datetimes with timezone info to a timezone with DST ( '2021-02-01T00:34:56-12:00', - tz.gettz('Europe/Berlin'), - datetime(2021, 2, 1, 13, 34, 56, tzinfo=tz.gettz('Europe/Berlin')), + ZoneInfo('Europe/Berlin'), + datetime(2021, 2, 1, 13, 34, 56, tzinfo=ZoneInfo('Europe/Berlin')), ), ( '2021-07-01T00:34:56-12:00', - tz.gettz('Europe/Berlin'), - datetime(2021, 7, 1, 14, 34, 56, tzinfo=tz.gettz('Europe/Berlin')), + ZoneInfo('Europe/Berlin'), + datetime(2021, 7, 1, 14, 34, 56, tzinfo=ZoneInfo('Europe/Berlin')), ), ], ) def test_with_target_timezone_valid(input_string, target_timezone, expected_datetime): """ Test DateTimeValidator with target_timezone parameter with valid input. """ validator = DateTimeValidator( - local_timezone=tz.gettz('Europe/Berlin'), + local_timezone=ZoneInfo('Europe/Berlin'), target_timezone=target_timezone, ) @@ -709,7 +709,7 @@ def test_datetime_range_with_local_timezone_valid(input_string, expected_datetim lower_boundary=datetime(2021, 9, 8, 14, 0, 0), upper_boundary=datetime(2021, 9, 8, 15, 0, 0), ) - validator = DateTimeValidator(local_timezone=tz.gettz('Europe/Berlin'), datetime_range=dt_range) + validator = DateTimeValidator(local_timezone=ZoneInfo('Europe/Berlin'), datetime_range=dt_range) assert validator.validate(input_string) == expected_datetime @@ -734,7 +734,7 @@ def test_datetime_range_with_local_timezone_invalid(input_string): lower_boundary=datetime(2021, 9, 8, 14, 0, 0), upper_boundary=datetime(2021, 9, 8, 15, 0, 0), ) - validator = DateTimeValidator(local_timezone=tz.gettz('Europe/Berlin'), datetime_range=dt_range) + validator = DateTimeValidator(local_timezone=ZoneInfo('Europe/Berlin'), datetime_range=dt_range) with pytest.raises(DateTimeRangeError) as exception_info: validator.validate(input_string) diff --git a/tests/validators/discard_validator_test.py b/tests/validators/discard_validator_test.py index 9949bf0..30211dd 100644 --- a/tests/validators/discard_validator_test.py +++ b/tests/validators/discard_validator_test.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, List +from typing import Any import pytest @@ -17,7 +17,7 @@ class DiscardValidatorTest: Unit tests for the DiscardValidator. """ - example_input_data: List[Any] = [ + example_input_data: list[Any] = [ None, True, False, diff --git a/tests/validators/float_validator_test.py b/tests/validators/float_validator_test.py index 3d423f7..19f4bf5 100644 --- a/tests/validators/float_validator_test.py +++ b/tests/validators/float_validator_test.py @@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ -from typing import Any, Dict +from typing import Any import pytest @@ -133,7 +133,7 @@ def test_float_value_range_invalid(min_value, max_value, input_data_list): validator = FloatValidator(min_value=min_value, max_value=max_value) # Construct error dict with min_value and/or max_value, depending on which is specified - expected_error_dict: Dict[str, Any] = {'code': 'number_range_error'} + expected_error_dict: dict[str, Any] = {'code': 'number_range_error'} expected_error_dict.update({'min_value': float(min_value)} if min_value is not None else {}) expected_error_dict.update({'max_value': float(max_value)} if max_value is not None else {}) diff --git a/tox.ini b/tox.ini index 298ef83..dc57a3a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.5.1 -envlist = clean,py{312,311,310,39,38},report,flake8,mypy +envlist = clean,py{312,311,310,39},report,flake8,mypy skip_missing_interpreters = true isolated_build = true @@ -23,7 +23,7 @@ skip_install = true deps = flake8 commands = flake8 src/ tests/ -[testenv:mypy,py{312,311,310,39,38}-mypy] +[testenv:mypy,py{312,311,310,39}-mypy] extras = testing commands = mypy @@ -32,7 +32,7 @@ skip_install = true deps = {[testenv:report]deps} commands = coverage erase -[testenv:report,py{312,311,310,39,38}-report] +[testenv:report,py{312,311,310,39}-report] skip_install = true deps = coverage