teams-bots/write-message/venv/lib/python3.10/site-packages/objc/_transform.py

701 lines
23 KiB
Python

"""
Helper module that is used by objc_class.setattr and objc_class.__new__
to transform class attributes and process the class dictionary (for the
latter).
The C code relies on this code to do most semantic checks, and only
performs checks that are needed to avoid crashes.
"""
import objc
import types
import inspect
import dis
import keyword
import warnings
import sys
from ._informal_protocol import _informal_protocol_for_selector
# only public objc_method until the python_method implementation
# in C is gone
__all__ = ("objc_method", "python_method")
NO_VALUE = object()
def processClassDict(
class_dict,
meta_dict,
class_object,
protocols,
hidden_instance_methods,
hidden_class_methods,
):
"""
First step into creating a subclass for a Cocoa class:
- Transform attributes
- Return (instance_variablees, instanstance_methods, class_methods)
"""
instance_variables = {}
instance_methods = set()
class_methods = set()
for p in protocols:
if not isinstance(p, (objc.formal_protocol, objc.informal_protocol)):
raise TypeError(
"protocols list contains object that isn't an Objective-C "
f"protocol, but type {type(p).__name__}"
)
if "__classcell__" in class_dict and "__module__" in class_dict:
mod = sys.modules.get(class_dict["__module__"], None)
if mod is not None:
try:
ok = mod.super is objc.super
except AttributeError:
ok = False
if not ok:
warnings.warn(
"Objective-C subclass uses super(), but super is not objc.super",
category=objc.ObjCSuperWarning,
stacklevel=2,
)
super_ivars = set()
for v in class_object.__dict__.values():
if isinstance(v, objc.ivar):
super_ivars.add(v.__name__)
class_dict["__objc_python_subclass__"] = True
process_slots(class_dict, instance_variables, class_object)
# First call all class setup hooks. Those can
# update the class dictiory, which is why this
# loop# cannot be merged into the next one.
for key, value in list(class_dict.items()):
setup = getattr(value, "__pyobjc_class_setup__", NO_VALUE)
if setup is not NO_VALUE:
setup(key, class_dict, instance_methods, class_methods)
for key, value in list(class_dict.items()):
old_value = value
new_value = transformAttribute(key, value, class_object, protocols)
if new_value is not value:
class_dict[key] = new_value
value = new_value
if isinstance(value, objc.selector):
if isinstance(old_value, python_method):
continue
if isinstance(value, objc.selector):
canonical = value.selector.decode().replace(":", "_")
if value.isClassMethod:
del class_dict[key]
if not value.isHidden:
meta_dict[key] = value
if canonical != key:
meta_dict[canonical] = value
class_methods.add(value)
else:
if value.isHidden:
del class_dict[key]
elif canonical != key:
class_dict[canonical] = value
instance_methods.add(value)
elif isinstance(value, objc.ivar):
if value.__name__ in instance_variables:
raise objc.error(f"{key!r} reimplements objc.ivar {value.__name__!r}")
if value.__name__ in super_ivars:
raise objc.error(
f"objc.ivar {key!r} overrides instance variable in super class"
)
instance_variables[value.__name__] = value
# Convert the collections to tuples for easier
# post-processing in the C code.
#
# The results are sorted for easier testing, this should
# be harmless because classes tend to be fairly simple.
instance_variables = tuple(
sorted(instance_variables.values(), key=lambda x: x.__name__)
)
instance_methods = tuple(
sorted(
instance_methods,
key=lambda x: x.selector if isinstance(x, objc.selector) else x,
)
)
class_methods = tuple(
sorted(
class_methods,
key=lambda x: x.selector if isinstance(x, objc.selector) else x,
)
)
validate_protocols(class_object, instance_methods, class_methods, protocols)
for sel in instance_methods:
if isinstance(sel, bytes):
hidden_instance_methods[sel] = None
elif sel.isHidden:
hidden_instance_methods[sel.selector] = sel
for sel in class_methods:
if isinstance(sel, bytes):
hidden_class_methods[sel] = None
elif sel.isHidden:
hidden_class_methods[sel.selector] = sel
return (
instance_variables,
instance_methods,
class_methods,
)
def validate_protocols(class_object, instance_methods, class_methods, protocols):
instance_map = {
sel.selector: sel for sel in instance_methods if isinstance(sel, objc.selector)
}
class_map = {
sel.selector: sel for sel in class_methods if isinstance(sel, objc.selector)
}
for proto in protocols:
for is_class_method, method_list in [
(False, proto.instanceMethods()),
(True, proto.classMethods()),
]:
for sel in method_list:
if is_class_method:
found = class_map.get(sel["selector"], None)
if found is None:
found = getattr(
class_object.pyobjc_classMethods,
sel["selector"].decode().replace(":", "_"),
None,
)
else:
found = instance_map.get(sel["selector"], None)
if found is None:
found = getattr(
class_object.pyobjc_instanceMethods,
sel["selector"].decode().replace(":", "_"),
None,
)
if found is None:
if sel["required"]:
raise TypeError(
f"class does not fully implemented protocol {proto.__name__!r}: "
"no implementation for "
f"{'class' if is_class_method else 'instance'} method "
f"{sel['selector'].decode()!r}"
)
continue
if sel["typestr"] != found.signature:
raise TypeError(
f"class does not correctly implement protocol {proto.__name__!r}: "
f"the signature for method {sel['selector'].decode()} is "
f"{found.signature.decode()!r} instead of {sel['typestr'].decode()!r}"
)
def process_slots(class_dict, instance_variables, class_object):
"""
Process the ``__slots__`` attribute, if any.
"""
slots = class_dict.get("__slots__", NO_VALUE)
if slots is NO_VALUE:
if not class_object.__hasdict__:
instance_variables["__dict__"] = objc.ivar(
"__dict__", objc._C_PythonObject, isSlot=True
)
else:
if isinstance(slots, str):
if slots in class_dict:
raise objc.error(f"slot {slots!r} redefines {class_dict[slots]!r}")
class_dict[slots] = objc.ivar(slots, objc._C_PythonObject, isSlot=True)
else:
for nm in slots:
if nm in class_dict:
raise objc.error(f"slot {nm!r} redefines {class_dict[nm]!r}")
class_dict[nm] = objc.ivar(nm, objc._C_PythonObject, isSlot=True)
class_dict["__slots__"] = ()
def transformAttribute(name, value, class_object, protocols):
"""
Transform 'value' before it will be added to a class
For callables this will convert the value to an
'objc.selector' object where appropriate, other values
are returned as-is.
"""
if not callable(value) and not isinstance(value, classmethod):
return value
elif isinstance(value, (objc.native_selector, staticmethod, type)):
return value
elif isinstance(value, objc.selector):
if value.callable is None:
raise ValueError(f"{name!r}: selector object without callable")
return objc.selector(
value.callable,
value.selector,
value.signature,
isClassMethod=value.isClassMethod,
isRequired=value.isRequired,
isHidden=value.isHidden,
)
elif is_generator_or_async(value):
return value
elif isinstance(value, python_method):
return value.__wrapped__
isclass = None
if not isinstance(name, str):
selname = None
else:
selname = default_selector(name)
signature = None
if selname is not None and isinstance(class_object, objc.objc_class):
registered = objc._registeredMetadataForSelector(class_object, selname)
else:
registered = None
if selname in {b"copy", b"copyWithZone:", b"mutableCopy", b"mutableCopyWithZone:"}:
# Specials: NSObject defines copy and copyWithZone as class methods,
# while most users would expect to define an instance method.
isclass = False
helper_signature = NO_VALUE
# DWIM: Copy classmethod-ness and signature from
# a pre-existing method on the class, but prefer
# using an instance method over a class method, as
# this makes it a lot easier to implement methods from
# the NSObject protocol (amongst others)
if isinstance(name, str) and helper_signature is NO_VALUE:
current = getattr(class_object.pyobjc_instanceMethods, name, NO_VALUE)
if current is NO_VALUE:
current = getattr(class_object.pyobjc_classMethods, name, NO_VALUE)
if isinstance(current, objc.selector):
if isclass is None:
isclass = current.isClassMethod
# ishidden = current.isHidden
signature = current.signature
selname = current.selector
if isinstance(value, classmethod):
# Unwrap "classmethod" instances.
# XXX: Switch to __wrapped__ when support for 3.9 is dropped
value = value.__func__
isclass = True
if isinstance(value, python_method):
# Deal with:
# @classmethod
# @python_method
# def ...
#
return classmethod(value.__wrapped__)
assert not isinstance(value, classmethod)
if isinstance(value, objc_method):
# Unwrap "objc_method" instances
if value.isclass is not None:
isclass = value.isclass
if value.selector is not None:
selname = value.selector
registered = objc._registeredMetadataForSelector(class_object, selname)
if value.signature is not None:
signature = value.signature
registered = None
if selname is None:
raise ValueError(
f"{name!r} is an objc_method instance, but cannot determine Objective-C selector"
)
if isinstance(value.__wrapped__, classmethod):
# Deal with:
# @objc_method
# @classsmethod
# def ...
#
if value.isclass is not None:
raise ValueError(
f"{name!r} is objc_method with isclass specified wraps classmethod"
)
isclass = True
# XXX switch from __func__ to __wrapped-__ when support
# for 3.9 ends.
value = value.__wrapped__.__func__
else:
value = value.__wrapped__
if registered and "full_signature" in registered:
signature = registered["full_signature"]
if selname is None:
# The 'name' does not fit into the selector naming
# convention, and nothing provided a better name.
#
# Assume this is meant to be a regular python method.
if not isinstance(name, str):
raise TypeError(
f"method name is of type {type(name).__name__}, not a string"
)
if isclass:
return classmethod(value)
return value
argcount = selname.count(b":")
if signature is None:
# Look for the signature in an informal protocol when it
# isn't set by some other means.
informal = _informal_protocol_for_selector(selname)
if informal is not None:
for meth in informal.selectors:
if meth.selector == selname:
if isclass is not None and meth.isClassMethod != isclass:
# "classmethod"-ness was explicitly set (inherit,
# classmethod, or objc_method), ignore the informal
# protocol if it doesn't match.
continue
signature = meth.signature
isclass = meth.isClassMethod
if isclass is None:
isclass = False
if signature is None and protocols:
for prot in protocols:
if isclass:
info = prot.descriptionForClassMethod_(selname)
else:
info = prot.descriptionForInstanceMethod_(selname)
if info is not None:
signature = info[1]
if signature is None:
# Calculate a default signature based on the selector shape
if isinstance(value, (types.FunctionType, types.MethodType)):
returns_object = returns_value(value)
else:
returns_object = True
signature = (
(objc._C_ID if returns_object else objc._C_VOID)
+ objc._C_ID
+ objc._C_SEL
+ (objc._C_ID * argcount)
)
if registered:
overridden_rval = (
registered["retval"].get("type", None)
if registered["retval"] is not None
else None
)
overridden_args = {}
for idx, a in enumerate(registered["arguments"]):
if a and "type" in a:
overridden_args[idx] = a["type"]
if overridden_rval or overridden_args:
signature_parts = list(objc.splitSignature(signature))
if overridden_rval:
signature_parts[0] = overridden_rval
for idx in overridden_args:
if (idx < 0) or idx >= len(signature_parts) - 1:
raise objc.error(
f"{name!r} has invalid metadata, index {idx} out of range"
)
signature_parts[idx + 1] = overridden_args[idx]
signature = b"".join(signature_parts)
# All information for creating a selector is available, do a
# last consistency check.
try:
pysig = inspect.signature(value)
except (AttributeError, ValueError, TypeError):
pass
else:
pos = 0
pos_default = 0
kwonly = 0
kwonly_default = 0
has_var_pos = False
for p in pysig.parameters.values():
if p.kind == inspect.Parameter.KEYWORD_ONLY:
kwonly += 1
if p.default is not inspect.Parameter.empty:
kwonly_default += 1
elif p.kind == inspect.Parameter.VAR_KEYWORD:
pass
elif p.kind == inspect.Parameter.VAR_POSITIONAL:
has_var_pos = True
else:
pos += 1
if p.default is not inspect.Parameter.empty:
pos_default += 1
if kwonly - kwonly_default:
# Calls from Obejctive-C to Python will never have keyword arguments,
# therefore this callable is not compatible with use as a selector.
raise objc.BadPrototypeError(
f"{value!r} has {kwonly - kwonly_default} keyword-only arguments without a default"
)
if not has_var_pos and (
(pos < argcount + 1) or (pos - pos_default > argcount + 1)
):
# 'value' does not have a '*arg' argument, and does not accept calls
# with exactly 'argcount' arguments, therefore this callable is not
# compatible with us as a selector.
#
# 'argcount' does not count the implicit self argument
if pos_default:
raise objc.BadPrototypeError(
f"{selname.decode()!r} expects {argcount} arguments, "
f"{value!r} has between {pos-pos_default-1} and {pos-1} positional arguments"
)
else:
raise objc.BadPrototypeError(
f"{selname.decode()!r} expects {argcount} arguments, "
f"{value!r} has {pos-1} positional arguments"
)
# XXX: This is needed because SomeClass.pyobjc_instanceMethods.hiddenSelector.isHidden
# is false :-(
ishidden = False
for cls in class_object.mro(): # pragma: no branch
if not issubclass(cls, objc.objc_object):
# Skip all non-objc class in the MRO to ignore
# mixin classes.
continue
if selname in cls.pyobjc_hiddenSelectors(isclass):
ishidden = True
break
return objc.selector(
value, selname, signature, isClassMethod=isclass, isHidden=ishidden
)
def is_generator_or_async(value):
"""
Returns true for generators and async functions
"""
# This guard is there to deal with some edge-case testing the
# PyObjC test suite, should never trigger in production code.
if not isinstance(value, types.FunctionType):
return False
if not isinstance(value.__code__, types.CodeType):
return False
if inspect.iscoroutine(value) or inspect.iscoroutinefunction(value):
return True
elif inspect.isgenerator(value) or inspect.isgeneratorfunction(value):
return True
return False
def returns_value(func):
"""
Return True if 'func' explicitly returns a value
"""
# This will return False for functions where all explicit
# returns are of the form "return" or "return None". The
# latter is a false negative, but cannot be avoided with
# bytecode inspection.
# XXX: This will give a false positive for functions
# that only contain "return None" paths for
# returning a value.
if not isinstance(func.__code__, types.CodeType):
return True
prev = None
for inst in dis.get_instructions(func):
if inst.opname == "RETURN_VALUE":
assert prev is not None
if prev.opname == "LOAD_CONST" and prev.arg != 0:
return True
elif prev.opname != "LOAD_CONST":
return True
elif inst.opname == "RETURN_CONST" and inst.arg != 0:
# New in Python 3.12.
return True
prev = inst
return False
def default_selector(name):
"""
Returns a byte string with the selector for 'name',
or None if there is no default selector
"""
if name.endswith("__") and keyword.iskeyword(name[:-2]):
name = name[:-2]
if name.startswith("__") and name.endswith("__"):
return None
if "_" in name[1:] and not name.endswith("_"):
return None
value = name.replace("_", ":").encode()
if value.startswith(b":"):
value = b"_" + value[1:]
return value
class objc_method:
"""
Decorator that marks a method that should definitely
be represented as an Objective-C selector
This decorator can be used in two ways:
- Without parens:
@objc_method
def someMethod(self): ...
- With parens:
@objc_method()
def someMethod(self): ...
In the second form a number of keyword-only arguments can
be used:
- selname [bytes]: Objective-C selector name for the method
- signature [bytes]: Objective-C type encoding for the method
- isclass [bool]: Determines if the value is a class method
When these attributes are not provided they will be calculated
from the context (name, callable, class to which this
method is added)
"""
__slots__ = ("__wrapped__", "selector", "signature", "isclass")
def __init__(self, value=None, *, selector=None, signature=None, isclass=None):
self.__wrapped__ = value
if self.__wrapped__ is not None:
if not callable(self.__wrapped__) and not isinstance(
self.__wrapped__, classmethod
):
raise TypeError("'value' is not a callable")
self.selector = selector
self.signature = signature
self.isclass = isclass
def __call__(self, *args, **kwds):
if self.__wrapped__ is None:
if len(kwds) != 0:
raise TypeError("Unexpected keyword arguments")
if len(args) != 1:
raise TypeError("Expecting exactly 1 argument")
self.__wrapped__ = args[0]
if not callable(self.__wrapped__) and not isinstance(
self.__wrapped__, classmethod
):
raise TypeError("'value' is not a callable")
return self
return self.__wrapped__(*args, **kwds)
class python_method:
"""
Decorator that marks a method that should definitely
not be represented as an Objective-C selector
This decorator can be used in two ways:
- Without parens:
@python_method
def someMethod(self): ...
- With parens:
@python_method()
def someMethod(self): ...
The second form has no keyword arguments and is
provided for compatibility with objc_method.
"""
__slots__ = ("__wrapped__",)
def __init__(self, value=None):
self.__wrapped__ = value
if self.__wrapped__ is not None:
if not callable(self.__wrapped__) and not isinstance(
self.__wrapped__, classmethod
):
raise TypeError(f"{value!r} is not a callable")
def __get__(self, instance, owner):
return self.__wrapped__.__get__(instance, owner)
def __call__(self, *args, **kwds):
if self.__wrapped__ is None:
if len(kwds) != 0:
raise TypeError("Unexpected keyword arguments")
if len(args) != 1:
raise TypeError("Expecting exactly 1 argument")
self.__wrapped__ = args[0]
if not callable(self.__wrapped__) and not isinstance(
self.__wrapped__, classmethod
):
raise TypeError("'value' is not a callable")
return self
return self.__wrapped__(*args, **kwds)
@property
def callable(self): # noqa: A003
# Compatibility with older version of PyObjC
warnings.warn(
"python_method.callable is deprecated, use __wrapped__ instead",
DeprecationWarning,
stacklevel=2,
)
return self.__wrapped__
objc.options._transformAttribute = transformAttribute
objc.options._processClassDict = processClassDict