Repository: zopefoundation/zope.interface (language: python)

Issue:
zope.interface 7 causing breakage
In an unrelated project I am seeing the traceback shown below during GHA test runs using tox and zope.testrunner. The mentioned `_zope_interface_coptimizations.ObjectSpecificationDescriptor` is an addition in zope.interface version 7. I had to pin zope.interface to the previous release to make it go away.

```
py311-gha: commands[1]> coverage run /home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/bin/test
Traceback (most recent call last):
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/bin/test", line 41, in <module>
    sys.exit(zope.testrunner.run((['-cv']) + [
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/testrunner/__init__.py", line 31, in run
    failed = run_internal(defaults, args, script_parts=script_parts, cwd=cwd,
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/testrunner/__init__.py", line 52, in run_internal
    from zope.testrunner.runner import Runner
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/testrunner/runner.py", line 48, in <module>
    import zope.testrunner.tb_format
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/testrunner/tb_format.py", line 20, in <module>
    import zope.exceptions.exceptionformatter
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/exceptions/__init__.py", line 37, in <module>
    import zope.security  # noqa: suppress unused import warning from flake8
    ^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/security/__init__.py", line 20, in <module>
    import zope.security.decorator
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/security/decorator.py", line 22, in <module>
    from zope.proxy.decorator import SpecificationDecoratorBase
  File "/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/zope/proxy/decorator.py", line 30, in <module>
    class DecoratorSpecificationDescriptor(ObjectSpecificationDescriptor):
TypeError: type '_zope_interface_coptimizations.ObjectSpecificationDescriptor' is not an acceptable base type
/home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/lib/python3.11/site-packages/coverage/control.py:894: CoverageWarning: No data was collected. (no-data-collected)
  self._warn("No data was collected.", slug="no-data-collected")
py311-gha: exit 1 (0.56 seconds) /home/runner/work/mw.importlib/mw.importlib> coverage run /home/runner/work/mw.importlib/mw.importlib/.tox/py311-gha/bin/test pid=3627
  py311-gha: FAIL code 1 (103.80=setup[36.75]+cmd[66.49,0.56] seconds)
  evaluation failed :( (104.00 seconds)
```


Discussion / hints:

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index fc4de730..0cebebdc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,10 +2,11 @@
  Changes
 =========
 
-7.1 (unreleased)
-================
+7.0.1 (unreleased)
+==================
 
-- Nothing changed yet.
+- Fix subclassability of ``ObjectSpecificationDescriptor`` (broken in 7.0).
+  (`#312 <https://github.com/zopefoundation/zope.interface/issues/312>`_)
 
 
 7.0 (2024-08-06)
diff --git a/src/zope/interface/_zope_interface_coptimizations.c b/src/zope/interface/_zope_interface_coptimizations.c
index 99f36399..c66f4cb3 100644
--- a/src/zope/interface/_zope_interface_coptimizations.c
+++ b/src/zope/interface/_zope_interface_coptimizations.c
@@ -57,10 +57,6 @@
  *      #c.PyTypeObject.tp_weaklistoffset
  */
 #define USE_EXPLICIT_WEAKREFLIST 0
-#define LEAFTYPE_FLAGS \
-    Py_TPFLAGS_DEFAULT | \
-    Py_TPFLAGS_MANAGED_WEAKREF | \
-    Py_TPFLAGS_HAVE_GC
 #define BASETYPE_FLAGS \
     Py_TPFLAGS_DEFAULT | \
     Py_TPFLAGS_BASETYPE | \
@@ -74,9 +70,6 @@
  *      #c.PyTypeObject.tp_weaklistoffset
  */
 #define USE_EXPLICIT_WEAKREFLIST 1
-#define LEAFTYPE_FLAGS \
-    Py_TPFLAGS_DEFAULT | \
-    Py_TPFLAGS_HAVE_GC
 #define BASETYPE_FLAGS \
     Py_TPFLAGS_DEFAULT | \
     Py_TPFLAGS_BASETYPE | \
@@ -546,7 +539,7 @@ static PyType_Slot OSD_type_slots[] = {
 static PyType_Spec OSD_type_spec = {
     .name               = OSD__name__,
     .basicsize          = 0,
-    .flags              = LEAFTYPE_FLAGS,
+    .flags              = BASETYPE_FLAGS,
     .slots              = OSD_type_slots
 };
 


Test patch:
diff --git a/src/zope/interface/tests/__init__.py b/src/zope/interface/tests/__init__.py
index 11125132..c5fe9248 100644
--- a/src/zope/interface/tests/__init__.py
+++ b/src/zope/interface/tests/__init__.py
@@ -2,21 +2,16 @@
 
 
 class OptimizationTestMixin:
-    """
-    Helper for testing that C optimizations are used
-    when appropriate.
+    """Mixin testing that C optimizations are used when appropriate.
     """
 
     def _getTargetClass(self):
-        """
-        Define this to return the implementation in use,
-        without the 'Py' or 'Fallback' suffix.
+        """Return the implementation in use, without 'Py' or 'Fallback' suffix.
         """
         raise NotImplementedError
 
     def _getFallbackClass(self):
-        """
-        Define this to return the fallback Python implementation.
+        """Return the fallback Python implementation.
         """
         # Is there an algorithmic way to do this? The C
         # objects all come from the same module so I don't see how we can
@@ -33,6 +28,20 @@ def test_optimizations(self):
             self.assertIs(used, fallback)
 
 
+class SubclassableMixin:
+
+    def _getTargetClass(self):
+        """Return the implementation in use without 'Py' or 'Fallback' suffix.
+        """
+        raise NotImplementedError
+
+    def test_can_subclass(self):
+        klass = self._getTargetClass()
+
+        class Derived(klass):  # no raise
+            pass
+
+
 class MissingSomeAttrs:
     """
     Helper for tests that raises a specific exception
diff --git a/src/zope/interface/tests/test_declarations.py b/src/zope/interface/tests/test_declarations.py
index bec87b88..3adbc024 100644
--- a/src/zope/interface/tests/test_declarations.py
+++ b/src/zope/interface/tests/test_declarations.py
@@ -17,6 +17,7 @@
 
 from zope.interface.tests import MissingSomeAttrs
 from zope.interface.tests import OptimizationTestMixin
+from zope.interface.tests import SubclassableMixin
 from zope.interface.tests.test_interface import \
     NameAndModuleComparisonTestsMixin
 
@@ -1803,8 +1804,11 @@ class Bar(Foo):
         self.assertRaises(AttributeError, getattr, bar, '__provides__')
 
 
-class ClassProvidesBaseTests(OptimizationTestMixin,
-                             ClassProvidesBaseFallbackTests):
+class ClassProvidesBaseTests(
+    OptimizationTestMixin,
+    ClassProvidesBaseFallbackTests,
+    SubclassableMixin,
+):
     # Repeat tests for C optimizations
 
     def _getTargetClass(self):
@@ -2632,7 +2636,9 @@ def __provides__(self):
 
 class ObjectSpecificationDescriptorTests(
         ObjectSpecificationDescriptorFallbackTests,
-        OptimizationTestMixin):
+        OptimizationTestMixin,
+        SubclassableMixin,
+):
     # Repeat tests for C optimizations
 
     def _getTargetClass(self):
diff --git a/src/zope/interface/tests/test_interface.py b/src/zope/interface/tests/test_interface.py
index 12f75bbe..9b868b7f 100644
--- a/src/zope/interface/tests/test_interface.py
+++ b/src/zope/interface/tests/test_interface.py
@@ -34,6 +34,7 @@
 from zope.interface.tests import CleanUp
 from zope.interface.tests import MissingSomeAttrs
 from zope.interface.tests import OptimizationTestMixin
+from zope.interface.tests import SubclassableMixin
 
 
 _marker = object()
@@ -216,8 +217,11 @@ def _implementedBy(obj):
             self.assertFalse(sb.implementedBy(object()))
 
 
-class SpecificationBaseTests(GenericSpecificationBaseTests,
-                             OptimizationTestMixin):
+class SpecificationBaseTests(
+    GenericSpecificationBaseTests,
+    OptimizationTestMixin,
+    SubclassableMixin,
+):
     # Tests that use the C implementation
 
     def _getTargetClass(self):
@@ -436,9 +440,12 @@ def test___call___w_no_conform_catches_only_AttributeError(self):
         )
 
 
-class InterfaceBaseTests(InterfaceBaseTestsMixin,
-                         OptimizationTestMixin,
-                         unittest.TestCase):
+class InterfaceBaseTests(
+    InterfaceBaseTestsMixin,
+    OptimizationTestMixin,
+    SubclassableMixin,
+    unittest.TestCase,
+):
     # Tests that work with the C implementation
     def _getTargetClass(self):
         from zope.interface.interface import InterfaceBase