[Checkins] SVN: classix/ A library that helps to glue classes to XML elements (lxml).

Martijn Faassen faassen at infrae.com
Fri Jun 6 12:42:30 EDT 2008


Log message for revision 87209:
  A library that helps to glue classes to XML elements (lxml).
  

Changed:
  A   classix/
  A   classix/trunk/
  A   classix/trunk/buildout.cfg
  A   classix/trunk/setup.py
  A   classix/trunk/src/
  A   classix/trunk/src/classix/
  A   classix/trunk/src/classix/README.txt
  A   classix/trunk/src/classix/__init__.py
  A   classix/trunk/src/classix/components.py
  A   classix/trunk/src/classix/directive.py
  A   classix/trunk/src/classix/meta.py
  A   classix/trunk/src/classix/tests.py

-=-
Added: classix/trunk/buildout.cfg
===================================================================
--- classix/trunk/buildout.cfg	                        (rev 0)
+++ classix/trunk/buildout.cfg	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,12 @@
+[buildout]
+develop = .
+parts = devpython test
+
+[devpython]
+recipe = zc.recipe.egg
+interpreter = devpython
+eggs = classix
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = classix

Added: classix/trunk/setup.py
===================================================================
--- classix/trunk/setup.py	                        (rev 0)
+++ classix/trunk/setup.py	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,20 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='classix',
+    version='0.1',
+    author='Martijn Faassen',
+    author_email='faassen at startifact.com',
+    description="""\
+Declarative way to associate classes with lxml XML elements.
+""",
+    packages=find_packages('src'),
+    package_dir = {'': 'src'},
+    include_package_data = True,
+    zip_safe=False,
+    install_requires=[
+    'setuptools',
+    'lxml == 2.0.6',
+    'martian >= 0.10',
+    ],
+)

Added: classix/trunk/src/classix/README.txt
===================================================================
--- classix/trunk/src/classix/README.txt	                        (rev 0)
+++ classix/trunk/src/classix/README.txt	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,168 @@
+Classix
+=======
+
+Classix is a thin layer over lxml's `custom element classes`_
+functionality that lets you configure them declaratively. It uses
+Martian_ to accomplish this effect, but to you this should be an
+implementation detail.
+
+.. _`custom element classes`: http://codespeak.net/lxml/element_classes.html
+
+.. _`Martian`: http://pypi.python.org/pypi/martian
+
+To parse an XML document you need a parser. An `XMLParser`` can be
+configured with custom element classes that will be associated to the
+right elements in the XML document after parsing. This way you can
+enrich the content model of your XML document.
+
+First we need to do the configuration for this package. This only needs to
+happen once for this package. First, we need to set up a ``GrokkerRegistry``.
+
+  >>> import martian
+  >>> reg = martian.GrokkerRegistry()
+
+Now we can grok the grokkers in this package::
+
+  >>> from classix import meta
+  >>> reg.grok('meta', meta)
+  True
+
+Now we can start using classix. You need to create an ``XMLParser``
+class that you will associate your custom element classes with::
+
+  >>> import classix
+  >>> class MyParser(classix.XMLParser):
+  ...     pass
+
+Let's grok the parser::
+
+  >>> reg.grok('MyParser', MyParser)
+  True
+
+Now you can set up classes to associate with particular elements in particular
+namespaces, for that parser::
+
+  >>> XMLNS = 'http://ns.example.com'
+  >>> class Test(classix.Element):
+  ...    classix.namespace(XMLNS)
+  ...    classix.parser(MyParser)
+  ... 
+  ...    def custom_method(self):
+  ...        return "The custom method"
+
+We also need to grok this::
+
+  >>> reg.grok('Test', Test)
+  True
+
+Now that we have it all set up, we can initialize the parser::
+
+  >>> parser = MyParser()
+
+Let's parse a bit of XML::
+
+  >>> xml = '''\
+  ...   <test xmlns="http://ns.example.com" />
+  ...   '''
+  >>> from lxml import etree
+  >>> root = etree.XML(xml, parser)
+  >>> root.custom_method()
+  'The custom method'
+
+No namespace
+------------
+
+Sometimes you want to associate a class with an element in no
+namespace at all. Do do this, you can set the namepace to None
+explicitly::
+
+  >>> reg = martian.GrokkerRegistry()
+  >>> reg.grok('meta', meta)
+  True
+
+  >>> class MyParser(classix.XMLParser):
+  ...     pass
+  >>> reg.grok('MyParser', MyParser)
+  True
+  >>> class Test(classix.Element):
+  ...     classix.namespace(None)
+  ...     classix.parser(MyParser)
+  ...     def custom_method(self):
+  ...        return 'The custom method for no namespace'
+  >>> reg.grok('Test', Test)
+  True
+  >>> parser = MyParser()
+  >>> no_ns_xml = '''\
+  ...   <test />
+  ...   '''
+  >>> root = etree.XML(no_ns_xml, parser)
+  >>> root.custom_method()
+  'The custom method for no namespace'
+
+When supplied with an element with a namespace, the ``Test`` class will
+not be associated with that element::
+
+  >>> root = etree.XML(xml, parser)
+  >>> root.custom_method()
+  Traceback (most recent call last):
+    ...
+  AttributeError: 'lxml.etree._Element' object has no attribute 'custom_method'
+
+Namespaces are assumed to be ``None`` if the ``classix.namespace``
+directive isn't used at all::
+
+  >>> class MyParser(classix.XMLParser):
+  ...     pass
+  >>> reg.grok('MyParser', MyParser)
+  True
+  >>> class Test(classix.Element):
+  ...     classix.parser(MyParser)
+  ...     def custom_method(self):
+  ...        return 'The custom method for no namespace 2'
+  >>> reg.grok('Test', Test)
+  True
+  >>> parser = MyParser()
+  >>> no_ns_xml = '''\
+  ...   <test />
+  ...   '''
+  >>> root = etree.XML(no_ns_xml, parser)
+  >>> root.custom_method()
+  'The custom method for no namespace 2'
+
+Namespaces in parser
+--------------------
+
+As a convenience, you can also configure the default namespace in the
+parser, as a fall-back so you don't have to specify it in all the
+element classes::
+
+  >>> reg = martian.GrokkerRegistry()
+  >>> reg.grok('meta', meta)
+  True
+
+  >>> class MyParserWithNamespace(classix.XMLParser):
+  ...    classix.namespace(XMLNS)
+  >>> reg.grok('MyParserWithNamespace', MyParserWithNamespace)
+  True
+ 
+  >>> class Test2(classix.Element):
+  ...    classix.parser(MyParserWithNamespace)
+  ...    classix.name('test') # also override name
+  ...    def custom_method(self):
+  ...        return "Another custom method"
+  >>> reg.grok('Test2', Test2)
+  True
+ 
+  >>> parser_ns = MyParserWithNamespace()
+  >>> root = etree.XML(xml, parser_ns)
+  >>> root.custom_method()
+  'Another custom method'
+
+Absent classix.parser directive
+-------------------------------
+
+If the ``classix.parser` directive is absent, ``classix.Element``
+subclasses will be automatically associated with the one
+``classix.Parser`` available in the module.
+
+

