Drop Python 3.7 (#1340)
* Drop Python 3.7 * Add news fragment * update Ruff
This commit is contained in:
parent
5c3af47603
commit
a8e24b0186
|
@ -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'
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Python 3.7 has been dropped.
|
|
@ -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]
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: ...
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
6
tox.ini
6
tox.ini
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue