# 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, dirname, exists, isdir, islink, join, splitext from subprocess import call from shlex import split #import codecs import logging logging.basicConfig() log = logging.getLogger(__name__) VERSIONSEP = '/' ONTSEP = '#' TEMPLATE_GENSHI = join(dirname(__file__), '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)