Added: classix/trunk/src/classix/__init__.py
===================================================================
--- classix/trunk/src/classix/__init__.py	                        (rev 0)
+++ classix/trunk/src/classix/__init__.py	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,2 @@
+from classix.directive import namespace, name, parser
+from classix.components import XMLParser, Element

Added: classix/trunk/src/classix/components.py
===================================================================
--- classix/trunk/src/classix/components.py	                        (rev 0)
+++ classix/trunk/src/classix/components.py	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,9 @@
+from lxml import etree
+
+class Element(etree.ElementBase):
+    pass
+    
+class XMLParser(etree.XMLParser):
+    def __init__(self, *args, **kw):
+        super(XMLParser, self).__init__(*args, **kw)
+        self.set_element_class_lookup(self._lookup)

Added: classix/trunk/src/classix/directive.py
===================================================================
--- classix/trunk/src/classix/directive.py	                        (rev 0)
+++ classix/trunk/src/classix/directive.py	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,21 @@
+import martian
+        
+class parser(martian.Directive):
+    scope = martian.CLASS
+    store = martian.ONCE
+    validate = martian.validateClass
+    
+def validateTextOrNone(self, value):
+    if value is None:
+        return value
+    return martian.validateText(self, value)
+
+class namespace(martian.Directive):
+    scope = martian.CLASS
+    store = martian.ONCE
+    validate = validateTextOrNone
+
+class name(martian.Directive):
+    scope = martian.CLASS
+    store = martian.ONCE
+    validate = martian.validateText

Added: classix/trunk/src/classix/meta.py
===================================================================
--- classix/trunk/src/classix/meta.py	                        (rev 0)
+++ classix/trunk/src/classix/meta.py	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,42 @@
+from lxml import etree
+import martian
+from classix.components import Element, XMLParser
+from classix.directive import namespace, name, parser
+
+def default_namespace(class_, module, parser, **data):
+    return namespace.bind().get(parser)
+
+def default_name(class_, module, parser, **data):
+    return class_.__name__.lower()
+
+class ElementGrokker(martian.ClassGrokker):
+    martian.component(Element)
+
+    martian.directive(parser)
+    martian.directive(namespace, get_default=default_namespace)
+    martian.directive(name, get_default=default_name)
+
+    def execute(self, class_, parser, namespace, name, **kw):
+        parser._lookup.add_element_class(namespace, name, class_)
+        return True
+
+class Lookup(etree.CustomElementClassLookup):
+    def __init__(self):
+        super(Lookup, self).__init__()
+        self._classes = {}
+        
+    def add_element_class(self, namespace, name, class_):
+        self._classes[(namespace, name)] = class_
+
+    def lookup(self, node_type, document, namespace, name):
+        return self._classes.get((namespace, name))
+
+class XMLParserGrokker(martian.ClassGrokker):
+    martian.component(XMLParser)
+    martian.priority(martian.priority.bind().get(ElementGrokker) + 1)
+
+    martian.directive(namespace)
+    
+    def execute(self, class_, **kw):
+        class_._lookup = Lookup()
+        return True

Added: classix/trunk/src/classix/tests.py
===================================================================
--- classix/trunk/src/classix/tests.py	                        (rev 0)
+++ classix/trunk/src/classix/tests.py	2008-06-06 16:42:29 UTC (rev 87209)
@@ -0,0 +1,13 @@
+import unittest, doctest
+
+globs = {}
+optionflags = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
+        
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTests([
+        doctest.DocFileSuite('README.txt',
+                             globs=globs,
+                             optionflags=optionflags),
+        ])
+    return suite



More information about the Checkins mailing list