Repository: python-attrs/attrs
Pull request #1265 (2024-03-16 16:36:12)

Remove attrs.validators.provides
As per our deprecation policy.

I have copied the trivial code to https://gist.github.com/hynek/9eaaaeb659808f3519870dfa16d2b6b2 for convenient copy and pasting.


Patch:
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index 3646f59..039df07 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -8,7 +8,7 @@ Therefore we only support the latest version.
 Put simply, you shouldn't ever be afraid to upgrade as long as you're only using our public APIs.
 Whenever there is a need to break compatibility, it is announced in the changelog, and raises a `DeprecationWarning` for a year (if possible) before it's finally really broken.
 
-> **Warning**
+> [!WARNING]
 > The structure of the `attrs.Attribute` class is exempt from this rule.
 > It *will* change in the future, but since it should be considered read-only, that shouldn't matter.
 >
diff --git a/changelog.d/1264.breaking.md b/changelog.d/1264.breaking.md
new file mode 100644
index 0000000..f268e2d
--- /dev/null
+++ b/changelog.d/1264.breaking.md
@@ -0,0 +1,2 @@
+`attrs.evolve()` doesn't accept the *inst* argument as a keyword argument anymore.
+Pass it as the first positional argument instead.
diff --git a/changelog.d/1265.breaking.md b/changelog.d/1265.breaking.md
new file mode 100644
index 0000000..1edd7d1
--- /dev/null
+++ b/changelog.d/1265.breaking.md
@@ -0,0 +1,2 @@
+`attrs.validators.provides()` has been removed.
+The removed code is available as a [gist](https://gist.github.com/hynek/9eaaaeb659808f3519870dfa16d2b6b2) for convenient copy and pasting.
diff --git a/docs/api.rst b/docs/api.rst
index 116dd2f..ce14617 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -451,8 +451,6 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
       ...
       ValueError: 'val' must be in [1, 2, 3] (got 4), Attribute(name='val', default=NOTHING, validator=<in_ validator with options [1, 2, 3]>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), [1, 2, 3], 4)
 
-.. autofunction:: attrs.validators.provides
-
 .. autofunction:: attrs.validators.and_
 
    For convenience, it's also possible to pass a list to `attrs.field`'s validator argument.
diff --git a/pyproject.toml b/pyproject.toml
index ff7bc22..c7ec207 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ tests-mypy = [
   # pin.
   'mypy>=1.6; platform_python_implementation == "CPython" and python_version >= "3.8"',
 ]
