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

View File

@ -9,7 +9,7 @@ repos:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2
rev: v0.6.3
hooks:
- id: ruff
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"
authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }]
license = { text = "MIT" }
requires-python = ">=3.7"
requires-python = ">=3.8"
description = "Classes Without Boilerplate"
keywords = ["class", "attribute", "boilerplate"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
@ -26,7 +25,7 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy",
"Typing :: Typed",
]
dependencies = ["importlib_metadata;python_version<'3.8'"]
dependencies = []
dynamic = ["version", "readme"]
[project.optional-dependencies]

View File

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

View File

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

View File

@ -10,7 +10,6 @@ from typing import _GenericAlias
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_10_PLUS = sys.version_info[:2] >= (3, 10)
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)
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
import annotationlib

View File

@ -20,7 +20,6 @@ from operator import itemgetter
# having the thread-local in the globals here.
from . import _compat, _config, setters
from ._compat import (
PY_3_8_PLUS,
PY_3_10_PLUS,
PY_3_11_PLUS,
_AnnotationExtractor,
@ -790,16 +789,11 @@ class _ClassBuilder:
):
names += ("__weakref__",)
if PY_3_8_PLUS:
cached_properties = {
name: cached_property.func
for name, cached_property in cd.items()
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 = {}
cached_properties = {
name: cached_property.func
for name, cached_property in cd.items()
if isinstance(cached_property, functools.cached_property)
}
# Collect methods with a `__class__` reference that are shadowed in the new class.
# 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__`.
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 "
return (
f"""def {method_name}(self, {args}):

View File

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

View File

@ -2,6 +2,8 @@
import types
from typing import Protocol
import pytest
import attr
@ -59,5 +61,5 @@ def test_attrsinstance_subclass_protocol():
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: ...

View File

@ -21,7 +21,7 @@ from hypothesis.strategies import booleans, integers, lists, sampled_from, text
import attr
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 (
Attribute,
Factory,
@ -1837,7 +1837,6 @@ class TestClassBuilder:
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.")
def test_no_references_to_original_when_using_cached_property(self):
"""

View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: MIT
import sys
from importlib import metadata
import pytest
@ -8,12 +9,6 @@ import attr
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))
def _mod(request):
return request.param

View File

@ -15,7 +15,7 @@ import pytest
import attr
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.
@ -722,7 +722,6 @@ def test_slots_super_property_get_shortcut():
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():
"""
cached_property in slotted class allows call.
@ -739,7 +738,6 @@ def test_slots_cached_property_allows_call():
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__():
"""
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)
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_works_on_frozen_isntances():
"""
Infers type of cached property.
@ -774,7 +771,6 @@ def test_slots_cached_property_works_on_frozen_isntances():
assert A(x=1).f == 1
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
@pytest.mark.xfail(
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}
@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():
"""
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
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_raising_attributeerror():
"""
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
@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():
"""
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"
@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():
"""
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"
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_getattr_in_subclass_gets_superclass_cached_property():
"""
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"
@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():
"""
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
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_with_multiple_cached_property_subclasses_works():
"""
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"
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slotted_cached_property_can_access_super():
"""
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
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_sub_class_avoids_duplicated_slots():
"""
Duplicating the slots is a waste of memory.
@ -1034,7 +1021,6 @@ def test_slots_sub_class_avoids_duplicated_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():
"""
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__ == ()
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_is_not_called_at_construction():
"""
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
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_repeat_call_only_once():
"""
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
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_property_called_independent_across_instances():
"""
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
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_cached_properties_work_independently():
"""
Multiple cached properties should work independently.

View File

@ -2,7 +2,7 @@
min_version = 4
env_list =
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,
pypy3,
pyright,
@ -26,7 +26,7 @@ commands =
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
[testenv:py3{7,10,12}-tests]
[testenv:py3{8,10,12}-tests]
extras = cov
# Python 3.6+ has a number of compile-time warnings on invalid string escapes.
# 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
base_python = py312
# Keep depends in-sync with testenv above that has cov extra.
depends = py3{7,10,12}
depends = py3{8,10,12}
skip_install = true
deps = coverage[toml]>=5.3
commands =