diff options
author | michele.simionato <devnull@localhost> | 2008-08-13 05:37:38 +0000 |
---|---|---|
committer | michele.simionato <devnull@localhost> | 2008-08-13 05:37:38 +0000 |
commit | c87ec2d3a2a83d8e4c93e52d4834d5a22b3369b1 (patch) | |
tree | 6f73d0bc7a31d671a0ca212bfb10ef0c3f139a20 /mtraits | |
parent | 587eddf6049de93a1288f10261011d685968bba0 (diff) | |
download | micheles-c87ec2d3a2a83d8e4c93e52d4834d5a22b3369b1.tar.gz |
First version of mtraits committed
Diffstat (limited to 'mtraits')
-rw-r--r-- | mtraits/doc.py | 594 | ||||
-rw-r--r-- | mtraits/mtrait.py | 298 |
2 files changed, 892 insertions, 0 deletions
diff --git a/mtraits/doc.py b/mtraits/doc.py new file mode 100644 index 0000000..0dcaa1b --- /dev/null +++ b/mtraits/doc.py @@ -0,0 +1,594 @@ +r"""An implementation of traits in Python +================================================================== + +:Author: Michele Simionato +:Date: XXX +:Version: XXX +:Licence: BSD + +Motivation +------------------------------------------------ + +Multiple inheritance is a hotly debated topic. +The supporters of multiple inheritance +claim that it makes code shorter and easier +to read, whereas the opposers claim that is makes +code more coupled and more difficult to understand. I have +spent some time in the past facing the intricacies of `multiple +inheritance in Python`_ ; moreover I have worked with frameworks making +large use of multiple inheritance (I mean Zope 2) and nowadays I am in +the number of the people who oppose multiple inheritance. Therefore I +am interested in alternatives. In recent years, the approach +of traits_ has gained some traction in a few languages, so I have +decided to write a library to implement traits in Python, for +experimentation purposes. The library is meant for framework builders, +people who are thinking about writing a framework based on multiple +inheritance (typically via the common mixin approach) but +are not convinced that this is the best solution and would like to try +an alternative. This library is also for the authors of existing frameworks +based on mixins that are unsatisfied and would like to convert their +framework to traits. + +Are traits a better solution than multiple inheritance and mixins? +In theory I think so, otherwise I would not have write this library, but +in practice (as always) things may be different. It may well be that in +practice using traits or using mixins does not make a big difference +and that changing paradigm is not worth the effort; or the opposite may be +true. The only way to know is to try, to build software based on traits +and to see how it scale *in the large* (in the small, more or less any +approach works fine; it is only by programming in the large that you +can see the differences). +This is the reason why I am releasing +this library with a liberal licence, so that people can try it out and +see how it works. The library is meant to play well with pre-existing +frameworks. As an example, I will show here how you could rewrite +Tkinter to use traits instead of mixins. Of course, I am not advocating +rewriting Tkinter: it would be silly at this point of its history and +pointless; but it may have sense (or not) to rewrite +your own framework using traits, perhaps a framework which is used +in house but has not been released yet. + +Finally, let me notice that I am not the only one to have +implemented traits for Python; after finishing my implementation +I made a research and discovered a few implementations. The +biggest effort seems to be `Enthought Traits`_ which I did +not try, since the tutorial required Microsoft Windows, +wxPython and enthought.traits.ui.wx to run. My implementation +does not require anything, is short and I am committed +to keep it short even in the future, according to +the principle of `less is more`_. + +.. _less is more: http://www.artima.com/weblogs/viewpost.jsp?thread=236286 +.. https://svn.enthought.com/enthought/wiki/Traits + + +.. _multiple inheritance in Python: MRO + +What are traits? +------------------------------------------------------------ + +The word *traits* has many meanings; I will refer to it in the sense of +the paper `Traits - Composable Units of Behavior`_ which +implements them in Squeak/Smalltalk. The paper appeared in 2003, but most +of the ideas underlying traits have been floating around for at least +30 years. There is also a trait implementation for `PLT Scheme`_ which is +close in spirit (if not in practice) to what I am advocating here. +The library +you are reading about is by no means intended as a porting of the Smalltalk +library: I am just stealing some of the ideas from that paper to implement +a Pythonic alternative to mixins which, for lack of a better name, I have +decided to call traits, but I feel no obligation whatsoever to be consistent +with the Smalltalk library. In doing so, I am following a long tradition, +since a lot of languages use the name "traits" to mean something completely +different from the Smalltalk meaning. For instance the languages +Fortress and Scala use the name "trait" but they mean by it what is +usually called a "mixin". +For me a trait is a bunch of methods and attributes will the following +properties: + +1. the methods/attributes go logically together; +2. traits can be used to enhance independent classes; +3. the ordering is not important, so enhancing a class first with trait T1 and + then with trait T2 or viceversa is the same; +4. if traits T1 and T2 have names in common, enhancing a class both + with T1 and T2 raises an error unless unless you specify *explicitely* + how the overriding of the common names has to be made; +5. a class can be seen both as a composition of traits and as an homogeneous + entity. + +Properties 3 ,4, 5 are the distinguishing properties of traits with respect +to multiple inheritance and mixins. +In particular, because of 3 and 4, all the complications with the Method +Resolution Order disappear and the overriding is never implicit. +Property 5 has to be intended in the sense that a trait implementation +must provide introspection facilities to make seemless the transition +between classes viewed as atomic entities and as composed entities. + +A hands-on example +------------------------------------------------------ + +For pedagogical purpose I will show here how you could rewrite a +Tkinter class to use traits instead of mixins. Consider the +``Tkinter.Widget`` class, which is derived by the base class +``BaseWidget`` and the mixin classes +``Tkinter.Grid``, ``Tkinter.Pack`` and ``Tkinter.Place``: we want to +rewrite it to use traits instead of mixins. The ``mtrait`` module +provides a class decorator named ``include`` to do exactly that; it is +enough to replace the multiple inheritance syntax + +.. code-block:: python + + class Widget(BaseWidget, Grid, Pack, Place): + pass + +with a class decorator syntax: + +.. code-block:: python + + @include(Pack, Place, Grid) # this syntax requires Python 2.6+ + class Widget(BaseWidget): + pass + +For compatibility with old versions of Python (before Python 2.6) +the ``include`` class decorator provides some magic to work even +inside classes: + +.. code-block:: python + + class Widget(BaseWidget): # this syntax works in Python 2.2+ + include(Pack, Place, Grid) + +The preferred way however is to use the ``@`` notation, if available. +I said that the conversion from mixins to traits was easy: but actually +I lied since if you try to execute the code I just wrote you will +get an ``OverridingError``: + +>>> from Tkinter import * +>>> class Widget(BaseWidget): +... include(Pack, Place, Grid) +Traceback (most recent call last): + ... +OverridingError: Pack.{info, config, configure, slaves, forget} overriding names in Place + +The reason for the error is clear: both ``Pack`` and ``Place`` provide +methods called ``{info, config, configure, slaves, forget}`` +and the traits implementation cannot figure out +which ones to use. This is a feature, since it forces you to be +explicit. In this case, if we want to be consistent with +multiple inheritance rules, clearly we want the methods coming from +the first class (i.e. ``Pack``) to have precedence and we must say so: + +$$TOSWidget + +Notice that we had to specify the ``propagate`` method too, since +it is common between ``Pack`` and ``Grid``. + +You can check that the TOSWidget class works, for instance by defining a +label widget as follows (remember that ``TOSWidget`` inherits its signature +from ``BaseWidget``): + +>>> label = TOSWidget(master=None, widgetName='label', cnf=dict(text="hello")) + +You may visualize the widget by calling the ``.pack`` method: + +>>> label.pack() + +This should open a small window with the message "hello" inside it. +As you see, in a lot of cases replacing mixins with traits is fairly +straightforward: however, in some cases you can run into problems. In +particular if you have a cooperative multiple inheritance hierarchy +(i.e. you use ``super`` in the mixin classes) you will have to rewrite +your code not to use ``super`` (there are no problem if you use +``super`` ony in methods of the base class). This is on purpose: +traits are not a replacement for +cooperative multiple inheritance, you should uses traits if you think +that cooperative multiple inheritance is a bad idea and you are +willing to remove it from your code. Old frameworks written before the +introduction of ``super`` (such as Tkinter and Zope 2) are easier to +convert in this respect, since they are non-cooperative. +The ``mtrait`` module is +intended for framework writers, so it assumes you can change the source +code of your framework if you want; of course, it aims to +reduce the needed changes as much as possible. + +How does it work +--------------------------------------------------------- + +The implementation of traits provided is an ``mtrait`` is short and +relatively simple but if makes use of sophisticated techniques. The +class decorator ``include`` takes a set of mixin classes in input, +determines the right metaclass to use and returns a class in output; +the metaclass enhances the class in output by adding to it a special +attribute ``__traits__``, a custom ``__getattr__`` method and a custom +``__getstate__`` method. The +``__traits__`` attribute is a ``TraitContainer`` instance akin to a +dictionary of trait objects (one for each mixin class); the +``__getattr__`` method allows automatic dispatching to them; +the ``__getstate__`` method is needed to preserve pickleability of +the instances (assuming the original class before decoration was +pickleable, of course)> + +Now let me describe a trait object. The ``mtrait`` module provides a +``Trait`` class which you can use directly, instead of relying on the +magic of ``include``. You can see a trait as a wrapper around an +inner object. Usually you want to convert a mixin class into a trait; you +can do it as follows: + + ``trait = Trait(Mixin, Mixin.__name__)`` + +It is also common to convert a module into a trait: + + ``trait = Trait(module, module.__name__)`` + +Trait objects are attribute descriptors, i.e. they can used as class +attributes; when a trait is called from a class or an instance, we +say that it is bound to the class or the instance. + +Here is an example of usage: + +>>> pack = Trait(Pack, 'Pack') # define an unbound trait +>>> pack +<Trait Pack {config, configure, forget, info, pack, pack_configure, pack_forget, pack_info, pack_propagate, pack_slaves, propagate, slaves} > + +>>> class MyWidget(BaseWidget): # add the trait to a class +... p = pack + +Here is an example of trait a bound to a class: + +>>> MyWidget.p #doctest: +ELLIPSIS +<Trait Pack {config, configure, forget, info, pack, pack_configure, pack_forget, pack_info, pack_propagate, pack_slaves, propagate, slaves} bound to <class __main__.MyWidget at 0x...>> + +Here is an example of a trait bound to an instance: + +>>> lbl = MyWidget(None, 'label', dict(text='hello')) +>>> lbl.p #doctest: +ELLIPSIS +<Trait Pack {config, configure, forget, info, pack, pack_configure, pack_forget, pack_info, pack_propagate, pack_slaves, propagate, slaves} bound to <__main__.MyWidget instance at 0x...>> + +You can call the methods of a trait as follows: + +>>> lbl.p.pack() +>>> lbl.p.info().keys() +['side', 'ipady', 'ipadx', 'in', 'pady', 'padx', 'anchor', 'expand', 'fill'] + +The descriptor magic works in such a way that the instance methods get +as first argument the instance to which the trait is bound; on the other +hand, for traits bound to classes, you get an unbound method associated +to the given class: + +>>> MyWidget.p.pack +<unbound method MyWidget.pack_configure> + +Different traits or mixins can be composed into a ``TraitContainer``: +for instance, you could define + +>>> tc = TraitContainer.from_([Pack, Place, Grid]) +>>> tc +<Traits Grid, Place, Pack > + +The ``TraitContainer`` object has attributes ``Pack``, ``Place`` and ``Grid`` +which are traits corresponding to the mixin classes; for instance + +>>> tc.Pack +<Trait Pack {config, configure, forget, info, pack, pack_configure, pack_forget, pack_info, pack_propagate, pack_slaves, propagate, slaves} > + +The magic of ``include`` +---------------------------------------------------------------------- + +Even if you could build your trait objects yourself as explained in +the previous paragraph, and you could implement the dispatch to traits +by hand, usually it is easier if you just rely on the magic of +the ``include`` class decorator, which is doing a lot of work on your +behalf. In particular, internally the class decorator +builds a trait object for each mixin and store all of them into the +``__traits__`` attribute; +moreover it adds a suitable ``__getattr__`` +method to the decorated class. +Finally the class decorator adds a suitable ``__getstate_`` +method so that your objects stay pickleable if they were originally +pickleable (adding ``__getattr__`` without adding ``__getstate__`` would +break pickle). Notice that the original class should not define +``__getattr__`` or ``__getstate__`` of its own, otherwise an +``OverridingError`` is raised. This is intended to prevent +accidental overriding; if you really know what you are doing, +you can always replace the default implementation with your own +after class creation. +If you want to understand the details, you are welcome +to give a look at the implementation, which is pretty small. + +The goal of the ``mtrait`` module it to modify the standard +Python object model, turning it into a Trait Object System (TOS for short): +TOS classes (i.e. instances of ``MetaTOS``) behave differently from regular +classes. In particular TOS classes must not support multiple inheritance +and must forbid your from defining your own ``__getattr__`` and +``__getstate__`` methods; these +two fundamental properties must be preserved under inheritance (i.e. the +son of a TOS class must be a TOS class) and therefore the implementation +requires necessarily metaclasses. + +People who do not like magic may do everything explicitely, by using +the ``__metaclass__`` hook and by specifying the mixins with ``__mixins__``: + +$$TOSWidget2 + +``include`` does more than that, since it +takes care automatically of possible metaclass conflicts. In the case of +Tkinter there is no such problem, since ``BaseWidget`` is just a traditional +old-style class and the metaclass for ``TOSWidget2`` is just ``TOSMeta``: + +>>> type(TOSWidget) +<class 'mtrait.TOSMeta'> + +However, in general you may need to build your Trait Based Framework +on top of pre-existing classes with a nontrivial metaclass, for +instance Zope classes; in that case having a class decorator smart +enough to figure out the right metaclass to use is a convenient. Here +is an example using Plone classes: + +>> from OSF.Folder import Folder + + +when you call +``super(<class>, <subclass>).method`` +>> from zope.plone import BaseContent +>> class + +>> type(X) +ExtensionClassMetaTOS + + +<type 'ExtensionClass.ExtensionClass'> + +Why multiple inheritance is forbidden +---------------------------------------------------------- + +As I said, the mother metaclass of the Trait Object System ``MetaTOS`` forbids +multiple inheritance and if you try to multiple inherit from a TOS +class and another class you will get a ``TypeError``: + +>>> class C:pass +... +>>> class Widget2(TOSWidget, C): #doctest: +ELLIPSIS +... pass +... +Traceback (most recent call last): + ... +TypeError: Multiple inheritance of bases (<class '__main__.TOSWidget'>, <class __main__.C at 0x...>) is forbidden for TOS classes + +This behavior is intentional: with this restriction you can simulate +an ideal world in which Python did not support multiple +inheritance. Suppose you want to claim that supporting multiple +inheritance was a mistake and that Python would have been better +without it (which is the position I tend to have nowadays, but see the +note below): how can you prove that claim? Simply by writing code that +does not use multiple inheritance and it is clearer and more +mantainable that code using multiple inheritance. I am releasing this +trait implementation hoping you will help me to prove (or possibly +disprove) the point. You may see traits as a restricted form of +multiple inheritance without method resolution order and without name +clashes which does not pollute the namespace of the original class; it +does not have cooperative methods either. Still I think these are +acceptable restrictions since they give back in return many +advantages: + +1. many people use multiple inheritance incorrectly, confusing the ``is-a`` +relation with the ``has-a`` relation; with traits, there is no confusion, +since the features coming +by the base class correspond to ``is-a``, whereas the features coming +from the traits correspond to ``has-a``. + +2. ``super`` becomes trivial, since each class has a single superclass. + +Are there disadvantages of my proposed trait implementation with +respect to multiple inheritance? I don't think there are serious +disadvantages, since you can always work around them. For instance, a nice +property of inheritance is that if you have a class ``C`` inheriting from +class ``M`` and you change a method in ``M`` at runtime, after ``C`` has been +created and instantiated, automagically all instances of ``C`` gets the +new version of the method (this is pretty useful for debugging +purposes). This feature is not lost: the trait implementation +is fully dynamic and if you change the mixin the instances will +be changed too. + +*Note*: even if I think that a language would be better off without +full multiple inheritance with cooperative methods, that does not mean +that I am against interfaces. I think a class should be able to +implement multiple interfaces at the same time, but this has nothing +to do with multiple inheritance. In Python instead (starting from +Python 2.6) multiple inheritance is abused to simulate interface +requirements so that if you want to implement many interfaces at the +same time you are suggested to inherit from many abstract base classes +at the same time. In an ideal language without multiple inheritance +you could just add an ``__interfaces__`` attribute to classes. In an +ideal language interfaces would be abstract objects (a little more +than a list of names) whereas in Python they are concrete classes and +that explain why you inherit from them. + +Introspection +------------------------------------------------------------------ + +As I said, a trait implementation must provide introspection facilities. +To this aim, ``mtrait`` provides a ``get_traits(obj)`` function returning +a ``TraitContainer`` if ``obj`` has a ``__traits__`` attribute or the +empty tuple otherwise. For instance + +>>> #get_traits(label) is label.__traits__ + +>>> #get_traits(cls=TOSWidget) is TOSWidget.__traits__ + +>>> TOSWidget.__traits__ +<Traits Grid, Place, Pack bound to <class '__main__.TOSWidget'>> + +>>> label.__traits__ #doctest: +ELLIPSIS +<Traits Grid, Place, Pack bound to <__main__.TOSWidget object at 0x...>> + +Future work +-------------------------------------------------------- + +The Smalltalk implementation of traits provides method renaming +out of the box. The Python implementation has no facilities in +this sense. In the future I may decide to give some support for +renaming, or I may not. At the present I am not sure renaming is a good +idea: after all, it is pretty easy to create a new trait with different +names. For instance I can rename two read/write methods + +$$ReadWriteMixin + +as dump/restore methods quite easily: + +$$DumpRestoreMixin + +Also, in the future I may decide to add some kind of adaptation mechanism +or I may not: after all the primary goal of this implementation is semplicity +and I don't want to clutter it with too many features. + +I am very open to feedback and criticism: I am releasing this module with +the hope that it will be used in real life situations to gather experience +with the traits concept. Clearly I am not proposing that Python should +remove multiple inheritance in favor of traits: that will never happen. +I am just looking for a few adventurous volunteers wanting to experiment +with traits; if the experiment goes well, and people start using (multiple) +inheritance less than they do now, I will be happy. The point I am +trying to make is that Python is not Java: whereas in Java you have +very little alternatives to inheritance, in Python you have lots of +alternatives involving composition, +so you should not keep programming Java in Python. + +.. _traits: http://www.iam.unibe.ch/~scg/Research/Traits/ +.. _Traits - Composable Units of Behavior: http://www.iam.unibe.ch/%7Escg/Archive/Papers/Scha03aTraits.pdf +.. _PLT Scheme: http://www.cs.utah.edu/plt/publications/aplas06-fff.pdf + +""" +import cPickle as pickle +from mtrait import * +from Tkinter import * + +class TOSWidget(BaseWidget): + include(Pack, Place, Grid) + info = Pack.info.im_func + config = Pack.config.im_func + configure = Pack.configure.im_func + slaves = Pack.slaves.im_func + forget = Pack.forget.im_func + propagate = Pack.propagate.im_func + +class TOSWidget2(BaseWidget): + __mixins__ = Pack, Place, Grid + __metaclass__ = TOSMeta + info = Pack.info.im_func + config = Pack.config.im_func + configure = Pack.configure.im_func + slaves = Pack.slaves.im_func + forget = Pack.forget.im_func + propagate = Pack.propagate.im_func + +label = TOSWidget(master=None, widgetName='label', cnf=dict(text="hello")) + +pickle.dumps(label) + +class HTTP(object): + def GET(self): + print 'calling HTTP.GET from %s' % self + def POST(self): + print 'calling HTTP.POST from %s' % self + @classmethod + def cm(cls): + print 'calling HTTP.cm from %s' % cls + +class FTP(object): + def SEND(self): + print 'calling FTP.SEND from %s' % self + def RECV(self): + print 'calling FTP.RECV from %s' % self + @staticmethod + def sm(): + print 'calling staticmethod' + +class Mixin(object): + def helper(self): + return 1 + def method(self): + return self.helper() + 1 + +class Meta(type): + pass + +def test_getattr(): + class C: + __metaclass__ = TOSMeta + def __getattr__(self, name): + pass + +def test_multi_include(): + class B(object): + __metaclass__ = Meta + include(FTP) + class C(B): + include(HTTP) + print type(C) + +def test_Trait_pickle(): + t = Trait(Mixin, Mixin.__name__) + pickle.loads(pickle.dumps(t)) + +class C: + "An example not using include" + __metaclass__ = TOSMeta + __mixins__ = HTTP, FTP, Mixin + +class ReadWriteMixin(object): + def read(): + pass + def write(): + pass + +class DumpRestoreMixin(object): + dump = ReadWriteMixin.write.im_func + restore = ReadWriteMixin.read.im_func + +def setup(): + global c + c = C() + print c + +class A(object): + def save(self): + print 'A.save' + +class B(A): + def save(self): + print 'B.save' + super(B, self).save() + +class C(A): + def save(self): + print 'C.save' + Super(self).save() + +class D(B, C): + def save(self): + print 'D.save' + super(D, self).save() + +C.C_save = C.save.im_func + +class E(B): + include(C) + def save(self): + print 'D.save' + print 'C.save' + B.save(self) + +check_overridden([B, C], set(), raise_="warning") + +d = D() +e = E() +e.save() + +if __name__ == '__main__': + import doctest; doctest.testmod() + try: + import nose + except ImportError: + pass + else: + nose.runmodule() diff --git a/mtraits/mtrait.py b/mtraits/mtrait.py new file mode 100644 index 0000000..c70d4b1 --- /dev/null +++ b/mtraits/mtrait.py @@ -0,0 +1,298 @@ +import re, sys, inspect, types, warnings + +class OverridingError(NameError): + pass + +class OverridingWarning(Warning): + pass + +def getnames(obj): + "Get the nonspecial attributes in obj" + return set(name for name in dir(obj) + if not (name.startswith('__') and name.endswith('__'))) + +def find_common_names(mixins): + "Perform n*(n-1)/2 namespace overlapping checks on a set of n mixins" + n = len(mixins) + if n <= 1: return + names = [set(getnames(obj)) for obj in mixins] + for i in range(0, n): + for j in range(i+1, n): + ci, cj = mixins[i], mixins[j] + common = names[i] & names[j] + if common: + yield common, ci, cj + +def check_overridden(mixins, exclude, raise_='error'): + "Raise an OverridingError for common names not in the exclude set" + for common, c1, c2 in find_common_names(mixins): + overridden = ', '.join(common - exclude) + if ',' in overridden: # for better display of the names + overridden = '{%s}' % overridden + if overridden: + msg = '%s.%s overriding names in %s' % ( + c1.__name__, overridden, c2.__name__) + if raise_ == 'error': + raise OverridingError(msg) + elif raise_ == 'warning': + warnings.warn(msg, OverridingWarning, stacklevel=2) + +class Trait(object): + """ + Class for mixin dispatchers. Mixin dispatchers are instantiated through the + .to classmethod and possess a __mixin__ attribute. They are descriptors + acting as proxies to an inner dictionary. + There are bound and unbound dispatchers, just as there are methods and + functions. A bound dispatcher is a dispatcher instance bound to a class or + an object, whereas Trait(mixin(), name) returns an unbound dispatcher. + """ + + def __init__(self, inner, name, obj=None, objcls=None): + if isinstance(inner, self.__class__): # already a trait + self.__inner = inner._Trait__inner + else: + self.__inner = inner + self.__name__ = name + self.__obj = obj + self.__objcls = objcls + + def __get__(self, obj, objcls=None): + "Return a bound dispatcher" + return self.__class__(self.__inner, self.__name__, obj, objcls) + + def __getattr__(self, name): + "obj.dispatcher.method(args) returns mixin.method(obj, args)" + value = getattr(self.__inner, name) + try: # if (unbound) method, go back to the function + value = value.im_func + except AttributeError: + pass + obj, objcls = self.__obj, self.__objcls + if obj or objcls: + try: # return the bound descriptor + return value.__get__(obj, objcls) + except AttributeError: # not a descriptor + pass + return value + + def __iter__(self): + return iter(getnames(self.__inner)) + + def __repr__(self): + names = ', '.join(sorted(self)) + bound_obj = self.__obj or self.__objcls + if bound_obj: + msg = 'bound to %r' % bound_obj + else: + msg = '' + return '<%s %s {%s} %s>' % ( + self.__class__.__name__, self.__name__, names, msg) + + ## we could live with nonpickeable traits, but still ... + + def __getstate__(self): + return self.__inner + + def __setstate__(self, inner): + self.__init__(inner, inner.__name__) + +def __getattr__(obj, name): + "__getattr__ added by TOSMeta" + objcls = obj.__class__ + for trait in obj.__traits__: + bt = trait.__get__(obj, objcls) # bound trait + try: + return getattr(bt, name) + except AttributeError: + continue + raise AttributeError(name) + +class TOSMeta(type): + """The metaclass for the Trait Object System. It is intended to be + called only indirectly via ``include``. It provides the following features + to its instances: + 1. forbids multiple inheritance + 2. checks for accidental overriding of __getattr__ and __getstate__ + 3. provides the class with the correct base __getattr__ and __getstate__ + 4. provides the basic empty __traits__ attribute + """ + + def __new__(mcl, name, bases, dic): + if len(bases) > 1: + raise TypeError( + 'Multiple inheritance of bases %s is forbidden for TOS classes' + % str(bases)) + elif oldstyle(bases): # ensure new-style class + bases += (object, ) + for meth in ('__getattr__', '__getstate__'): + if meth in dic: + raise OverridingError('class %s defines %s' % (name, meth)) + traits = getattr(bases[0], '__traits__', ()) + if not traits: # the first time + dic['__getattr__'] = dic.get('__getattr__', __getattr__) + dic['__getstate__'] = dic.get('__getstate__', vars) + basemixins = () + else: + basemixins = tuple(t._Trait__inner for t in traits) + mixins = dic.get('__mixins__', ()) + if mixins: + commonset = set(basemixins) & set(mixins) + if commonset: + raise TypeError("Redundant mixins %s!", commonset) + mixins = basemixins + mixins + check_overridden(mixins, exclude=set(dic)) + dic['__traits__'] = TraitContainer.from_(mixins) + return super(TOSMeta, mcl).__new__(mcl, name, bases, dic) + + def __getattr__(cls, name): + for trait in cls.__traits__: + bt = trait.__get__(None, cls) # class bound trait + try: + return getattr(bt, name) + except AttributeError: + continue + raise AttributeError(name) + +cache = {type: TOSMeta, types.ClassType: TOSMeta} # metaclass cache + +def getrightmeta(metacls, mcl): + "Determine the right metaclass to use between metacls and mcl (=TOSMeta)" + if issubclass(metacls, mcl): # has TOSMeta functionality + return metacls + else: # add TOSMeta functionality + try: + return cache[metacls] + except KeyError: + cache[metacls] = type('TOS' + metacls.__name__, (mcl, metacls), {}) + return cache[metacls] + +def new(mcl, objcls, mixins): + "Returns a class akin to objcls, but meta-enhanced with mcl (TOSMeta)" + dic = vars(objcls).copy() + dic['__mixins__'] = mixins + metacls = getrightmeta(type(objcls), mcl) + return metacls(objcls.__name__, objcls.__bases__, dic) + +def oldstyle(bases): + "Return True if there are not bases or all bases are old-style" + return not bases or set(map(type, bases)) == set([types.ClassType]) + +def include(*mixins): + "Class decorator factory" + frame = sys._getframe(1) + if ('__module__' in frame.f_locals and not # we are in a class + '__module__' in frame.f_code.co_varnames): + # usage as a Python < 2.6 class decorator + mcl = frame.f_locals.get("__metaclass__") + def makecls(name, bases, dic): + if mcl: + cls = mcl(name, bases, dic) + elif oldstyle(bases): + cls = types.ClassType(name, bases, dic) + else: # there is at least a new style base + cls = type(name, bases, dic) + return new(TOSMeta, cls, mixins) + frame.f_locals["__metaclass__"] = makecls + else: + # usage as a Python >= 2.6 class decorator + def _include(cls): + return new(TOSMeta, cls, mixins) + _include.__name__ = 'include_%s>' % '_'.join(m.__name__ for m in mixins) + return _include + +class TraitContainer(object): + + @classmethod + def from_(cls, mixins): + return cls(dict((m.__name__, Trait(m, m.__name__)) for m in mixins)) + + def __init__(self, dic, obj=None, objcls=None): + self.__traits = dic # a dictionary name -> trait + self.__obj = obj + self.__objcls = objcls + + def __getattr__(self, name): + try: + trait = self.__traits[name] + except KeyError: + raise AttributeError(name) + else: + return trait.__get__(self.__obj, self.__objcls) + + def __iter__(self): + return self.__traits.itervalues() + + def __len__(self): + return len(self.__traits) + + def __bool__(self): + return bool(self.__traits) + + def __get__(self, obj, objcls=None): + return self.__class__(self.__traits, obj, objcls) + + def __repr__(self): + bound_obj = self.__obj or self.__objcls + if bound_obj: + msg = 'bound to %r' % bound_obj + else: + msg = '' + return '<Traits %s %s>' % (', '.join(self.__traits), msg) + + def __getstate__(self): + return self.__traits + + def __setstate__(self, dic): + self.__init__(dic) + +def get_traits(obj): + "Returns a container of traits for introspection purposes" + return getattr(obj, '__traits__', ()) + +class Super(object): + """ + A simple implementation of the super object valid for single inheritance + hierarchies. + """ + def __init__(self, obj=None, cls=None): + assert obj or cls, 'Super objects must be bound to something' + self.__obj = obj + self.__objcls = cls or obj.__class__ + + def __getattribute__(self, name, get=object.__getattribute__): + obj, objcls = get(self, '_Super__obj'), get(self, '_Super__objcls') + attr = getattr(objcls.__bases__[0], name) + try: # return the bound descriptor + return attr.__get__(obj, objcls) + except AttributeError: # not a descriptor + return attr + +import readline, rlcompleter, re + +try: + from IPython.completer import Completer as BaseCompleter +except ImportError: + from rlcompleter import Completer as BaseCompleter + +class Completer(BaseCompleter): + def attr_matches(self, text): + m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text) + if not m: + return + expr, attr = m.group(1, 3) + object = eval(expr, self.namespace) + words = dir(object) + if hasattr(object,'__class__'): + words.append('__class__') + words += rlcompleter.get_class_members(object.__class__) + if hasattr(object, '_Trait__dic'): + words += object._Trait__dic.keys() + matches = [] + n = len(attr) + for word in words: + if word[:n] == attr and word != "__builtins__": + matches.append("%s.%s" % (expr, word)) + return matches + +readline.set_completer(Completer().complete) +readline.parse_and_bind("TAB: complete") |