3"""Copyright (c) 2005-2016, University of Oxford.
6University of Oxford means the Chancellor, Masters and Scholars of the
7University of Oxford, having an administrative office at Wellington
8Square, Oxford OX1 2JD, UK.
10This file is part of Chaste.
12Redistribution and use in source and binary forms, with or without
13modification, 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.
23THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS"
24AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
27LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
29GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35# Validator for CellML 1.0
36# Author: Jonathan Cooper
39from __future__ import division
48# Make sure PyCml is on sys.path
49pycml_path = os.path.dirname(os.path.realpath(__file__))
50sys.path[0:0] = [pycml_path]
52# Common CellML processing stuff
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).
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')
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
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)
243 logger = logging.getLogger(
'validator')
249 """Providing for compatibility with RvpRelaxngValidator; does nothing."""
253class 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))
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
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:
666if __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 setup_logging(show_errors=True, error_stream=sys.stderr, show_warnings=True, warning_stream=sys.stderr, space_errors=False, loglevel=logging.WARNING, **kwargs)
def __init__(self, create_relaxng_validator=True)
def validate(self, source, return_doc=False, assume_valid=False, **kw)
def cleanup_logging(handlers)
def validate(self, stream)
def __init__(self, schemaBase)
def __init__(self, schemaBase)
def __init__(self, value)
def amara_parse_cellml(source, uri=None, prefixes=None)
def open_output_stream(fname)
def close_output_stream(stream)
def DEBUG(facility, *args)
def validate(self, stream)
def check_repo(repo_dir='../../models/all_from_repository', model_suffix='xml', invalid_if_warnings=False, compare=True)
Convenience functions #.
def compare_output_files(new_stream, old_filename)
def quit(self)
def del(self): """ Tell our RVP process to quit.
def _attribute(self, cur, name, val)
def get_options(args)
For running as an executable #.
def _characters(self, data)
def get_validation_errors(self)
def _end_element(self, name)
def _start(self, grammar='0')
def _start_tag_close(self, cur, name)
def _start_element(self, name, attrs)
def _end_tag(self, cur, name)
def _textonly(self, cur, text)
def _mixed(self, cur, text)
def _error_found(self, line, col, msg)
def _start_tag_open(self, cur, name)