Skip to content

Commit

Permalink
Merge branch 'gh-275'
Browse files Browse the repository at this point in the history
This merges the implemention of a generic __new__ into
the master branch, as part of the implementation for #275
  • Loading branch information
ronaldoussoren committed May 8, 2024
2 parents 45ac712 + 8ed363d commit 8a83555
Show file tree
Hide file tree
Showing 106 changed files with 3,062 additions and 120 deletions.
1 change: 1 addition & 0 deletions docs/_templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ <h1>General documentation</h1>
<b>Various notes</b>

<ul>
<li><a href="{{ pathto("core/instantiation") }}">Instantiation Objective-C objects</a>
<li><a href="{{ pathto("core/protocols") }}">Using Objective-C protocols</a>
<li><a href="{{ pathto("core/blocks") }}">Using Objective-C blocks</a>
<li><a href="{{ pathto("core/vector-types") }}">Using Objective-C SIMD types</a>
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ General documentation
metadata/index
tutorials/index
notes/exceptions
notes/instantiation
notes/quartz-vs-coregraphics
notes/using-nsxpcinterface
notes/ctypes
Expand Down
32 changes: 32 additions & 0 deletions docs/metadata/manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,38 @@ Metadata for Objective-C methods and classes

.. versionadded:: 3.0

.. function:: registerUnavailableMethod(classname, selector)

Register the selector (a byte string with the Objective-C name for the selector)
as being unavailable in the named class. Calling this method later will
result in an exception being raised.

This is primairly meant to be used to mirror the effects
of ``NS_UNAVAILABLE`` in Objective-C headers.

.. versionadded: 10.4
.. function:: registerNewKeywordsFromSelector(classname, selector)

Register keywords calculated from *selector* as passible
keyword arguments for ``__new__`` for class *classname*. The
selector should start with "init".

.. versionadded: 10.4
.. function:: registerNewKeywords(classname, keywords, methodname)

Register the keyword tuple *keywords* as a set of keyword
arguments for ``__new__`` for class *classname* that will result
in the invocation the method named *methodname*.

If the *methodname* startswith "init" invocation of ``__new__``
with this tuple of keywords is equivalent to ``classname.alloc().methodname()``,
otherwise it is equivalent to ``classname.methodname()``.

.. versionadded: 10.4
Register proxy types
....................

Expand Down
82 changes: 82 additions & 0 deletions docs/notes/instantiating.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
Instantiating Objective-C objects
=================================


.. versionchanged:: 10.4

The Pythonic interface for instantiation classes
was added.

Objective-C uses a two step object instantatin
process, similar to Python's ``__new__`` and
``__init__`` methods, but with explicit invocation
of the methods ``alloc`` and ``init``. By default
this is mirrored in the Python proxies:

.. sourcecode:: python

some_value = NSObject.alloc().init()

some_button = NSButton.alloc().initWithFrame_(some_frame)


This looks very foreign in Python, therefore PyObjC
also supports a condensed version of this by directly
calling the class::

.. sourcecode:: python

some_value = NSObject()

some_button = NSButton(frame=some_frame)

The generic rules for instantiation objects through calling
the class are:

* Calling the class without arguments in supported unless
the ``init`` method has been marked with ``NS_UNAVAILABLE``
in Apple's SDK.

* Every instance selector of the Objective-C with a name starting
with ``init`` adds a possible set of keyword arguments using
the following algorithm:

1. Strip ``initWith`` or ``init`` from the start of the selector;

2. Lowercase the first character of the result

3. All segments are now keyword only arguments, in that order.

For example given, ``-[SomeClass initWithX:y:z]`` the
following invocation is valid: ``SomeClass(x=1, y=2, z=3)``.
Using the keywords in a different order is not valid.

* Some classes may have additional sets of keyword arguments,
whose will be documented in the framework notes. In general
those will be based on factory class methods for which there
are no equivalent using the ``alloc().init...(...)`` pattern.

For classes in system frameworks the possibly init method are
registered using frmaework data.

For classes implemented in Python the possible init methods
are detected automatically, just implement one or more Objective-C
style init methods to add sets of keyword arguments for ``__new__``
(and don't implement ``__new__`` or ``__init__`` in the Python
class).

Set ``init`` to ``None`` to require using one or more keyword
arguments, that is:

.. sourcecode:: python

class MyObject(NSObject):
init = None # Calling MyOjbect() is not allowed

def initWithX_y_(self, x_value, y_value):
self = super.init()
self.x = x_value
self.y = y_value
return self

value = MyValue(x=42, y=24)
12 changes: 10 additions & 2 deletions pyobjc-core/Lib/PyObjCTools/KeyValueCoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@
import collections.abc
import types

