Summary
Calling any method that returns an unregistered struct with double fields
(e.g. UIEdgeInsets) raises KeyError(b'd') on Python 3. The root cause
is a bytes/str mismatch introduced by the Python 2→3 migration: the
types dict in objc_py_types.py has string keys ('d') but the
Cython layer passes bytes when looking up struct field types (b'd').
Reproducer
from pyobjus import autoclass
UIApplication = autoclass("UIApplication")
app = UIApplication.sharedApplication()
win = app.windows.firstObject()
insets = win.safeAreaInsets() # KeyError(b'd')
Traceback (innermost relevant frame):
File ".../pyobjus/objc_py_types.py", line 144, in make_type
field_list.append((field_name, types[_type]))
KeyError: b'd'
Root cause
objc_py_types.py defines the type-encoding map with string keys:
types = {
...
'f': ctypes.c_float,
'd': ctypes.c_double, # str key
...
}
The Cython layer passes bytes when iterating struct field types. On Python 2
'd' == b'd'; on Python 3 they are different objects, so the lookup fails.
The common UIKit struct types (CGRect, CGPoint, CGSize, NSRange) are
pre-registered in Factory.init, so find_object returns them from the
registry before ever calling make_type. Any struct that is not
pre-registered — including UIEdgeInsets — falls through to the broken path.
Fix
In objc_py_types.py, decode _type before the lookup in make_type:
# line ~144 in make_type
_key = _type.decode("ascii") if isinstance(_type, bytes) else _type
field_list.append((field_name, types[_key]))
Alternatively, change the types dict to use bytes keys throughout to
match what the Cython layer actually passes.
Workaround
def get_safe_area() -> dict[str, float]:
try:
from pyobjus import autoclass
# Derive insets from safeAreaLayoutGuide.layoutFrame vs window.frame.
# Both return CGRect, which pyobjus pre-registers as NSRect — this
# avoids the UIEdgeInsets struct-return path that triggers a pyobjus
# 1.2.x Python-3 bug (KeyError on unregistered struct field types).
app = autoclass("UIApplication").sharedApplication()
win = app.windows.firstObject()
guide = win.safeAreaLayoutGuide().layoutFrame # ObjcProperty → NSRect
frame = win.frame() # ObjcMethod → NSRect
return {
"top": float(guide.origin.y),
"left": float(guide.origin.x),
"bottom": float(frame.size.height - (guide.origin.y + guide.size.height)),
"right": float(frame.size.width - (guide.origin.x + guide.size.width)),
}
except Exception:
return {"top": 0.0, "bottom": 0.0, "left": 0.0, "right": 0.0}
Environment
- pyobjus 1.2.4
- Python 3.15 (iOS wheel, cp315) Self built and locally hosted.
- iOS 16+ simulator and device (arm64)
Summary
Calling any method that returns an unregistered struct with double fields
(e.g. UIEdgeInsets) raises KeyError(b'd') on Python 3. The root cause
is a bytes/str mismatch introduced by the Python 2→3 migration: the
types dict in objc_py_types.py has string keys ('d') but the
Cython layer passes bytes when looking up struct field types (b'd').
Reproducer
Traceback (innermost relevant frame):
Root cause
objc_py_types.py defines the type-encoding map with string keys:
The Cython layer passes bytes when iterating struct field types. On Python 2
'd' == b'd'; on Python 3 they are different objects, so the lookup fails.
The common UIKit struct types (CGRect, CGPoint, CGSize, NSRange) are
pre-registered in Factory.init, so find_object returns them from the
registry before ever calling make_type. Any struct that is not
pre-registered — including UIEdgeInsets — falls through to the broken path.
Fix
In objc_py_types.py, decode _type before the lookup in make_type:
Alternatively, change the types dict to use bytes keys throughout to
match what the Cython layer actually passes.
Workaround
Environment