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

Drop support for Python 3.8 #128

Merged
merged 6 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- '3.8'
- '3.9'
- '3.10'
- '3.11'
Expand Down
12 changes: 4 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
binaryDiv marked this conversation as resolved.
Show resolved Hide resolved
docker-tox:
docker run --rm --tty \
--user $(DOCKER_USER) \
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions docs/01-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand All @@ -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()


Expand Down
4 changes: 1 addition & 3 deletions docs/03-basic-validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/05-dataclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down
5 changes: 1 addition & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +29,7 @@ classifiers =
package_dir =
= src
packages = find:
python_requires = >=3.8
python_requires = ~=3.9
install_requires =
typing-extensions ~= 4.3

Expand All @@ -45,5 +44,3 @@ testing =
coverage-conditional-plugin ~= 0.5
flake8 ~= 7.0
mypy ~= 1.9
python-dateutil
types-python-dateutil
3 changes: 2 additions & 1 deletion src/validataclass/dataclasses/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 10 additions & 9 deletions src/validataclass/dataclasses/validataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]]:
...


Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/validataclass/dataclasses/validataclass_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/validataclass/dataclasses/validataclass_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import dataclasses
import warnings
from typing import Any, Dict, cast
from typing import Any, cast

from typing_extensions import Self

Expand All @@ -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).

Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions src/validataclass/exceptions/base_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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:
Expand All @@ -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).

Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/validataclass/exceptions/common_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions src/validataclass/exceptions/dataclass_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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
Expand Down
Loading
Loading