Drop Python 3.7 (#1340)

* Drop Python 3.7

* Add news fragment

* update Ruff
This commit is contained in:
Hynek Schlawack 2024-08-29 17:35:04 +02:00 committed by GitHub
parent 5c3af47603
commit a8e24b0186
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 37 additions and 92 deletions

View File

@ -63,7 +63,7 @@ jobs:
V=${{ matrix.python-version }} V=${{ matrix.python-version }}
DO_MYPY=1 DO_MYPY=1
if [[ "$V" == "3.7" || "$V" == "3.8" ]]; then if [[ "$V" == "3.8" ]]; then
DO_MYPY=0 DO_MYPY=0
fi fi
@ -73,7 +73,6 @@ jobs:
uv pip install --system tox uv pip install --system tox
- run: uv pip install --system tox-uv - run: uv pip install --system tox-uv
if: matrix.python-version != '3.7'
- run: python -Im tox run -e ${{ env.TOX_PYTHON }}-mypy - run: python -Im tox run -e ${{ env.TOX_PYTHON }}-mypy
if: env.DO_MYPY == '1' if: env.DO_MYPY == '1'

View File

@ -9,7 +9,7 @@ repos:
- id: black - id: black
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2 rev: v0.6.3
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]

View File

@ -0,0 +1 @@
Python 3.7 has been dropped.

View File

@ -9,13 +9,12 @@ build-backend = "hatchling.build"
name = "attrs" name = "attrs"
authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }] authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }]
license = { text = "MIT" } license = { text = "MIT" }
requires-python = ">=3.7" requires-python = ">=3.8"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
keywords = ["class", "attribute", "boilerplate"] keywords = ["class", "attribute", "boilerplate"]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
@ -26,7 +25,7 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = ["importlib_metadata;python_version<'3.8'"] dependencies = []
dynamic = ["version", "readme"] dynamic = ["version", "readme"]
[project.optional-dependencies] [project.optional-dependencies]

View File

