[Python-Dev] Arbitrary attributes on funcs and methods

Barry Warsaw bwarsaw@python.org
Mon, 10 Apr 2000 11:52:43 -0400 (EDT)


A number of people have played FAST and loose with function and method
docstrings, including John Aycock[1], Zope's ORB[2].  Docstrings are
handy because they are the one attribute on funcs and methods that are
easily writable.  But as more people overload the semantics for
docstrings, we'll get collisions.  I've had a number of discussions
with folks about adding attribute dictionaries to functions and
methods so that you can essentially add any attribute.  Namespaces are
one honking great idea -- let's do more of those!

Below is a very raw set of patches to add an attribute dictionary to
funcs and methods.  It's only been minimally tested, but if y'all like
the idea, I'll clean it up, sanity check the memory management, and
post the changes to patches@python.org.  Here's some things you can
do:

-------------------- snip snip --------------------
Python 1.6a2 (#10, Apr 10 2000, 11:27:59)  [GCC 2.8.1] on sunos5
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def a(): pass
... 
>>> a.publish = 1
>>> a.publish
1
>>> a.__doc__
>>> a.__doc__ = 'a doc string'
>>> a.__doc__
'a doc string'
>>> a.magic_string = a.__doc__
>>> a.magic_string
'a doc string'
>>> dir(a)
['__doc__', '__name__', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name', 'magic_string', 'publish']
>>> class F:
...  def a(self): pass
... 
>>> f = F()
>>> f.a.publish
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: publish
>>> f.a.publish = 1
>>> f.a.publish
1
>>> f.a.__doc__
>>> f.a.__doc__ = 'another doc string'
>>> f.a.__doc__
'another doc string'
>>> f.a.magic_string = f.a.__doc__
>>> f.a.magic_string
'another doc string'
>>> dir(f.a)
['__dict__', '__doc__', '__name__', 'im_class', 'im_func', 'im_self', 'magic_string', 'publish']
>>> 
-------------------- snip snip --------------------

-Barry

[1] Aycock, "Compiling Little Languages in Python", http://www.foretec.com/python/workshops/1998-11/proceedings/papers/aycock-little/aycock-little.html

[2] http://classic.zope.org:8080/Documentation/Reference/ORB


P.S. I promised to add a little note about setattr and getattr
vs. setattro and getattro.  There's very little documentation about
the differences, and searching on python.org doesn't seem to turn up
anything.  The differences are simple.  setattr/getattr take a char*
argument naming the attribute to change, while setattro/getattro take
a PyObject* (hence the trailing `o' -- for Object).  This stuff should
get documented in the C API, but at least now, it'll turn up in a SIG
search. :)

-------------------- snip snip --------------------
Index: funcobject.h
===================================================================
RCS file: /projects/cvsroot/python/dist/src/Include/funcobject.h,v
retrieving revision 2.16
diff -c -r2.16 funcobject.h
*** funcobject.h	1998/12/04 18:48:02	2.16
--- funcobject.h	2000/04/07 21:30:40
***************
*** 44,49 ****
--- 44,50 ----
  	PyObject *func_defaults;
  	PyObject *func_doc;
  	PyObject *func_name;
+ 	PyObject *func_dict;
  } PyFunctionObject;
  
  extern DL_IMPORT(PyTypeObject) PyFunction_Type;
Index: classobject.c
===================================================================
RCS file: /projects/cvsroot/python/dist/src/Objects/classobject.c,v
retrieving revision 2.84
diff -c -r2.84 classobject.c
*** classobject.c	2000/04/10 13:03:19	2.84
--- classobject.c	2000/04/10 15:27:15
***************
*** 1550,1577 ****
  	/* Dummies that are not handled by getattr() except for __members__ */
  	{"__doc__",	T_INT,		0},
  	{"__name__",	T_INT,		0},
  	{NULL}	/* Sentinel */
  };
  
  static PyObject *
  instancemethod_getattr(im, name)
  	register PyMethodObject *im;
! 	PyObject *name;
  {
! 	char *sname = PyString_AsString(name);
! 	if (sname[0] == '_') {
  		/* Inherit __name__ and __doc__ from the callable object
  		   implementing the method */
! 	        if (strcmp(sname, "__name__") == 0 ||
! 		    strcmp(sname, "__doc__") == 0)
! 			return PyObject_GetAttr(im->im_func, name);
  	}
  	if (PyEval_GetRestricted()) {
  		PyErr_SetString(PyExc_RuntimeError,
  	    "instance-method attributes not accessible in restricted mode");
  		return NULL;
  	}
! 	return PyMember_Get((char *)im, instancemethod_memberlist, sname);
  }
  
  static void
--- 1550,1608 ----
  	/* Dummies that are not handled by getattr() except for __members__ */
  	{"__doc__",	T_INT,		0},
  	{"__name__",	T_INT,		0},
+ 	{"__dict__",    T_INT,          0},
  	{NULL}	/* Sentinel */
  };
  
+ static int
+ instancemethod_setattr(im, name, v)
+ 	register PyMethodObject *im;
+ 	char *name;
+ 	PyObject *v;
+ {
+ 	int rtn;
+ 
+ 	if (PyEval_GetRestricted() ||
+ 	    strcmp(name, "im_func") == 0 ||
+ 	    strcmp(name, "im_self") == 0 ||
+ 	    strcmp(name, "im_class") == 0)
+ 	{
+ 		PyErr_Format(PyExc_TypeError, "read-only attribute: %s", name);
+ 		return -1;
+ 	}
+ 	return PyObject_SetAttrString(im->im_func, name, v);
+ }
+ 
+ 
  static PyObject *
  instancemethod_getattr(im, name)
  	register PyMethodObject *im;
! 	char *name;
  {
! 	PyObject *rtn;
! 
! 	if (strcmp(name, "__name__") == 0 ||
! 	    strcmp(name, "__doc__") == 0) {
  		/* Inherit __name__ and __doc__ from the callable object
  		   implementing the method */
! 		return PyObject_GetAttrString(im->im_func, name);
  	}
  	if (PyEval_GetRestricted()) {
  		PyErr_SetString(PyExc_RuntimeError,
  	    "instance-method attributes not accessible in restricted mode");
  		return NULL;
+ 	}
+ 	if (strcmp(name, "__dict__") == 0)
+ 		return PyObject_GetAttrString(im->im_func, name);
+ 
+ 	rtn = PyMember_Get((char *)im, instancemethod_memberlist, name);
+ 	if (rtn == NULL) {
+ 		PyErr_Clear();
+ 		rtn = PyObject_GetAttrString(im->im_func, name);
+ 		if (rtn == NULL)
+ 			PyErr_SetString(PyExc_AttributeError, name);
  	}
! 	return rtn;
  }
  
  static void
***************
*** 1662,1669 ****
  	0,
  	(destructor)instancemethod_dealloc, /*tp_dealloc*/
  	0,			/*tp_print*/
! 	0,			/*tp_getattr*/
! 	0,			/*tp_setattr*/
  	(cmpfunc)instancemethod_compare, /*tp_compare*/
  	(reprfunc)instancemethod_repr, /*tp_repr*/
  	0,			/*tp_as_number*/
--- 1693,1700 ----
  	0,
  	(destructor)instancemethod_dealloc, /*tp_dealloc*/
  	0,			/*tp_print*/
! 	(getattrfunc)instancemethod_getattr, /*tp_getattr*/
! 	(setattrfunc)instancemethod_setattr, /*tp_setattr*/
  	(cmpfunc)instancemethod_compare, /*tp_compare*/
  	(reprfunc)instancemethod_repr, /*tp_repr*/
  	0,			/*tp_as_number*/
***************
*** 1672,1678 ****
  	(hashfunc)instancemethod_hash, /*tp_hash*/
  	0,			/*tp_call*/
  	0,			/*tp_str*/
! 	(getattrofunc)instancemethod_getattr, /*tp_getattro*/
  	0,			/*tp_setattro*/
  };
  
--- 1703,1709 ----
  	(hashfunc)instancemethod_hash, /*tp_hash*/
  	0,			/*tp_call*/
  	0,			/*tp_str*/
! 	0,			/*tp_getattro*/
  	0,			/*tp_setattro*/
  };
  
Index: funcobject.c
===================================================================
RCS file: /projects/cvsroot/python/dist/src/Objects/funcobject.c,v
retrieving revision 2.18
diff -c -r2.18 funcobject.c
*** funcobject.c	1998/05/22 00:55:34	2.18
--- funcobject.c	2000/04/07 22:15:33
***************
*** 62,67 ****
--- 62,68 ----
  			doc = Py_None;
  		Py_INCREF(doc);
  		op->func_doc = doc;
+ 		op->func_dict = PyDict_New();
  	}
  	return (PyObject *)op;
  }
***************
*** 133,138 ****
--- 134,140 ----
  	{"__name__",	T_OBJECT,	OFF(func_name),		READONLY},
  	{"func_defaults",T_OBJECT,	OFF(func_defaults)},
  	{"func_doc",	T_OBJECT,	OFF(func_doc)},
+ 	{"func_dict",   T_OBJECT,       OFF(func_dict)},
  	{"__doc__",	T_OBJECT,	OFF(func_doc)},
  	{NULL}	/* Sentinel */
  };
***************
*** 142,153 ****
  	PyFunctionObject *op;
  	char *name;
  {
  	if (name[0] != '_' && PyEval_GetRestricted()) {
  		PyErr_SetString(PyExc_RuntimeError,
  		  "function attributes not accessible in restricted mode");
  		return NULL;
  	}
! 	return PyMember_Get((char *)op, func_memberlist, name);
  }
  
  static int
--- 144,167 ----
  	PyFunctionObject *op;
  	char *name;
  {
+ 	PyObject* rtn;
+ 
  	if (name[0] != '_' && PyEval_GetRestricted()) {
  		PyErr_SetString(PyExc_RuntimeError,
  		  "function attributes not accessible in restricted mode");
  		return NULL;
+ 	}
+ 	if (strcmp(name, "__dict__") == 0)
+ 		return op->func_dict;
+ 	
+ 	rtn = PyMember_Get((char *)op, func_memberlist, name);
+ 	if (rtn == NULL) {
+ 		PyErr_Clear();
+ 		rtn = PyDict_GetItemString(op->func_dict, name);
+ 		if (rtn == NULL)
+ 			PyErr_SetString(PyExc_AttributeError, name);
  	}
! 	return rtn;
  }
  
  static int
***************
*** 156,161 ****
--- 170,177 ----
  	char *name;
  	PyObject *value;
  {
+ 	int rtn;
+ 	
  	if (PyEval_GetRestricted()) {
  		PyErr_SetString(PyExc_RuntimeError,
  		  "function attributes not settable in restricted mode");
***************
*** 178,185 ****
  		}
  		if (value == Py_None)
  			value = NULL;
  	}
! 	return PyMember_Set((char *)op, func_memberlist, name, value);
  }
  
  static void
--- 194,214 ----
  		}
  		if (value == Py_None)
  			value = NULL;
+ 	}
+ 	else if (strcmp(name, "func_dict") == 0) {
+ 		if (value == NULL || !PyDict_Check(value)) {
+ 			PyErr_SetString(
+ 				PyExc_TypeError,
+ 				"func_dict must be set to a dict object");
+ 			return -1;
+ 		}
+ 	}
+ 	rtn = PyMember_Set((char *)op, func_memberlist, name, value);
+ 	if (rtn < 0) {
+ 		PyErr_Clear();
+ 		rtn = PyDict_SetItemString(op->func_dict, name, value);
  	}
! 	return rtn;
  }
  
  static void
***************
*** 191,196 ****
--- 220,226 ----
  	Py_DECREF(op->func_name);
  	Py_XDECREF(op->func_defaults);
  	Py_XDECREF(op->func_doc);
+ 	Py_XDECREF(op->func_dict);
  	PyMem_DEL(op);
  }