[registry] introduce RegistrableObject and RegistrableInstance base classes. Closes #98742

and make them mandatory for automatic registration. Cleanup automatic registration code accordingly.

Instances are now registrable, and automatically registered provided they inherit from RegistrableInstance.

authorSylvain Thénault <sylvain.thenault@logilab.fr>
changesetdc5f7cbd3134
branchdefault
phasepublic
hiddenno
parent revision#56cf68a9c4f0 [registry] cleanup the doc and add some comments
child revision#b1a0f91d9bdc [registry] use register_all when no registration callback defined. Closes #111011
files modified by this revision
ChangeLog
registry.py
test/data/regobjects.py
test/data/regobjects2.py
test/unittest_registry.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1358168928 -3600
# Mon Jan 14 14:08:48 2013 +0100
# Node ID dc5f7cbd31343756bf327691f1636a07833707f3
# Parent 56cf68a9c4f0ccf4763103ad1f2e44db6861fabe
[registry] introduce RegistrableObject and RegistrableInstance base classes. Closes #98742

and make them mandatory *for automatic registration*. Cleanup automatic registration
code accordingly.

Instances are now registrable, and automatically registered provided they inherit
from RegistrableInstance.

diff --git a/ChangeLog b/ChangeLog
@@ -1,12 +1,20 @@
1  ChangeLog for logilab.common
2  ============================
3 
4  --
5      * registry:
6 -      - introduce objid and objname methods on Registry instead of classid
7 -        function and inlined code (closes #98742)
8 +
9 +      - introduce RegistrableObject base class, mandatory to make
10 +        classes automatically registrable, and cleanup code
11 +        accordingly
12 +
13 +      - introduce objid and objname methods on Registry instead of
14 +        classid function and inlined code plus other refactorings to allow
15 +        arbitrary objects to be registered, provided they inherit from new
16 +        RegistrableInstance class (closes #98742)
17 +
18        - deprecate usage of leading underscore to skip object registration, using
19          __abstract__ explicitly is better and notion of registered object 'name'
20          is now somewhat fuzzy
21 
22      * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436)
@@ -310,11 +318,11 @@
23  2010-02-10  --  0.47.0
24      * adbh: changed backup / restore api (BREAKS COMPAT):
25        - backup_command is now backup_commands (eg return a list of commands)
26        - each command returned in backup_commands/restore_commands may now
27          be list that may be used as argument to subprocess.call, or a string
28 -	which will the requires a subshell
29 +        which will the requires a subshell
30        - new sql_rename_col method
31 
32      * deprecation: deprecated now takes an optional 'stacklevel' argument, default to 2
33 
34      * date: some functions to ease python's datetime module usage have been backported
@@ -324,13 +332,13 @@
35 
36  2009-12-23  --  0.46.0
37      * db / adbh: added SQL Server support using Pyodbc
38 
39      * db:
40 -	- New optional extra_args argument to get_connection.
41 -	- Support Windows Auth for SQLServer by giving
42 -	  extra_args='Trusted_Connection' to the sqlserver2005 driver
43 +        - New optional extra_args argument to get_connection.
44 +        - Support Windows Auth for SQLServer by giving
45 +          extra_args='Trusted_Connection' to the sqlserver2005 driver
46 
47 
48 
49  2009-11-23  --  0.45.2
50      * configuration:
@@ -499,11 +507,11 @@
51 
52 
53  2008-10-01  --  0.35.2
54      * configuration:
55        - fix #6011: lgc.configuration ignore customized option values
56 -      - fix #3278: man page generation broken	
57 +      - fix #3278: man page generation broken   
58 
59      * dropped context.py module which broke the debian package when
60        some python <2.5 is installed (#5979)
61 
62 
@@ -650,11 +658,11 @@
63 
64 
65 
66  2007-12-11  --  0.25.1
67      * pytest: new --profile option, setup module / teardown module hook,
68 -	      other fixes and enhancements
69 +              other fixes and enhancements
70 
71      * db: mysql support fixes
72 
73      * adbh: fix postgres list_indices implementation
74 
@@ -699,11 +707,11 @@
75 
76      * clcommands: pop_args accept None as value for expected_size_after,
77        meaning remaining args should not be checked
78 
79      * interface: new extend function to dynamically add an implemented interface
80 -      to a new style class	
81 +      to a new style class      
82 
83 
84 
85  2007-06-25  --  0.22.2
86      * new 'typechanged' action for configuration.read_old_config
@@ -771,11 +779,11 @@
87      * moved some stuff which was in common __init__ file into specific
88        module. At this occasion new "decorators" and "deprecation" modules
89        has been added
90 
91      * deprecated fileutils.[files_by_ext,include_files_by_ext,exclude_files_by_ext]
92 -      functions in favor of new function shellutils.find	
93 +      functions in favor of new function shellutils.find        
94 
95      * mark the following modules for deprecation, they will be removed in a
96        near version:
97 
98      * astutils: moved to astng
@@ -1000,12 +1008,12 @@
99 
100  2005-09-06  --  0.12.0
101      * shellutils: bug fix in mv()
102 
103      * compat:
104 - 	  - use set when available
105 -	  - added sorted and reversed
106 +          - use set when available
107 +          - added sorted and reversed
108 
109      * table: new methods and some optimizations
110 
111      * tree: added some deprecation warnings
112 
diff --git a/registry.py b/registry.py
@@ -76,15 +76,19 @@
113  __docformat__ = "restructuredtext en"
114 
115  import sys
116  import types
117  import weakref
118 +import traceback as tb
119  from os import listdir, stat
120  from os.path import join, isdir, exists
121  from logging import getLogger
122 +from warnings import warn
123 
124 +from logilab.common.modutils import modpath_from_file
125  from logilab.common.logging_ext import set_log_methods
126 +from logilab.common.decorators import classproperty
127 
128 
129  class RegistryException(Exception):
130      """Base class for registry exception."""
131 
@@ -110,48 +114,96 @@
132      def __str__(self):
133          return ('args: %s, kwargs: %s\ncandidates: %s'
134                  % (self.args, self.kwargs.keys(), self.objects))
135 
136 
137 +def _modname_from_path(path, extrapath=None):
138 +    modpath = modpath_from_file(path, extrapath)
139 +    # omit '__init__' from package's name to avoid loading that module
140 +    # once for each name when it is imported by some other object
141 +    # module. This supposes import in modules are done as::
142 +    #
143 +    #   from package import something
144 +    #
145 +    # not::
146 +    #
147 +    #   from package.__init__ import something
148 +    #
149 +    # which seems quite correct.
150 +    if modpath[-1] == '__init__':
151 +        modpath.pop()
152 +    return '.'.join(modpath)
153 +
154 +
155  def _toload_info(path, extrapath, _toload=None):
156      """Return a dictionary of <modname>: <modpath> and an ordered list of
157      (file, module name) to load
158      """
159 -    from logilab.common.modutils import modpath_from_file
160      if _toload is None:
161          assert isinstance(path, list)
162          _toload = {}, []
163      for fileordir in path:
164          if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
165              subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
166              _toload_info(subfiles, extrapath, _toload)
167          elif fileordir[-3:] == '.py':
168 -            modpath = modpath_from_file(fileordir, extrapath)
169 -            # omit '__init__' from package's name to avoid loading that module
170 -            # once for each name when it is imported by some other object
171 -            # module. This supposes import in modules are done as::
172 -            #
173 -            #   from package import something
174 -            #
175 -            # not::
176 -            #
177 -            #   from package.__init__ import something
178 -            #
179 -            # which seems quite correct.
180 -            if modpath[-1] == '__init__':
181 -                modpath.pop()
182 -            modname = '.'.join(modpath)
183 +            modname = _modname_from_path(fileordir, extrapath)
184              _toload[0][modname] = fileordir
185              _toload[1].append((fileordir, modname))
186      return _toload
187 
188 
189 -def class_registries(cls, registryname):
190 -    """return a tuple of registry names (see __registries__)"""
191 -    if registryname:
192 -        return (registryname,)
193 -    return cls.__registries__
194 +class RegistrableObject(object):
195 +    """This is the base class for registrable objects which are selected
196 +    according to a context.
197 +
198 +    :attr:`__registry__`
199 +      name of the registry for this object (string like 'views',
200 +      'templates'...). You may want to define `__registries__` directly if your
201 +      object should be registered in several registries.
202 +
203 +    :attr:`__regid__`
204 +      object's identifier in the registry (string like 'main',
205 +      'primary', 'folder_box')
206 +
207 +    :attr:`__select__`
208 +      class'selector
209 +
210 +    Moreover, the `__abstract__` attribute may be set to True to indicate that a
211 +    class is abstract and should not be registered.
212 +
213 +    You don't have to inherit from this class to put it in a registry (having
214 +    `__regid__` and `__select__` is enough), though this is needed for classes
215 +    that should be automatically registered.
216 +    """
217 +
218 +    __registry__ = None
219 +    __regid__ = None
220 +    __select__ = None
221 +    __abstract__ = True # see doc snipppets below (in Registry class)
222 +
223 +    @classproperty
224 +    def __registries__(cls):
225 +        if cls.__registry__ is None:
226 +            return ()
227 +        return (cls.__registry__,)
228 +
229 +
230 +class RegistrableInstance(RegistrableObject):
231 +    """Inherit this class if you want instances of the classes to be
232 +    automatically registered.
233 +    """
234 +
235 +    def __new__(cls, *args, **kwargs):
236 +        """Add a __module__ attribute telling the module where the instance was
237 +        created, for automatic registration.
238 +        """
239 +        obj = super(RegistrableInstance, cls).__new__(cls)
240 +        # XXX subclass must no override __new__
241 +        filepath = tb.extract_stack(limit=2)[0][0]
242 +        obj.__module__ = _modname_from_path(filepath)
243 +        return obj
244 
245 
246  class Registry(dict):
247      """The registry store a set of implementations associated to identifier:
248 
@@ -343,31 +395,42 @@
249              if self.debugmode:
250                  # raise bare exception in debug mode
251                  raise Exception(msg % (winners, args, kwargs.keys()))
252              self.error(msg, winners, args, kwargs.keys())
253          # return the result of calling the object
254 -        return winners[0](*args, **kwargs)
255 +        return self.selected(winners[0], args, kwargs)
256 +
257 +    def selected(self, winner, args, kwargs):
258 +        """override here if for instance you don't want "instanciation"
259 +        """
260 +        return winner(*args, **kwargs)
261 
262      # these are overridden by set_log_methods below
263      # only defining here to prevent pylint from complaining
264      info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
265 
266 
267 +def obj_registries(cls, registryname=None):
268 +    """return a tuple of registry names (see __registries__)"""
269 +    if registryname:
270 +        return (registryname,)
271 +    return cls.__registries__
272 +
273 +
274  class RegistryStore(dict):
275      """This class is responsible for loading objects and storing them
276      in their registry which is created on the fly as needed.
277 
278      It handles dynamic registration of objects and provides a
279      convenient api to access them. To be recognized as an object that
280      should be stored into one of the store's registry
281      (:class:`Registry`), an object must provide the following
282      attributes, used control how they interact with the registry:
283 
284 -    :attr:`__registry__` or `__registries__`
285 -      name of the registry for this object (string like 'views', 'templates'...)
286 -      or list of registry names if you want your object to be added to multiple
287 -      registries
288 +    :attr:`__registries__`
289 +      list of registry names (string like 'views', 'templates'...) into which
290 +      the object should be registered
291 
292      :attr:`__regid__`
293        object identifier in the registry (string like 'main',
294        'primary', 'folder_box')
295 
@@ -511,70 +574,64 @@
296          except RegistryNotFound:
297              self[regid] = self.registry_class(regid)(self.debugmode)
298              return self[regid]
299 
300      def register_all(self, objects, modname, butclasses=()):
301 -        """register all given `objects`. Objects which are not from the module
302 +        """register registrable objects into `objects`.
303 +
304 +        Registrable objects are properly configured subclasses of
305 +        :class:`RegistrableObject`.  Objects which are not defined in the module
306          `modname` or which are in `butclasses` won't be registered.
307 
308          Typical usage is:
309 
310          .. sourcecode:: python
311 
312              store.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
313 
314          So you get partially automatic registration, keeping manual registration
315          for some object (to use
316 -        :meth:`~logilab.common.registry.RegistryStore.register_and_replace`
317 -        for instance)
318 +        :meth:`~logilab.common.registry.RegistryStore.register_and_replace` for
319 +        instance).
320          """
321          assert isinstance(modname, basestring), \
322              'modname expected to be a module name (ie string), got %r' % modname
323          for obj in objects:
324 -            try:
325 -                if obj.__module__ != modname or obj in butclasses:
326 -                    continue
327 +            if self.is_registrable(obj) and obj.__module__ == modname and not obj in butclasses:
328                  oid = obj.__regid__
329 -            except AttributeError:
330 -                continue
331 -            if oid and not obj.__dict__.get('__abstract__'):
332 -                self.register(obj, oid=oid)
333 +                if oid and not obj.__dict__.get('__abstract__'):
334 +                    self.register(obj, oid=oid)
335 
336      def register(self, obj, registryname=None, oid=None, clear=False):
337          """register `obj` implementation into `registryname` or
338 -        `obj.__registry__` if not specified, with identifier `oid` or
339 +        `obj.__registries__` if not specified, with identifier `oid` or
340          `obj.__regid__` if not specified.
341 
342          If `clear` is true, all objects with the same identifier will be
343          previously unregistered.
344          """
345          assert not obj.__dict__.get('__abstract__'), obj
346 -        try:
347 -            vname = obj.__name__
348 -        except AttributeError:
349 -            # XXX may occurs?
350 -            vname = obj.__class__.__name__
351 -        for registryname in class_registries(obj, registryname):
352 +        for registryname in obj_registries(obj, registryname):
353              registry = self.setdefault(registryname)
354              registry.register(obj, oid=oid, clear=clear)
355              self.debug("register %s in %s['%s']",
356                         registry.objname(obj), registryname, oid or obj.__regid__)
357              self._loadedmods.setdefault(obj.__module__, {})[registry.objid(obj)] = obj
358 
359      def unregister(self, obj, registryname=None):
360          """unregister `obj` object from the registry `registryname` or
361 -        `obj.__registry__` if not specified.
362 +        `obj.__registries__` if not specified.
363          """
364 -        for registryname in class_registries(obj, registryname):
365 +        for registryname in obj_registries(obj, registryname):
366              registry = self[registryname]
367              registry.unregister(obj)
368              self.debug("unregister %s from %s['%s']",
369                         registry.objname(obj), registryname, obj.__regid__)
370 
371      def register_and_replace(self, obj, replaced, registryname=None):
372          """register `obj` object into `registryname` or
373 -        `obj.__registry__` if not specified. If found, the `replaced` object
374 +        `obj.__registries__` if not specified. If found, the `replaced` object
375          will be unregistered first (else a warning will be issued as it is
376          generally unexpected).
377          """
378          for registryname in obj_registries(obj, registryname):
379              registry = self[registryname]
@@ -663,72 +720,111 @@
380          # load the module
381          module = load_module_from_name(modname)
382          self.load_module(module)
383 
384      def load_module(self, module):
385 -        """load objects from a module using registration_callback() when it exists
386 +        """Automatically handle module objects registration.
387 +
388 +        Instances are registered as soon as they are hashable and have the
389 +        following attributes:
390 +
391 +        * __regid__ (a string)
392 +        * __select__ (a callable)
393 +        * __registries__ (a tuple/list of string)
394 +
395 +        For classes this is a bit more complicated :
396 +
397 +        - first ensure parent classes are already registered
398 +
399 +        - class with __abstract__ == True in their local dictionary are skipped
400 +
401 +        - object class needs to have registries and identifier properly set to a
402 +          non empty string to be registered.
403          """
404          self.info('loading %s from %s', module.__name__, module.__file__)
405          if hasattr(module, 'registration_callback'):
406              module.registration_callback(self)
407          else:
408              for obj in vars(module).values():
409 -                self._load_ancestors_then_object(module.__name__, obj)
410 +                if self.is_registrable(obj) and obj.__module__ == module.__name__:
411 +                    if isinstance(obj, type):
412 +                        self._load_ancestors_then_object(module.__name__, obj)
413 +                    else:
414 +                        self.register(obj)
415 
416      def _load_ancestors_then_object(self, modname, objectcls):
417 -        """handle automatic object class registration:
418 -
419 -        - first ensure parent classes are already registered
420 -
421 -        - class with __abstract__ == True in their local dictionary are skipped
422 -
423 -        - object class needs to have __registry__ and __regid__ attributes
424 -          set to a non empty string to be registered.
425 +        """handle class registration according to rules defined in
426 +        :meth:`load_module`
427          """
428          # imported classes
429 -        objmodname = getattr(objectcls, '__module__', None)
430 +        objmodname = objectcls.__module__
431          if objmodname != modname:
432              # The module of the object is not the same as the currently
433              # worked on module, or this is actually an instance, which
434              # has no module at all
435              if objmodname in self._toloadmods:
436                  # if this is still scheduled for loading, let's proceed immediately,
437                  # but using the object module
438                  self.load_file(self._toloadmods[objmodname], objmodname)
439              return
440 -        # skip non registerable object
441 -        try:
442 -            if not (getattr(objectcls, '__regid__', None)
443 -                    and getattr(objectcls, '__select__', None)):
444 -                return
445 -        except TypeError:
446 -            return
447 -        reg = self.setdefault(class_registries(obj)[0])
448 -        clsid = reg.objid(obj)
449 +        # ensure object hasn't been already processed
450 +        clsid = '%s.%s' % (objmodname, objectcls.__name__)
451          if clsid in self._loadedmods[modname]:
452              return
453          self._loadedmods[modname][clsid] = objectcls
454 +        # ensure ancestors are registered
455          for parent in objectcls.__bases__:
456              self._load_ancestors_then_object(modname, parent)
457 -        if reg.objname(obj)[0] == '_':
458 +        # ensure object is registrable
459 +        if not self.is_registrable(objectcls):
460 +            return
461 +        # backward compat
462 +        reg = self.setdefault(obj_registries(objectcls)[0])
463 +        if reg.objname(objectcls)[0] == '_':
464              warn("[lgc 0.59] object whose name start with '_' won't be "
465                   "skipped anymore at some point, use __abstract__ = True "
466 -                 "instead (%s)" % obj, DeprecationWarning)
467 +                 "instead (%s)" % objectcls, DeprecationWarning)
468              return
469 +        # register, finally
470 +        self.register(objectcls)
471 
472 -        if (objectcls.__dict__.get('__abstract__')
473 -            or not objectcls.__registries__
474 -            or not objectcls.__regid__):
475 -            return
476 +    @classmethod
477 +    def is_registrable(cls, obj):
478 +        """ensure `obj` should be registered
479 
480 -        try:
481 -            self.register(objectcls)
482 -        except Exception, ex:
483 -            if self.debugmode:
484 -                raise
485 -            self.exception('object %s registration failed: %s',
486 -                           objectcls, ex)
487 +        as arbitrary stuff may be registered, do a lot of check and warn about
488 +        weird cases (think to dumb proxy objects)
489 +        """
490 +        if isinstance(obj, type):
491 +            if not issubclass(obj, RegistrableObject):
492 +                # ducktyping backward compat
493 +                if not (getattr(obj, '__registries__', None)
494 +                        and getattr(obj, '__regid__', None)
495 +                        and getattr(obj, '__select__', None)):
496 +                    return False
497 +            elif issubclass(obj, RegistrableInstance):
498 + 		return False
499 +        elif not isinstance(obj, RegistrableInstance):
500 +            return False
501 +        if not obj.__regid__:
502 +            return False # no regid
503 +        registries = obj.__registries__
504 +        if not registries:
505 +            return False # no registries
506 +        selector = obj.__select__
507 +        if not selector:
508 +            return False # no selector
509 +        if obj.__dict__.get('__abstract__', False):
510 +            return False
511 +        # then detect potential problems that should be warned
512 +        if not isinstance(registries, (tuple, list)):
513 +            cls.warning('%s has __registries__ which is not a list or tuple', obj)
514 +            return False
515 +        if not callable(selector):
516 +            cls.warning('%s has not callable __select__', obj)
517 +            return False
518 +        return True
519 
520      # these are overridden by set_log_methods below
521      # only defining here to prevent pylint from complaining
522      info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
523 
@@ -1004,8 +1100,11 @@
524 
525  from logilab.common.deprecation import deprecated
526 
527  @deprecated('[lgc 0.59] use Registry.objid class method instead')
528  def classid(cls):
529 -    """returns a unique identifier for an object class"""
530      return '%s.%s' % (cls.__module__, cls.__name__)
531 
532 +@deprecated('[lgc 0.59] use obj_registries function instead')
533 +def class_registries(cls, registryname):
534 +    return obj_registries(cls, registryname)
535 +
diff --git a/test/data/regobjects.py b/test/data/regobjects.py
@@ -0,0 +1,22 @@
536 +"""unittest_registry data file"""
537 +from logilab.common.registry import yes, RegistrableObject, RegistrableInstance
538 +
539 +class Proxy(object):
540 +    """annoying object should that not be registered, nor cause error"""
541 +    def __getattr__(self, attr):
542 +        return 1
543 +
544 +trap = Proxy()
545 +
546 +class AppObjectClass(RegistrableObject):
547 +    __registry__ = 'zereg'
548 +    __regid__ = 'appobject1'
549 +    __select__ = yes()
550 +
551 +class AppObjectInstance(RegistrableInstance):
552 +    __registry__ = 'zereg'
553 +    __select__ = yes()
554 +    def __init__(self, regid):
555 +        self.__regid__ = regid
556 +
557 +appobject2 = AppObjectInstance('appobject2')
diff --git a/test/data/regobjects2.py b/test/data/regobjects2.py
@@ -0,0 +1,8 @@
558 +from logilab.common.registry import RegistrableObject, RegistrableInstance, yes
559 +
560 +class MyRegistrableInstance(RegistrableInstance):
561 +    __regid__ = 'appobject3'
562 +    __select__ = yes()
563 +    __registry__ = 'zereg'
564 +
565 +instance = MyRegistrableInstance()
diff --git a/test/unittest_registry.py b/test/unittest_registry.py
@@ -1,6 +1,6 @@
566 -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
567 +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
568  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
569  #
570  # This file is part of Logilab-Common.
571  #
572  # Logilab-Common is free software: you can redistribute it and/or modify it under the
@@ -17,14 +17,21 @@
573  # with Logilab-Common.  If not, see <http://www.gnu.org/licenses/>.
574  """unit tests for selectors mechanism"""
575  from __future__ import with_statement
576 
577  import gc
578 +import logging
579 +import os.path as osp
580 +import sys
581  from operator import eq, lt, le, gt
582 +from contextlib import contextmanager
583 +
584 +logging.basicConfig(level=logging.ERROR)
585 +
586  from logilab.common.testlib import TestCase, unittest_main
587 
588 -from logilab.common.registry import Predicate, AndPredicate, OrPredicate, wrap_predicates
589 +from logilab.common.registry import *
590 
591 
592  class _1_(Predicate):
593      def __call__(self, *args, **kwargs):
594          return 1
@@ -157,7 +164,40 @@
595          self.assertEqual(s1(None), 2)
596          self.assertEqual(s2(None), 0)
597          self.assertEqual(s3(None), 0)
598          self.assertEqual(self.count, 8)
599 
600 +@contextmanager
601 +def prepended_syspath(path):
602 +    sys.path.insert(0, path)
603 +    yield
604 +    sys.path = sys.path[1:]
605 +
606 +class RegistryStoreTC(TestCase):
607 +
608 +    def test_autoload(self):
609 +        store = RegistryStore()
610 +        store.setdefault('zereg')
611 +        with prepended_syspath(self.datadir):
612 +            store.register_objects([self.datapath('regobjects.py'),
613 +                                    self.datapath('regobjects2.py')])
614 +        self.assertEqual(['zereg'], store.keys())
615 +        self.assertEqual(set(('appobject1', 'appobject2', 'appobject3')),
616 +                         set(store['zereg']))
617 +
618 +
619 +class RegistrableInstanceTC(TestCase):
620 +
621 +    def test_instance_modulename(self):
622 +        # no inheritance
623 +        obj = RegistrableInstance()
624 +        self.assertEqual(obj.__module__, 'unittest_registry')
625 +        # with inheritance from another python file
626 +        with prepended_syspath(self.datadir):
627 +            from regobjects2 import instance, MyRegistrableInstance
628 +            instance2 = MyRegistrableInstance()
629 +            self.assertEqual(instance.__module__, 'regobjects2')
630 +            self.assertEqual(instance2.__module__, 'unittest_registry')
631 +
632 +
633  if __name__ == '__main__':
634      unittest_main()