Repository: zopefoundation/zope.interface

Issue:
Test failure with Python 3.14
When testing the latest version of zope.interface with the latest available Python 3.14 alpha, the test failed with the following error:

```
python3 -m unittest discover -vv -s zope/interface -t .
zope.interface (unittest.loader._FailedTest.zope.interface) ... ERROR

======================================================================
ERROR: zope.interface (unittest.loader._FailedTest.zope.interface)
----------------------------------------------------------------------
ImportError: Failed to import test module: zope.interface
Traceback (most recent call last):
  File "/usr/lib64/python3.14/unittest/loader.py", line 429, in _find_test_path
    package = self._get_module_from_name(name)
  File "/usr/lib64/python3.14/unittest/loader.py", line 339, in _get_module_from_name
    __import__(name)
    ~~~~~~~~~~^^^^^^
  File "/builddir/build/BUILD/python-zope-interface-7.1.1-build/BUILDROOT/usr/lib64/python3.14/site-packages/zope/interface/__init__.py", line 58, in <module>
    _wire()
    ~~~~~^^
  File "/builddir/build/BUILD/python-zope-interface-7.1.1-build/BUILDROOT/usr/lib64/python3.14/site-packages/zope/interface/interface.py", line 1154, in _wire
    from zope.interface.interfaces import IElement
  File "/builddir/build/BUILD/python-zope-interface-7.1.1-build/BUILDROOT/usr/lib64/python3.14/site-packages/zope/interface/interfaces.py", line 56, in <module>
    class IElement(Interface):
    ...<93 lines>...
            """
  File "/builddir/build/BUILD/python-zope-interface-7.1.1-build/BUILDROOT/usr/lib64/python3.14/site-packages/zope/interface/interface.py", line 794, in __init__
    self.__attrs = self.__compute_attrs(attrs)
                   ~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/builddir/build/BUILD/python-zope-interface-7.1.1-build/BUILDROOT/usr/lib64/python3.14/site-packages/zope/interface/interface.py", line 813, in __compute_attrs
    aname: update_value(aname, aval)
           ~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/builddir/build/BUILD/python-zope-interface-7.1.1-build/BUILDROOT/usr/lib64/python3.14/site-packages/zope/interface/interface.py", line 809, in update_value
    raise InvalidInterface("Concrete attribute, " + aname)
zope.interface.exceptions.InvalidInterface: Concrete attribute, __classdictcell__


----------------------------------------------------------------------
Ran 1 test in 0.000s
```

I've tried to look at the `__classdictcell__` attribute but it seems it's not new in 3.14.

Discussion:

Solution patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index dccee042..120e9b7e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -103,6 +103,7 @@ jobs:
           - "3.11"
           - "3.12"
           - "3.13"
+          - "3.14"
         os: [ubuntu-latest, macos-latest, windows-latest]
         exclude:
           - os: macos-latest
@@ -152,10 +153,16 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-pip-
 
+      - name: Install Build Dependencies (3.14)
+        if: matrix.python-version == '3.14'
+        run: |
+          pip install -U pip
+          pip install -U "setuptools < 74" wheel twine
       - name: Install Build Dependencies
+        if: matrix.python-version != '3.14'
         run: |
           pip install -U pip
-          pip install -U "setuptools <74" wheel twine
+          pip install -U "setuptools < 74" wheel twine
 
       - name: Build zope.interface (macOS x86_64)
         if: >
@@ -193,7 +200,15 @@ jobs:
           python setup.py build_ext -i
           python setup.py bdist_wheel
 
+      - name: Install zope.interface and dependencies (3.14)
+        if: matrix.python-version == '3.14'
+        run: |
+          # Install to collect dependencies into the (pip) cache.
+          # Use "--pre" here because dependencies with support for this future
+          # Python release may only be available as pre-releases
+          pip install --pre .[test]
       - name: Install zope.interface and dependencies
+        if: matrix.python-version != '3.14'
         run: |
           # Install to collect dependencies into the (pip) cache.
           pip install .[test]
@@ -236,6 +251,7 @@ jobs:
           && startsWith(github.ref, 'refs/tags')
           && !startsWith(runner.os, 'Linux')
           && !startsWith(matrix.python-version, 'pypy')
+          && !startsWith(matrix.python-version, '3.14')
         env:
           TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
         run: |
@@ -255,6 +271,7 @@ jobs:
           - "3.11"
           - "3.12"
           - "3.13"
+          - "3.14"
         os: [ubuntu-latest, macos-latest, windows-latest]
         exclude:
           - os: macos-latest
