From 4fca88d5aef35bfa0eb312f4119feb1c7889a933 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Wed, 17 May 2023 16:32:16 +0200 Subject: [PATCH 1/2] Add Python 3.11 to test environments --- .github/workflows/tests.yml | 1 + .gitignore | 1 + Makefile | 13 +++++++------ setup.cfg | 1 + tox.ini | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 595c54f..b7db8ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,7 @@ jobs: - '3.8' - '3.9' - '3.10' + - '3.11' steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 7080065..fcfcb02 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ /.tox_docker /.eggs /.coverage +/.coverage.* /.pytest_cache /build /dist diff --git a/Makefile b/Makefile index 071c340..9c9e3e9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ # Settings -# NOTE: The multi-python image is a fork of fkrull/multi-python, which as of now has not been updated for Python 3.10 yet -DOCKER_MULTI_PYTHON_IMAGE = gnufede/multi-python:focal +DOCKER_MULTI_PYTHON_IMAGE = acidrain/multi-python:latest DOCKER_USER = "$(shell id -u):$(shell id -g)" # Default target @@ -38,7 +37,7 @@ venv-tox: # Only run pytest .PHONY: test test: - tox -e 'clean,py{310,39,38,37},report' + tox -e 'clean,py{311,310,39,38,37},report' # Only run flake8 linter .PHONY: flake8 @@ -62,7 +61,9 @@ docker-tox: tox --workdir .tox_docker $(TOX_ARGS) # Run partial tox test suites in Docker -.PHONY: docker-tox-py310 docker-tox-py39 docker-tox-py38 docker-tox-py37 +.PHONY: docker-tox-py311 docker-tox-py310 docker-tox-py39 docker-tox-py38 docker-tox-py37 +docker-tox-py311: TOX_ARGS="-e clean,py311,py311-report" +docker-tox-py311: docker-tox docker-tox-py310: TOX_ARGS="-e clean,py310,py310-report" docker-tox-py310: docker-tox docker-tox-py39: TOX_ARGS="-e clean,py39,py39-report" @@ -79,6 +80,7 @@ docker-tox-all: make docker-tox-py38 make docker-tox-py39 make docker-tox-py310 + make docker-tox-py311 # Pull the latest image of the multi-python Docker image .PHONY: docker-pull @@ -91,7 +93,7 @@ docker-pull: .PHONY: clean clean: - rm -rf .coverage .pytest_cache reports src/validataclass/_version.py + rm -rf .coverage .pytest_cache reports src/validataclass/_version.py .tox .tox_docker .eggs src/*.egg-info venv .PHONY: clean-dist clean-dist: @@ -99,4 +101,3 @@ clean-dist: .PHONY: clean-all clean-all: clean clean-dist - rm -rf .tox .tox_docker .eggs src/*.egg-info venv diff --git a/setup.cfg b/setup.cfg index 937ffba..2fbd4a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries :: Python Modules Topic :: Utilities diff --git a/tox.ini b/tox.ini index 07beb76..3c73d8b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = clean,py{310,39,38,37},flake8,report +envlist = clean,py{311,310,39,38,37},flake8,report skip_missing_interpreters = true isolated_build = true @@ -36,7 +36,7 @@ commands = # These environments basically are an alias for "report" that allow to specify the python version used for coverage. # tox 4 apparently will have a "labels" option that can be used to define aliases, but that version is not released yet. -[testenv:py{310,39,38,37}-report] +[testenv:py{311,310,39,38,37}-report] skip_install = true deps = {[testenv:report]deps} commands = {[testenv:report]commands} From 38721d77f073789c1b689f239e2a753594a11fd2 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Wed, 17 May 2023 16:40:01 +0200 Subject: [PATCH 2/2] Fix missing __hash__ of UnsetValue and Defaults This fixes an incompatibility with Python 3.11, where the dataclass implementation has changed to check for the immutability of default values by checking if they are hashable. This wasn't true for UnsetValue, so it couldn't be used as a default value anymore. For the Default classes, __hash__ isn't really necessary, but these objects still should be considered immutable. --- src/validataclass/dataclasses/defaults.py | 10 ++++++++++ src/validataclass/helpers/unset_value.py | 4 ++-- tests/dataclasses/defaults_test.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/validataclass/dataclasses/defaults.py b/src/validataclass/dataclasses/defaults.py index a72c3ab..ac47e4b 100644 --- a/src/validataclass/dataclasses/defaults.py +++ b/src/validataclass/dataclasses/defaults.py @@ -40,6 +40,9 @@ def __eq__(self, other): return self.value == other.value return NotImplemented + def __hash__(self): + return hash(self.value) + def get_value(self) -> Any: return deepcopy(self.value) @@ -75,6 +78,9 @@ def __eq__(self, other): return isinstance(other, DefaultFactory) and self.factory == other.factory return NotImplemented + def __hash__(self): + return hash(self.factory) + def get_value(self) -> Any: return self.factory() @@ -127,6 +133,10 @@ def __eq__(self, other): # Nothing is equal to NoDefault except itself return type(self) is type(other) + def __hash__(self): + # Use default implementation + return object.__hash__(self) + def get_value(self) -> NoReturn: raise ValueError('No default value specified!') diff --git a/src/validataclass/helpers/unset_value.py b/src/validataclass/helpers/unset_value.py index a0ac5d9..34fdb43 100644 --- a/src/validataclass/helpers/unset_value.py +++ b/src/validataclass/helpers/unset_value.py @@ -39,8 +39,8 @@ def __str__(self): def __bool__(self): return False - def __eq__(self, other): - return other is self + # Don't define __eq__ because the default implementation is fine (identity check), and because we would then have to + # implement __hash__ as well, otherwise UnsetValue would be considered mutable by @dataclass. # Create sentinel object and redefine __new__ so that the object cannot be cloned diff --git a/tests/dataclasses/defaults_test.py b/tests/dataclasses/defaults_test.py index c4d0493..c714f9c 100644 --- a/tests/dataclasses/defaults_test.py +++ b/tests/dataclasses/defaults_test.py @@ -87,6 +87,12 @@ def test_default_non_equality(first, second): assert first != second assert second != first + @staticmethod + @pytest.mark.parametrize('value', [None, 0, 42, 'banana']) + def test_default_hashable(value): + """ Test hashability (__hash__) of Default objects. """ + assert hash(Default(value)) == hash(value) + class DefaultFactoryTest: """ Tests for the DefaultFactory class. """ @@ -167,6 +173,11 @@ def test_default_factory_non_equality(first, second): assert first != second assert second != first + @staticmethod + def test_default_factory_hashable(): + """ Test hashability (__hash__) of DefaultFactory objects. """ + assert hash(DefaultFactory(list)) == hash(list) + class DefaultUnsetTest: """ Tests for the DefaultUnset sentinel object. """ @@ -238,6 +249,11 @@ def test_no_default_non_equality(other): assert NoDefault != other assert other != NoDefault + @staticmethod + def test_no_default_hashable(): + """ Test that NoDefault is hashable (i.e. implements __hash__). """ + assert hash(NoDefault) == object.__hash__(NoDefault) + @staticmethod def test_no_default_call(): """ Test that calling NoDefault returns the sentinel itself. """