Given the following Python code:
import ctypes
from collections.abc import Mapping
class StructureMeta(type(ctypes.Structure), type(Mapping)):
pass
class Structure(ctypes.Structure, Mapping, metaclass=StructureMeta):
pass
struct = Structure()
assert isinstance(struct, ctypes.Structure)
assert isinstance(struct, Mapping)
The metaclass is needed to avoid a metaclass conflict when deriving from both ctypes.Structure (metaclass _ctypes.PyCStructType
) and Mapping (metaclass abc.ABCMeta
).
This works fine when executed with Python 3.11. Alas, pylint 3.3.4 reports two errors:
test.py:5:0: E0240: Inconsistent method resolution order for class 'StructureMeta' (inconsistent-mro)
test.py:9:0: E1139: Invalid metaclass 'StructureMeta' used (invalid-metaclass)
How do I need to change the meta class to fix the error reported by pylint? Is it even a problem?
As you can attest, there is no "error" in this code.
Working with metaclasses is hard - and having to create a compatible metaclass in cases like this shows some of the side effects.
The problem is most of what metaclasses do are things that take effect as the code is executed (i.e. in runtime) - while analysis tools like pylint, pyright, mypy, all try to see Python as if it was a static language, and as they evolve, they incorporate patterns to understand ever a little bit more of the language's dynamism.
You simply reached a step that pylint (or whatever tool) haven't reached yet - the plain workaround is just to ignore its fake error message (by adding a # noQA or equivalent comment in the infringing line).
Mapping
class in this way is subject to unforeseen side effectsBut, for the sake of entertainment and completeness, this is the path to avoid the linter-error (involving much more experimental code practices and subject to surprise side effects than just silencing the QA tool):
A workaround there might be not to use the Mapping base class as it is, since it has a conflicting metaclass - it is possible for your resulting class to be registered as a Mapping (by using the Mapping.register
call as a decorator) - so that isinstance(struct, Mapping)
will still be true.
That would imply, of course, in reimplementing all the goodness collections.abc.Mapping
have, in wiring up all of a mapping logic so that one just have to implement a few methods.
Instead of that, we might just create a "non-abstract" version of Mapping by copying all of its methods over to another, plain, class - and use that to the multiple-inheritance with ctypes.Struct
from collections import abc
import ctypes
ConcreteMapping = type("ConcreteMapping", (), {key: getattr(abc.Mapping, key) for key in dir(abc.Mapping)})
@abc.Mapping.register
class Struct(ctypes.Structure, ConcreteMapping):
pass
s = Struct()
assert isinstance(s, abc.Mapping)
assert isinstance(s, ctypes.Structure)
(Needless to say, this throws away the only gain of abstractclasses in Python: the RuntimeError at the point one attempts to instantiate an incomplete "Mapping" class, without implementing one of the required methods as per the docs.)
Actually, there is a path forward, without potential side effects like either combining metaclasses (that, although working, may yield problems as Python versions change due to subtle modifications in how these metaclasses interact) - and without clonning collections.abc.Mapping
as above - if you are willing to let go of the most of the benefficts of inheriting from Mapping, namely, getting the .get
, __contains__
, values
, keys
, items
, __ne__
, __eq__
methods for free:
Just implement the methods your codebase will be actually using from Mapping (which might be just __getitem__
, in this case), and
register your class as a subclass of collections.abc.Mapping
: the isinstance check will return True. No side effects - just that if one attempts to use one of the non-existing methods for your class, it will raise a RuntimeError:
@collections.abc.Mapping.register
class Struct(ctypes.Structure):
_fields_ = ...
def __getitem__(self, key):
...