mirror of
https://github.com/python/cpython.git
synced 2025-11-25 12:44:13 +00:00
Patch # 1026 by Benjamin Aranguren (with Alex Martelli):
Backport abc.py and isinstance/issubclass overloading to 2.6. I had to backport test_typechecks.py myself, and make one small change to abc.py to avoid duplicate work when x.__class__ and type(x) are the same.
This commit is contained in:
parent
1ff91d95a2
commit
b55911378f
4 changed files with 465 additions and 0 deletions
206
Lib/abc.py
Normal file
206
Lib/abc.py
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# Copyright 2007 Google, Inc. All Rights Reserved.
|
||||||
|
# Licensed to PSF under a Contributor Agreement.
|
||||||
|
|
||||||
|
"""Abstract Base Classes (ABCs) according to PEP 3119."""
|
||||||
|
|
||||||
|
|
||||||
|
def abstractmethod(funcobj):
|
||||||
|
"""A decorator indicating abstract methods.
|
||||||
|
|
||||||
|
Requires that the metaclass is ABCMeta or derived from it. A
|
||||||
|
class that has a metaclass derived from ABCMeta cannot be
|
||||||
|
instantiated unless all of its abstract methods are overridden.
|
||||||
|
The abstract methods can be called using any of the the normal
|
||||||
|
'super' call mechanisms.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
class C(metaclass=ABCMeta):
|
||||||
|
@abstractmethod
|
||||||
|
def my_abstract_method(self, ...):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
funcobj.__isabstractmethod__ = True
|
||||||
|
return funcobj
|
||||||
|
|
||||||
|
|
||||||
|
class abstractproperty(property):
|
||||||
|
"""A decorator indicating abstract properties.
|
||||||
|
|
||||||
|
Requires that the metaclass is ABCMeta or derived from it. A
|
||||||
|
class that has a metaclass derived from ABCMeta cannot be
|
||||||
|
instantiated unless all of its abstract properties are overridden.
|
||||||
|
The abstract properties can be called using any of the the normal
|
||||||
|
'super' call mechanisms.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
class C(metaclass=ABCMeta):
|
||||||
|
@abstractproperty
|
||||||
|
def my_abstract_property(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
This defines a read-only property; you can also define a read-write
|
||||||
|
abstract property using the 'long' form of property declaration:
|
||||||
|
|
||||||
|
class C(metaclass=ABCMeta):
|
||||||
|
def getx(self): ...
|
||||||
|
def setx(self, value): ...
|
||||||
|
x = abstractproperty(getx, setx)
|
||||||
|
"""
|
||||||
|
__isabstractmethod__ = True
|
||||||
|
|
||||||
|
|
||||||
|
class _Abstract(object):
|
||||||
|
|
||||||
|
"""Helper class inserted into the bases by ABCMeta (using _fix_bases()).
|
||||||
|
|
||||||
|
You should never need to explicitly subclass this class.
|
||||||
|
|
||||||
|
There should never be a base class between _Abstract and object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwds):
|
||||||
|
am = cls.__dict__.get("__abstractmethods__")
|
||||||
|
if am:
|
||||||
|
raise TypeError("Can't instantiate abstract class %s "
|
||||||
|
"with abstract methods %s" %
|
||||||
|
(cls.__name__, ", ".join(sorted(am))))
|
||||||
|
if (args or kwds) and cls.__init__ is object.__init__:
|
||||||
|
raise TypeError("Can't pass arguments to __new__ "
|
||||||
|
"without overriding __init__")
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __subclasshook__(cls, subclass):
|
||||||
|
"""Abstract classes can override this to customize issubclass().
|
||||||
|
|
||||||
|
This is invoked early on by __subclasscheck__() below. It
|
||||||
|
should return True, False or NotImplemented. If it returns
|
||||||
|
NotImplemented, the normal algorithm is used. Otherwise, it
|
||||||
|
overrides the normal algorithm (and the outcome is cached).
|
||||||
|
"""
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
def _fix_bases(bases):
|
||||||
|
"""Helper method that inserts _Abstract in the bases if needed."""
|
||||||
|
for base in bases:
|
||||||
|
if issubclass(base, _Abstract):
|
||||||
|
# _Abstract is already a base (maybe indirectly)
|
||||||
|
return bases
|
||||||
|
if object in bases:
|
||||||
|
# Replace object with _Abstract
|
||||||
|
return tuple([_Abstract if base is object else base
|
||||||
|
for base in bases])
|
||||||
|
# Append _Abstract to the end
|
||||||
|
return bases + (_Abstract,)
|
||||||
|
|
||||||
|
|
||||||
|
class ABCMeta(type):
|
||||||
|
|
||||||
|
"""Metaclass for defining Abstract Base Classes (ABCs).
|
||||||
|
|
||||||
|
Use this metaclass to create an ABC. An ABC can be subclassed
|
||||||
|
directly, and then acts as a mix-in class. You can also register
|
||||||
|
unrelated concrete classes (even built-in classes) and unrelated
|
||||||
|
ABCs as 'virtual subclasses' -- these and their descendants will
|
||||||
|
be considered subclasses of the registering ABC by the built-in
|
||||||
|
issubclass() function, but the registering ABC won't show up in
|
||||||
|
their MRO (Method Resolution Order) nor will method
|
||||||
|
implementations defined by the registering ABC be callable (not
|
||||||
|
even via super()).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# A global counter that is incremented each time a class is
|
||||||
|
# registered as a virtual subclass of anything. It forces the
|
||||||
|
# negative cache to be cleared before its next use.
|
||||||
|
_abc_invalidation_counter = 0
|
||||||
|
|
||||||
|
def __new__(mcls, name, bases, namespace):
|
||||||
|
bases = _fix_bases(bases)
|
||||||
|
cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)
|
||||||
|
# Compute set of abstract method names
|
||||||
|
abstracts = set(name
|
||||||
|
for name, value in namespace.items()
|
||||||
|
if getattr(value, "__isabstractmethod__", False))
|
||||||
|
for base in bases:
|
||||||
|
for name in getattr(base, "__abstractmethods__", set()):
|
||||||
|
value = getattr(cls, name, None)
|
||||||
|
if getattr(value, "__isabstractmethod__", False):
|
||||||
|
abstracts.add(name)
|
||||||
|
cls.__abstractmethods__ = abstracts
|
||||||
|
# Set up inheritance registry
|
||||||
|
cls._abc_registry = set()
|
||||||
|
cls._abc_cache = set()
|
||||||
|
cls._abc_negative_cache = set()
|
||||||
|
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
|
||||||
|
return cls
|
||||||
|
|
||||||
|
def register(cls, subclass):
|
||||||
|
"""Register a virtual subclass of an ABC."""
|
||||||
|
if not isinstance(cls, type):
|
||||||
|
raise TypeError("Can only register classes")
|
||||||
|
if issubclass(subclass, cls):
|
||||||
|
return # Already a subclass
|
||||||
|
# Subtle: test for cycles *after* testing for "already a subclass";
|
||||||
|
# this means we allow X.register(X) and interpret it as a no-op.
|
||||||
|
if issubclass(cls, subclass):
|
||||||
|
# This would create a cycle, which is bad for the algorithm below
|
||||||
|
raise RuntimeError("Refusing to create an inheritance cycle")
|
||||||
|
cls._abc_registry.add(subclass)
|
||||||
|
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
|
||||||
|
|
||||||
|
def _dump_registry(cls, file=None):
|
||||||
|
"""Debug helper to print the ABC registry."""
|
||||||
|
print >> file, "Class: %s.%s" % (cls.__module__, cls.__name__)
|
||||||
|
print >> file, "Inv.counter: %s" % ABCMeta._abc_invalidation_counter
|
||||||
|
for name in sorted(cls.__dict__.keys()):
|
||||||
|
if name.startswith("_abc_"):
|
||||||
|
value = getattr(cls, name)
|
||||||
|
print >> file, "%s: %r" % (name, value)
|
||||||
|
|
||||||
|
def __instancecheck__(cls, instance):
|
||||||
|
"""Override for isinstance(instance, cls)."""
|
||||||
|
return any(cls.__subclasscheck__(c)
|
||||||
|
for c in set([instance.__class__, type(instance)]))
|
||||||
|
|
||||||
|
def __subclasscheck__(cls, subclass):
|
||||||
|
"""Override for issubclass(subclass, cls)."""
|
||||||
|
# Check cache
|
||||||
|
if subclass in cls._abc_cache:
|
||||||
|
return True
|
||||||
|
# Check negative cache; may have to invalidate
|
||||||
|
if cls._abc_negative_cache_version < ABCMeta._abc_invalidation_counter:
|
||||||
|
# Invalidate the negative cache
|
||||||
|
cls._abc_negative_cache = set()
|
||||||
|
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
|
||||||
|
elif subclass in cls._abc_negative_cache:
|
||||||
|
return False
|
||||||
|
# Check the subclass hook
|
||||||
|
ok = cls.__subclasshook__(subclass)
|
||||||
|
if ok is not NotImplemented:
|
||||||
|
assert isinstance(ok, bool)
|
||||||
|
if ok:
|
||||||
|
cls._abc_cache.add(subclass)
|
||||||
|
else:
|
||||||
|
cls._abc_negative_cache.add(subclass)
|
||||||
|
return ok
|
||||||
|
# Check if it's a direct subclass
|
||||||
|
if cls in subclass.__mro__:
|
||||||
|
cls._abc_cache.add(subclass)
|
||||||
|
return True
|
||||||
|
# Check if it's a subclass of a registered class (recursive)
|
||||||
|
for rcls in cls._abc_registry:
|
||||||
|
if issubclass(subclass, rcls):
|
||||||
|
cls._abc_registry.add(subclass)
|
||||||
|
return True
|
||||||
|
# Check if it's a subclass of a subclass (recursive)
|
||||||
|
for scls in cls.__subclasses__():
|
||||||
|
if issubclass(subclass, scls):
|
||||||
|
cls._abc_registry.add(subclass)
|
||||||
|
return True
|
||||||
|
# No dice; update negative cache
|
||||||
|
cls._abc_negative_cache.add(subclass)
|
||||||
|
return False
|
||||||
142
Lib/test/test_abc.py
Normal file
142
Lib/test/test_abc.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# Copyright 2007 Google, Inc. All Rights Reserved.
|
||||||
|
# Licensed to PSF under a Contributor Agreement.
|
||||||
|
|
||||||
|
"""Unit tests for abc.py."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from test import test_support
|
||||||
|
|
||||||
|
import abc
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
class TestABC(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_abstractmethod_basics(self):
|
||||||
|
@abc.abstractmethod
|
||||||
|
def foo(self): pass
|
||||||
|
self.assertEqual(foo.__isabstractmethod__, True)
|
||||||
|
def bar(self): pass
|
||||||
|
self.assertEqual(hasattr(bar, "__isabstractmethod__"), False)
|
||||||
|
|
||||||
|
def test_abstractproperty_basics(self):
|
||||||
|
@abc.abstractproperty
|
||||||
|
def foo(self): pass
|
||||||
|
self.assertEqual(foo.__isabstractmethod__, True)
|
||||||
|
def bar(self): pass
|
||||||
|
self.assertEqual(hasattr(bar, "__isabstractmethod__"), False)
|
||||||
|
|
||||||
|
class C:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
@abc.abstractproperty
|
||||||
|
def foo(self): return 3
|
||||||
|
class D(C):
|
||||||
|
@property
|
||||||
|
def foo(self): return super(D, self).foo
|
||||||
|
self.assertEqual(D().foo, 3)
|
||||||
|
|
||||||
|
def test_abstractmethod_integration(self):
|
||||||
|
for abstractthing in [abc.abstractmethod, abc.abstractproperty]:
|
||||||
|
class C:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
@abstractthing
|
||||||
|
def foo(self): pass # abstract
|
||||||
|
def bar(self): pass # concrete
|
||||||
|
self.assertEqual(C.__abstractmethods__, set(["foo"]))
|
||||||
|
self.assertRaises(TypeError, C) # because foo is abstract
|
||||||
|
class D(C):
|
||||||
|
def bar(self): pass # concrete override of concrete
|
||||||
|
self.assertEqual(D.__abstractmethods__, set(["foo"]))
|
||||||
|
self.assertRaises(TypeError, D) # because foo is still abstract
|
||||||
|
class E(D):
|
||||||
|
def foo(self): pass
|
||||||
|
self.assertEqual(E.__abstractmethods__, set())
|
||||||
|
E() # now foo is concrete, too
|
||||||
|
class F(E):
|
||||||
|
@abstractthing
|
||||||
|
def bar(self): pass # abstract override of concrete
|
||||||
|
self.assertEqual(F.__abstractmethods__, set(["bar"]))
|
||||||
|
self.assertRaises(TypeError, F) # because bar is abstract now
|
||||||
|
|
||||||
|
def test_registration_basics(self):
|
||||||
|
class A:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
class B:
|
||||||
|
pass
|
||||||
|
b = B()
|
||||||
|
self.assertEqual(issubclass(B, A), False)
|
||||||
|
self.assertEqual(isinstance(b, A), False)
|
||||||
|
A.register(B)
|
||||||
|
self.assertEqual(issubclass(B, A), True)
|
||||||
|
self.assertEqual(isinstance(b, A), True)
|
||||||
|
class C(B):
|
||||||
|
pass
|
||||||
|
c = C()
|
||||||
|
self.assertEqual(issubclass(C, A), True)
|
||||||
|
self.assertEqual(isinstance(c, A), True)
|
||||||
|
|
||||||
|
def test_registration_builtins(self):
|
||||||
|
class A:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
A.register(int)
|
||||||
|
self.assertEqual(isinstance(42, A), True)
|
||||||
|
self.assertEqual(issubclass(int, A), True)
|
||||||
|
class B(A):
|
||||||
|
pass
|
||||||
|
B.register(basestring)
|
||||||
|
self.assertEqual(isinstance("", A), True)
|
||||||
|
self.assertEqual(issubclass(str, A), True)
|
||||||
|
|
||||||
|
def test_registration_edge_cases(self):
|
||||||
|
class A:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
A.register(A) # should pass silently
|
||||||
|
class A1(A):
|
||||||
|
pass
|
||||||
|
self.assertRaises(RuntimeError, A1.register, A) # cycles not allowed
|
||||||
|
class B:
|
||||||
|
pass
|
||||||
|
A1.register(B) # ok
|
||||||
|
A1.register(B) # should pass silently
|
||||||
|
class C(A):
|
||||||
|
pass
|
||||||
|
A.register(C) # should pass silently
|
||||||
|
self.assertRaises(RuntimeError, C.register, A) # cycles not allowed
|
||||||
|
C.register(B) # ok
|
||||||
|
|
||||||
|
def test_registration_transitiveness(self):
|
||||||
|
class A:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
self.failUnless(issubclass(A, A))
|
||||||
|
class B:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
self.failIf(issubclass(A, B))
|
||||||
|
self.failIf(issubclass(B, A))
|
||||||
|
class C:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
A.register(B)
|
||||||
|
class B1(B):
|
||||||
|
pass
|
||||||
|
self.failUnless(issubclass(B1, A))
|
||||||
|
class C1(C):
|
||||||
|
pass
|
||||||
|
B1.register(C1)
|
||||||
|
self.failIf(issubclass(C, B))
|
||||||
|
self.failIf(issubclass(C, B1))
|
||||||
|
self.failUnless(issubclass(C1, A))
|
||||||
|
self.failUnless(issubclass(C1, B))
|
||||||
|
self.failUnless(issubclass(C1, B1))
|
||||||
|
C1.register(int)
|
||||||
|
class MyInt(int):
|
||||||
|
pass
|
||||||
|
self.failUnless(issubclass(MyInt, A))
|
||||||
|
self.failUnless(isinstance(42, A))
|
||||||
|
|
||||||
|
|
||||||
|
def test_main():
|
||||||
|
test_support.run_unittest(TestABC)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
77
Lib/test/test_typechecks.py
Normal file
77
Lib/test/test_typechecks.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"""Unit tests for __instancecheck__ and __subclasscheck__."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from test import test_support
|
||||||
|
|
||||||
|
|
||||||
|
class ABC(type):
|
||||||
|
|
||||||
|
def __instancecheck__(cls, inst):
|
||||||
|
"""Implement isinstance(inst, cls)."""
|
||||||
|
return any(cls.__subclasscheck__(c)
|
||||||
|
for c in set([type(inst), inst.__class__]))
|
||||||
|
|
||||||
|
def __subclasscheck__(cls, sub):
|
||||||
|
"""Implement issubclass(sub, cls)."""
|
||||||
|
candidates = cls.__dict__.get("__subclass__", set()) | set([cls])
|
||||||
|
return any(c in candidates for c in sub.mro())
|
||||||
|
|
||||||
|
|
||||||
|
class Integer:
|
||||||
|
|
||||||
|
__metaclass__ = ABC
|
||||||
|
|
||||||
|
__subclass__ = set([int])
|
||||||
|
|
||||||
|
|
||||||
|
class SubInt(Integer):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Evil:
|
||||||
|
def __instancecheck__(self, inst): return False
|
||||||
|
|
||||||
|
|
||||||
|
class TypeChecksTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def testIsSubclassInternal(self):
|
||||||
|
self.assertEqual(Integer.__subclasscheck__(int), True)
|
||||||
|
self.assertEqual(Integer.__subclasscheck__(float), False)
|
||||||
|
|
||||||
|
def testIsSubclassBuiltin(self):
|
||||||
|
self.assertEqual(issubclass(int, Integer), True)
|
||||||
|
self.assertEqual(issubclass(float, Integer), False)
|
||||||
|
|
||||||
|
def testIsInstanceBuiltin(self):
|
||||||
|
self.assertEqual(isinstance(42, Integer), True)
|
||||||
|
self.assertEqual(isinstance(3.14, Integer), False)
|
||||||
|
|
||||||
|
def testIsInstanceActual(self):
|
||||||
|
self.assertEqual(isinstance(Integer(), Integer), True)
|
||||||
|
|
||||||
|
def testIsSubclassActual(self):
|
||||||
|
self.assertEqual(issubclass(Integer, Integer), True)
|
||||||
|
|
||||||
|
def testSubclassBehavior(self):
|
||||||
|
self.assertEqual(issubclass(SubInt, Integer), True)
|
||||||
|
self.assertEqual(issubclass(SubInt, SubInt), True)
|
||||||
|
self.assertEqual(issubclass(Integer, SubInt), False)
|
||||||
|
self.assertEqual(issubclass(int, SubInt), False)
|
||||||
|
self.assertEqual(isinstance(SubInt(), Integer), True)
|
||||||
|
self.assertEqual(isinstance(SubInt(), SubInt), True)
|
||||||
|
self.assertEqual(isinstance(42, SubInt), False)
|
||||||
|
|
||||||
|
def testInfiniteRecursionCaughtProperly(self):
|
||||||
|
e = Evil()
|
||||||
|
# This invokes isinstance() recursively, until the stack is exhausted.
|
||||||
|
self.assertRaises(RuntimeError, isinstance, e, Evil)
|
||||||
|
# XXX How to check the same situation for issubclass()?
|
||||||
|
|
||||||
|
|
||||||
|
def test_main():
|
||||||
|
test_support.run_unittest(TypeChecksTest)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -2279,6 +2279,27 @@ recursive_isinstance(PyObject *inst, PyObject *cls, int recursion_depth)
|
||||||
int
|
int
|
||||||
PyObject_IsInstance(PyObject *inst, PyObject *cls)
|
PyObject_IsInstance(PyObject *inst, PyObject *cls)
|
||||||
{
|
{
|
||||||
|
PyObject *t, *v, *tb;
|
||||||
|
PyObject *checker;
|
||||||
|
PyErr_Fetch(&t, &v, &tb);
|
||||||
|
checker = PyObject_GetAttrString(cls, "__instancecheck__");
|
||||||
|
PyErr_Restore(t, v, tb);
|
||||||
|
if (checker != NULL) {
|
||||||
|
PyObject *res;
|
||||||
|
int ok = -1;
|
||||||
|
if (Py_EnterRecursiveCall(" in __instancecheck__")) {
|
||||||
|
Py_DECREF(checker);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
res = PyObject_CallFunctionObjArgs(checker, inst, NULL);
|
||||||
|
Py_LeaveRecursiveCall();
|
||||||
|
Py_DECREF(checker);
|
||||||
|
if (res != NULL) {
|
||||||
|
ok = PyObject_IsTrue(res);
|
||||||
|
Py_DECREF(res);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
return recursive_isinstance(inst, cls, Py_GetRecursionLimit());
|
return recursive_isinstance(inst, cls, Py_GetRecursionLimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2334,6 +2355,25 @@ recursive_issubclass(PyObject *derived, PyObject *cls, int recursion_depth)
|
||||||
int
|
int
|
||||||
PyObject_IsSubclass(PyObject *derived, PyObject *cls)
|
PyObject_IsSubclass(PyObject *derived, PyObject *cls)
|
||||||
{
|
{
|
||||||
|
PyObject *t, *v, *tb;
|
||||||
|
PyObject *checker;
|
||||||
|
PyErr_Fetch(&t, &v, &tb);
|
||||||
|
checker = PyObject_GetAttrString(cls, "__subclasscheck__");
|
||||||
|
PyErr_Restore(t, v, tb);
|
||||||
|
if (checker != NULL) {
|
||||||
|
PyObject *res;
|
||||||
|
int ok = -1;
|
||||||
|
if (Py_EnterRecursiveCall(" in __subclasscheck__"))
|
||||||
|
return ok;
|
||||||
|
res = PyObject_CallFunctionObjArgs(checker, derived, NULL);
|
||||||
|
Py_LeaveRecursiveCall();
|
||||||
|
Py_DECREF(checker);
|
||||||
|
if (res != NULL) {
|
||||||
|
ok = PyObject_IsTrue(res);
|
||||||
|
Py_DECREF(res);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
return recursive_issubclass(derived, cls, Py_GetRecursionLimit());
|
return recursive_issubclass(derived, cls, Py_GetRecursionLimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue