3 """Copyright (c) 2005-2016, University of Oxford. 
    6 University of Oxford means the Chancellor, Masters and Scholars of the 
    7 University of Oxford, having an administrative office at Wellington 
    8 Square, Oxford OX1 2JD, UK. 
   10 This file is part of Chaste. 
   12 Redistribution and use in source and binary forms, with or without 
   13 modification, are permitted provided that the following conditions are met: 
   14  * Redistributions of source code must retain the above copyright notice, 
   15    this list of conditions and the following disclaimer. 
   16  * Redistributions in binary form must reproduce the above copyright notice, 
   17    this list of conditions and the following disclaimer in the documentation 
   18    and/or other materials provided with the distribution. 
   19  * Neither the name of the University of Oxford nor the names of its 
   20    contributors may be used to endorse or promote products derived from this 
   21    software without specific prior written permission. 
   23 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
   24 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
   25 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
   26 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
   27 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
   28 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
   29 GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
   30 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
   31 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
   32 OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
   39 from __future__ 
import division
 
   49 pycml_path = os.path.dirname(os.path.realpath(__file__))
 
   50 sys.path[0:0] = [pycml_path]
 
   56 __version__ = 
"$Revision: 25790 $"[11:-2]
 
   61     """Base class for errors trying to run validator stages.""" 
   65         return repr(self.
value)
 
   70     def __init__(self, create_relaxng_validator=True):
 
   71         """Initialise a validator for CellML files.""" 
   74         if create_relaxng_validator:
 
   75             schema_base = os.path.join(pycml_path, 
'cellml1.0')
 
   77             for klass 
in [LxmlRelaxngValidator, RvpRelaxngValidator]:
 
   80                 except ValidatorError, e:
 
   81                     run_errors.append(str(e))
 
   85                 msg = 
'\n\t'.join([
"Unable to run a RELAX NG validator.  Please install lxml or rvp."]
 
   91         Since using __del__ is precarious, we provide this method to allow 
   92         the RVP process to be killed cleanly.  Call it when the validator 
   93         is finished with, or you'll get an interesting error when the program 
   94         terminates (if using RVP). 
   97             self.relaxng_validator.quit()
 
  102                       show_warnings=
True, warning_stream=sys.stderr,
 
  103                       space_errors=
False, loglevel=logging.WARNING,
 
  105         """Set up loggers for validation errors/warnings. 
  107         Set show_errors or show_warnings to False to suppress the output of 
  108         validation error or warning messages, respectively.  When not 
  109         suppressed, the messages will be output to the streams given by 
  110         error_stream and warning_stream; these should be file-like objects. 
  112         If space_errors is True, a blank line will be inserted between 
  115         logger = logging.getLogger(
'validator')
 
  116         logger.setLevel(loglevel)
 
  118             formatter = logging.Formatter(fmt=
"%(message)s\n")
 
  120             formatter = logging.Formatter(fmt=
"%(message)s")
 
  121         error_handler = logging.StreamHandler(error_stream)
 
  122         error_handler.setLevel(logging.ERROR)
 
  123         error_handler.setFormatter(formatter)
 
  124         warning_handler = logging.StreamHandler(warning_stream)
 
  126         warning_handler.setFormatter(formatter)
 
  128             error_handler.setLevel(logging.CRITICAL)
 
  129         if not show_warnings:
 
  130             warning_handler.setLevel(logging.CRITICAL)
 
  131         logger.addHandler(error_handler)
 
  132         logger.addHandler(warning_handler)
 
  133         return error_handler, warning_handler
 
  137         """Flush logger & remove handlers.""" 
  138         logger = logging.getLogger(
'validator')
 
  139         for handler 
