Class introspection and dynamically determining functionarguments

harold fellermann harold.fellermann at upf.edu
Fri Jan 21 23:11:46 EST 2005


On 20.01.2005, at 12:24, Mark English wrote:

> I'd like to write a Tkinter app which, given a class, pops up a
> window(s) with fields for each "attribute" of that class. The user 
> could
> enter values for the attributes and on closing the window would be
> returned an instance of the class. The actual application I'm 
> interested
> in writing would either have simple type attributes (int, string, 
> etc.),
> or attributes using types already defined in a c-extension, although 
> I'd
> prefer not to restrict the functionality to these requirements.

I am working on nearly the same thing! Small sort of generic attribute 
editor
in Tkinter (and Tix). Altough my implementation is still very unpythonic
(== ugly) in many places, it can edit the attributes of instances and 
classes,
as well as generate new instances / classes. I would like to create new
attributes or delete them as well, but haven't done it so far.
A still faraway dream would be, to allow the user to enter source code, 
that
will be compiled into byte code and assign it to the instance, so the 
user can
modify code at run time (if it is possible to disallow access to 
insecure
modules while code is compiled).

> Secondly, the code won't know exactly how to initialise the class
> instance used to determinte the attributes. Do I need to make it a
> prerequesite that all instances can be created with no arguments ?

I did it this way, too.
Maybe you can provide a parameterless __new__ in addition to the 
__init__.
I imagine this is the way that e.g. pickle does the job. Well, I don't
know. Let this sentence just be an invitation for an expert to write 
something
here.

> Should I force/allow the user to pass an instance instead of a class ?

However you like. I prefer passing classes, otherwise you end up in a
situation where you need to create dummy instances that are only used
as "copying templates". If you get an instance: foo = type(bar)()
would give you an instance of the same class as bar. But this just
looks like a hack to me when compared to foo = Bar().

Passing an instance allows you to look at its __dict__ of course.
But you have no assurance that the variables you find there are present
in all instances of that class. Phython is just to dynamic for that.

> Should I be using inspect.getargspec on the class __init__ method and
> then a loop with a try and a lot of except clauses, or is there a nicer
> way to do this ? Presumably the pickling code knows how do
> serialise/deserialise class instances but I'm not sure how I'd use this
> without already having a class instance to hand.

Personally, I ended up writing a class Attribute that provides access to
the attributes and allows more finetuning than a generic approach would
do (think of e.g. validation). My overall setting works like this:
For each attribute that you want to appear in the GUI you define an 
Attribute

class Attribute(object) :
	def __init__(self,
	             name,               # name of the attribute
	             type,               # specifies the widget type
	             values=None,        # then OptionMenu is used instead
	             validate=None,      # if given, a callable that returns
                                       # either True or False
	             get=None,           # callable to get the actual value
                                       # None: generic getattr()
	             widget_options={}   # passed to the Tix widget used
	) :
		pass # [...]

The "controller" of an editable object gets the object and a list
of those Attribute()'s. There is a generic one that handles validation
and generic setattr(), but can be overwritten to allow more 
sophisticated
stuff.

When creating a new instance, I ask for initial arguments which I know
(like __name__ and __doc__ if its a class that I create) in nearly the
same way.

If you already have your instance, it is possible to generate the
Attribute()-list from the dict of this instance:

attributes = [
     Attribute(name,type(getattr(instance,name))) for name in 
dir(instance)
]

> Lastly, does such an app already exist ?

As you see, my solution is not as general as what you might have in 
mind.
I needed to finetune so much of the underlying generic plan, that it
looks more like declaration-based now.

Anyway, if you are interested: This is the edit part of my app (It's 
only
the mapping from attributes to widgets, so no instance creation in this 
part).
As this was only a short snippet in the project I am doing, I could, 
never
give it the time it deserved. So I am sure that my code is not the best 
sollution,
but maybe it serves as a starting point for further discussion.

All the best,

- harold -



import Tkinter
import Tix

class Attribute(object) :
	def __init__(self,
	             name,
	             type,
	             values=None,
	             validate=None,
	             get=None,
	             widget_options={}
	) :
		if not get : get = lambda obj : getattr(obj,name)

		self.name = name
		self.type = type
		self.values = values
		self.get = get
		self.validate = validate
		self.widget_options = widget_options
		self.label = None
		self.widget = None


class Edit(Tkinter.Frame) :
	import types

	def __init__(self,obj,items) :
		self.obj   = obj
		self.items = items

	def create_widgets(self,master,**options) :
		Tkinter.Frame.__init__(self,master,**options)
		for item in self.items :
			if item.values :
				widget = Tix.OptionMenu(master,label=item.name)
				for v in item.values :
					widget.add_command(str(v),label=str(v))
				item.label = None
				item.widget = widget
			else :
				widget = self.widget_types[item.type](master)
				widget.config(label=item.name,**item.widget_options)
				item.label = widget.label
				item.widget = widget.entry
			widget.pack(side=Tkinter.TOP,fill=Tkinter.X)
		self.pack()

	def update_widget(self,item) :
		if isinstance(item.widget,Tix.OptionMenu) :
			item.widget.set_silent(item.get(self.obj))
		else :
			item.widget.delete(0,Tkinter.END)
			item.widget.insert(0,item.get(self.obj))

	def update_widgets(self) :
		for item in self.items :
			self.update_widget(item)

	def validate(self,item) :
		if isinstance(item.widget,Tix.OptionMenu) : return True
		value = item.widget.get()
		if item.validate :
			valid = item.validate(value)
		elif item.values :
			valid = value in item.values
		else :
			try :
				valid = item.type(value)
			except ValueError :
				valid = False
		if valid :
			item.label.config(fg="black")
		else :
			item.label.config(fg="red")
			item.widget.focus_set()
		return valid
		
	def isvalid(self) :
		for item in self.items :
			if not self.validate(item) : return False
		return True

	def apply(self) :
		if self.isvalid() :
			for item in self.items :
				try :
					value = item.type(item.widget.get())
				except AttributeError :
					value = item.type(item.widget.cget("value"))
				if value != item.get(self.obj) :
					setattr(self.obj,item.name,value)


	widget_types = {
		types.StringType : Tix.LabelEntry,
		types.IntType : Tix.Control,
		types.FloatType : Tix.LabelEntry,
	}
	


if __name__ == "__main__" :
	class myClass :
		string = "hello world"
		float  = 1.
		int    = 0

	proxy = Edit(
		myClass,
		[
			Attribute("string",type(str())),
			Attribute("float",type(float())),
			Attribute("int",type(int()),validate=lambda x : int(x) in [-1,0,1] )
		]
	)

	win = Tix.Tk(None)
	proxy.create_widgets(win)
	proxy.update_widgets()

	button1 = Tkinter.Button(
		win,
		text="apply",
		command=proxy.apply
	)
	button1.pack()
	
	button2 = Tkinter.Button(
		win,
		text="update",
		command=proxy.update_widgets
	)
	button2.pack()
	
	win.mainloop()




--
Man will occasionally stumble over the truth,
but most of the time he will pick himself up and continue on.
-- Winston Churchill




More information about the Python-list mailing list