Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove use of non-public C functions in ctypes_patch #114

Merged
merged 3 commits into from
Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions rubicon/objc/ctypes_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand Down
9 changes: 5 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still valid?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, flake8's current setup.py only allows pyflakes >= 1.5.0, < 1.7.0. The reason for the upper version limit is explained in the flake8 FAQ.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh - right, now I get it. This is about forcing the pyflakes version because of flake8's stated requirement.

deps =
flake8
git+https://github.com/PyCQA/pyflakes.git
pyflakes >= 2.0.0
commands = flake8 {posargs}