in handlers:
 
  141             logger.removeHandler(handler)
 
  143     def validate(self, source, return_doc=False, assume_valid=False, **kw):
 
  144         """Validate the given document. 
  146         source should be a file-like object, URI, local file name, 
  147         or '-' for standard input.  If a file-like object, it must support 
  148         the seek method to reset it. 
  150         If return_doc is True then the result is a tuple (valid, document), 
  151         where if valid==True then document is an Amara binding of the CellML 
  153         Otherwise just return True iff the document is valid. 
  155         If xml_context is True, then the failing XML tree will be displayed 
  156         with every units error. 
  158         The assume_valid option allows you to skip RELAX NG validation, along 
  159         with many of the checks in the Python code.  This is useful for speeding 
  160         transformation of models that are known to pass these checks. 
  162         See cellml_model.validate and setup_logging for other keyword arguments. 
  164         logging_info = CellMLValidator.setup_logging(**kw)
 
  167         DEBUG(
'validator', 
'CellML Validator version', __version__)
 
  172         elif hasattr(source, 
'read'):
 
  174         elif bt.Uri.IsAbsolute(source):
 
  175             stream = bt.Url.UrlOpen(source)
 
  177             if not os.path.isfile(source):
 
  179                 logging.getLogger(
'validator').error(
'File ' + source + 
' does not exist.')
 
  181                 stream = file(source, 
'r') 
  183         if res 
and not assume_valid:
 
  184             DEBUG(
'validator', 
'Starting RELAX NG validation')
 
  185             res = self.relaxng_validator.validate(stream)
 
  188             elif not stream == sys.stdin:
 
  190             DEBUG(
'validator', 
'Finished RELAX NG:', res)
 
  195             DEBUG(
'validator', 
'Loading model with Amara')
 
  197             DEBUG(
'validator', 
'Validating loaded model')
 
  198             res = doc.model.validate(assume_valid=assume_valid, **kw)
 
  199             DEBUG(
'validator', 
'Validation complete:', res)
 
  204         CellMLValidator.cleanup_logging(logging_info)
 
  217     A RELAX NG validator built on top of lxml (http://lxml.de/validation.html#relaxng). 
  218     Can validate against schemas written in the XML syntax. 
  221         """Initialise the RELAX NG validator. 
  223         Parses the schema into memory, and constructs lxml's validator object. 
  224         We are passed the path to the schema with no extension. 
  227             from lxml 
import etree
 
  228         except ImportError, e:
 
  230         fp = open(schemaBase + 
'.rng', 
'r') 
  231         schema_doc = etree.parse(fp) 
  235         """Validate an XML document, returning a boolean. 
  237         stream should be a file-like object containing the document to be validated. 
  238         Returns True iff the document was valid.""" 
  239         from lxml 
import etree
 
  240         doc = etree.parse(stream)
 
  241         res = self._validator.validate(doc)
 
  243         logger = logging.getLogger(
'validator')
 
  244         for e 
