The Objects: AutoPropertyObject
In this first article, I will begin doing a deep dive into NVDA's design. I will explain the most base object common to many components, and how it is used. I assume you have familiarity with the NVDA development Guide, and the Design overview.
Starting at the bottom of the inheritance hierarchy of objects you'll find NVDA using all the time are the AutopropertyObject, of AutoPropertyType. I aim to give you a basic understanding of how to utilize the full power of AutoPropertyType, butt understanding its source code is an exercise left for the reader of NVDA source code, or advanced developers with an intimate understanding of how python works internally.
AutopropertyObject
Okay, what's this AutoPropertyObject?
class AutoPropertyObject(object):
"""A class that dynamically supports properties, by looking up _get_* and _set_* methods at runtime.
_get_x will make property x with a getter (you can get its value).
_set_x will make a property x with a setter (you can set its value).
If there is a _get_x but no _set_x then setting x will override the property completely.
Properties can also be cached for the duration of one core pump cycle.
This is useful if the same property is likely to be fetched multiple times in one cycle. For example, several NVDAObject properties are fetched by both braille and speech.
Setting _cache_x to C{True} specifies that x should be cached. Setting it to C{False} specifies that it should not be cached.
If _cache_x is not set, L{cachePropertiesByDefault} is used.
"""
__metaclass__=AutoPropertyType
...
What in the world is this meta=AutoPropertyType? To explain this, let's bring out the NVDA Python Console! Fire up a browser, ensure NVDA is in browse mode, and type this in. Ignore the >>> stuff, that's the prompt as you probably know by now.
>>>type(focus.treeInterceptor)
<class 'virtualBuffers.gecko_ia2.Gecko_ia2'>
>>>type(type(focus.treeInterceptor))
<class 'baseObject.AutoPropertyType'>
>>> type(type(type(focus.treeInterceptor)))
<type 'type'>
Um ... so the type of the tree interceptor (More on those later) is of type virtualBuffers.gecko_ia2.Gecko_ia2. However, things are strange. the type of the type is not type! But it's type's type's type is type. Most python types are of type type. However, the meta class, which is indeed an advanced topic beyond the scope of this article, changes the type of an object. it's metta of. What's happening here is that virtualBuffers.gecko_ia2.Gecko_ia2 inherits from this AutoPropertyObject with some number of other classes sandwiched in the middle of this hierarchy, thus, it takes on the type of AutoPropertyType. This 'type' exists to create some special behavior in the core objects NVDA uses. So let's discover what AutoPropertyObject does.
The magic starts here!
Assuming you aren't familiar with python properties, here's a ten second crash course.
A property is just like a variable, but with magic hooked up. Let's assume we have a class Rectangle. We instantiate a rectangle with sides 5 and 7, with an area and perimeter.
Let's get some properties from it.
Here's class Rectangle.
class Rectangle(object):
def __init__(self, length=0, width = 0):
self.__length = length
self.__width = width
@property
def length(self):
return self.__length
@length.setter
def length(self, length):
if length < 0:
raise AttributeError("In this universe, rectangles are tangible thingies.")
self.__length = length
@property
def width(self):
return self.__width
#Same method name yes, but we have two arguments, so overloading.
@width.setter
def width(self, width):
if width < 0:
raise ValueError("Negative widths result in invalid rectangles. Learn math, and try again.")
self.__width = width
@property
def area(self):
return self.length * self.width
@property
def perimeter(self):
return self.length * 2 + self.width * 2
#You can also use the property method, but that's considered homework for the user.
rect = Rectangle(5,7)
#Let's do stuff.
rect.area #35
rect.perimeter #calculates 5*2+7*2 and outputs 24
#Let's do some horrible things.
rect.area = 55 #ouch, this raised AttributeError? Why?
rect.length = 2 #oh, hey, it worked
rect.length #Hey, looky, it returned 2.
rect.perimeter #calculates 2*2+7*2 and outputs 18
rect.width #Hey, 5 came out.
rect.width = -3 #ValueError is raised. How neat.
rect.area #14 comes out. Wow, it computed that on the fly?
As you can see, properties can allow us to control what happens when we ask for a value. We can ask python for rect.area, and behind the scenes, a function defined by the property called a getter is asked for the current value. (Getter: get the value associated with the property). If we do rect.length = 1000000, the properties setter is called with the value we want to set. This way, we can make a read only var, or control what val gets set, etc.
Basically, what autoPropertyObject, by way of the AutoPropertyType, does for us automagically, is allows properties to be defined with some fancy method naming. If we read the doc for this class, we see that functions called _get_x and _set_x are mentioned. Why? Also, what's this caching about?
class TestFancyProperties(baseObject.AutoPropertyObject):
cachePropertiesByDefault = True
def _get_x(self):
return getattr(self, "_x", 1)
def _set_x(self, x):
if x < 0:
raise ValueError("Negative number? Um, I don't think so!")
self._x = x
def _get_xSquared(self):
return self.x * self.x
def _set_xSquared(self, x):
raise ValueError("No! You can't change properties of math, not in this universe at least!")
See how cool that is? now, when we instantiate that, and do thing.x, we'll either get 0, or the previously set x. If we do thing.x = 5, x will magically report as 5 from now on. If we do thing.xthing.xthing.xthing.xSquaredthing.xSquared
, you'd think that would be more expensive given that it calls into the _get_x function, right? No. It is actually caching the value of x for one core pump of NVDA. This is good, because thing.x being gotten multiple times will save us from calling the getter. In this example, that is a really cheap operation, but let's assume that getter is getting the accessible parent of an object. That may easily be a COM call away. We don't want to write code like this do we?
if self.parent and self.parent.windowClassName == u"window":
doThingWith(self.parent.windowClassName)
If we do that without a cache, that might be 4 or 5 com calls. We can reduce that to 2 com calls with caching getters, and that's done magically for us.
Take away message!
Basically, if you see a method _get_thing or _set_thing, remember that you can just say blah.thing and set blah.thing = "to a value". Also, if you can't find for example, how self.name is defined on an NVDA Object, that would be because self.name is really an AutoProperty. I have one last word of wisdom about AutoProperties (Or dynamic properties).
This is really really really important. I can't stress this point enough. If I had known this when I started developing NVDA addons, I would have saved myself several hours of wishing I were not debugging silly errors. Any constructor you override, if the class derives from AutoPropertyObject, Must call super. Yes, you really really should always call super if you override something, but let's face it. We're all stupidly lazy. The goal of a programmer is 1. Write the least code possible, and 2. write the least comments possible explaining the code, so that we have job security in 20 years still (Hahahahahahaahahahaha boss, take that). Back to the important stuff. If you ever do make the mistake of forgetting to call super in a class that is derived from AutoPropertyObject, no matter how far down into the chain it is, you will get an error to the effect of "I can't find _autoPropertyCache." This is because it tries to cache things, but the cache is never created in the constructor.
I hope this was useful. Please feel free to contact me if you have comments or questions. I'm lazy, and can't be bothered to deal with comment spam bots on this site. Sorry.