@@ -507,6 +524,8 @@ jobs:
           name: manylinux_${{ matrix.image }}_wheels.zip
       - name: Restore pip cache permissions
         run: sudo chown -R $(whoami) ${{ steps.pip-cache-default.outputs.dir }}
+      - name: Prevent publishing wheels for unreleased Python versions
+        run: VER=$(echo '3.14' | tr -d .) && ls -al wheelhouse && sudo rm -f wheelhouse/*-cp${VER}*.whl && ls -al wheelhouse
       - name: Publish package to PyPI
         uses: pypa/gh-action-pypi-publish@release/v1
         if: >
diff --git a/.manylinux-install.sh b/.manylinux-install.sh
index 7efbd2ed..7a338397 100755
--- a/.manylinux-install.sh
+++ b/.manylinux-install.sh
@@ -34,6 +34,7 @@ tox_env_map() {
         *"cp311"*) echo 'py311';;
         *"cp312"*) echo 'py312';;
         *"cp313"*) echo 'py313';;
+        *"cp314"*) echo 'py314';;
         *) echo 'py';;
     esac
 }
@@ -46,9 +47,15 @@ for PYBIN in /opt/python/*/bin; do
        [[ "${PYBIN}" == *"cp310/"* ]] || \
        [[ "${PYBIN}" == *"cp311/"* ]] || \
        [[ "${PYBIN}" == *"cp312/"* ]] || \
-       [[ "${PYBIN}" == *"cp313/"* ]] ; then
-        "${PYBIN}/pip" install -e /io/
-        "${PYBIN}/pip" wheel /io/ -w wheelhouse/
+       [[ "${PYBIN}" == *"cp313/"* ]] || \
+       [[ "${PYBIN}" == *"cp314/"* ]] ; then
+        if [[ "${PYBIN}" == *"cp314/"* ]] ; then
+            "${PYBIN}/pip" install --pre -e /io/
+            "${PYBIN}/pip" wheel /io/ --pre -w wheelhouse/
+        else
+            "${PYBIN}/pip" install -e /io/
+            "${PYBIN}/pip" wheel /io/ -w wheelhouse/
+        fi
         if [ `uname -m` == 'aarch64' ]; then
           cd /io/
           ${PYBIN}/pip install tox
diff --git a/.meta.toml b/.meta.toml
index 67fe6188..3fe4302d 100644
--- a/.meta.toml
+++ b/.meta.toml
@@ -2,13 +2,13 @@
 # https://github.com/zopefoundation/meta/tree/master/config/c-code
 [meta]
 template = "c-code"
-commit-id = "85622de1"
+commit-id = "baf6089f"
 
 [python]
 with-pypy = true
 with-sphinx-doctests = true
 with-windows = true
-with-future-python = false
+with-future-python = true
 with-docs = true
 with-macos = false
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 31355bbd..87afbc6e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,11 @@
 7.1.2 (unreleased)
 ==================
 
+- Add preliminary support for Python 3.14a2, this means that
+  ``.common.builtins.IByteString`` and ``.common.collections.IByteString`` are
+  no longer available from this Python version onwards as Python 3.14 dropped
+  ``collections.abc.ByteString``.
+
 
 7.1.1 (2024-10-23)
 ==================
diff --git a/pyproject.toml b/pyproject.toml
index 17da486f..c787aa9e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@
 # https://github.com/zopefoundation/meta/tree/master/config/c-code
 
 [build-system]
-requires = ["setuptools<74"]
+requires = ["setuptools < 74"]
 build-backend = "setuptools.build_meta"
 
 [tool.coverage.run]
diff --git a/src/zope/interface/_compat.py b/src/zope/interface/_compat.py
index bc3f8671..38032953 100644
--- a/src/zope/interface/_compat.py
+++ b/src/zope/interface/_compat.py
@@ -22,6 +22,9 @@
 import sys
 
 
+PY313_OR_OLDER = sys.version_info < (3, 14)
+
+
 def _normalize_name(name):
     if isinstance(name, bytes):
         name = str(name, 'ascii')
