# 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 modules, stdout, stderr from datetime import date from textwrap import TextWrapper, dedent from os import extsep, getcwd, mkdir, symlink, remove from os.path import basename, dirname, exists, isdir, islink, join, splitext, \ sep as pathsep from subprocess import call from shlex import split #import codecs import logging logging.basicConfig() log = logging.getLogger(__name__) #VERSIONSEP = sep # non-unix paths -> problems VERSIONSEP = '/' ONTSEP = '#' INDEX = 'index.html' TEMPLATE_GENSHI = join(dirname(__file__), 'ontogen.template') TEMPLATE_XSLT = join(dirname(__file__), '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): """Allows docstrings to be inherited (restricted to external classes)""" def __init__(this, name, bases, attrs): if __name__ not in (this.__module__, this.__base__.__module__): if not getattr(this, '__doc__', None): this.__doc__ = super(this, this).__doc__ super(InheritDocstring, InheritDocstring).__init__(this, name, bases, attrs) def custom_symlink(source, link_name, report=False): """symlink + overwriting (unlink-new) of existing symlinks""" if exists(link_name): if islink(link_name): remove(link_name) else: raise RuntimeError('{0} is in our way'.format(link_name)) if report is not False: print '{0}: {1}'.format(report, 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 '' # combine this "example" with Makefile to trigger the magic class ExampleSelfFigureProto(type): def __new__(self, ontology): base = ontology.base class ExampleSelfFigure(Example): """Illustrative figure of the vocabulary""" image_full = ontology.version + VERSIONSEP + base + extsep + 'svg' image = base + extsep + 'svg' # referenced from html symlink = base + extsep + 'svg' return ExampleSelfFigure # # 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 documentation(this): return this.__doc__ @ClassProperty @classmethod def label(this): return '\n'.join(this.documentation.strip().splitlines()[:1]).strip() @ClassProperty @classmethod def comment(this): return '\n\n'.join( map( twd, '\n'.join(map( str.rstrip, this.documentation.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 - override @ClassProperty @classmethod def documentation(this): ret = getattr(this, '__doc__', '') or modules[this.__module__].__doc__ return ret # members @ClassProperty @classmethod def base_uri(this): module = modules[this.__module__] if hasattr(module, 'base_uri'): return module.base_uri raise NotImplementedError @ClassProperty @classmethod def version(this): # sorting key raise NotImplementedError @ClassProperty @classmethod def base_uri_full(this): return this.base_uri + VERSIONSEP + this.version @ClassProperty @classmethod def base(this): # TODO: cache me please? return this.base_uri.rpartition('/')[2] @ClassProperty @classmethod def creator(this): module = modules[this.__module__] if hasattr(module, '__author__'): return module.__author__ 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 _prepare_targets(this): base = this.base version = this.version if not exists(version): mkdir(version) elif not isdir(version): raise RuntimeError('{0} is not a directory'.format(version)) return (version + VERSIONSEP + base + extsep + 'rdf', base + extsep + 'rdf') @classmethod def gendoc(this, infile, outfile, template=TEMPLATE_XSLT): """Generate HTML as per RDF (ns-schema.xsl wrapper)""" 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}; pwd={1}' .format(cmd, getcwd())) @classmethod def generate(this, ontologies=(), template=None, outfile='-', gendoc=True): """Generate RDF as per declarations""" 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._prepare_targets() 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 = join(dirname(outfile), INDEX) this.gendoc(outfile, htmlfile) # symlink to base (non-versioned) only for the newest ontology if ontologies[-1] is this and symfile: custom_symlink(outfile, symfile, 'output') if htmlfile: custom_symlink(htmlfile, splitext(symfile)[0] + extsep + 'html', 'html') for ex in [e for e in this.examples if e.image]: img = ex.image if ex.__name__ == 'ExampleSelfFigure': custom_symlink(ex.image_full, ex.symlink, 'self-figure') elif not pathsep in img: custom_symlink(join(this.base, img), img, 'image') if outobj is not stdout: outobj.close() @classmethod def __str__(this): this.generate() class PrependFigure(InheritDocstring): """Attaches self-figure (to be provided externally) as a first example""" def __new__(this, name, bases, attrs): ret = super(PrependFigure, PrependFigure).__new__(this, name, bases, attrs) if attrs['__module__'] == __name__: return ret # nothing more to do here for internal classes try: if all(map(lambda x: isinstance(x, basestring), map(lambda x: getattr(ret, x, None), ('base_uri_full',)) )): setattr(ret, 'examples', [ExampleSelfFigureProto(ret)] + list(getattr(ret, 'examples', ()))) except NotImplementedError: pass finally: return ret class OntologyWithFigure(Ontology): __metaclass__ = PrependFigure # # 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) map(lambda a: setattr(a, '_final', True), 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)