From 1d005a6d0dc5008a8936e8072a496e2cd9be29fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:55:01 +0000 Subject: [PATCH 1/2] feat: implement ObjCMetaclass and make ObjCClass an instance of it Agent-Logs-Url: https://github.com/qqfunc/objctypes/sessions/69a21f61-1d7d-42d9-94c6-231dd58f160c Co-authored-by: qqfunc <148628110+qqfunc@users.noreply.github.com> --- csrc/objcclass.c | 2 +- csrc/objcmetaclass.c | 259 ++++++++++++++++++++++++++++++++++++ csrc/objcmetaclass.h | 21 +++ csrc/objctypes.c | 28 +++- csrc/objctypes.h | 4 + csrc/objctypes_cache.cc | 43 ++++++ csrc/objctypes_cache.h | 42 ++++++ csrc/objctypes_module.h | 20 +++ src/objctypes/__init__.pyi | 42 +++++- tests/test_objcmetaclass.py | 134 +++++++++++++++++++ 10 files changed, 591 insertions(+), 4 deletions(-) create mode 100644 csrc/objcmetaclass.c create mode 100644 csrc/objcmetaclass.h create mode 100644 tests/test_objcmetaclass.py diff --git a/csrc/objcclass.c b/csrc/objcclass.c index 8866cb0..e3b22ad 100644 --- a/csrc/objcclass.c +++ b/csrc/objcclass.c @@ -222,7 +222,7 @@ ObjCClass_from_address(PyTypeObject *type, PyObject *address) if (class_isMetaClass(cls)) { PyErr_Format(PyExc_TypeError, "The Objective-C class at %p is a metaclass. Use " - "ObjCMetaClass.from_address() instead.", + "ObjCMetaclass.from_address() instead.", cls); return NULL; } diff --git a/csrc/objcmetaclass.c b/csrc/objcmetaclass.c new file mode 100644 index 0000000..d3139b3 --- /dev/null +++ b/csrc/objcmetaclass.c @@ -0,0 +1,259 @@ +/** + * @file objcmetaclass.c + * @brief Source declarations and definitions for objcmetaclass.c. + */ + +#include + +#include "objcmetaclass.h" + +#include "objctypes.h" +#include "objctypes_cache.h" +#include "objctypes_module.h" + +/// @brief Destruct an ObjCMetaclass. +static void +ObjCMetaclass_dealloc(PyObject *self) +{ + PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &objctypes_module); + if (module != NULL) { + objctypes_state *state = PyModule_GetState(module); + ObjCMetaclassState *cls_state = + PyObject_GetTypeData(self, state->ObjCMetaclass_Type); + if (cls_state != NULL) { + PyMutex_Lock(&state->ObjCMetaclass_cache_mutex); + ObjCMetaclass_cache_del(module, cls_state->value); + PyMutex_Unlock(&state->ObjCMetaclass_cache_mutex); + } + } + Py_TYPE(self)->tp_free((PyObject *)self); +} + +/// @brief `ObjCMetaclass.__repr__()` +static PyObject * +ObjCMetaclass_repr(PyObject *self) +{ + PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &objctypes_module); + if (module == NULL) { + return NULL; + } + + objctypes_state *state = PyModule_GetState(module); + ObjCMetaclassState *cls_state = + PyObject_GetTypeData(self, state->ObjCMetaclass_Type); + + if (cls_state->value == NULL) { + return PyUnicode_FromString(""); + } + return PyUnicode_FromFormat("", + class_getName(cls_state->value)); +} + +/// @brief `ObjCMetaclass.address` +static PyObject * +ObjCMetaclass_address(PyObject *self, void *Py_UNUSED(closure)) +{ + PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &objctypes_module); + if (module == NULL) { + return NULL; + } + + objctypes_state *state = PyModule_GetState(module); + ObjCMetaclassState *cls_state = + PyObject_GetTypeData(self, state->ObjCMetaclass_Type); + + return PyLong_FromVoidPtr(cls_state->value); +} + +/// @brief `ObjCMetaclass.name` +static PyObject * +ObjCMetaclass_name(PyObject *self, PyObject *Py_UNUSED(closure)) +{ + PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &objctypes_module); + if (module == NULL) { + return NULL; + } + + objctypes_state *state = PyModule_GetState(module); + ObjCMetaclassState *cls_state = + PyObject_GetTypeData(self, state->ObjCMetaclass_Type); + + if (cls_state->value == NULL) { + return Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + return PyUnicode_FromString(class_getName(cls_state->value)); +} + +/// @brief Get an ObjCMetaclass from a Python type and an Objective-C +/// metaclass. +static PyObject * +_ObjCMetaclass_FromClass(PyTypeObject *type, Class cls, int lock_cache) +{ + PyObject *module = PyType_GetModuleByDef(type, &objctypes_module); + if (module == NULL) { + return NULL; + } + + objctypes_state *state = PyModule_GetState(module); + + if (lock_cache) { + PyMutex_Lock(&state->ObjCMetaclass_cache_mutex); + } + + PyObject *self = ObjCMetaclass_cache_get(module, cls); + + if (self == NULL) { + PyObject *args = Py_BuildValue("(s(O){})", class_getName(cls), + &PyBaseObject_Type); + PyObject *kwds = PyDict_New(); + self = PyType_Type.tp_new(type, args, kwds); + Py_XDECREF(args); + Py_XDECREF(kwds); + + if (self != NULL) { + ObjCMetaclassState *cls_state = + PyObject_GetTypeData(self, state->ObjCMetaclass_Type); + cls_state->value = cls; + ObjCMetaclass_cache_set(module, cls, self); + } + } + + if (lock_cache) { + PyMutex_Unlock(&state->ObjCMetaclass_cache_mutex); + } + + return self; +} + +/// @brief `ObjCMetaclass.from_address()` +static PyObject * +ObjCMetaclass_from_address(PyTypeObject *type, PyObject *address) +{ + if (!PyLong_Check(address)) { + PyErr_Format( + PyExc_TypeError, + "ObjCMetaclass.from_address() argument 1 must be int, not %T", + address); + return NULL; + } + + Class cls = PyLong_AsVoidPtr(address); + + // Make sure the metaclass is not Nil. + if (cls == NULL) { + PyErr_SetString(PyExc_TypeError, + "the specified Objective-C metaclass is Nil"); + return NULL; + } + + // Make sure the pointer refers to an Objective-C class. + if (!object_isClass((id)cls)) { + PyErr_Format(PyExc_TypeError, + "The Objective-C object at %p is not a class.", + cls); + return NULL; + } + + // Make sure the class is a metaclass. + if (!class_isMetaClass(cls)) { + PyErr_Format(PyExc_TypeError, + "The Objective-C class at %p is not a metaclass. Use " + "ObjCClass.from_address() instead.", + cls); + return NULL; + } + + return _ObjCMetaclass_FromClass(type, cls, 1); +} + +/// @brief `ObjCMetaclass.from_name()` +static PyObject * +ObjCMetaclass_from_name(PyTypeObject *type, PyObject *name) +{ + if (!PyUnicode_Check(name)) { + PyErr_Format( + PyExc_TypeError, + "ObjCMetaclass.from_name() argument 1 must be str, not %T", + name); + return NULL; + } + + PyObject *module = PyType_GetModuleByDef(type, &objctypes_module); + if (module == NULL) { + return NULL; + } + + const char *cls_name = PyUnicode_AsUTF8(name); + + Class cls = objc_getMetaClass(cls_name); + if (cls == NULL) { + PyErr_Format(PyExc_NameError, "Objective-C class '%s' is not defined", + cls_name); + return NULL; + } + + return _ObjCMetaclass_FromClass(type, cls, 1); +} + +static PyMethodDef ObjCMetaclass_methods[] = { + { + "from_address", + (PyCFunction)ObjCMetaclass_from_address, + METH_O | METH_CLASS, + PyDoc_STR("Get an ObjCMetaclass from the memory address."), + }, + { + "from_name", + (PyCFunction)ObjCMetaclass_from_name, + METH_O | METH_CLASS, + PyDoc_STR("Get an ObjCMetaclass from the class name."), + }, + {.ml_name = NULL}, +}; + +static PyGetSetDef ObjCMetaclass_getset[] = { + { + "address", + (getter)ObjCMetaclass_address, + NULL, + PyDoc_STR("The address of the Objective-C metaclass."), + NULL, + }, + { + "name", + (getter)ObjCMetaclass_name, + NULL, + PyDoc_STR("The name of the Objective-C metaclass."), + NULL, + }, + {.name = NULL}, +}; + +static PyType_Slot ObjCMetaclass_slots[] = { + {Py_tp_dealloc, ObjCMetaclass_dealloc}, + {Py_tp_repr, ObjCMetaclass_repr}, + {Py_tp_doc, "Python wrapper for Objective-C metaclass."}, + {Py_tp_methods, ObjCMetaclass_methods}, + {Py_tp_getset, ObjCMetaclass_getset}, + {0, NULL}, +}; + +PyType_Spec ObjCMetaclass_spec = { + .name = "objctypes.ObjCMetaclass", + .basicsize = -(long)sizeof(ObjCMetaclassState), + .itemsize = 0, + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS, + .slots = ObjCMetaclass_slots, +}; + +PyObject * +ObjCMetaclass_FromClass(PyObject *module, Class cls) +{ + objctypes_state *state = PyModule_GetState(module); + if (state->ObjCMetaclass_Type == NULL) { + return NULL; + } + + return _ObjCMetaclass_FromClass( + (PyTypeObject *)state->ObjCMetaclass_Type, cls, 1); +} diff --git a/csrc/objcmetaclass.h b/csrc/objcmetaclass.h new file mode 100644 index 0000000..dffba1a --- /dev/null +++ b/csrc/objcmetaclass.h @@ -0,0 +1,21 @@ +/** + * @file objcmetaclass.h + * @brief Source declarations and definitions for objcmetaclass.h. + */ + +#ifndef OBJCMETACLASS_H +#define OBJCMETACLASS_H + +#include + +#include + +/// ObjCMetaclass + +typedef struct { + Class value; +} ObjCMetaclassState; + +extern PyType_Spec ObjCMetaclass_spec; + +#endif // OBJCMETACLASS_H diff --git a/csrc/objctypes.c b/csrc/objctypes.c index 72a0d9b..8fb4465 100644 --- a/csrc/objctypes.c +++ b/csrc/objctypes.c @@ -9,6 +9,7 @@ #include "objcbool.h" #include "objcclass.h" +#include "objcmetaclass.h" #include "objcmethod.h" #include "objcobject.h" #include "objcselector.h" @@ -40,8 +41,22 @@ objctypes_module_exec(PyObject *module) state->ObjCSelector_cache_mutex = (PyMutex){0}; - state->ObjCClass_Type = (PyTypeObject *)PyType_FromModuleAndSpec( - module, &ObjCClass_spec, (PyObject *)&PyType_Type); + state->ObjCMetaclass_Type = (PyTypeObject *)PyType_FromModuleAndSpec( + module, &ObjCMetaclass_spec, (PyObject *)&PyType_Type); + if (state->ObjCMetaclass_Type == NULL) { + return -1; + } + + ObjCMetaclass_cache_init(module); + if (state->ObjCMetaclass_cache == NULL) { + return -1; + } + + state->ObjCMetaclass_cache_mutex = (PyMutex){0}; + + state->ObjCClass_Type = (PyTypeObject *)PyType_FromMetaclass( + state->ObjCMetaclass_Type, module, &ObjCClass_spec, + (PyObject *)&PyType_Type); if (state->ObjCClass_Type == NULL) { return -1; } @@ -84,6 +99,12 @@ objctypes_module_exec(PyObject *module) return -1; } + // Add ObjCMetaclass + if (PyModule_AddType(module, (PyTypeObject *)state->ObjCMetaclass_Type) + < 0) { + return -1; + } + // Add ObjCClass if (PyModule_AddType(module, (PyTypeObject *)state->ObjCClass_Type) < 0) { return -1; @@ -128,6 +149,7 @@ objctypes_module_traverse(PyObject *module, visitproc visit, void *arg) Py_VISIT(state->ObjCBool_YES); Py_VISIT(state->ObjCBool_NO); Py_VISIT(state->ObjCClass_Type); + Py_VISIT(state->ObjCMetaclass_Type); Py_VISIT(state->ObjCMethod_Type); Py_VISIT(state->ObjCObject_Type); Py_VISIT(state->ObjCSelector_Type); @@ -142,6 +164,7 @@ objctypes_module_clear(PyObject *module) Py_CLEAR(state->ObjCBool_YES); Py_CLEAR(state->ObjCBool_NO); Py_CLEAR(state->ObjCClass_Type); + Py_CLEAR(state->ObjCMetaclass_Type); Py_CLEAR(state->ObjCMethod_Type); Py_CLEAR(state->ObjCObject_Type); Py_CLEAR(state->ObjCSelector_Type); @@ -152,6 +175,7 @@ static void objctypes_module_free(void *module) { ObjCClass_cache_deinit(module); + ObjCMetaclass_cache_deinit(module); ObjCMethod_cache_deinit(module); ObjCObject_cache_deinit(module); ObjCSelector_cache_deinit(module); diff --git a/csrc/objctypes.h b/csrc/objctypes.h index 6309ae6..f7d593e 100644 --- a/csrc/objctypes.h +++ b/csrc/objctypes.h @@ -15,6 +15,10 @@ PyObject * ObjCClass_FromClass(PyObject *module, Class cls); +/// Get an ObjCMetaclass from an Objective-C metaclass. +PyObject * +ObjCMetaclass_FromClass(PyObject *module, Class cls); + /// Get an ObjCObject from an Objective-C id. PyObject * ObjCObject_FromId(PyObject *module, id obj); diff --git a/csrc/objctypes_cache.cc b/csrc/objctypes_cache.cc index 22cfd1c..df0ae32 100644 --- a/csrc/objctypes_cache.cc +++ b/csrc/objctypes_cache.cc @@ -16,6 +16,49 @@ typedef std::map cache_map; +void +ObjCMetaclass_cache_init(PyObject *module) +{ + objctypes_state *state = (objctypes_state *)PyModule_GetState(module); + state->ObjCMetaclass_cache = new (std::nothrow) cache_map(); +} + +void +ObjCMetaclass_cache_deinit(PyObject *module) +{ + objctypes_state *state = (objctypes_state *)PyModule_GetState(module); + delete (cache_map *)state->ObjCMetaclass_cache; +} + +PyObject * +ObjCMetaclass_cache_get(PyObject *module, Class cls) +{ + objctypes_state *state = (objctypes_state *)PyModule_GetState(module); + cache_map *cache = (cache_map *)state->ObjCMetaclass_cache; + + const auto it = cache->find(cls); + if (it != cache->end()) { + return Py_NewRef(it->second); + } + return NULL; +} + +void +ObjCMetaclass_cache_set(PyObject *module, Class cls, PyObject *obj) +{ + objctypes_state *state = (objctypes_state *)PyModule_GetState(module); + cache_map *cache = (cache_map *)state->ObjCMetaclass_cache; + (*cache)[cls] = (PyObject *)obj; +} + +void +ObjCMetaclass_cache_del(PyObject *module, Class cls) +{ + objctypes_state *state = (objctypes_state *)PyModule_GetState(module); + cache_map *cache = (cache_map *)state->ObjCMetaclass_cache; + cache->erase(cls); +} + void ObjCClass_cache_init(PyObject *module) { diff --git a/csrc/objctypes_cache.h b/csrc/objctypes_cache.h index 2d441d1..4c9e6c7 100644 --- a/csrc/objctypes_cache.h +++ b/csrc/objctypes_cache.h @@ -18,6 +18,48 @@ extern "C" { #endif +/** + * @brief Initialize a cache map for `ObjCMetaclass`. + * @param module The Python module object. + */ +void +ObjCMetaclass_cache_init(PyObject *module); + +/** + * @brief Deinitialize the cache map for `ObjCMetaclass`. + * @param module The Python module object. + */ +void +ObjCMetaclass_cache_deinit(PyObject *module); + +/** + * @brief Get an `ObjCMetaclass` from the cache if it exists, otherwise return + * `NULL`. + * @param module The Python module object. + * @param cls The Objective-C metaclass to look up. + * @return A new reference to the cached `ObjCMetaclass` object, or `NULL` if + * not found. + */ +PyObject * +ObjCMetaclass_cache_get(PyObject *module, Class cls); + +/** + * @brief Set an `ObjCMetaclass` in the cache. + * @param module The Python module object. + * @param cls The Objective-C metaclass to cache. + * @param obj The `ObjCMetaclass` to associate with the metaclass. + */ +void +ObjCMetaclass_cache_set(PyObject *module, Class cls, PyObject *obj); + +/** + * @brief Delete an `ObjCMetaclass` from the cache. + * @param module The Python module object. + * @param cls The Objective-C metaclass to delete from the cache. + */ +void +ObjCMetaclass_cache_del(PyObject *module, Class cls); + /** * @brief Initialize a cache map for `ObjCClass`. * @param module The Python module object. diff --git a/csrc/objctypes_module.h b/csrc/objctypes_module.h index c4b032d..6a9afef 100644 --- a/csrc/objctypes_module.h +++ b/csrc/objctypes_module.h @@ -27,6 +27,26 @@ typedef struct { */ PyObject *ObjCBool_NO; + /// @brief The `ObjCMetaclass` type. + PyTypeObject *ObjCMetaclass_Type; + + /** + * @brief Cache for `ObjCMetaclass` instances. + * @details This is a pointer to a C++ `std::map` that maps Objective-C + * metaclass pointers to their corresponding `ObjCMetaclass` Python + * objects. + * @warning Do not manipulate this field outside of the `ObjCMetaclass` + * type. + */ + void *ObjCMetaclass_cache; + + /** + * @brief Mutex for synchronizing access to the `ObjCMetaclass` cache. + * @warning Do not manipulate this field outside of the `ObjCMetaclass` + * type. + */ + PyMutex ObjCMetaclass_cache_mutex; + /// @brief The `ObjCClass` type. PyTypeObject *ObjCClass_Type; diff --git a/src/objctypes/__init__.pyi b/src/objctypes/__init__.pyi index 132dc7e..34c6492 100644 --- a/src/objctypes/__init__.pyi +++ b/src/objctypes/__init__.pyi @@ -2,7 +2,47 @@ from types import GenericAlias from typing import Any, Self, final # @final # NOTE: final? -class ObjCClass(type): # NOTE: type? +class ObjCMetaclass(type): + """A Python wrapper class for an Objective-C metaclass. + + Equivalent to the metaclass of an Objective-C class, which describes + the class object itself (e.g. class methods). + """ + + @classmethod + def from_address(cls, address: int, /) -> ObjCMetaclass: + """Retrieve an Objective-C metaclass from the specified address. + + :param address: The address of the Objective-C metaclass. + :return: The Objective-C metaclass that was retrieved. + :raises TypeError: if the address points to a non-metaclass + Objective-C object or regular class + + .. warning:: + Passing an invalid address may cause crashes. + """ + + @classmethod + def from_name(cls, name: str, /) -> ObjCMetaclass: + """Retrieve an Objective-C metaclass by class name. + + :param name: The name of the Objective-C class whose metaclass + to retrieve. + :return: The Objective-C metaclass that was retrieved. + :raises NameError: if the specified name does not correspond to + any Objective-C class + """ + + @property + def address(cls) -> int: + """The address of the Objective-C metaclass.""" + + @property + def name(cls) -> str: + """The name of the Objective-C metaclass.""" + +# @final # NOTE: final? +class ObjCClass(type, metaclass=ObjCMetaclass): # NOTE: type? """A Python wrapper class for an Objective-C class. Equivalent to diff --git a/tests/test_objcmetaclass.py b/tests/test_objcmetaclass.py new file mode 100644 index 0000000..8d349c0 --- /dev/null +++ b/tests/test_objcmetaclass.py @@ -0,0 +1,134 @@ +"""Test functions for ObjCMetaclass.""" + +import pytest + +from objctypes import ObjCClass, ObjCMetaclass + + +def test_objcmetaclass_is_metaclass_of_objcclass() -> None: + """Test that ObjCClass is an instance of ObjCMetaclass.""" + assert type(ObjCClass) is ObjCMetaclass + + +def test_objcmetaclass_inherits_type() -> None: + """Test that ObjCMetaclass inherits from type.""" + assert issubclass(ObjCMetaclass, type) + + +def test_objcmetaclass_from_name() -> None: + """Test ObjCMetaclass.from_name().""" + ObjCMetaclass.from_name("NSObject") + ObjCMetaclass.from_name("NSString") + ObjCMetaclass.from_name("NSNumber") + + with pytest.raises(NameError) as excinfo: + ObjCMetaclass.from_name("NonexistentClass") + + assert ( + str(excinfo.value) + == "Objective-C class 'NonexistentClass' is not defined" + ) + + +def test_objcmetaclass_doc() -> None: + """Test docstring of ObjCMetaclass.""" + assert ObjCMetaclass.__doc__ is not None + + +def test_objcmetaclass_repr() -> None: + """Test ObjCMetaclass.__repr__().""" + NSObjectMeta = ObjCMetaclass.from_name("NSObject") # noqa: N806 + NSStringMeta = ObjCMetaclass.from_name("NSString") # noqa: N806 + NSNumberMeta = ObjCMetaclass.from_name("NSNumber") # noqa: N806 + + assert repr(NSObjectMeta) == "" + assert repr(NSStringMeta) == "" + assert repr(NSNumberMeta) == "" + + +def test_objcmetaclass_cache() -> None: + """Test if ObjCMetaclass objects are cached.""" + NSObjectMeta = ObjCMetaclass.from_name("NSObject") # noqa: N806 + NSStringMeta = ObjCMetaclass.from_name("NSString") # noqa: N806 + NSNumberMeta = ObjCMetaclass.from_name("NSNumber") # noqa: N806 + + assert NSObjectMeta is ObjCMetaclass.from_name("NSObject") + assert NSStringMeta is ObjCMetaclass.from_name("NSString") + assert NSNumberMeta is ObjCMetaclass.from_name("NSNumber") + + assert NSObjectMeta is not NSStringMeta + assert NSStringMeta is not NSNumberMeta + assert NSNumberMeta is not NSObjectMeta + + +def test_objcmetaclass_address() -> None: + """Test ObjCMetaclass.address.""" + NSObjectMeta = ObjCMetaclass.from_name("NSObject") # noqa: N806 + NSStringMeta = ObjCMetaclass.from_name("NSString") # noqa: N806 + NSNumberMeta = ObjCMetaclass.from_name("NSNumber") # noqa: N806 + + assert NSObjectMeta.address == ObjCMetaclass.from_name("NSObject").address + assert NSStringMeta.address == ObjCMetaclass.from_name("NSString").address + assert NSNumberMeta.address == ObjCMetaclass.from_name("NSNumber").address + + assert NSObjectMeta.address != NSStringMeta.address + assert NSStringMeta.address != NSNumberMeta.address + assert NSNumberMeta.address != NSObjectMeta.address + + +def test_objcmetaclass_name() -> None: + """Test ObjCMetaclass.name.""" + NSObjectMeta = ObjCMetaclass.from_name("NSObject") # noqa: N806 + NSStringMeta = ObjCMetaclass.from_name("NSString") # noqa: N806 + NSNumberMeta = ObjCMetaclass.from_name("NSNumber") # noqa: N806 + + assert NSObjectMeta.name == "NSObject" + assert NSStringMeta.name == "NSString" + assert NSNumberMeta.name == "NSNumber" + + +def test_objcmetaclass_from_address() -> None: + """Test ObjCMetaclass.from_address().""" + NSObjectMeta = ObjCMetaclass.from_name("NSObject") # noqa: N806 + NSStringMeta = ObjCMetaclass.from_name("NSString") # noqa: N806 + NSNumberMeta = ObjCMetaclass.from_name("NSNumber") # noqa: N806 + + assert ( + NSObjectMeta.address + == ObjCMetaclass.from_address(NSObjectMeta.address).address + ) + assert ( + NSStringMeta.address + == ObjCMetaclass.from_address(NSStringMeta.address).address + ) + assert ( + NSNumberMeta.address + == ObjCMetaclass.from_address(NSNumberMeta.address).address + ) + + +def test_objcmetaclass_from_address_wrong_arg() -> None: + """Test ObjCMetaclass.from_address() with wrong argument type.""" + with pytest.raises(TypeError) as excinfo: + ObjCMetaclass.from_address("wrong argument") # ty: ignore[invalid-argument-type] + assert ( + str(excinfo.value) + == "ObjCMetaclass.from_address() argument 1 must be int, not str" + ) + + +def test_objcmetaclass_from_address_nil() -> None: + """Test ObjCMetaclass.from_address() with Nil.""" + with pytest.raises(TypeError) as excinfo: + ObjCMetaclass.from_address(0) + assert str(excinfo.value) == "the specified Objective-C metaclass is Nil" + + +def test_objcclass_from_address_rejects_metaclass() -> None: + """Test that ObjCClass.from_address() rejects a metaclass.""" + NSObjectMeta = ObjCMetaclass.from_name("NSObject") # noqa: N806 + + with pytest.raises(TypeError) as excinfo: + ObjCClass.from_address(NSObjectMeta.address) + + assert "ObjCMetaclass.from_address() instead" in str(excinfo.value) From c682533e92f1114a7b0ee32bede17a29cc2e5b4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:57:23 +0000 Subject: [PATCH 2/2] fix: correct header file doc comment and keep ty: ignore style in test Agent-Logs-Url: https://github.com/qqfunc/objctypes/sessions/69a21f61-1d7d-42d9-94c6-231dd58f160c Co-authored-by: qqfunc <148628110+qqfunc@users.noreply.github.com> --- csrc/objcmetaclass.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csrc/objcmetaclass.h b/csrc/objcmetaclass.h index dffba1a..72e63df 100644 --- a/csrc/objcmetaclass.h +++ b/csrc/objcmetaclass.h @@ -1,6 +1,6 @@ /** * @file objcmetaclass.h - * @brief Source declarations and definitions for objcmetaclass.h. + * @brief Source declarations and definitions for objcmetaclass.c. */ #ifndef OBJCMETACLASS_H