Commits

Ronald Oussoren  committed 5261fd1

Improvements to the struct wrappers

* Add _asdict(), _replace() and _fields to struct wrappers
These mirror the namedtuple API. The long term goal is to make
struct wrappers immutable, these new additions allow experimentation
w.r.t. the feasability of that goal and provide a smooth migration
path.

* Improve documentation for createStructType

  • Participants
  • Parent commits fe0458c

Comments (0)

Files changed (4)

File pyobjc-core/Doc/lib/module-objc.rst

    * *pack* can be used to specify the value of "#pragma pack" for the structure
      (default is to use the default platform packing for structures).
 
+
+   The created type behaves itself simular to a mutable :func:`namedtuple <collections.namedtuple>`,
+   that is items can be accessed both using attribute access and using the sequence interface.
+
+   An example::
+
+      Point = objc.createStructType("Point", b"{Point=dd}", ["x", "y"])
+
+      p = Point(3.0, 4.0)
+
+      # Set the X field in two ways:
+      p.x = 5
+      p[0] = 6
+
+   The generated type als has a number of methods:
+
+   * *_asdict()*:  Returns a dict that maps from field names to attribute values
+
+   * *_replace(**kwds)*: Return a copy of the struct and replace attribute values with values from the keyword arguments
+
+   * *copy()*: Return a copy of the struct. If an attribute is another struct that attribute gets copied as well, other attributes
+     are not copied. That is, struct types are deep copied other types are shallow copied.
+
+   And the following attributes are present:
+
+   * *_fields*: A list of field names
+
+   * *__typestr__*: The Objective-C type encoding for the struct (without embedded field names)
+
+
    .. versionchanged:: 2.5
       The function creates a class method on :class:`objc.ivar`.
 
+   .. versionchanged:: 2.5
+      The type now implements the "_asdict" and "_replace" methods that
+      are also present on :func:`collections.namedtuple` types. The
+      attribute "_fields" was added as well.
+
 .. function:: registerStructAlias(typestr, structType)
 
    Tell the brige that structures with encoding *typestr* should also be 

File pyobjc-core/Modules/objc/struct-wrapper.m

 }
 
 static PyObject*
