From c7aa97589f3c808a9a48ba87bb365fd074a03edc Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 22 May 2018 20:27:08 +0200 Subject: [PATCH 1/2] Remove use of non-public PyType_stgdict C function in ctypes_patch Fixes #113. --- rubicon/objc/ctypes_patch.py | 58 +++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/rubicon/objc/ctypes_patch.py b/rubicon/objc/ctypes_patch.py index a5e72d60..b0a47312 100644 --- a/rubicon/objc/ctypes_patch.py +++ b/rubicon/objc/ctypes_patch.py @@ -12,6 +12,7 @@ import ctypes import sys +import types import warnings # This module relies on the layout of a few internal Python and ctypes structures. @@ -72,6 +73,15 @@ class PyDictObject(ctypes.Structure): ] +# The mappingproxyobject struct from "Objects/descrobject.c". +# This structure is not officially stable across Python versions, but its layout hasn't changed since 2001. +class mappingproxyobject(ctypes.Structure): + _fields_ = [ + ("ob_base", PyObject), + ("mapping", ctypes.py_object), + ] + + # The ffi_type structure from libffi's "include/ffi.h". # This is a forward declaration, because the structure contains pointers to itself. class ffi_type(ctypes.Structure): @@ -108,9 +118,48 @@ class StgDictObject(ctypes.Structure): ] -# The PyObject_stgdict function from "Modules/_ctypes/ctypes.h". -ctypes.pythonapi.PyType_stgdict.restype = ctypes.POINTER(StgDictObject) -ctypes.pythonapi.PyType_stgdict.argtypes = [ctypes.py_object] +def unwrap_mappingproxy(proxy): + """Return the mapping contained in a mapping proxy object.""" + + if not isinstance(proxy, types.MappingProxyType): + raise TypeError( + 'Expected a mapping proxy object, not {tp.__module__}.{tp.__qualname__}' + .format(tp=type(proxy)) + ) + + return mappingproxyobject.from_address(id(proxy)).mapping + + +def get_stgdict_of_type(tp): + """Return the given ctypes type's StgDict object. If the object's dict is not a StgDict, an error is raised. + + This function is roughly equivalent to the PyType_stgdict function in the ctypes source code. We cannot use that + function directly, because it is not part of CPython's public C API, and thus not accessible on some systems + (see #113). + """ + + if not isinstance(tp, type): + raise TypeError( + 'Expected a type object, not {tptp.__module__}.{tptp.__qualname__}' + .format(tptp=type(tp)) + ) + + stgdict = tp.__dict__ + if isinstance(stgdict, types.MappingProxyType): + # If the type's __dict__ is wrapped in a mapping proxy, we need to unwrap it. + # (This appears to always be the case, so the isinstance check above could perhaps be left out, but it + # doesn't hurt to check.) + stgdict = unwrap_mappingproxy(stgdict) + + # The StgDict type is not publicly exposed anywhere, so we can't use isinstance. Checking the name is the best + # we can do here. + if type(stgdict).__name__ != 'StgDict': + raise TypeError( + "The given type's dict must be a StgDict, not {tp.__module__}.{tp.__qualname__}" + .format(tp=type(stgdict)) + ) + + return stgdict def make_callback_returnable(ctype): @@ -127,7 +176,8 @@ def make_callback_returnable(ctype): return ctype # Extract the StgDict from the ctype. - stgdict_c = ctypes.pythonapi.PyType_stgdict(ctype).contents + stgdict = get_stgdict_of_type(ctype) + stgdict_c = StgDictObject.from_address(id(stgdict)) # Ensure that there is no existing getfunc or setfunc on the stgdict. if ctypes.cast(stgdict_c.getfunc, ctypes.c_void_p).value is not None: From 14a2477af5640187d48bf343f96831276bcf7a88 Mon Sep 17 00:00:00 2001 From: dgelessus Date: Tue, 22 May 2018 20:36:54 +0200 Subject: [PATCH 2/2] Update pyflakes dependency from "git version" to 2.0.0 We can't remove the manual dependency completely yet, but it's better to use a release version than the development version from the repo. --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index c6986d16..ffb3cc17 100644 --- a/tox.ini +++ b/tox.ini @@ -25,10 +25,11 @@ setenv = [testenv:flake8] basepython = python3.6 -# FIXME Remove the pyflakes git dependency once a new version of pyflakes (> 1.6.0) is released. -# Currently we need to use the development version of pyflakes in order to use __class__, because pyflakes <= 1.6.0 -# doesn't understand __class__. This has been fixed in the development version, but not in any release yet. +# FIXME Remove the manual pyflakes dependency once flake8 depends on pyflakes >= 2.0.0. +# Currently we need to manually update pyflakes in order to use __class__, because pyflakes <= 1.6.0 +# doesn't understand __class__. This has been fixed in pyflakes 2.0.0, but flake8's requirements don't allow that +# version of pyflakes yet. deps = flake8 - git+https://github.com/PyCQA/pyflakes.git + pyflakes >= 2.0.0 commands = flake8 {posargs}