import objc

__all__ = ("getKey", "setKey", "getKeyPath", "setKeyPath")
_null = objc.lookUpClass("NSNull").null()


def keyCaps(s):
Expand Down Expand Up @@ -382,3 +380,13 @@ def __setitem__(self, item, value):
if not isinstance(item, str):
raise TypeError("Keys must be strings")
setKeyPath(self.__pyobjc_object__, item, value)


# The import of 'objc' is at the end of the module
# to avoid problems when importing this module before
# importing objc due to the objc package importing bits
# of this module.

import objc # noqa: E402

_null = objc.lookUpClass("NSNull").null()
1 change: 1 addition & 0 deletions pyobjc-core/Lib/objc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def _update(g):
from . import _callable_docstr # noqa: F401, F403, E402
from . import _pycoder # noqa: F401, F403, E402
from ._informal_protocol import * # noqa: F401, F403, E402
from . import _new # noqa: F401, E402


# Helper function for new-style metadata modules
Expand Down
71 changes: 69 additions & 2 deletions pyobjc-core/Lib/objc/_convenience.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@
selector,
)
import PyObjCTools.KeyValueCoding as kvc

__all__ = ("addConvenienceForClass", "registerABCForClass")
from objc._new import make_generic_new, NEW_MAP
from objc._transform import _selectorToKeywords

__all__ = (
"addConvenienceForClass",
"registerABCForClass",
"registerUnavailableMethod",
"registerNewKeywords",
"registerNewKeywordsFromSelector",
)

CLASS_METHODS = {}
CLASS_ABC = {}
Expand Down Expand Up @@ -54,6 +62,15 @@ class name to a list of Python method names and implementation.
Matching entries from both mappings are added to the 'type_dict'.
"""

# Only add the generic __new__ to pure ObjC classes,
# __new__ will be added to Python subclasses by
# ._transform.
if not cls.__has_python_implementation__ and type( # noqa: E721
cls.__mro__[1].__new__
) != type(lambda: None):
type_dict["__new__"] = make_generic_new(cls)

for nm, value in CLASS_METHODS.get(cls.__name__, ()):
type_dict[nm] = value

Expand All @@ -79,6 +96,56 @@ def bundleForClass(cls):
return selector(bundleForClass, isClassMethod=True)


def registerUnavailableMethod(classname, selector):
"""
Mark *selector* as unavailable for *classname*.
"""
if not isinstance(selector, bytes):
raise TypeError("selector should by a bytes object")
selname = selector.decode()

# This adds None as a replacement value instead of
# registering metadata because NS_UNAVAILABLE is
# used to mark abstract base classes with concrete
# public subclasses.
# addConvenienceForClass(classname, ((selname.replace(":", "_"), None),))
registerMetaDataForSelector(
classname.encode(),
selector,
{"suggestion": f"{selector.decode()!r} is NS_UNAVAILABLE"},
)

if selname.startswith("init"):
kw = _selectorToKeywords(selname)
NEW_MAP.setdefault(classname, {})[kw] = None


def registerNewKeywordsFromSelector(classname, selector):
"""
Register keywords calculated from 'selector' as passible
keyword arguments for __new__ for the given class. The
selector should be an 'init' method.
"""
if not isinstance(selector, bytes):
raise TypeError("selector should by a bytes object")
selname = selector.decode()
kw = _selectorToKeywords(selname)
NEW_MAP.setdefault(classname, {})[kw] = selname.replace(":", "_")


def registerNewKeywords(classname, keywords, methodname):
"""
Register the keyword tuple 'keywords' as a set of keyword
arguments for __new__ for the given class that will result
in the invocation of the given method.
Method should be either an init method or a class method.
"""
if not isinstance(keywords, tuple) or not all(isinstance(x, str) for x in keywords):
raise TypeError("keywords must be tuple of strings")
NEW_MAP.setdefault(classname, {})[keywords] = methodname


def registerABCForClass(classname, *abc_class):
"""
Register *classname* with the *abc_class*-es when
Expand Down
12 changes: 10 additions & 2 deletions pyobjc-core/Lib/objc/_convenience_nsarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from objc._objc import _C_ID, _C_NSInteger
from objc._objc import _NSNotFound as NSNotFound
from objc._objc import lookUpClass, registerMetaDataForSelector
from ._new import NEW_MAP

NSArray = lookUpClass("NSArray")
NSMutableArray = lookUpClass("NSMutableArray")
Expand Down Expand Up @@ -297,6 +298,10 @@ def nsarray_new(cls, sequence=None):
return NSArray.arrayWithArray_(sequence)