+struct_replace(PyObject* self, PyObject* args, PyObject* kwds)
+{
+	/* XXX: This is a fairly inefficient implementation, first
+	 * perform a deep copy, then replace attributes. The deep copy
+	 * provides the nicest transition path to read-only structs: 
+	 * the result of _replace is completely independent of the original.
+	 */
+	PyObject* result;
+	Py_ssize_t pos = 0;
+	PyObject* key;
+	PyObject* value;
+
+	if (args && PySequence_Length(args) != 0) {
+		PyErr_SetString(PyExc_TypeError,
+		 	"_replace called with positional arguments");
+		return NULL;
+	}
+
+	result = struct_copy(self);
+	if (result == NULL) {
+		return NULL;
+	}
+
+	while (PyDict_Next(kwds, &pos, &key, &value)) {
+		int r = PyObject_SetAttr(result, key, value);
+		if (r == -1) {
+			Py_DECREF(result);
+			return NULL;
+		}
+	}
+
+	return result;
+}
+
+static PyObject*
+struct_asdict(PyObject* self)
+{
+	PyObject* result;
+	PyMemberDef* member = Py_TYPE(self)->tp_members;
+	int r;
+	
+	result = PyDict_New();
+	if (result == NULL) {
+		return NULL;
+	}
+
+	while (member && member->name) {
+		if (member->type != T_OBJECT) {
+			member++;
+			continue;
+		}
+		PyObject* t = GET_FIELD(self, member);
+		r = PyDict_SetItemString(result, member->name, t);
+		if (r == -1) {
+			Py_DECREF(result);
+			return NULL;
+		}
+		member++;
+	}
+
+	return result;
+}
+
+static PyObject*
 struct_mp_subscript(PyObject* self, PyObject* item)
 {
 	if (PyIndex_Check(item)) {
 		METH_NOARGS,
 		NULL,
 	},
+	/* NamedTuple interface */
+	{
+		"_asdict", 
+		(PyCFunction)struct_asdict,
+		METH_NOARGS, 
+		NULL
+	}, 
+	{
+		"_replace", 
+		(PyCFunction)struct_replace,
+		METH_KEYWORDS, 
+		NULL
+	}, 
 	{ NULL, NULL, 0, NULL }
 };
 
 {
 	struct StructTypeObject* result;
 	PyMemberDef* members;
+	PyObject* fields;
 	Py_ssize_t i;
 
+	fields = PyTuple_New(numFields);
+	if (fields == NULL) {
+		return NULL;
+	}
+
 	members = PyMem_Malloc(sizeof(PyMemberDef) * (numFields+1));
 	if (members == NULL) {
+		Py_DECREF(fields);
 		PyErr_NoMemory();
 		return NULL;
 	}
 	for (i = 0; i < numFields; i++) {
+		PyObject* nm = PyText_FromString(fieldnames[i]);
+		if (nm == NULL) {
+			Py_DECREF(fields);
+			PyMem_Free(members);
+			return NULL;
+		}
+		PyTuple_SET_ITEM(fields, i, nm); nm = NULL;
 		members[i].name = (char*)fieldnames[i];
 		members[i].type = T_OBJECT;
 		members[i].offset = sizeof(PyObject) + i*sizeof(PyObject*);
 		members[i].flags = 0; /* A read-write field */
 		members[i].doc = NULL;
+		
 	}
 	members[numFields].name = NULL;
 
 	result = PyMem_Malloc(sizeof(struct StructTypeObject));
 	if (result == NULL) {
+		Py_DECREF(fields);
 		PyMem_Free(members);
 		PyErr_NoMemory();
 		return NULL;
 	result->base.tp_doc = (char*)doc;
 	result->base.tp_dict = PyDict_New();
 	if (result->base.tp_dict == NULL) {
+		Py_DECREF(fields);
 		PyMem_Free(members);
 		PyMem_Free(result);
 		return NULL;
 	Py_REFCNT(result) = 1;
 	result->base.tp_members = members;
 	result->base.tp_basicsize = sizeof(PyObject) + numFields*sizeof(PyObject*);
+	if (PyDict_SetItemString(result->base.tp_dict, "_fields", fields)==-1){
+		Py_DECREF(fields);
+		PyMem_Free(members);
+		PyMem_Free(result);
+		return NULL;
+	}
+	Py_CLEAR(fields);
+
 	if (tpinit) {
 		result->base.tp_init = tpinit;
 	} else {
 		}
 	}
 
+	/* XXX: Add _fields to tp_dict (NamedTuple interface */
+
 	result->pack = pack;
 
 	if (PyType_Ready((PyTypeObject*)result) == -1) {

File pyobjc-core/NEWS.txt

 - :func:`objc.accessor` and :func:`objc.typedAccessor` didn't support the entire
   set of KVC accessors.
 
+- Add methods "_asdict" and "_replace" and field "_fields" to the struct wrapper
+  types. These new attributes mirror the :class:`collections.namedtuple` interface.
+
+  .. note::
+
+     In the long run I'd like to make struct wrappers immutable to allow using
+     them as dictionary keys. This is a first step in that direction and makes
+     it possible to verify that immutable struct wrappers are useable.
+
 - Added :func:`objc.createStructAlias`, and deprecated 
   :func:`objc.registerStructAlias`. The new function has a "name" argument
   and can register types with the :class:`objc.ivar` type (see previous item)

File pyobjc-core/PyObjCTest/test_structs.py

         self.assertIsInstance(tp, type)
         self.assertEqual(tp.__typestr__, b"{_FooStruct=ffff}")
 
+        self.assertEqual(tp._fields, ("a", "b", "c", "d"))
+
         o = tp()
         self.assertHasAttr(o, 'a')
         self.assertHasAttr(o, 'b')
         self.assertHasAttr(o, 'c')
         self.assertHasAttr(o, 'd')
 
+
+
+    def testNamedTupleAPI(self):
+        Point = objc.createStructType("OCPoint", b"{_OCPoint=dd}", ["x", "y"])
+        Line  = objc.createStructType("OCLine",  b"{_OCLine={_OCPoint=dd}{_OCPoint=dd}}d", ["start", "stop", "width"])
+
+        self.assertEqual(Point._fields, ("x", "y"))
+        self.assertEqual(Line._fields, ("start", "stop", "width"))
+
+        p = Point(3, 4)
+        self.assertEqual(p.x, 3.0)
+        self.assertEqual(p.y, 4.0)
+
+        self.assertEqual(p._asdict(), {"x": 3.0, "y": 4.0})
+
+        p2 = p._replace(y=5)
+        self.assertEqual(p.x, 3.0)
+        self.assertEqual(p.y, 4.0)
+        self.assertEqual(p2.x, 3.0)
+        self.assertEqual(p2.y, 5)
+
+        l = Line(Point(1,2), Point(8,9), 7)
+        self.assertEqual(l.start.x, 1.0)
+        self.assertEqual(l.start.y, 2.0)
+        self.assertEqual(l.stop.x, 8.0)
+        self.assertEqual(l.stop.y, 9.0)
+        self.assertEqual(l.width, 7.0)
+
+        self.assertEqual(l._asdict(), 
+            {"start": Point(1,2), "stop":Point(8,9), "width": 7.0})
+
+        l2 = l._replace(stop=Point(3,4), width=0.5)
+        self.assertEqual(l.start.x, 1.0)
+        self.assertEqual(l.start.y, 2.0)
+        self.assertEqual(l.stop.x, 8.0)
+        self.assertEqual(l.stop.y, 9.0)
+        self.assertEqual(l.width, 7.0)
+
+        self.assertEqual(l2.start.x, 1.0)
+        self.assertEqual(l2.start.y, 2.0)
+        self.assertEqual(l2.stop.x, 3.0)
+        self.assertEqual(l2.stop.y, 4.0)
+        self.assertEqual(l2.width, 0.5)
+    
+
     def testCreateImplicit(self):
         tp = objc.createStructType("BarStruct", b'{_BarStruct="e"f"f"f"g"f"h"f}', None)
         self.assertIsInstance(tp, type)
         self.assertHasAttr(o, 'g')
         self.assertHasAttr(o, 'h')
 
+        self.assertEqual(tp._fields, ("e", "f", "g", "h"))
+
         self.assertRaises(ValueError, objc.createStructType, "Foo2", b'{_Foo=f"a"}', None) 
         self.assertRaises(ValueError, objc.createStructType, "Foo3", b'{_Foo="a"f', None) 
         self.assertRaises(ValueError, objc.createStructType, "Foo4", b'^{_Foo="a"f}', None)