From a8d838c8d268c054a3a369a3a98c08182958f19b Mon Sep 17 00:00:00 2001 From: Jan Pokorný Date: Fri, 22 Mar 2013 20:59:59 +0100 Subject: Now we can account for ontogen project, not a mere stylesheet anymore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also add README Signed-off-by: Jan Pokorný --- ontogen.py | 390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 ontogen.py (limited to 'ontogen.py') 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ý +# 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) -- cgit