@ -5,11 +5,10 @@ Classes Without Boilerplate
""" """
from functools import partial from functools import partial
from typing import Callable from typing import Callable, Protocol
from . import converters, exceptions, filters, setters, validators from . import converters, exceptions, filters, setters, validators
from ._cmp import cmp_using from ._cmp import cmp_using
from ._compat import Protocol
from ._config import get_run_validators, set_run_validators from ._config import get_run_validators, set_run_validators
from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types
from ._make import ( from ._make import (
@ -85,10 +84,7 @@ def _make_getattr(mod_name: str) -> Callable:
msg = f"module {mod_name} has no attribute {name}" msg = f"module {mod_name} has no attribute {name}"
raise AttributeError(msg) raise AttributeError(msg)
try: from importlib.metadata import metadata
from importlib.metadata import metadata
except ImportError:
from importlib_metadata import metadata
meta = metadata("attrs") meta = metadata("attrs")

View File

@ -76,29 +76,20 @@ NOTHING = _Nothing.NOTHING
# NOTE: Factory lies about its return type to make this possible: # NOTE: Factory lies about its return type to make this possible:
# `x: List[int] # = Factory(list)` # `x: List[int] # = Factory(list)`
# Work around mypy issue #4554 in the common case by using an overload. # Work around mypy issue #4554 in the common case by using an overload.
if sys.version_info >= (3, 8): from typing import Literal
from typing import Literal
@overload
def Factory(factory: Callable[[], _T]) -> _T: ...
@overload
def Factory(
factory: Callable[[Any], _T],
takes_self: Literal[True],
) -> _T: ...
@overload
def Factory(
factory: Callable[[], _T],
takes_self: Literal[False],
) -> _T: ...
else: @overload
@overload def Factory(factory: Callable[[], _T]) -> _T: ...
def Factory(factory: Callable[[], _T]) -> _T: ... @overload
@overload def Factory(
def Factory( factory: Callable[[Any], _T],
factory: Union[Callable[[Any], _T], Callable[[], _T]], takes_self: Literal[True],
takes_self: bool = ..., ) -> _T: ...
) -> _T: ... @overload
def Factory(
factory: Callable[[], _T],
takes_self: Literal[False],
) -> _T: ...
In = TypeVar("In") In = TypeVar("In")
Out = TypeVar("Out") Out = TypeVar("Out")

View File

@ -10,7 +10,6 @@ from typing import _GenericAlias
PYPY = platform.python_implementation() == "PyPy" PYPY = platform.python_implementation() == "PyPy"
PY_3_8_PLUS = sys.version_info[:2] >= (3, 8)
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
@ -19,14 +18,6 @@ PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14) PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
if sys.version_info < (3, 8):
try:
from typing_extensions import Protocol
except ImportError: # pragma: no cover
Protocol = object
else:
from typing import Protocol # noqa: F401
if PY_3_14_PLUS: # pragma: no cover if PY_3_14_PLUS: # pragma: no cover
import annotationlib import annotationlib

View File

@ -20,7 +20,6 @@ from operator import itemgetter
# having the thread-local in the globals here. # having the thread-local in the globals here.
from . import _compat, _config, setters from . import _compat, _config, setters
from ._compat import ( from ._compat import (
PY_3_8_PLUS,
PY_3_10_PLUS, PY_3_10_PLUS,
PY_3_11_PLUS, PY_3_11_PLUS,
_AnnotationExtractor, _AnnotationExtractor,
@ -790,16 +789,11 @@ class _ClassBuilder:
): ):
names += ("__weakref__",) names += ("__weakref__",)
if PY_3_8_PLUS: cached_properties = {
cached_properties = { name: cached_property.func
name: cached_property.func for name, cached_property in cd.items()
for name, cached_property in cd.items() if isinstance(cached_property, functools.cached_property)
if isinstance(cached_property, functools.cached_property) }
}
else:
# `functools.cached_property` was introduced in 3.8.
# So can't be used before this.
cached_properties = {}
# Collect methods with a `__class__` reference that are shadowed in the new class. # Collect methods with a `__class__` reference that are shadowed in the new class.
# To know to update them. # To know to update them.
@ -2213,7 +2207,7 @@ def _attrs_to_init_script(
# If pre init method has arguments, pass same arguments as `__init__`. # If pre init method has arguments, pass same arguments as `__init__`.
lines[0] = f"self.__attrs_pre_init__({pre_init_args})" lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
# Python 3.7 doesn't allow backslashes in f strings. # Python <3.12 doesn't allow backslashes in f-strings.
NL = "\n " NL = "\n "
return ( return (
f"""def {method_name}(self, {args}): f"""def {method_name}(self, {args}):

View File

@ -13,8 +13,6 @@ from hypothesis import strategies as st
import attr import attr
from attr._compat import PY_3_8_PLUS
from .utils import make_class from .utils import make_class
@ -189,9 +187,7 @@ def simple_classes(
cls_dict["__init__"] = init cls_dict["__init__"] = init
bases = (object,) bases = (object,)
if cached_property or ( if cached_property or (cached_property is None and cached_property_flag):
PY_3_8_PLUS and cached_property is None and cached_property_flag
):
class BaseWithCachedProperty: class BaseWithCachedProperty:
@functools.cached_property @functools.cached_property

View File

@ -2,6 +2,8 @@
import types import types
from typing import Protocol
import pytest import pytest
import attr import attr
@ -59,5 +61,5 @@ def test_attrsinstance_subclass_protocol():
It's possible to subclass AttrsInstance and Protocol at once. It's possible to subclass AttrsInstance and Protocol at once.
""" """
class Foo(attr.AttrsInstance, attr._compat.Protocol): class Foo(attr.AttrsInstance, Protocol):
def attribute(self) -> int: ... def attribute(self) -> int: ...

View File

@ -21,7 +21,7 @@ from hypothesis.strategies import booleans, integers, lists, sampled_from, text
import attr import attr
from attr import _config from attr import _config
from attr._compat import PY_3_8_PLUS, PY_3_10_PLUS, PY_3_14_PLUS from attr._compat import PY_3_10_PLUS, PY_3_14_PLUS
from attr._make import ( from attr._make import (
Attribute, Attribute,
Factory, Factory,
@ -1837,7 +1837,6 @@ class TestClassBuilder:
assert [C2] == C.__subclasses__() assert [C2] == C.__subclasses__()
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
@pytest.mark.xfail(PY_3_14_PLUS, reason="Currently broken on nightly.") @pytest.mark.xfail(PY_3_14_PLUS, reason="Currently broken on nightly.")
def test_no_references_to_original_when_using_cached_property(self): def test_no_references_to_original_when_using_cached_property(self):
""" """

View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import sys
from importlib import metadata
import pytest import pytest
@ -8,12 +9,6 @@ import attr
import attrs import attrs
if sys.version_info < (3, 8):
import importlib_metadata as metadata
else:
from importlib import metadata
@pytest.fixture(name="mod", params=(attr, attrs)) @pytest.fixture(name="mod", params=(attr, attrs))
def _mod(request): def _mod(request):
return request.param return request.param

View File

@ -15,7 +15,7 @@ import pytest
import attr import attr
import attrs import attrs
from attr._compat import PY_3_8_PLUS, PY_3_14_PLUS, PYPY from attr._compat import PY_3_14_PLUS, PYPY
# Pympler doesn't work on PyPy. # Pympler doesn't work on PyPy.
@ -722,7 +722,6 @@ def test_slots_super_property_get_shortcut():
assert B(17).f == 289 assert B(17).f == 289
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_allows_call(): def test_slots_cached_property_allows_call():
""" """
cached_property in slotted class allows call. cached_property in slotted class allows call.
@ -739,7 +738,6 @@ def test_slots_cached_property_allows_call():
assert A(11).f == 11 assert A(11).f == 11
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_class_does_not_have__dict__(): def test_slots_cached_property_class_does_not_have__dict__():
""" """
slotted class with cached property has no __dict__ attribute. slotted class with cached property has no __dict__ attribute.
@ -757,7 +755,6 @@ def test_slots_cached_property_class_does_not_have__dict__():
assert "__dict__" not in dir(A) assert "__dict__" not in dir(A)
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_works_on_frozen_isntances(): def test_slots_cached_property_works_on_frozen_isntances():
""" """
Infers type of cached property. Infers type of cached property.
@ -774,7 +771,6 @@ def test_slots_cached_property_works_on_frozen_isntances():
assert A(x=1).f == 1 assert A(x=1).f == 1
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
@pytest.mark.xfail( @pytest.mark.xfail(
PY_3_14_PLUS, reason="3.14 returns weird annotation for cached_properies" PY_3_14_PLUS, reason="3.14 returns weird annotation for cached_properies"
) )
@ -794,7 +790,6 @@ def test_slots_cached_property_infers_type():
assert A.__annotations__ == {"x": int, "f": int} assert A.__annotations__ == {"x": int, "f": int}
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_with_empty_getattr_raises_attribute_error_of_requested(): def test_slots_cached_property_with_empty_getattr_raises_attribute_error_of_requested():
""" """
Ensures error information is not lost. Ensures error information is not lost.
@ -815,7 +810,6 @@ def test_slots_cached_property_with_empty_getattr_raises_attribute_error_of_requ
a.z a.z
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_raising_attributeerror(): def test_slots_cached_property_raising_attributeerror():
""" """
Ensures AttributeError raised by a property is preserved by __getattr__() Ensures AttributeError raised by a property is preserved by __getattr__()
@ -854,7 +848,6 @@ def test_slots_cached_property_raising_attributeerror():
assert a.q == 2 assert a.q == 2
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_with_getattr_calls_getattr_for_missing_attributes(): def test_slots_cached_property_with_getattr_calls_getattr_for_missing_attributes():
""" """
Ensure __getattr__ implementation is maintained for non cached_properties. Ensure __getattr__ implementation is maintained for non cached_properties.
@ -876,7 +869,6 @@ def test_slots_cached_property_with_getattr_calls_getattr_for_missing_attributes
assert a.z == "z" assert a.z == "z"
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_getattr_in_superclass__is_called_for_missing_attributes_when_cached_property_present(): def test_slots_getattr_in_superclass__is_called_for_missing_attributes_when_cached_property_present():
""" """
Ensure __getattr__ implementation is maintained in subclass. Ensure __getattr__ implementation is maintained in subclass.
@ -900,7 +892,6 @@ def test_slots_getattr_in_superclass__is_called_for_missing_attributes_when_cach
assert b.z == "z" assert b.z == "z"
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_getattr_in_subclass_gets_superclass_cached_property(): def test_slots_getattr_in_subclass_gets_superclass_cached_property():
""" """
Ensure super() in __getattr__ is not broken through cached_property re-write. Ensure super() in __getattr__ is not broken through cached_property re-write.
@ -931,7 +922,6 @@ def test_slots_getattr_in_subclass_gets_superclass_cached_property():
assert b.z == "z" assert b.z == "z"
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_sub_class_with_independent_cached_properties_both_work(): def test_slots_sub_class_with_independent_cached_properties_both_work():
""" """
Subclassing shouldn't break cached properties. Subclassing shouldn't break cached properties.
@ -955,7 +945,6 @@ def test_slots_sub_class_with_independent_cached_properties_both_work():
assert B(1).g == 2 assert B(1).g == 2
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_with_multiple_cached_property_subclasses_works(): def test_slots_with_multiple_cached_property_subclasses_works():
""" """
Multiple sub-classes shouldn't break cached properties. Multiple sub-classes shouldn't break cached properties.
@ -991,7 +980,6 @@ def test_slots_with_multiple_cached_property_subclasses_works():
assert ab.h == "h" assert ab.h == "h"
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slotted_cached_property_can_access_super(): def test_slotted_cached_property_can_access_super():
""" """
Multiple sub-classes shouldn't break cached properties. Multiple sub-classes shouldn't break cached properties.
@ -1010,7 +998,6 @@ def test_slotted_cached_property_can_access_super():
assert B(x=1).f == 2 assert B(x=1).f == 2
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_sub_class_avoids_duplicated_slots(): def test_slots_sub_class_avoids_duplicated_slots():
""" """
Duplicating the slots is a waste of memory. Duplicating the slots is a waste of memory.
@ -1034,7 +1021,6 @@ def test_slots_sub_class_avoids_duplicated_slots():
assert B.__slots__ == () assert B.__slots__ == ()
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_sub_class_with_actual_slot(): def test_slots_sub_class_with_actual_slot():
""" """
A sub-class can have an explicit attrs field that replaces a cached property. A sub-class can have an explicit attrs field that replaces a cached property.
@ -1056,7 +1042,6 @@ def test_slots_sub_class_with_actual_slot():
assert B.__slots__ == () assert B.__slots__ == ()
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_is_not_called_at_construction(): def test_slots_cached_property_is_not_called_at_construction():
""" """
A cached property function should only be called at property access point. A cached property function should only be called at property access point.
@ -1077,7 +1062,6 @@ def test_slots_cached_property_is_not_called_at_construction():
assert call_count == 0 assert call_count == 0
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_repeat_call_only_once(): def test_slots_cached_property_repeat_call_only_once():
""" """
A cached property function should be called only once, on repeated attribute access. A cached property function should be called only once, on repeated attribute access.
@ -1100,7 +1084,6 @@ def test_slots_cached_property_repeat_call_only_once():
assert call_count == 1 assert call_count == 1
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_called_independent_across_instances(): def test_slots_cached_property_called_independent_across_instances():
""" """
A cached property value should be specific to the given instance. A cached property value should be specific to the given instance.
@ -1121,7 +1104,6 @@ def test_slots_cached_property_called_independent_across_instances():
assert obj_2.f == 2 assert obj_2.f == 2
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_properties_work_independently(): def test_slots_cached_properties_work_independently():
""" """
Multiple cached properties should work independently. Multiple cached properties should work independently.

View File

@ -2,7 +2,7 @@
min_version = 4 min_version = 4
env_list = env_list =
pre-commit, pre-commit,
py3{7,8,9,10,11,12,13}-tests, py3{8,9,10,11,12,13}-tests,
py3{9,10,11,12,13}-mypy, py3{9,10,11,12,13}-mypy,
pypy3, pypy3,
pyright, pyright,
@ -26,7 +26,7 @@ commands =
mypy: mypy tests/typing_example.py mypy: mypy tests/typing_example.py
mypy: mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi mypy: mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi
[testenv:py3{7,10,12}-tests] [testenv:py3{8,10,12}-tests]
extras = cov extras = cov
# Python 3.6+ has a number of compile-time warnings on invalid string escapes. # Python 3.6+ has a number of compile-time warnings on invalid string escapes.
# PYTHONWARNINGS=d makes them visible during the tox run. # PYTHONWARNINGS=d makes them visible during the tox run.
@ -41,7 +41,7 @@ commands = coverage run -m pytest {posargs:-n auto --dist loadfile}
# Keep base_python in-sync with .python-version-default # Keep base_python in-sync with .python-version-default
base_python = py312 base_python = py312
# Keep depends in-sync with testenv above that has cov extra. # Keep depends in-sync with testenv above that has cov extra.
depends = py3{7,10,12} depends = py3{8,10,12}
skip_install = true skip_install = true
deps = coverage[toml]>=5.3 deps = coverage[toml]>=5.3
commands = commands =