for cls in ("NSArray", "__NSArrayI", "__NSArrayM", "__NSArray0"):
NEW_MAP.setdefault(cls, {})[()] = nsarray_new


def nsmutablearray_new(cls, sequence=None):
if not sequence:
return NSMutableArray.array()
Expand All @@ -314,6 +319,11 @@ def nsmutablearray_new(cls, sequence=None):
return NSMutableArray.arrayWithArray_(sequence)


for cls in ("NSMutableArray",):
d = NEW_MAP.setdefault(cls, {})
d[()] = nsmutablearray_new


def nsarray__contains__(self, elem):
return bool(self.containsObject_(container_wrap(elem)))

Expand Down Expand Up @@ -374,7 +384,6 @@ def nsarray__iter__(self):
addConvenienceForClass(
"NSArray",
(
("__new__", staticmethod(nsarray_new)),
("__add__", nsarray_add),
("__radd__", nsarray_radd),
("__mul__", nsarray_mul),
Expand Down Expand Up @@ -412,7 +421,6 @@ def nsmutablearray__copy__(self):
addConvenienceForClass(
"NSMutableArray",
(
("__new__", staticmethod(nsmutablearray_new)),
("__copy__", nsmutablearray__copy__),
("__setitem__", nsarray__setitem__),
("__delitem__", nsarray__delitem__),
Expand Down
6 changes: 5 additions & 1 deletion pyobjc-core/Lib/objc/_convenience_nsdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from objc._convenience import addConvenienceForClass
from objc._objc import registerMetaDataForSelector
from ._new import NEW_MAP
import sys
import operator

Expand Down Expand Up @@ -53,6 +54,10 @@ def nsdata__new__(cls, value=None):
return cls.dataWithBytes_length_(view, len(view))


for cls in ("NSData", "NSMutableData"):
NEW_MAP.setdefault(cls, {})[()] = nsdata__new__


def nsdata__str__(self):
if len(self) == 0:
return str(b"")
Expand Down Expand Up @@ -249,7 +254,6 @@ def nsdata_isascii(self, *args, **kwds):
addConvenienceForClass(
"NSData",
(
("__new__", staticmethod(nsdata__new__)),
("__len__", lambda self: self.length()),
("__str__", nsdata__str__),
("__getitem__", nsdata__getitem__),
Expand Down
4 changes: 3 additions & 1 deletion pyobjc-core/Lib/objc/_convenience_nsdecimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from objc._convenience import addConvenienceForClass
from objc._objc import NSDecimal, lookUpClass
from ._new import NEW_MAP

NSDecimalNumber = lookUpClass("NSDecimalNumber")

Expand Down Expand Up @@ -39,10 +40,11 @@ def decimal_new(cls, value=None):
raise TypeError("Value is not a number")


NEW_MAP.setdefault("NSDecimalNumber", {})[()] = decimal_new

addConvenienceForClass(
"NSDecimalNumber",
(
("__new__", staticmethod(decimal_new)),
(
"__add__",
lambda self, other: NSDecimalNumber(operator.add(NSDecimal(self), other)),
Expand Down
6 changes: 3 additions & 3 deletions pyobjc-core/Lib/objc/_convenience_nsset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from objc._convenience import addConvenienceForClass, container_unwrap, container_wrap
from objc._objc import lookUpClass
from ._new import NEW_MAP

NSSet = lookUpClass("NSSet")
NSMutableSet = lookUpClass("NSMutableSet")
Expand Down Expand Up @@ -342,6 +343,5 @@ def nsmutableset_new(cls, sequence=None):
return value


addConvenienceForClass("NSSet", (("__new__", staticmethod(nsset_new)),))

addConvenienceForClass("NSMutableSet", (("__new__", staticmethod(nsmutableset_new)),))
NEW_MAP.setdefault("NSSet", {})[()] = nsset_new
NEW_MAP.setdefault("NSMutableSet", {})[()] = nsmutableset_new
5 changes: 4 additions & 1 deletion pyobjc-core/Lib/objc/_convenience_nsstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

from objc._convenience import addConvenienceForClass
from ._new import NEW_MAP

__all__ = ()

Expand All @@ -16,12 +17,14 @@ def nsstring_new(cls, value=_no_value):
return cls.alloc().initWithString_(value)


for cls in ("NSString", "NSMutableString"):
NEW_MAP.setdefault(cls, {})[()] = nsstring_new

addConvenienceForClass(
"NSString",
(
("__len__", lambda self: self.length()),
("endswith", lambda self, pfx: self.hasSuffix_(pfx)),
("startswith", lambda self, pfx: self.hasPrefix_(pfx)),
("__new__", staticmethod(nsstring_new)),
),
)
Loading

0 comments on commit 8a83555

Please sign in to comment.