Commit 20326322 authored by Robert Bradshaw's avatar Robert Bradshaw Committed by GitHub

Merge pull request #1927 from robertwb/multi-inheritance

Multiple inheritance
parents 59d95f6d edbd9db3
......@@ -8,6 +8,10 @@ Cython Changelog
Features added
--------------
* Cdef classes can now multiply inherit from ordinary Python classes.
(The primary base must still be a c class, possibly ``object``, and
the other bases must *not* be cdef classes.)
* Type inference is now supported for Pythran compiled NumPy expressions.
Patch by Nils Braun. (Github issue #1954)
......
......@@ -2948,8 +2948,8 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
else:
self.generate_base_type_import_code(env, entry, code)
self.generate_exttype_vtable_init_code(entry, code)
self.generate_type_ready_code(env, entry, code)
self.generate_typeptr_assignment_code(entry, code)
if entry.type.early_init:
self.generate_type_ready_code(entry, code)
def generate_base_type_import_code(self, env, entry, code):
base_type = entry.type.base_type
......@@ -3023,98 +3023,8 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
not type.is_external or type.is_subclassed,
error_code))
def generate_type_ready_code(self, env, entry, code):
# Generate a call to PyType_Ready for an extension
# type defined in this module.
type = entry.type
typeobj_cname = type.typeobj_cname
scope = type.scope
if scope: # could be None if there was an error
if entry.visibility != 'extern':
for slot in TypeSlots.slot_table:
slot.generate_dynamic_init_code(scope, code)
code.putln(
"if (PyType_Ready(&%s) < 0) %s" % (
typeobj_cname,
code.error_goto(entry.pos)))
# Don't inherit tp_print from builtin types, restoring the
# behavior of using tp_repr or tp_str instead.
code.putln("%s.tp_print = 0;" % typeobj_cname)
# Fix special method docstrings. This is a bit of a hack, but
# unless we let PyType_Ready create the slot wrappers we have
# a significant performance hit. (See trac #561.)
for func in entry.type.scope.pyfunc_entries:
is_buffer = func.name in ('__getbuffer__', '__releasebuffer__')
if (func.is_special and Options.docstrings and
func.wrapperbase_cname and not is_buffer):
slot = TypeSlots.method_name_to_slot[func.name]
preprocessor_guard = slot.preprocessor_guard_code()
if preprocessor_guard:
code.putln(preprocessor_guard)
code.putln('#if CYTHON_COMPILING_IN_CPYTHON')
code.putln("{")
code.putln(
'PyObject *wrapper = PyObject_GetAttrString((PyObject *)&%s, "%s"); %s' % (
typeobj_cname,
func.name,
code.error_goto_if_null('wrapper', entry.pos)))
code.putln(
"if (Py_TYPE(wrapper) == &PyWrapperDescr_Type) {")
code.putln(
"%s = *((PyWrapperDescrObject *)wrapper)->d_base;" % (
func.wrapperbase_cname))
code.putln(
"%s.doc = %s;" % (func.wrapperbase_cname, func.doc_cname))
code.putln(
"((PyWrapperDescrObject *)wrapper)->d_base = &%s;" % (
func.wrapperbase_cname))
code.putln("}")
code.putln("}")
code.putln('#endif')
if preprocessor_guard:
code.putln('#endif')
if type.vtable_cname:
code.putln(
"if (__Pyx_SetVtable(%s.tp_dict, %s) < 0) %s" % (
typeobj_cname,
type.vtabptr_cname,
code.error_goto(entry.pos)))
code.globalstate.use_utility_code(
UtilityCode.load_cached('SetVTable', 'ImportExport.c'))
if not type.scope.is_internal and not type.scope.directives['internal']:
# scope.is_internal is set for types defined by
# Cython (such as closures), the 'internal'
# directive is set by users
code.putln(
'if (PyObject_SetAttrString(%s, "%s", (PyObject *)&%s) < 0) %s' % (
Naming.module_cname,
scope.class_name,
typeobj_cname,
code.error_goto(entry.pos)))
weakref_entry = scope.lookup_here("__weakref__") if not scope.is_closure_class_scope else None
if weakref_entry:
if weakref_entry.type is py_object_type:
tp_weaklistoffset = "%s.tp_weaklistoffset" % typeobj_cname
if type.typedef_flag:
objstruct = type.objstruct_cname
else:
objstruct = "struct %s" % type.objstruct_cname
code.putln("if (%s == 0) %s = offsetof(%s, %s);" % (
tp_weaklistoffset,
tp_weaklistoffset,
objstruct,
weakref_entry.cname))
else:
error(weakref_entry.pos, "__weakref__ slot must be of type 'object'")
if scope.lookup_here("__reduce_cython__") if not scope.is_closure_class_scope else None:
# Unfortunately, we cannot reliably detect whether a
# superclass defined __reduce__ at compile time, so we must
# do so at runtime.
code.globalstate.use_utility_code(
UtilityCode.load_cached('SetupReduce', 'ExtensionTypes.c'))
code.putln('if (__Pyx_setup_reduce((PyObject*)&%s) < 0) %s' % (
typeobj_cname,
code.error_goto(entry.pos)))
def generate_type_ready_code(self, entry, code):
Nodes.CClassDefNode.generate_type_ready_code(entry, code)
def generate_exttype_vtable_init_code(self, entry, code):
# Generate code to initialise the C method table of an
......@@ -3145,15 +3055,6 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
cast,
meth_entry.func_cname))
def generate_typeptr_assignment_code(self, entry, code):
# Generate code to initialise the typeptr of an extension
# type defined in this module to point to its type object.
type = entry.type
if type.typeobj_cname:
code.putln(
"%s = &%s;" % (
type.typeptr_cname, type.typeobj_cname))
def generate_cfunction_declaration(entry, env, code, definition):
from_cy_utility = entry.used and entry.utility_code_definition
if entry.used and entry.inline_func_in_pxd or (not entry.in_cinclude and (
......
......@@ -4453,36 +4453,13 @@ class PyClassDefNode(ClassDefNode):
if self.is_py3_style_class:
error(self.classobj.pos, "Python3 style class could not be represented as C class")
return
bases = self.classobj.bases.args
if len(bases) == 0:
base_class_name = None
base_class_module = None
elif len(bases) == 1:
base = bases[0]
path = []
from .ExprNodes import AttributeNode, NameNode
while isinstance(base, AttributeNode):
path.insert(0, base.attribute)
base = base.obj
if isinstance(base, NameNode):
path.insert(0, base.name)
base_class_name = path[-1]
if len(path) > 1:
base_class_module = u'.'.join(path[:-1])
else:
base_class_module = None
else:
error(self.classobj.bases.args.pos, "Invalid base class")
else:
error(self.classobj.bases.args.pos, "C class may only have one base class")
return None
from . import ExprNodes
return CClassDefNode(self.pos,
visibility='private',
module_name=None,
class_name=self.name,
base_class_module=base_class_module,
base_class_name=base_class_name,
bases=self.classobj.bases or ExprNodes.TupleNode(self.pos, args=[]),
decorators=self.decorators,
body=self.body,
in_pxd=False,
......@@ -4574,8 +4551,7 @@ class CClassDefNode(ClassDefNode):
# module_name string or None For import of extern type objects
# class_name string Unqualified name of class
# as_name string or None Name to declare as in this scope
# base_class_module string or None Module containing the base class
# base_class_name string or None Name of the base class
# bases TupleNode Base class(es)
# objstruct_name string or None Specified C name of object struct
# typeobj_name string or None Specified C name of type object
# in_pxd boolean Is in a .pxd file
......@@ -4655,44 +4631,34 @@ class CClassDefNode(ClassDefNode):
self.module.has_extern_class = 1
env.add_imported_module(self.module)
if self.base_class_name:
if self.base_class_module:
base_class_scope = env.find_imported_module(self.base_class_module.split('.'), self.pos)
if not base_class_scope:
error(self.pos, "'%s' is not a cimported module" % self.base_class_module)
return
if self.bases.args:
base = self.bases.args[0]
base_type = base.analyse_as_type(env)
if base_type in (PyrexTypes.c_int_type, PyrexTypes.c_long_type, PyrexTypes.c_float_type):
# Use the Python rather than C variant of these types.
base_type = env.lookup(base_type.sign_and_name()).type
if base_type is None:
error(base.pos, "First base of '%s' is not an extension type" % self.class_name)
elif base_type == PyrexTypes.py_object_type:
base_class_scope = None
elif not base_type.is_extension_type and \
not (base_type.is_builtin_type and base_type.objstruct_cname):
error(base.pos, "'%s' is not an extension type" % base_type)
elif not base_type.is_complete():
error(base.pos, "Base class '%s' of type '%s' is incomplete" % (
base_type.name, self.class_name))
elif base_type.scope and base_type.scope.directives and \
base_type.is_final_type:
error(base.pos, "Base class '%s' of type '%s' is final" % (
base_type, self.class_name))
elif base_type.is_builtin_type and \
base_type.name in ('tuple', 'str', 'bytes'):
error(base.pos, "inheritance from PyVarObject types like '%s' is not currently supported"
% base_type.name)
else:
base_class_scope = env
if self.base_class_name == 'object':
# extension classes are special and don't need to inherit from object
if base_class_scope is None or base_class_scope.lookup('object') is None:
self.base_class_name = None
self.base_class_module = None
base_class_scope = None
if base_class_scope:
base_class_entry = base_class_scope.find(self.base_class_name, self.pos)
if base_class_entry:
if not base_class_entry.is_type:
error(self.pos, "'%s' is not a type name" % self.base_class_name)
elif not base_class_entry.type.is_extension_type and \
not (base_class_entry.type.is_builtin_type and
base_class_entry.type.objstruct_cname):
error(self.pos, "'%s' is not an extension type" % self.base_class_name)
elif not base_class_entry.type.is_complete():
error(self.pos, "Base class '%s' of type '%s' is incomplete" % (
self.base_class_name, self.class_name))
elif base_class_entry.type.scope and base_class_entry.type.scope.directives and \
base_class_entry.type.is_final_type:
error(self.pos, "Base class '%s' of type '%s' is final" % (
self.base_class_name, self.class_name))
elif base_class_entry.type.is_builtin_type and \
base_class_entry.type.name in ('tuple', 'str', 'bytes'):
error(self.pos, "inheritance from PyVarObject types like '%s' is not currently supported"
% base_class_entry.type.name)
else:
self.base_type = base_class_entry.type
if env.directives.get('freelist', 0) > 0:
warning(self.pos, "freelists cannot be used on subtypes, only the base class can manage them", 1)
self.base_type = base_type
if env.directives.get('freelist', 0) > 0 and base_type != PyrexTypes.py_object_type:
warning(self.pos, "freelists cannot be used on subtypes, only the base class can manage them", 1)
has_body = self.body is not None
if has_body and self.base_type and not self.base_type.scope:
......@@ -4752,6 +4718,31 @@ class CClassDefNode(ClassDefNode):
else:
scope.implemented = 1
if len(self.bases.args) > 1:
if not has_body or self.in_pxd:
error(self.bases.args[1].pos, "Only declare first base in declaration.")
for other_base in self.bases.args[1:]:
if other_base.analyse_as_type(env):
# TODO(robertwb): We may also want to enforce some checks
# at runtime.
error(other_base.pos, "Only one extension type base class allowed.")
if not self.scope.lookup("__dict__"):
#TODO(robertwb): See if this can be safely removed.
error(self.pos, "Extension types with multiple bases must have a __dict__ attribute")
self.entry.type.early_init = 0
from . import ExprNodes
self.type_init_args = ExprNodes.TupleNode(
self.pos,
args=[ExprNodes.IdentifierStringNode(self.pos, value=self.class_name),
self.bases,
ExprNodes.DictNode(self.pos, key_value_pairs=[])])
elif self.base_type:
self.entry.type.early_init = self.base_type.is_external or self.base_type.early_init
self.type_init_args = None
else:
self.entry.type.early_init = 1
self.type_init_args = None
env.allocate_vtable_names(self.entry)
for thunk in self.entry.type.defered_declarations:
......@@ -4761,6 +4752,8 @@ class CClassDefNode(ClassDefNode):
if self.body:
scope = self.entry.type.scope
self.body = self.body.analyse_expressions(scope)
if self.type_init_args:
self.type_init_args.analyse_expressions(env)
return self
def generate_function_definitions(self, env, code):
......@@ -4774,8 +4767,160 @@ class CClassDefNode(ClassDefNode):
code.mark_pos(self.pos)
if self.body:
self.body.generate_execution_code(code)
if not self.entry.type.early_init:
if self.type_init_args:
self.type_init_args.generate_evaluation_code(code)
bases = "PyTuple_GET_ITEM(%s, 1)" % self.type_init_args.result()
first_base = "((PyTypeObject*)PyTuple_GET_ITEM(%s, 0))" % bases
# Let Python do the base types compatibility checking.
trial_type = code.funcstate.allocate_temp(PyrexTypes.py_object_type, True)
code.putln("%s = PyType_Type.tp_new(&PyType_Type, %s, NULL);" % (
trial_type, self.type_init_args.result()))
code.putln(code.error_goto_if_null(trial_type, self.pos))
code.put_gotref(trial_type)
code.putln("if (((PyTypeObject*) %s)->tp_base != %s) {" % (
trial_type, first_base))
code.putln("PyErr_Format(PyExc_TypeError, \"best base '%s' must be equal to first base '%s'\",")
code.putln(" ((PyTypeObject*) %s)->tp_base->tp_name, %s->tp_name);" % (
trial_type, first_base))
code.putln(code.error_goto(self.pos))
code.putln("}")
code.funcstate.release_temp(trial_type)
code.put_incref(bases, PyrexTypes.py_object_type)
code.put_giveref(bases)
code.putln("%s.tp_bases = %s;" % (self.entry.type.typeobj_cname, bases))
code.put_decref_clear(trial_type, PyrexTypes.py_object_type)
self.type_init_args.generate_disposal_code(code)
self.type_init_args.free_temps(code)
self.generate_type_ready_code(self.entry, code, True)
# Also called from ModuleNode for early init types.
@staticmethod
def generate_type_ready_code(entry, code, heap_type_bases=False):
# Generate a call to PyType_Ready for an extension
# type defined in this module.
type = entry.type
typeobj_cname = type.typeobj_cname
scope = type.scope
if not scope: # could be None if there was an error
return
if entry.visibility != 'extern':
for slot in TypeSlots.slot_table:
slot.generate_dynamic_init_code(scope, code)
if heap_type_bases:
# As of https://bugs.python.org/issue22079
# PyType_Ready enforces that all bases of a non-heap type
# are non-heap. We know this is the case for the solid base,
# but other bases may be heap allocated and are kept alive
# though the bases reference.
# Other than this check, this flag is unused in this method.
code.putln("#if PY_VERSION_HEX >= 0x03050000")
code.putln("%s.tp_flags |= Py_TPFLAGS_HEAPTYPE;" % typeobj_cname)
code.putln("#endif")
code.putln(
"if (PyType_Ready(&%s) < 0) %s" % (
typeobj_cname,
code.error_goto(entry.pos)))
if heap_type_bases:
code.putln("#if PY_VERSION_HEX >= 0x03050000")
code.putln("%s.tp_flags &= ~Py_TPFLAGS_HEAPTYPE;" % typeobj_cname)
code.putln("#endif")
# Don't inherit tp_print from builtin types, restoring the
# behavior of using tp_repr or tp_str instead.
code.putln("%s.tp_print = 0;" % typeobj_cname)
# Fix special method docstrings. This is a bit of a hack, but
# unless we let PyType_Ready create the slot wrappers we have
# a significant performance hit. (See trac #561.)
for func in entry.type.scope.pyfunc_entries:
is_buffer = func.name in ('__getbuffer__', '__releasebuffer__')
if (func.is_special and Options.docstrings and
func.wrapperbase_cname and not is_buffer):
slot = TypeSlots.method_name_to_slot[func.name]
preprocessor_guard = slot.preprocessor_guard_code()
if preprocessor_guard:
code.putln(preprocessor_guard)
code.putln('#if CYTHON_COMPILING_IN_CPYTHON')
code.putln("{")
code.putln(
'PyObject *wrapper = PyObject_GetAttrString((PyObject *)&%s, "%s"); %s' % (
typeobj_cname,
func.name,
code.error_goto_if_null('wrapper', entry.pos)))
code.putln(
"if (Py_TYPE(wrapper) == &PyWrapperDescr_Type) {")
code.putln(
"%s = *((PyWrapperDescrObject *)wrapper)->d_base;" % (
func.wrapperbase_cname))
code.putln(
"%s.doc = %s;" % (func.wrapperbase_cname, func.doc_cname))
code.putln(
"((PyWrapperDescrObject *)wrapper)->d_base = &%s;" % (
func.wrapperbase_cname))
code.putln("}")
code.putln("}")
code.putln('#endif')
if preprocessor_guard:
code.putln('#endif')
if type.vtable_cname:
code.globalstate.use_utility_code(
UtilityCode.load_cached('SetVTable', 'ImportExport.c'))
code.putln(
"if (__Pyx_SetVtable(%s.tp_dict, %s) < 0) %s" % (
typeobj_cname,
type.vtabptr_cname,
code.error_goto(entry.pos)))
if heap_type_bases:
code.globalstate.use_utility_code(
UtilityCode.load_cached('MergeVTables', 'ImportExport.c'))
code.putln("if (__Pyx_MergeVtables(&%s) < 0) %s" % (
typeobj_cname,
code.error_goto(entry.pos)))
if not type.scope.is_internal and not type.scope.directives['internal']:
# scope.is_internal is set for types defined by
# Cython (such as closures), the 'internal'
# directive is set by users
code.putln(
'if (PyObject_SetAttrString(%s, "%s", (PyObject *)&%s) < 0) %s' % (
Naming.module_cname,
scope.class_name,
typeobj_cname,
code.error_goto(entry.pos)))
weakref_entry = scope.lookup_here("__weakref__") if not scope.is_closure_class_scope else None
if weakref_entry:
if weakref_entry.type is py_object_type:
tp_weaklistoffset = "%s.tp_weaklistoffset" % typeobj_cname
if type.typedef_flag:
objstruct = type.objstruct_cname
else:
objstruct = "struct %s" % type.objstruct_cname
code.putln("if (%s == 0) %s = offsetof(%s, %s);" % (
tp_weaklistoffset,
tp_weaklistoffset,
objstruct,
weakref_entry.cname))
else:
error(weakref_entry.pos, "__weakref__ slot must be of type 'object'")
if scope.lookup_here("__reduce_cython__") if not scope.is_closure_class_scope else None:
# Unfortunately, we cannot reliably detect whether a
# superclass defined __reduce__ at compile time, so we must
# do so at runtime.
code.globalstate.use_utility_code(
UtilityCode.load_cached('SetupReduce', 'ExtensionTypes.c'))
code.putln('if (__Pyx_setup_reduce((PyObject*)&%s) < 0) %s' % (
typeobj_cname,
code.error_goto(entry.pos)))
# Generate code to initialise the typeptr of an extension
# type defined in this module to point to its type object.
if type.typeobj_cname:
code.putln(
"%s = &%s;" % (
type.typeptr_cname, type.typeobj_cname))
def annotate(self, code):
if self.type_init_args:
self.type_init_args.annotate(code)
if self.body:
self.body.annotate(code)
......
......@@ -3435,19 +3435,15 @@ def p_c_class_definition(s, pos, ctx):
as_name = class_name
objstruct_name = None
typeobj_name = None
base_class_module = None
base_class_name = None
bases = None
if s.sy == '(':
s.next()
base_class_path = [p_ident(s)]
while s.sy == '.':
s.next()
base_class_path.append(p_ident(s))
if s.sy == ',':
s.error("C class may only have one base class", fatal=False)
s.expect(')')
base_class_module = ".".join(base_class_path[:-1])
base_class_name = base_class_path[-1]
positional_args, keyword_args = p_call_parse_args(s, allow_genexp=False)
if keyword_args:
s.error("C classes cannot take keyword bases.")
bases, _ = p_call_build_packed_args(pos, positional_args, keyword_args)
if bases is None:
bases = ExprNodes.TupleNode(pos, args=[])
if s.sy == '[':
if ctx.visibility not in ('public', 'extern') and not ctx.api:
error(s.position(), "Name options only allowed for 'public', 'api', or 'extern' C class")
......@@ -3487,8 +3483,7 @@ def p_c_class_definition(s, pos, ctx):
module_name = ".".join(module_path),
class_name = class_name,
as_name = as_name,
base_class_module = base_class_module,
base_class_name = base_class_name,
bases = bases,
objstruct_name = objstruct_name,
typeobj_name = typeobj_name,
in_pxd = ctx.level == 'module_pxd',
......
......@@ -1302,10 +1302,12 @@ class PyExtensionType(PyObjectType):
# vtabstruct_cname string Name of C method table struct
# vtabptr_cname string Name of pointer to C method table
# vtable_cname string Name of C method table definition
# early_init boolean Whether to initialize early (as opposed to during module execution).
# defered_declarations [thunk] Used to declare class hierarchies in order
is_extension_type = 1
has_attributes = 1
early_init = 1
objtypedef_cname = None
......
......@@ -656,6 +656,67 @@ bad:
}
/////////////// MergeVTables.proto ///////////////
//@requires: GetVTable
static int __Pyx_MergeVtables(PyTypeObject *type); /*proto*/
/////////////// MergeVTables ///////////////
static int __Pyx_MergeVtables(PyTypeObject *type) {
int i;
void** base_vtables;
void* unknown = (void*)-1;
PyObject* bases = type->tp_bases;
int base_depth = 0;
{
PyTypeObject* base = type->tp_base;
while (base) {
base_depth += 1;
base = base->tp_base;
}
}
base_vtables = (void**) malloc(sizeof(void*) * (base_depth + 1));
base_vtables[0] = unknown;
// Could do MRO resolution of individual methods in the future, assuming
// compatible vtables, but for now simply require a common vtable base.
// Note that if the vtables of various bases are extended separately,
// resolution isn't possible and we must reject it just as when the
// instance struct is so extended. (It would be good to also do this
// check when a multiple-base class is created in pure Python as well.)
for (i = 1; i < PyTuple_GET_SIZE(bases); i++) {
void* base_vtable = __Pyx_GetVtable(((PyTypeObject*)PyTuple_GET_ITEM(bases, i))->tp_dict);
if (base_vtable != NULL) {
int j;
PyTypeObject* base = type->tp_base;
for (j = 0; j < base_depth; j++) {
if (base_vtables[j] == unknown) {
base_vtables[j] = __Pyx_GetVtable(base->tp_dict);
base_vtables[j + 1] = unknown;
}
if (base_vtables[j] == base_vtable) {
break;
} else if (base_vtables[j] == NULL) {
// No more potential matching bases (with vtables).
goto bad;
}
base = base->tp_base;
}
}
}
PyErr_Clear();
free(base_vtables);
return 0;
bad:
PyErr_Format(
PyExc_TypeError,
"multiple bases have vtable conflict: '%s' and '%s'",
type->tp_base->tp_name, ((PyTypeObject*)PyTuple_GET_ITEM(bases, i))->tp_name);
free(base_vtables);
return -1;
}
/////////////// ImportNumPyArray.proto ///////////////
static PyObject *__pyx_numpy_ndarray = NULL;
......
......@@ -12,7 +12,7 @@ cdef class MyStr(str): # only in Py2, but can't know that during compilation
pass
_ERRORS = """
5:5: inheritance from PyVarObject types like 'tuple' is not currently supported
8:5: inheritance from PyVarObject types like 'bytes' is not currently supported
11:5: inheritance from PyVarObject types like 'str' is not currently supported
5:19: inheritance from PyVarObject types like 'tuple' is not currently supported
8:19: inheritance from PyVarObject types like 'bytes' is not currently supported
11:17: inheritance from PyVarObject types like 'str' is not currently supported
"""
......@@ -10,5 +10,5 @@ cdef class SubType(FinalClass):
pass
_ERRORS = """
9:5: Base class 'FinalClass' of type 'SubType' is final
9:19: Base class 'FinalClass' of type 'SubType' is final
"""
cdef class CBase(object):
cdef int a
cdef c_method(self):
return "CBase"
cpdef cpdef_method(self):
return "CBase"
class PyBase(object):
def py_method(self):
return "PyBase"
cdef class Both(CBase, PyBase):
cdef dict __dict__
"""
>>> b = Both()
>>> b.py_method()
'PyBase'
>>> b.cp_method()
'Both'
>>> b.call_c_method()
'Both'
>>> isinstance(b, CBase)
True
>>> isinstance(b, PyBase)
True
"""
cdef c_method(self):
return "Both"
cpdef cp_method(self):
return "Both"
def call_c_method(self):
return self.c_method()
cdef class BothSub(Both):
"""
>>> b = BothSub()
>>> b.py_method()
'PyBase'
>>> b.cp_method()
'Both'
>>> b.call_c_method()
'Both'
"""
pass
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment