6996768 - DataObjectCache should provide a method for importing all DataObjects from a given module
authorDarren Kenny <Darren.Kenny@Oracle.COM>
Fri, 05 Nov 2010 11:26:05 +0000
changeset 918 6b573787d03e
parent 917 64c14627472e
child 919 999ceaea57d4
6996768 - DataObjectCache should provide a method for importing all DataObjects from a given module
usr/src/lib/install_doc/data_object/__init__.py
usr/src/lib/install_doc/data_object/cache.py
usr/src/lib/install_doc/test/test_data_object_cache_registration.py
--- a/usr/src/lib/install_doc/data_object/__init__.py	Thu Nov 04 09:15:13 2010 -0700
+++ b/usr/src/lib/install_doc/data_object/__init__.py	Fri Nov 05 11:26:05 2010 +0000
@@ -36,6 +36,7 @@
 from abc import ABCMeta, abstractmethod
 
 from lxml import etree
+from solaris_install.logger import INSTALL_LOGGER_NAME
 
 # Define various Data Object specific exceptions
 
@@ -92,6 +93,9 @@
     # Define regular expression for extracting paths from strings.
     __STRING_REPLACEMENT_RE = re.compile("%{([^}]+)}")
 
+    # Reference for Install Logger
+    __logger = None
+
     def __init__(self, name):
         self._name = name
         self._parent = None
@@ -102,6 +106,30 @@
         # MutatableSequence sub-class
         self._children = []
 
+    @classmethod
+    def get_logger(cls):
+        ''' Returns reference to logger class
+
+        Use a class method and variable instead of an instance variable as
+        pickle has problems when it tries to pickle the reference to the 
+        logger.
+
+        Mainly used for logging from class methods, so most will just use
+        self.logger property if it's an object instance.
+        '''
+        if cls.__logger is None:
+            cls.__logger = logging.getLogger(INSTALL_LOGGER_NAME)
+
+        return cls.__logger
+
+    @property
+    def logger(self):
+        '''Instance accessor for the logger.
+
+        The use of a property is a nicer interface sub-classes to use.
+        '''
+        return DataObjectBase.get_logger()
+
     # Abstract class methods.
     # These methods must be implemented by DataObject sub-classes or an
     # Exception will be raised when they are instantiated.
@@ -440,7 +468,7 @@
 
         if not isinstance(obj, DataObjectBase):
             msg = "Invalid Child Type: %s" % (obj.__class__.__name__)
-            logging.error(msg)
+            DataObjectBase.get_logger().error(msg)
             raise TypeError(msg)
 
     # Methods for cloning / duplication objects
@@ -756,7 +784,7 @@
                     else:
                         value_str += "%s%s" % (value_separator, repr(value))
 
-                logging.debug("Replacing reference to '%s' with '%s'" %
+                self.logger.debug("Replacing reference to '%s' with '%s'" %
                     (matches.group(0), value_str))
                 new_string = new_string.replace(matches.group(0),
                     value_str, 1)
@@ -880,7 +908,7 @@
 
         if before is not None and after is not None:
             msg = "Both 'before' and 'after' should not be specified."
-            logging.error(msg)
+            self.logger.error(msg)
             raise ValueError(msg)
         elif before is not None:
             self._check_object_type(before)
@@ -888,7 +916,7 @@
                 insert_at = self._children.index(before)
             except ValueError:
                 msg = "Invalid value for 'before' while inserting children"
-                logging.error(msg)
+                self.logger.error(msg)
                 raise ObjectNotFoundError(msg)
         elif after is not None:
             self._check_object_type(after)
@@ -896,7 +924,7 @@
                 insert_at = self._children.index(after) + 1
             except ValueError:
                 msg = "Invalid value for 'after' while inserting children"
-                logging.error(msg)
+                self.logger.error(msg)
                 raise ObjectNotFoundError(msg)
         else:
             insert_at = len(self._children)
--- a/usr/src/lib/install_doc/data_object/cache.py	Thu Nov 04 09:15:13 2010 -0700
+++ b/usr/src/lib/install_doc/data_object/cache.py	Fri Nov 05 11:26:05 2010 +0000
@@ -25,7 +25,7 @@
 """Mechanism for providing a central store of in-memory data in the installer.
 """
 
-import logging
+import inspect
 import pickle
 
 from lxml import etree
@@ -155,7 +155,7 @@
             child.delete_children()
 
         msg = "DataObjectCache cleared!"
-        logging.info(msg)
+        self.logger.info(msg)
 
     @property
     def is_empty(self):
@@ -269,8 +269,12 @@
         importing XML to find classes that support the handling of XML
         snippets by calling the 'can_handle()' and 'from_xml()' methods.
 
-        'new_class_obj' may be either a single class object, or an iterable
-        object containing class objects.
+        'new_class_obj' may be either any of:
+        - a single class object or module
+        - an iterable object containing class objects and/or modules
+
+        Modules to be registered should contain classes that are sub-classes
+        of DataObjectBase.
 
         The priority defines the order in which classes are checked, with
         lower numbers being checked first - the default is for all registered
@@ -279,8 +283,8 @@
         Exceptions:
 
             TypeError
-            - thrown if class_obj is not a sub-class of DataObject or
-              an iterable object.
+            - thrown if class_obj is not a sub-class of DataObjectBase,
+              a Module, or an iterable object containing either of them.
 
             ValueError
             - thrown if priority is not in the range 0-100 inclusive.
@@ -299,7 +303,25 @@
 
         # Should have iterable at this point, so loop through.
         for class_ref in class_list:
-            if issubclass(class_ref, DataObjectBase):
+            if inspect.ismodule(class_ref):
+                # It's not really a class, it's a module, so get it's classes
+                to_register = []
+                for name, value in \
+                    inspect.getmembers(class_ref, inspect.isclass):
+
+                    if inspect.getmodule(value) != class_ref:
+                        # Skip anything that didn't originate from the module
+                        # itself (e.g. DataObject imported into the module)
+                        continue
+                    if issubclass(value, DataObjectBase):
+                        to_register.append(value)
+
+                if len(to_register) > 0:
+                    # Do a recursive call to self to register found classes
+                    DataObjectCache.register_class(to_register, priority)
+
+            elif issubclass(class_ref, DataObjectBase):
+                # It's definitely a valid class to register it.
                 _CACHE_CLASS_REGISTRY.setdefault(priority, [])\
                     .append(class_ref)
             else:
--- a/usr/src/lib/install_doc/test/test_data_object_cache_registration.py	Thu Nov 04 09:15:13 2010 -0700
+++ b/usr/src/lib/install_doc/test/test_data_object_cache_registration.py	Fri Nov 05 11:26:05 2010 +0000
@@ -33,6 +33,7 @@
 import solaris_install.data_object.cache as DOC
 from simple_data_object import SimpleDataObject, SimpleDataObject2, \
     SimpleDataObject3, SimpleDataObject4
+import simple_data_object
 
 # Define two classes that both handle the same tag, but have
 # different priorities.
@@ -168,6 +169,59 @@
             "EXPECTED:\n%s\nGOT:\n%s\n" %
             (expected_registered_classes_str, txt))
 
+    def test_doc_registration_classes_from_module_same_prio(self):
+        '''Validate registration of classes in a module at same priority'''
+        # Compensate for indent
+        indentation = '''\
+        '''
+        expected_registered_classes_str = '''\
+        ============================
+        Registered Classes:
+        [Priority = 30]
+            <class 'simple_data_object.SimpleDataObject'>
+            <class 'simple_data_object.SimpleDataObject2'>
+            <class 'simple_data_object.SimpleDataObject3'>
+            <class 'simple_data_object.SimpleDataObject4'>
+            <class 'simple_data_object.SimpleDataObject5'>
+            <class 'simple_data_object.SimpleDataObjectHandlesChildren'>
+            <class 'simple_data_object.SimpleDataObjectNoXml'>
+        ============================
+        '''.replace(indentation, "")
+        DataObjectCache.register_class(simple_data_object, priority=30)
+
+        txt = self.doc.get_registered_classes_str()
+
+        self.assertEquals(expected_registered_classes_str, txt,
+            "EXPECTED:\n%s\nGOT:\n%s\n" %
+            (expected_registered_classes_str, txt))
+
+    def test_doc_registration_classes_from_module_in_list_same_prio(self):
+        '''Validate registration of classes in a module list at same priority
+        '''
+        # Compensate for indent
+        indentation = '''\
+        '''
+        expected_registered_classes_str = '''\
+        ============================
+        Registered Classes:
+        [Priority = 30]
+            <class 'simple_data_object.SimpleDataObject'>
+            <class 'simple_data_object.SimpleDataObject2'>
+            <class 'simple_data_object.SimpleDataObject3'>
+            <class 'simple_data_object.SimpleDataObject4'>
+            <class 'simple_data_object.SimpleDataObject5'>
+            <class 'simple_data_object.SimpleDataObjectHandlesChildren'>
+            <class 'simple_data_object.SimpleDataObjectNoXml'>
+        ============================
+        '''.replace(indentation, "")
+        DataObjectCache.register_class([simple_data_object], priority=30)
+
+        txt = self.doc.get_registered_classes_str()
+
+        self.assertEquals(expected_registered_classes_str, txt,
+            "EXPECTED:\n%s\nGOT:\n%s\n" %
+            (expected_registered_classes_str, txt))
+
     def test_doc_registration_multiple_classes_same_prio(self):
         '''Validate registration of multiple classes at same priority'''
         # Compensate for indent