diff --git a/src/zope/interface/common/builtins.py b/src/zope/interface/common/builtins.py
index 09de5b3b..9e543c83 100644
--- a/src/zope/interface/common/builtins.py
+++ b/src/zope/interface/common/builtins.py
@@ -19,6 +19,7 @@
 """
 
 from zope.interface import classImplements
+from zope.interface._compat import PY313_OR_OLDER
 from zope.interface.common import collections
 from zope.interface.common import io
 from zope.interface.common import numbers
@@ -67,17 +68,18 @@ class ITextString(collections.ISequence):
     extra_classes = (str,)
 
 
-class IByteString(collections.IByteString):
-    """
-    Interface for immutable byte strings.
+if PY313_OR_OLDER:
+    class IByteString(collections.IByteString):
+        """
+        Interface for immutable byte strings.
 
-    On all Python versions this is :class:`bytes`.
+        On all Python versions this is :class:`bytes`.
 
-    Unlike :class:`zope.interface.common.collections.IByteString`
-    (the parent of this interface) this does *not* include
-    :class:`bytearray`.
-    """
-    extra_classes = (bytes,)
+        Unlike :class:`zope.interface.common.collections.IByteString`
+        (the parent of this interface) this does *not* include
+        :class:`bytearray`.
+        """
+        extra_classes = (bytes,)
 
 
 class INativeString(ITextString):
diff --git a/src/zope/interface/common/collections.py b/src/zope/interface/common/collections.py
index 543266d9..defa8a15 100644
--- a/src/zope/interface/common/collections.py
+++ b/src/zope/interface/common/collections.py
@@ -38,6 +38,7 @@
 from collections import UserString
 from collections import abc
 
+from zope.interface._compat import PY313_OR_OLDER
 from zope.interface.common import ABCInterface
 from zope.interface.common import optional
 
@@ -190,13 +191,14 @@ class IMutableSequence(ISequence):
     extra_classes = (UserList,)
 
 
-class IByteString(ISequence):
-    """
-    This unifies `bytes` and `bytearray`.
-    """
-    abc = _new_in_ver(
-        'ByteString', True, (ISequence.getABC(),), (bytes, bytearray),
-    )
+if PY313_OR_OLDER:
+    class IByteString(ISequence):
+        """
+        This unifies `bytes` and `bytearray`.
+        """
+        abc = _new_in_ver(
+            'ByteString', True, (ISequence.getABC(),), (bytes, bytearray),
+        )
 
 
 class ISet(ICollection):
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index e5dddb88..ad8a7de6 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -825,6 +825,8 @@ def update_value(aname, aval):
                 # __firstlineno__: Python 3.13b1+
                 # https://github.com/python/cpython/pull/118475
                 '__firstlineno__',
+                # __classdictcell__: Python 3.14
+                '__classdictcell__',
             ) and
             aval is not _decorator_non_return  # noqa W503
         }
diff --git a/tox.ini b/tox.ini
index ed7830b5..1a686364 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,13 +10,16 @@ envlist =
     py311,py311-pure
     py312,py312-pure
     py313,py313-pure
+    py314,py314-pure
     pypy3
     docs
     coverage
 
 [testenv]
+pip_pre = py314: true
 deps =
-    setuptools  <74
+    setuptools < 74
+    Sphinx
 setenv =
     pure: PURE_PYTHON=1
     !pure-!pypy3: PURE_PYTHON=0
@@ -55,7 +58,7 @@ description = ensure that the distribution is ready to release
 basepython = python3
 skip_install = true
 deps =
-    setuptools <74
+    setuptools < 74
     twine
     build
     check-manifest


Test patch:
diff --git a/src/zope/interface/common/tests/test_builtins.py b/src/zope/interface/common/tests/test_builtins.py
index cf7019bf..3c764392 100644
--- a/src/zope/interface/common/tests/test_builtins.py
+++ b/src/zope/interface/common/tests/test_builtins.py
@@ -12,6 +12,7 @@
 
 import unittest
 
+from zope.interface._compat import PY313_OR_OLDER
 from zope.interface.common import builtins
 
 from . import VerifyClassMixin
@@ -24,16 +25,22 @@ class TestVerifyClass(VerifyClassMixin,
     pass
 
 
-add_verify_tests(TestVerifyClass, (
+VERIFY_TESTS = [
     (builtins.IList, (list,)),
     (builtins.ITuple, (tuple,)),
     (builtins.ITextString, (str,)),
-    (builtins.IByteString, (bytes,)),
     (builtins.INativeString, (str,)),
     (builtins.IBool, (bool,)),
     (builtins.IDict, (dict,)),
     (builtins.IFile, ()),
-))
+
+]
+if PY313_OR_OLDER:
+    VERIFY_TESTS.append(
+        (builtins.IByteString, (bytes,))
+    )
+
+add_verify_tests(TestVerifyClass, tuple(VERIFY_TESTS))
 
 
 class TestVerifyObject(VerifyObjectMixin,