-tests-no-zope = [
+tests = [
   # For regression test to ensure cloudpickle compat doesn't break.
   'cloudpickle; platform_python_implementation == "CPython"',
   "hypothesis",
@@ -46,7 +46,6 @@ tests-no-zope = [
   "pytest-xdist[psutil]",
   "attrs[tests-mypy]",
 ]
-tests = ["attrs[tests-no-zope]", "zope.interface"]
 cov = [
   "attrs[tests]",
   # Ensure coverage is new enough for `source_pkgs`.
@@ -56,7 +55,6 @@ docs = [
   "furo",
   "myst-parser",
   "sphinx",
-  "zope.interface",
   "sphinx-notfound-page",
   "sphinxcontrib-towncrier",
   "towncrier",
diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py
index 5be1adb..0912a45 100644
--- a/src/attr/_funcs.py
+++ b/src/attr/_funcs.py
@@ -371,7 +371,8 @@ def evolve(*args, **changes):
     Create a new instance, based on the first positional argument with
     *changes* applied.
 
-    :param inst: Instance of a class with *attrs* attributes.
+    :param inst: Instance of a class with *attrs* attributes. *inst* must be
+        passed as a positional argument.
     :param changes: Keyword changes in the new copy.
 
     :return: A copy of inst with *changes* incorporated.
@@ -387,30 +388,16 @@ def evolve(*args, **changes):
        *inst*. It will raise a warning until at least April 2024, after which
        it will become an error. Always pass the instance as a positional
        argument.
+    .. versionchanged:: 24.1.0
+       *inst* can't be passed as a keyword argument anymore.
     """
-    # Try to get instance by positional argument first.
-    # Use changes otherwise and warn it'll break.
-    if args:
-        try:
-            (inst,) = args
-        except ValueError:
-            msg = f"evolve() takes 1 positional argument, but {len(args)} were given"
-            raise TypeError(msg) from None
-    else:
-        try:
-            inst = changes.pop("inst")
-        except KeyError:
-            msg = "evolve() missing 1 required positional argument: 'inst'"
-            raise TypeError(msg) from None
-
-        import warnings
-
-        warnings.warn(
-            "Passing the instance per keyword argument is deprecated and "
-            "will stop working in, or after, April 2024.",
-            DeprecationWarning,
-            stacklevel=2,
+    try:
+        (inst,) = args
+    except ValueError:
+        msg = (
+            f"evolve() takes 1 positional argument, but {len(args)} were given"
         )
+        raise TypeError(msg) from None
 
     cls = inst.__class__
     attrs = fields(cls)
diff --git a/src/attr/validators.py b/src/attr/validators.py
index 0cc8c11..952060b 100644
--- a/src/attr/validators.py
+++ b/src/attr/validators.py
@@ -35,7 +35,6 @@ __all__ = [
     "min_len",
     "not_",
     "optional",
-    "provides",
     "set_disabled",
 ]
 
@@ -190,54 +189,6 @@ def matches_re(regex, flags=0, func=None):
     return _MatchesReValidator(pattern, match_func)
 
 
-@attrs(repr=False, slots=True, hash=True)
-class _ProvidesValidator:
-    interface = attrib()
-
-    def __call__(self, inst, attr, value):
-        """
-        We use a callable class to be able to change the ``__repr__``.
-        """
-        if not self.interface.providedBy(value):
-            msg = f"'{attr.name}' must provide {self.interface!r} which {value!r} doesn't."
-            raise TypeError(
-                msg,
-                attr,
-                self.interface,
-                value,
-            )
-
-    def __repr__(self):
-        return f"<provides validator for interface {self.interface!r}>"
-
-
-def provides(interface):
-    """
-    A validator that raises a `TypeError` if the initializer is called
-    with an object that does not provide the requested *interface* (checks are
-    performed using ``interface.providedBy(value)`` (see `zope.interface
-    <https://zopeinterface.readthedocs.io/en/latest/>`_).
-
-    :param interface: The interface to check for.
-    :type interface: ``zope.interface.Interface``
-
-    :raises TypeError: With a human readable error message, the attribute
-        (of type `attrs.Attribute`), the expected interface, and the
-        value it got.
-
-    .. deprecated:: 23.1.0
-    """
-    import warnings
-
-    warnings.warn(
-        "attrs's zope-interface support is deprecated and will be removed in, "
-        "or after, April 2024.",
-        DeprecationWarning,
-        stacklevel=2,
-    )
-    return _ProvidesValidator(interface)
-
-
 @attrs(repr=False, slots=True, hash=True)
 class _OptionalValidator:
     validator = attrib()
diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi
index caca56c..8b2617a 100644
--- a/src/attr/validators.pyi
+++ b/src/attr/validators.pyi
@@ -44,7 +44,6 @@ def instance_of(
 ) -> _ValidatorType[_T1 | _T2 | _T3]: ...
 @overload
 def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ...
-def provides(interface: Any) -> _ValidatorType[Any]: ...
 def optional(
     validator: (
         _ValidatorType[_T]


Test patch:
diff --git a/tests/test_funcs.py b/tests/test_funcs.py
index 398ba35..67c9bc9 100644
--- a/tests/test_funcs.py
+++ b/tests/test_funcs.py
@@ -781,26 +781,13 @@ class TestEvolve:
             obj1a, param1=obj2b
         )
 
-    def test_inst_kw(self):
-        """
-        If `inst` is passed per kw argument, a warning is raised.
-        See #1109
-        """
-
-        @attr.s
-        class C:
-            pass
-
-        with pytest.warns(DeprecationWarning) as wi:
-            evolve(inst=C())
-
-        assert __file__ == wi.list[0].filename
-
     def test_no_inst(self):
         """
         Missing inst argument raises a TypeError like Python would.
         """
-        with pytest.raises(TypeError, match=r"evolve\(\) missing 1"):
+        with pytest.raises(
+            TypeError, match=r"evolve\(\) takes 1 positional argument"
+        ):
             evolve(x=1)
 
     def test_too_many_pos_args(self):
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 856da49..e0ee313 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -30,25 +30,11 @@ from attr.validators import (
     min_len,
     not_,
     optional,
-    provides,
 )
 
 from .utils import simple_attr
 
 
-@pytest.fixture(scope="module")
-def zope_interface():
-    """Provides ``zope.interface`` if available, skipping the test if not."""
-    try:
-        import zope.interface
-    except ImportError:
-        raise pytest.skip(
-            "zope-related tests skipped when zope.interface is not installed"
-        )
-
-    return zope.interface
-
-
 class TestDisableValidators:
     @pytest.fixture(autouse=True)
     def _reset_default(self):
@@ -314,79 +300,6 @@ class TestAnd:
         assert C.__attrs_attrs__[0].validator == C.__attrs_attrs__[1].validator
 
 
-@pytest.fixture(scope="module")
-def ifoo(zope_interface):
-    """Provides a test ``zope.interface.Interface`` in ``zope`` tests."""
-
-    class IFoo(zope_interface.Interface):
-        """
-        An interface.
-        """
-
-        def f():
-            """
-            A function called f.
-            """
-
-    return IFoo
-
-
-class TestProvides:
-    """
-    Tests for `provides`.
-    """
-
-    def test_in_all(self):
-        """
-        Verify that this validator is in ``__all__``.
-        """
-        assert provides.__name__ in validator_module.__all__
-
-    def test_success(self, zope_interface, ifoo):
-        """
-        Nothing happens if value provides requested interface.
-        """
-
-        @zope_interface.implementer(ifoo)
-        class C:
-            def f(self):
-                pass
-
-        with pytest.deprecated_call():
-            v = provides(ifoo)
-
-        v(None, simple_attr("x"), C())
-
-    def test_fail(self, ifoo):
-        """
-        Raises `TypeError` if interfaces isn't provided by value.
-        """
-        value = object()
-        a = simple_attr("x")
-
-        with pytest.deprecated_call():
-            v = provides(ifoo)
-
-        with pytest.raises(TypeError) as e:
-            v(None, a, value)
-
-        assert (
-            f"'x' must provide {ifoo!r} which {value!r} doesn't.",
-            a,
-            ifoo,
-            value,
-        ) == e.value.args
-
-    def test_repr(self, ifoo):
-        """
-        Returned validator has a useful `__repr__`.
-        """
-        with pytest.deprecated_call():
-            v = provides(ifoo)
-
-        assert (f"<provides validator for interface {ifoo!r}>") == repr(v)
-
-
 @pytest.mark.parametrize(
     "validator",
     [