diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a4755b1c8..beaaffac5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-beta - 3.11", "pypy-3.7", "pypy-3.8"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-beta - 3.11", "pypy-3.7", "pypy-3.8"] steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce0040ef7..20c8761e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v2.37.3 hooks: - id: pyupgrade - args: [--py3-plus, --keep-percent-format] + args: [--py36-plus, --keep-percent-format] exclude: "tests/test_slots.py" - repo: https://github.com/PyCQA/isort diff --git a/README.rst b/README.rst index ced233302..9b5dcc6a6 100644 --- a/README.rst +++ b/README.rst @@ -113,7 +113,7 @@ Project Information - **Changelog**: https://www.attrs.org/en/stable/changelog.html - **Get Help**: please use the ``python-attrs`` tag on `StackOverflow `_ - **Third-party Extensions**: https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs -- **Supported Python Versions**: 3.5 and later (last 2.7-compatible release is `21.4.0 `_) +- **Supported Python Versions**: 3.6 and later (last 2.7-compatible release is `21.4.0 `_) If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! diff --git a/changelog.d/988.breaking.rst b/changelog.d/988.breaking.rst new file mode 100644 index 000000000..58c1dbde3 --- /dev/null +++ b/changelog.d/988.breaking.rst @@ -0,0 +1 @@ +Python 3.5 is not supported anymore. diff --git a/conftest.py b/conftest.py index 33cc6a6cb..f6e5d8d6c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,9 +1,8 @@ # SPDX-License-Identifier: MIT - from hypothesis import HealthCheck, settings -from attr._compat import PY36, PY310 +from attr._compat import PY310 def pytest_configure(config): @@ -15,14 +14,5 @@ def pytest_configure(config): collect_ignore = [] -if not PY36: - collect_ignore.extend( - [ - "tests/test_annotations.py", - "tests/test_hooks.py", - "tests/test_init_subclass.py", - "tests/test_next_gen.py", - ] - ) if not PY310: collect_ignore.extend(["tests/test_pattern_matching.py"]) diff --git a/docs/api.rst b/docs/api.rst index a273d19c2..a609833c5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -14,7 +14,6 @@ As of version 21.3.0, ``attrs`` consists of **two** top-level package names: - The classic ``attr`` that powered the venerable `attr.s` and `attr.ib` - The modern ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. Additionally it offers some ``attr`` APIs with nicer defaults (e.g. `attrs.asdict`). - Using this namespace requires Python 3.6 or later. The ``attrs`` namespace is built *on top of* ``attr`` which will *never* go away. diff --git a/docs/examples.rst b/docs/examples.rst index ae5ffa78e..ab9fe2d03 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -473,7 +473,7 @@ If you're the author of a third-party library with ``attrs`` integration, please Types ----- -``attrs`` also allows you to associate a type with an attribute using either the *type* argument to `attr.ib` or -- as of Python 3.6 -- using :pep:`526`-annotations: +``attrs`` also allows you to associate a type with an attribute using either the *type* argument to `attr.ib` or using :pep:`526`-annotations: .. doctest:: diff --git a/docs/extending.rst b/docs/extending.rst index d5775adcc..13d66edf3 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -124,7 +124,7 @@ Types ``attrs`` offers two ways of attaching type information to attributes: -- :pep:`526` annotations on Python 3.6 and later, +- :pep:`526` annotations, - and the *type* argument to `attr.ib`. This information is available to you: diff --git a/docs/names.rst b/docs/names.rst index 8fb59c306..6773db20c 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -19,7 +19,6 @@ We recommend our modern APIs for new code: - and `attrs.field()` to define an attribute. They have been added in ``attrs`` 20.1.0, they are expressive, and they have modern defaults like slots and type annotation awareness switched on by default. -They are only available in Python 3.6 and later. Sometimes they're referred to as *next-generation* or *NG* APIs. As of ``attrs`` 21.3.0 you can also import them from the ``attrs`` package namespace. diff --git a/setup.py b/setup.py index 45a23985f..874fe757c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ import os import platform import re -import sys from setuptools import find_packages, setup @@ -33,7 +32,6 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -57,10 +55,7 @@ "pytest>=4.3.0", # 4.3.0 dropped last use of `convert` ], } -if ( - sys.version_info[:2] >= (3, 6) - and platform.python_implementation() != "PyPy" -): +if platform.python_implementation() != "PyPy": EXTRAS_REQUIRE["tests_no_zope"].extend( ["mypy>=0.900,!=0.940", "pytest-mypy-plugins"] ) @@ -92,11 +87,11 @@ def find_meta(meta): Extract __*meta*__ from META_FILE. """ meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M + rf"^__{meta}__ = ['\"]([^'\"]*)['\"]", META_FILE, re.M ) if meta_match: return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) + raise RuntimeError(f"Unable to find __{meta}__ string.") LOGO = """ @@ -119,7 +114,7 @@ def find_meta(meta): re.S, ).group(1) + "\n\n`Full changelog " - + "<{url}en/stable/changelog.html>`_.\n\n".format(url=URL) + + f"<{URL}en/stable/changelog.html>`_.\n\n" + read("AUTHORS.rst") ) @@ -141,7 +136,7 @@ def find_meta(meta): long_description_content_type="text/x-rst", packages=PACKAGES, package_dir={"": "src"}, - python_requires=">=3.5", + python_requires=">=3.6", zip_safe=False, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 69b558600..92e8920b5 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -1,8 +1,5 @@ # SPDX-License-Identifier: MIT - -import sys - from functools import partial from . import converters, exceptions, filters, setters, validators @@ -20,6 +17,7 @@ make_class, validate, ) +from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo @@ -56,15 +54,19 @@ "attrs", "cmp_using", "converters", + "define", "evolve", "exceptions", + "field", "fields", "fields_dict", "filters", + "frozen", "get_run_validators", "has", "ib", "make_class", + "mutable", "resolve_types", "s", "set_run_validators", @@ -72,8 +74,3 @@ "validate", "validators", ] - -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - - __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 582649325..cc98c88b5 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -12,19 +12,9 @@ PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 PY310 = sys.version_info[:2] >= (3, 10) -if PYPY or PY36: - ordered_dict = dict -else: - from collections import OrderedDict - - ordered_dict = OrderedDict - - def just_warn(*args, **kw): warnings.warn( "Running interpreter doesn't sufficiently support code object " diff --git a/src/attr/_make.py b/src/attr/_make.py index 977d93d39..f39258264 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -11,14 +11,7 @@ # We need to import _compat itself in addition to the _compat members to avoid # having the thread-local in the globals here. from . import _compat, _config, setters -from ._compat import ( - HAS_F_STRINGS, - PY310, - PYPY, - _AnnotationExtractor, - ordered_dict, - set_closure_cell, -) +from ._compat import PY310, PYPY, _AnnotationExtractor, set_closure_cell from .exceptions import ( DefaultAlreadySetError, FrozenInstanceError, @@ -201,9 +194,9 @@ def attrib( value is converted before being passed to the validator, if any. :param metadata: An arbitrary mapping, to be used by third-party components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see :pep:`526`). + + :param type: The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). This argument is provided for backward compatibility. Regardless of the approach used, the type will be stored on ``Attribute.type``. @@ -323,7 +316,7 @@ def _make_method(name, script, filename, globs): if old_val == linecache_tuple: break else: - filename = "{}-{}>".format(base_filename[:-1], count) + filename = f"{base_filename[:-1]}-{count}>" count += 1 _compile_and_eval(script, globs, locs, filename) @@ -341,9 +334,9 @@ class MyClassAttributes(tuple): __slots__ = () x = property(itemgetter(0)) """ - attr_class_name = "{}Attributes".format(cls_name) + attr_class_name = f"{cls_name}Attributes" attr_class_template = [ - "class {}(tuple):".format(attr_class_name), + f"class {attr_class_name}(tuple):", " __slots__ = ()", ] if attr_names: @@ -418,13 +411,6 @@ def _get_annotations(cls): return {} -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - def _collect_base_attrs(cls, taken_attr_names): """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. @@ -502,9 +488,6 @@ def _transform_attrs( if these is not None: ca_list = [(name, ca) for name, ca in these.items()] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) elif auto_attribs is True: ca_names = { name @@ -735,7 +718,7 @@ def __init__( ) = self._make_getstate_setstate() def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + return f"<_ClassBuilder(cls={self._cls.__name__})>" def build_class(self): """ @@ -1218,10 +1201,7 @@ def attrs( If *these* is not ``None``, ``attrs`` will *not* search the class body for attributes and will *not* remove any attributes from it. - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. + The order is deduced from the order of the attributes inside *these*. :type these: `dict` of `str` to `attr.ib` @@ -1329,7 +1309,7 @@ def attrs( :param bool weakref_slot: Make instances weak-referenceable. This has no effect unless ``slots`` is also enabled. :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated - attributes (Python 3.6 and later only) from the class body. + attributes from the class body. In this case, you **must** annotate every field. If ``attrs`` encounters a field that is set to an `attr.ib` but lacks a type @@ -1833,126 +1813,61 @@ def _add_eq(cls, attrs=None): return cls -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, (repr if a.repr is True else a.repr), a.init) - for a in attrs - if a.repr is not False +def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - globs["AttributeError"] = AttributeError - globs["NOTHING"] = NOTHING - attribute_fragments = [] - for name, r, i in attr_names_with_reprs: - accessor = ( - "self." + name - if i - else 'getattr(self, "' + name + '", NOTHING)' - ) - fragment = ( - "%s={%s!r}" % (name, accessor) - if r == repr - else "%s={%s_repr(%s)}" % (name, name, accessor) - ) - attribute_fragments.append(fragment) - repr_fragment = ", ".join(attribute_fragments) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - lines = [ - "def __repr__(self):", - " try:", - " already_repring = _compat.repr_context.already_repring", - " except AttributeError:", - " already_repring = {id(self),}", - " _compat.repr_context.already_repring = already_repring", - " else:", - " if id(self) in already_repring:", - " return '...'", - " else:", - " already_repring.add(id(self))", - " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), - " finally:", - " already_repring.remove(id(self))", - ] - - return _make_method( - "__repr__", "\n".join(lines), unique_filename, globs=globs + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) -else: - - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full name. - """ - - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - class_name = real_cls.__qualname__.rsplit(">.", 1)[-1] - else: - class_name = ns + "." + real_cls.__name__ + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" - # Since 'self' remains on the stack (i.e.: strongly referenced) - # for the duration of this call, it's safe to depend on id(...) - # stability, and not need to track the instance and therefore - # worry about properties like weakref- or hash-ability. - already_repring.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False - else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - already_repring.remove(id(self)) + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), + " finally:", + " already_repring.remove(id(self))", + ] - return __repr__ + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) def _add_repr(cls, ns=None, attrs=None): @@ -1988,9 +1903,7 @@ def fields(cls): raise TypeError("Passed object must be a class.") attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) + raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") return attrs @@ -2005,10 +1918,7 @@ def fields_dict(cls): :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` class. - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. + :rtype: dict .. versionadded:: 18.1.0 """ @@ -2016,10 +1926,8 @@ def fields_dict(cls): raise TypeError("Passed object must be a class.") attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict((a.name, a) for a in attrs) + raise NotAnAttrsClassError(f"{cls!r} is not an attrs-decorated class.") + return {a.name: a for a in attrs} def validate(inst): @@ -2579,7 +2487,7 @@ def from_counting_attr(cls, name, ca, type=None): type=type, cmp=None, inherited=False, - **inst_dict + **inst_dict, ) # Don't use attr.evolve since fields(Attribute) doesn't work @@ -2865,10 +2773,9 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): :param attrs: A list of names or a dictionary of mappings of names to attributes. - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. + The order is deduced from the order of the names or attributes inside + *attrs*. Otherwise the order of the definition of the attributes is + used. :type attrs: `list` or `dict` :param tuple bases: Classes that the new class will subclass. diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 5a06a7438..260519f1c 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: MIT """ -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. """ diff --git a/src/attr/converters.py b/src/attr/converters.py index a73626c26..4cada106b 100644 --- a/src/attr/converters.py +++ b/src/attr/converters.py @@ -141,4 +141,4 @@ def to_bool(val): except TypeError: # Raised when "val" is not hashable (e.g., lists) pass - raise ValueError("Cannot convert value to bool: {}".format(val)) + raise ValueError(f"Cannot convert value to bool: {val}") diff --git a/src/attr/validators.py b/src/attr/validators.py index eece517da..f27049b3d 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -391,7 +391,7 @@ def __repr__(self): iterable_identifier = ( "" if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) + else f" {self.iterable_validator!r}" ) return ( "".format(max=self.max_length) + return f"" def max_len(length): @@ -579,7 +579,7 @@ def __call__(self, inst, attr, value): ) def __repr__(self): - return "".format(min=self.min_length) + return f"" def min_len(length): diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 18f0d21cf..fb996a6a9 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -2,8 +2,6 @@ """ Tests for PEP-526 type annotations. - -Python 3.6+ only. """ import sys @@ -397,7 +395,7 @@ def test_annotations_strings(self, slots): """ String annotations are passed into __init__ as is. - It fails on 3.6 due to a bug in Python. + The strings keep changing between releases. """ import typing as t diff --git a/tests/test_funcs.py b/tests/test_funcs.py index d73d94c51..f77bfd4ab 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -15,7 +15,7 @@ import attr from attr import asdict, assoc, astuple, evolve, fields, has -from attr._compat import Mapping, Sequence, ordered_dict +from attr._compat import Mapping, Sequence from attr.exceptions import AttrsAttributeNotFoundError from attr.validators import instance_of @@ -196,7 +196,7 @@ def test_asdict_preserve_order(self, cls): Field order should be preserved when dumping to an ordered_dict. """ instance = cls() - dict_instance = asdict(instance, dict_factory=ordered_dict) + dict_instance = asdict(instance, dict_factory=dict) assert [a.name for a in fields(cls)] == list(dict_instance.keys()) @@ -483,9 +483,7 @@ def test_unknown(self, C): ) as e, pytest.deprecated_call(): assoc(C(), aaaa=2) - assert ( - "aaaa is not an attrs attribute on {cls!r}.".format(cls=C), - ) == e.value.args + assert (f"aaaa is not an attrs attribute on {C!r}.",) == e.value.args def test_frozen(self): """ diff --git a/tests/test_functional.py b/tests/test_functional.py index 09f504802..741068012 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -17,7 +17,6 @@ import attr -from attr._compat import PY36 from attr._make import NOTHING, Attribute from attr.exceptions import FrozenInstanceError @@ -224,9 +223,9 @@ def test_subclassing_with_extra_attrs(self, cls): assert i.x is i.meth() is obj assert i.y == 2 if cls is Sub: - assert "Sub(x={obj}, y=2)".format(obj=obj) == repr(i) + assert f"Sub(x={obj}, y=2)" == repr(i) else: - assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i) + assert f"SubSlots(x={obj}, y=2)" == repr(i) @pytest.mark.parametrize("base", [Base, BaseSlots]) def test_subclass_without_extra_attrs(self, base): @@ -241,7 +240,7 @@ class Sub2(base): obj = object() i = Sub2(x=obj) assert i.x is i.meth() is obj - assert "Sub2(x={obj})".format(obj=obj) == repr(i) + assert f"Sub2(x={obj})" == repr(i) @pytest.mark.parametrize( "frozen_class", @@ -701,7 +700,6 @@ class D(C): assert "self.y = y" in src assert object.__setattr__ == D.__setattr__ - @pytest.mark.skipif(not PY36, reason="NG APIs are 3.6+") @pytest.mark.parametrize("slots", [True, False]) def test_no_setattr_with_ng_defaults(self, slots): """ diff --git a/tests/test_init_subclass.py b/tests/test_init_subclass.py index 863e79437..c686e414e 100644 --- a/tests/test_init_subclass.py +++ b/tests/test_init_subclass.py @@ -2,8 +2,6 @@ """ Tests for `__init_subclass__` related tests. - -Python 3.6+ only. """ import pytest diff --git a/tests/test_make.py b/tests/test_make.py index 96e07f333..fe8c5e613 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -22,7 +22,7 @@ import attr from attr import _config -from attr._compat import PY310, ordered_dict +from attr._compat import PY310 from attr._make import ( Attribute, Factory, @@ -297,7 +297,7 @@ def test_these_ordered(self): b = attr.ib(default=2) a = attr.ib(default=1) - @attr.s(these=ordered_dict([("a", a), ("b", b)])) + @attr.s(these=dict([("a", a), ("b", b)])) class C: pass @@ -1071,7 +1071,7 @@ def test_make_class_ordered(self): b = attr.ib(default=2) a = attr.ib(default=1) - C = attr.make_class("C", ordered_dict([("a", a), ("b", b)])) + C = attr.make_class("C", dict([("a", a), ("b", b)])) assert "C(a=1, b=2)" == repr(C()) @@ -1114,7 +1114,7 @@ def test_handler_non_attrs_class(self): fields(object) assert ( - "{o!r} is not an attrs-decorated class.".format(o=object) + f"{object!r} is not an attrs-decorated class." ) == e.value.args[0] @given(simple_classes()) @@ -1156,7 +1156,7 @@ def test_handler_non_attrs_class(self): fields_dict(object) assert ( - "{o!r} is not an attrs-decorated class.".format(o=object) + f"{object!r} is not an attrs-decorated class." ) == e.value.args[0] @given(simple_classes()) @@ -1166,7 +1166,7 @@ def test_fields_dict(self, C): """ d = fields_dict(C) - assert isinstance(d, ordered_dict) + assert isinstance(d, dict) assert list(fields(C)) == list(d.values()) assert [a.name for a in fields(C)] == [field_name for field_name in d] @@ -1214,7 +1214,7 @@ def test_converter_factory_property(self, val, init): """ C = make_class( "C", - ordered_dict( + dict( [ ("y", attr.ib()), ( diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index 1f13de0aa..78fd0e52d 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT """ -Python 3-only integration tests for provisional next-generation APIs. +Integration tests for next-generation APIs. """ import re diff --git a/tests/test_pyright.py b/tests/test_pyright.py index e055ebb8c..eddb31ae9 100644 --- a/tests/test_pyright.py +++ b/tests/test_pyright.py @@ -4,17 +4,13 @@ import os.path import shutil import subprocess -import sys import pytest import attr -if sys.version_info < (3, 6): - _found_pyright = False -else: - _found_pyright = shutil.which("pyright") +_found_pyright = shutil.which("pyright") @attr.s(frozen=True) diff --git a/tox.ini b/tox.ini index f93fa449a..1fd8d925d 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ filterwarnings = # Keep docs in sync with docs env and .readthedocs.yml. [gh-actions] python = - 3.5: py35 3.6: py36 3.7: py37 3.8: py38, changelog @@ -21,7 +20,7 @@ python = [tox] -envlist = typing,pre-commit,py35,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report +envlist = typing,pre-commit,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report isolated_build = True @@ -40,12 +39,7 @@ extras = tests commands = python -m pytest {posargs} -[testenv:py35] -extras = tests -commands = coverage run -m pytest {posargs} - - -[testenv:py37] +[testenv:py36] extras = tests commands = coverage run -m pytest {posargs} @@ -63,7 +57,7 @@ commands = coverage run -m pytest {posargs} [testenv:coverage-report] basepython = python3.10 -depends = py35,py37,py310 +depends = py36,py310 skip_install = true deps = coverage[toml]>=5.4 commands =