in self._validator.error_log:
 
  249         """Providing for compatibility with RvpRelaxngValidator; does nothing.""" 
  253 class RvpRelaxngValidator(object):
 
  255     A RELAX NG validator built on top of RVP (http://www.davidashen.net/rnv.html). 
  256     Can validate against schemas written in the compact syntax. 
  259         """Raised if the response from RVP is not understood.""" 
  263         """Initialise the RELAX NG validator. 
  264         Launches RVP as a parallel process. 
  265         schema_filename should be the name of a file containing the RELAX NG schema, in compact syntax. 
  267         self.
_ws = re.compile(
'[^\t\n\r ]')
 
  268         schema_filename = schemaBase + 
'.rnc' 
  271             self.
_rvp_pipe = subprocess.Popen([
'rvp', schema_filename],
 
  272                                               stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=
True)
 
  274             raise self.
RvpProtocolError(
"Failed to run rvp for CellML syntax validation: " + str(e))
 
  276         self._rvpin, self.
_rvpout = self._rvp_pipe.stdin.fileno(), self._rvp_pipe.stdout.fileno()
 
  278         import xml.parsers.expat
 
  289         """Call this method when the validator is finished with. 
  290         It terminates the associated RVP process cleanly. 
  291         Failure to do so will probably result in an error when your program exits. 
  297         """Validate an XML document, returning a boolean. 
  299         stream should be a file-like object containing the document to 
  301         Returns True iff the document was valid. 
  305         self._errors, self._error_messages = 
False, []
 
  306         self._prevline, self._prevcol = -1, -1
 
  307         self._pat = self._start()
 
  309         parser = self.expat.ParserCreate(namespace_separator=
':')
 
  310         parser.StartElementHandler = self._start_element
 
  311         parser.EndElementHandler = self._end_element
 
  312         parser.CharacterDataHandler = self._characters
 
  313         self._parser = parser
 
  316             self._parser.ParseFile(stream)
 
  317         except self.expat.ExpatError, e:
 
  319             self._error_found(e.lineno, e.offset, str(e))
 
  321         return not self._errors
 
  325         Return the list of all errors found while validating 
  328         return self._error_messages
 
  331         report = 
"%d,%d:%s" % (line, col, msg.strip())
 
  332         logging.getLogger(
'validator').error(report)
 
  333         self._error_messages.append(report)
 
  337         self._send(
'start-tag-open '+cur+
' '+name)
 
  340         self._send(
'attribute '+cur+
' '+name+
' '+val)
 
  343         self._send(
'start-tag-close '+cur+
' '+name)
 
  346         self._send(
'end-tag '+cur+
' '+name)
 
  349         self._send(
'text '+cur+
' '+text)
 
  353         In mixed content, whitespace is discarded, and any non-whitespace 
  356         if self._ws.search(text):
 
  357             self._send(
'mixed '+cur+
' .')
 
  362         self._send(
'start '+grammar)
 
  368         Terminate string with zero, encode in UTF-8 and send to RVP. 
  370         os.write(self._rvpin, s.encode(
'UTF-8') + 
'\0')
 
  373         Receive a zero-terminated response from RVP; drop zero byte. 
  379             s = s + os.read(self._rvpout, 16)
 
  380             if s[-1] == 
'\0': 
break 
  384         Get a reply from RVP. 
  385         If an error occurs, log the message. 
  386         Return the current pattern value. 
  388         r = self._recv().split(
' ', 3)
 
  389         if r[0] == 
'ok': 
return r[1]
 
  393                 line = self._parser.CurrentLineNumber
 
  394                 col  = self._parser.CurrentColumnNumber
 
  395                 if line != self._prevline 
or col != self._prevcol:
 
  397                     self._error_found(line, col, r[3])
 
  398                     self._prevline, self._prevcol = line, col
 
  404         raise self.RvpProtocolError, 
"unexpected response '"+r[0]+
"'" 
  410         Apparently Expat doesn't concatenate text nodes, so we do it 
  411         manually; the CharDataHandler collects the text, and this 
  412         method passes it to the validator. 
  416                 self._pat = self._mixed(self._pat, self._text)
 
  418                 self._pat = self._textonly(self._pat, self._text)
 
  423         self._pat = self._start_tag_open(self._pat, name)
 
  424         self._ismixed = 
False 
  425         for n, v 
in attrs.items():
 
  426             self._pat = self._attribute(self._pat, n, v)
 
  427         self._pat = self._start_tag_close(self._pat, name)
 
  430         self._pat = self._end_tag(self._pat, name)
 
  433         self._text = self._text + data
 
  444 def check_repo(repo_dir = '../../models/all_from_repository',
 
  445                model_suffix = 
'xml',
 
  446                invalid_if_warnings = 
False,
 
  449     Validate every model in the CellML repository, and return a list 
  452     If compare is False, writes errors & warnings to log files in the 
  453     same folder as the models, otherwise compares the output to log 
  454     files already present, and notes differences. 
  456     Displays total run time. 
  458     def close_log_file(stream, filename):
 
  461             size = os.path.getsize(filename)
 
  466     import glob, time, gc
 
  467     start_time = time.time()
 
  470     files = glob.glob(repo_dir + 
'/*.' + model_suffix)
 
  472     for filename 
in files:
 
  473         model = os.path.basename(filename)[:-4]
 
  474         fn = os.path.splitext(filename)[0]
 
  475         warnfn, errfn = fn + 
'_warnings.log', fn + 
'_errors.log' 
  477             warn_stream = StringIO()
 
  478             err_stream = StringIO()
 
  480             warn_stream = open(warnfn, 
'w')
 
  481             err_stream = open(errfn, 
'w')
 
  482         print "Checking model",model,
"at",time.strftime(
"%X %x"),
 
  484         valid = v.validate(filename, error_stream=err_stream,
 
  485                            warning_stream=warn_stream,
 
  486                            invalid_if_warnings=invalid_if_warnings)
 
  488             print max(1,4 - (5+len(model))//8) * 
'\t', 
"X" 
  495             close_log_file(err_stream, errfn)
 
  496             close_log_file(warn_stream, warnfn)
 
  498             invalid.append(model)
 
  500     elapsed_time = time.time() - start_time
 
  501     mins,secs = int(elapsed_time//60), int(elapsed_time%60)
 
  502     print len(files),
"models checked in",mins,
"minutes",secs,
"seconds." 
  503     print len(invalid),
"invalid" 
  508     def save_new_output():
 
  509         nfp = open(old_filename + 
'-new', 
'w')
 
  510         nfp.write(new_stream.getvalue())
 
  512     new_stream.seek(0, 2)
 
  513     new_len = new_stream.tell()
 
  515         fp = open(old_filename, 
'r') 
  518             print "Log file", old_filename, 
"doesn't exist,", \
 
  519                   "but we have new output" 
  521                 ofp = open(os.path.join(os.path.dirname(old_filename),
 
  523                 print >>ofp, 
"Log file", old_filename, 
"doesn't exist,", \
 
  524                       "but we have new output" 
  531     new_lines = set(new_stream.readlines())
 
  532     old_lines = set(fp.readlines())
 
  533     if old_lines != new_lines:
 
  534         print "Output set differs from log file", old_filename
 
  535         print "Lines added:", new_lines - old_lines
 
  536         print "Lines removed:", old_lines - new_lines
 
  538             ofp = open(os.path.join(os.path.dirname(old_filename), 
'new'), 
'a')
 
  539             print >>ofp, 
"Output set differs from log file", old_filename
 
  540             print >>ofp, 
"Lines added:", new_lines - old_lines
 
  541             print >>ofp, 
"Lines removed:", old_lines - new_lines, 
"\n" 
  555     """get_options(args): 
  556     Process our command-line options. 
  558     args is a list of options & positional arguments. 
  560     usage = 
'usage: %prog [options] <cellml file or URI> ...' 
  561     parser = optparse.OptionParser(version=
"%%prog %s" % __version__,
 
  563     parser.add_option(
'-o', dest=
'outfilename', metavar=
'OUTFILE',
 
  564                       help=
'write *all* output to OUTFILE (overrides -e and -w)')
 
  565     parser.add_option(
'-e', 
'--error-file',
 
  566                       dest=
'errfilename', metavar=
'ERRFILE',
 
  568                       help=
'write errors to ERRFILE [default %default]')
 
  569     parser.add_option(
'-w', 
'--warning-file',
 
  570                       dest=
'warnfilename', metavar=
'WARNFILE',
 
  572                       help=
'write warnings to WARNFILE [default %default]')
 
  573     parser.add_option(
'-q', 
'--quiet',
 
  574                       dest=
'quiet', action=
'store_true', default=
False,
 
  575                       help=
"don't display any output, just set exit status")
 
  576     parser.add_option(
'--no-warnings', 
'--only-errors',
 
  577                       dest=
'show_warnings', action=
'store_false',
 
  579                       help=
"don't display warning messages")
 
  580     parser.add_option(
'-s', 
'--space-messages',
 
  581                       dest=
'space_errors', action=
'store_true',
 
  583                       help=
"print a blank line after every warning/error message")
 
  584     parser.add_option(
'-x', 
'--xml-context',
 
  585                       dest=
'xml_context', action=
'store_true', default=
False,
 
  586                       help=
"display the MathML tree of any expression with invalid units")
 
  587     parser.add_option(
'-u', 
'--warn-on-unit-conversions',
 
  588                       action=
'store_true', default=
False,
 
  589                       help=
"generate a warning if unit conversions are required")
 
  590     parser.add_option(
'--Wu', 
'--warn-on-units-errors',
 
  591                       action=
'store_true', default=
False,
 
  592                       dest=
'warn_on_units_errors',
 
  593                       help=
"give a warning instead of an error for" 
  594                       " dimensional inconsistencies")
 
  595     parser.add_option(
'-i', 
'--interactive',
 
  596                       action=
'store_true', default=
False,
 
  597                       help=
"use with python -i to enter an interactive session" 
  599     parser.add_option(
'-d', 
'--debug', action=
'store_true', default=
False,
 
  600                       help=
"output debug info to stderr")
 
  601     parser.add_option(
'--profile', action=
'store_true', default=
False,
 
  602                       help=
"turn on profiling of PyCml")
 
  604     options, args = parser.parse_args(args)
 
  606         parser.error(
"an input CellML file must be specified")
 
  616     if not options.outfilename 
is None:
 
  618         err_s = warn_s = out_s
 
  625     kwargs = {
'show_warnings': options.show_warnings,
 
  626               'warning_stream': warn_s,
 
  627               'show_errors': 
not options.quiet,
 
  628               'error_stream': err_s,
 
  629               'space_errors': options.space_errors,
 
  630               'xml_context': options.xml_context,
 
  631               'warn_on_units_errors': options.warn_on_units_errors,
 
  632               'check_for_units_conversions': options.warn_on_unit_conversions}
 
  635         formatter = logging.Formatter(fmt=
"%(name)s %(asctime)s: %(message)s")
 
  636         handler = logging.StreamHandler(sys.stderr)
 
  637         handler.setFormatter(formatter)
 
  639         logging.getLogger().addHandler(handler)
 
  640         logging.getLogger().setLevel(logging.DEBUG)
 
  641         kwargs[
'loglevel'] = logging.DEBUG
 
  646         if not options.quiet:
 
  647             print >>out_s, 
"Validating file", f, 
"against CellML 1.0" 
  648         res = validator.validate(f, **kwargs)
 
  649         if not options.quiet:
 
  650             print >>out_s, 
"File is%s valid CellML 1.0" % ((
' NOT',
'')[res])
 
  651         result = result 
and res
 
  662     if not options.interactive:
 
  666 if __name__ == 
'__main__':
 
  667     if '--profile' in sys.argv:
 
  668         import time, cProfile
 
  669         profile_name = 
'/tmp/pycml-profile-%f-%d' % (time.time(), os.getpid()) 
 
  670         cProfile.run(
'run()', profile_name)
 
def get_validation_errors
 
def check_repo
Convenience functions #. 
 
def quit
def del(self): """ Tell our RVP process to quit. 
 
def get_options
For running as an executable #.