summaryrefslogtreecommitdiffstats
path: root/ontogen.py
diff options
context:
space:
mode:
Diffstat (limited to 'ontogen.py')
-rw-r--r--ontogen.py390
1 files changed, 390 insertions, 0 deletions
diff --git a/ontogen.py b/ontogen.py
new file mode 100644
index 0000000..75318a0
--- /dev/null
+++ b/ontogen.py
@@ -0,0 +1,390 @@
+# vim: set fileencoding=UTF-8:
+# Copyright 2013 Red Hat, Inc.
+# Author: Jan Pokorný <jpokorny at redhat dot com>
+# Licensed under LGPL v2.1 (the same as original ns-schema.xsl)
+
+from sys import stdout, stderr
+from datetime import date
+from textwrap import TextWrapper, dedent
+from os import extsep, mkdir, symlink, remove
+from os.path import basename, exists, isdir, islink, splitext
+from subprocess import call
+from shlex import split
+#import codecs
+
+import logging
+
+logging.basicConfig()
+log = logging.getLogger(__name__)
+
+
+VERSIONSEP = '/'
+ONTSEP = '#'
+
+TEMPLATE_GENSHI = 'ontogen.template'
+TEMPLATE_XSLT = 'ns-schema.xsl'
+TEXT_INDENT = ' '
+TW = TextWrapper(initial_indent=TEXT_INDENT, subsequent_indent=TEXT_INDENT)
+tw = lambda x: TW.fill(x)
+twd = lambda x: tw(dedent(x))
+norm = lambda x: reduce(
+ lambda a, b: a and a + (b.islower() and b or '-' + b.lower()) or b.lower(),
+ x,
+ ''
+).replace('_', '-')
+
+
+#
+# Helpers
+#
+
+
+# classics: http://stackoverflow.com/a/1383402
+class ClassProperty(property):
+ def __get__(self, this, owner):
+ return self.fget.__get__(None, owner)()
+
+
+# another common fixer (adjusted): http://stackoverflow.com/a/13937525
+class InheritDocstring(type):
+ def __init__(this, name, bases, attrs):
+ if getattr(this, '__doc__', None) is None:
+ setattr(this, '__doc__', super(this, this).__doc__)
+
+
+def force_symlink(source, link_name):
+ if exists(link_name):
+ if islink(link_name):
+ remove(link_name)
+ else:
+ raise RuntimeError('{0} is in our way'.format(link_name))
+ symlink(source, link_name)
+
+
+#
+# Pure documentation level
+#
+
+
+class Example(object):
+ """Important data regarding examples"""
+ @ClassProperty
+ @classmethod
+ def comment(this):
+ return tw('\n'.join(this.__doc__.strip().splitlines()[:1]))
+
+ @ClassProperty
+ @classmethod
+ def pfx(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def code(this):
+ return '\n'.join(map(str.rstrip, this.__doc__.strip().splitlines()[2:]))
+
+ @ClassProperty
+ @classmethod
+ def image(this):
+ return ''
+
+
+#
+# Abstract RDF level
+#
+
+
+class RDFEntity(object):
+ """Common base for property, class and ontology"""
+
+ __metaclass__ = InheritDocstring
+
+ @ClassProperty
+ @classmethod
+ def id(this):
+ return ONTSEP + norm(this.__name__)
+
+ @ClassProperty
+ @classmethod
+ def namespaces(this):
+ return {}
+
+ @ClassProperty
+ @classmethod
+ def label(this):
+ return '\n'.join(this.__doc__.strip().splitlines()[:1]).strip()
+
+ @ClassProperty
+ @classmethod
+ def comment(this):
+ return '\n\n'.join(
+ map(
+ twd,
+ '\n'.join(map(
+ str.rstrip,
+ this.__doc__.strip().splitlines()[2:])).split('\n\n')
+ )
+ )
+
+ @ClassProperty
+ @classmethod
+ def status(this):
+ return ''
+
+
+#
+# RDF entities
+#
+
+
+class Class(RDFEntity):
+ """Important data regarding RDF class"""
+
+ # decorators
+
+ @classmethod
+ def inDomainOf(this, prop):
+ """Tells that decorated Property has decorating Class as a domain"""
+ assert issubclass(prop, Property)
+ if not prop.domain:
+ prop.domain = this.id
+ else:
+ log.warning('{0} already has domain set'
+ .format(prop))
+ return prop
+
+ @classmethod
+ def inRangeOf(this, prop):
+ """Tells that decorated Property has decorating Class as a range"""
+ assert issubclass(prop, Property)
+ if not prop.range:
+ prop.range = this.id
+ else:
+ log.warning('{0} already has range set'
+ .format(prop))
+ return prop
+
+ # members
+
+ @ClassProperty
+ @classmethod
+ def subClassOf(this):
+ return '' if this.__base__ == Class else this.__base__.id
+
+
+class Property(RDFEntity):
+ """Important data regarding RDF property"""
+
+ # members
+
+ @ClassProperty
+ @classmethod
+ def domain(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def range(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def subPropertyOf(this):
+ return '' if this.__base__ == Property else this.__base__.id
+
+
+class Ontology(RDFEntity):
+ """Important data regarding ontology"""
+
+ # decorators
+
+ @classmethod
+ def supersededBy(this, onto):
+ """Tells that decorated Ontology supersedes the decorating Ontology"""
+ assert issubclass(onto, Ontology)
+ if onto.priorVersion is None:
+ onto.priorVersion = this
+ else:
+ log.warning('{0} already has priorVersion set'.format(onto))
+ return onto
+
+ # members
+
+ @ClassProperty
+ @classmethod
+ def base_uri(this):
+ raise NotImplementedError
+
+ @ClassProperty
+ @classmethod
+ def version(this):
+ # sorting key
+ raise NotImplementedError
+
+ @ClassProperty
+ @classmethod
+ def base(this):
+ return this.base_uri + VERSIONSEP + this.version
+
+ @ClassProperty
+ @classmethod
+ def creator(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def issued(this):
+ return ''
+
+ @ClassProperty
+ @classmethod
+ def modified(this):
+ return date.today().isoformat()
+
+ @ClassProperty
+ @classmethod
+ def priorVersion(this):
+ return None
+
+ @ClassProperty
+ @classmethod
+ def classes(this):
+ return []
+
+ @ClassProperty
+ @classmethod
+ def properties(this):
+ return []
+
+ @ClassProperty
+ @classmethod
+ def examples(this):
+ return []
+
+ # extras
+
+ @classmethod
+ def valid(this):
+ proper_subclasses = all((
+ all(issubclass(c, Class) for c in this.classes),
+ all(issubclass(p, Property) for p in this.properties),
+ all(issubclass(e, Example) for e in this.examples),
+ ))
+ if not proper_subclasses:
+ log.warning('Not proper subclasses used')
+
+ ids = [
+ x.id for x in
+ reduce(lambda a, b: a + b,
+ (getattr(this, ent_name) for ent_name in ('classes',
+ 'properties')))
+ ]
+ unique_ids = len(ids) == len(set(ids))
+ if not unique_ids:
+ log.warning('Not unique IDs used')
+
+ return proper_subclasses and unique_ids
+
+ @classmethod
+ def _auto_filename(this):
+ base = this.base_uri.rpartition('/')[2]
+ if not exists(base):
+ mkdir(base)
+ elif not isdir(base):
+ raise RuntimeError('{0} is not a directory'.format(base))
+ return (base + VERSIONSEP + this.version + extsep + 'rdf',
+ base + extsep + 'rdf')
+
+ @classmethod
+ def gendoc(this, infile, outfile, template=TEMPLATE_XSLT):
+ cmd = 'xsltproc --stringparam xmlfile {reference} -o {outfile}' \
+ ' {template} {infile}'.format(
+ reference=basename(infile),
+ infile=infile,
+ outfile=outfile,
+ template=template
+ )
+ log.debug('running {0}'.format(split(cmd)))
+ if call(split(cmd)):
+ raise RuntimeError('Something went wrong while running {0}'
+ .format(cmd))
+
+ @classmethod
+ def generate(this, ontologies=(), template=None, outfile='-', gendoc=True):
+ from genshi.template import MarkupTemplate
+
+ namespaces = {}
+ for ns, ns_uri in reduce(
+ lambda a, b: a + b.namespaces.items(),
+ [this] + this.classes + this.properties,
+ []
+ ):
+ if namespaces.setdefault('xmlns:' + ns, ns_uri) != ns_uri:
+ print >>stderr, 'NS clash: {0} vs {1}'.format(
+ namespaces['xmlns:' + ns], ns_uri)
+
+ tmpl_filename = template or TEMPLATE_GENSHI
+ tmpl_fileobj = open(tmpl_filename)
+ #tmpl_fileobj = codecs.open(tmpl_filename, encoding='UTF-8')
+ tmpl = MarkupTemplate(tmpl_fileobj, tmpl_filename)
+ tmpl_fileobj.close()
+
+ symfile = None
+ if outfile is '-':
+ outobj = stdout
+ else:
+ if outfile == 'AUTO':
+ outfile, symfile = this._auto_filename()
+ outobj = open(outfile, "w")
+
+ tmpldict = dict(Ontology=this, namespaces=namespaces)
+ print >>outobj, \
+ tmpl.generate(**tmpldict).render('xml', strip_whitespace=False)
+ outobj.flush() # usually failing without this
+
+ # htmldoc
+ htmlfile = None
+ if gendoc and outfile != '-':
+ htmlfile = splitext(outfile)[0] + extsep + 'html'
+ this.gendoc(outfile, htmlfile)
+
+ # symlink to base (non-versioned) only for the newest ontology
+ if ontologies[-1] is this and symfile:
+ force_symlink(outfile, symfile)
+ if htmlfile:
+ force_symlink(htmlfile, splitext(symfile)[0] + extsep + 'html')
+
+ if outobj is not stdout:
+ outobj.close()
+
+ @classmethod
+ def __str__(this):
+ this.generate()
+
+
+#
+# Generalized view
+#
+
+class Ontologies(object):
+ """Groups multiple versions of ontologies (presumably of the same base)"""
+ def __init__(self, ontologies=()):
+ self._ontologies = []
+ if ontologies:
+ self.include(*ontologies)
+
+ # decorators
+
+ def include(self, *ontologies):
+ assert all(map(lambda o: o.valid(), ontologies))
+ assert reduce(lambda a, b: issubclass(a, b) or issubclass(b, a),
+ ontologies)
+ merge = self._ontologies + list(ontologies)
+ self._ontologies = list(sorted(set(merge), key=lambda o: o.version))
+ # make it decorator-compatible
+ return (lambda first=None, *others: first)(*ontologies)
+
+ # members
+
+ def generate_latest(self, **kwargs):
+ self._ontologies[-1].generate(tuple(self._ontologies), **kwargs)