Update to release v1.1.8
- Added coverage and flake8 to testing - Removed py34 from tox.ini - Added py38 with a limited testing - ElementPathMissingContextError renamed to MissingContextError - Added exceptions to API docs
This commit is contained in:
parent
46ac3a6a1c
commit
1d23f5b13f
|
@ -2,6 +2,13 @@
|
|||
CHANGELOG
|
||||
*********
|
||||
|
||||
`v1.1.8`_ (2019-05-20)
|
||||
======================
|
||||
* Added code coverage and flake8 checks
|
||||
* Drop Python 3.4 support
|
||||
* Use more specific XPath errors for functions and namespace resolving
|
||||
* Fix for issue #4
|
||||
|
||||
`v1.1.7`_ (2019-04-25)
|
||||
======================
|
||||
* Added Parser.is_spaced() method for checking if the current token has extra spaces before or after
|
||||
|
@ -123,3 +130,4 @@ CHANGELOG
|
|||
.. _v1.1.5: https://github.com/brunato/elementpath/compare/v1.1.4...v1.1.5
|
||||
.. _v1.1.6: https://github.com/brunato/elementpath/compare/v1.1.5...v1.1.6
|
||||
.. _v1.1.7: https://github.com/brunato/elementpath/compare/v1.1.6...v1.1.7
|
||||
.. _v1.1.8: https://github.com/brunato/elementpath/compare/v1.1.7...v1.1.8
|
||||
|
|
|
@ -31,7 +31,7 @@ author = 'Davide Brunato'
|
|||
# The short X.Y version
|
||||
version = ''
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '1.1.7'
|
||||
release = '1.1.8'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
|
|
@ -87,3 +87,23 @@ implementing concrete interfaces to other types of XML Schema processors.
|
|||
.. automethod:: is_instance
|
||||
.. automethod:: iter_atomic_types
|
||||
.. automethod:: get_primitive_type
|
||||
|
||||
|
||||
Exception classes
|
||||
=================
|
||||
|
||||
.. autoexception:: elementpath.ElementPathError
|
||||
.. autoexception:: elementpath.MissingContextError
|
||||
|
||||
Other exceptions
|
||||
----------------
|
||||
|
||||
There are some exceptions derived from the base exception and Python built-in exceptions:
|
||||
|
||||
.. autoexception:: elementpath.ElementPathKeyError
|
||||
.. autoexception:: elementpath.ElementPathLocaleError
|
||||
.. autoexception:: elementpath.ElementPathNameError
|
||||
.. autoexception:: elementpath.ElementPathSyntaxError
|
||||
.. autoexception:: elementpath.ElementPathTypeError
|
||||
.. autoexception:: elementpath.ElementPathValueError
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#
|
||||
# @author Davide Brunato <brunato@sissa.it>
|
||||
#
|
||||
__version__ = '1.1.7'
|
||||
__version__ = '1.1.8'
|
||||
__author__ = "Davide Brunato"
|
||||
__contact__ = "brunato@sissa.it"
|
||||
__copyright__ = "Copyright 2018-2019, SISSA"
|
||||
|
@ -16,7 +16,10 @@ __license__ = "MIT"
|
|||
__status__ = "Production/Stable"
|
||||
|
||||
|
||||
from .exceptions import *
|
||||
from .exceptions import ElementPathError, MissingContextError, \
|
||||
ElementPathSyntaxError, ElementPathNameError, ElementPathKeyError, \
|
||||
ElementPathTypeError, ElementPathLocaleError, ElementPathValueError
|
||||
|
||||
from . import datatypes
|
||||
from .tdop_parser import Token, Parser
|
||||
from .xpath_context import XPathContext, XPathSchemaContext
|
||||
|
|
|
@ -42,6 +42,10 @@ class ElementPathError(Exception):
|
|||
__str__ = __unicode__
|
||||
|
||||
|
||||
class MissingContextError(ElementPathError):
|
||||
"""Raised when the dynamic context is required for evaluate the XPath expression."""
|
||||
|
||||
|
||||
class ElementPathNameError(ElementPathError, NameError):
|
||||
pass
|
||||
|
||||
|
@ -66,10 +70,6 @@ class ElementPathLocaleError(ElementPathError, locale.Error):
|
|||
pass
|
||||
|
||||
|
||||
class ElementPathMissingContextError(ElementPathError):
|
||||
pass
|
||||
|
||||
|
||||
def xpath_error(code, message=None, token=None, prefix='err'):
|
||||
"""
|
||||
Returns an XPath error instance related with a code. An XPath/XQuery/XSLT error code
|
||||
|
@ -93,7 +93,7 @@ def xpath_error(code, message=None, token=None, prefix='err'):
|
|||
if code == 'XPST0001':
|
||||
return ElementPathValueError(message or 'Parser not bound to a schema', pcode, token)
|
||||
elif code == 'XPDY0002':
|
||||
return ElementPathMissingContextError(message or 'Dynamic context required for evaluate', pcode, token)
|
||||
return MissingContextError(message or 'Dynamic context required for evaluate', pcode, token)
|
||||
elif code == 'XPTY0004':
|
||||
return ElementPathTypeError(message or 'Type is not appropriate for the context', pcode, token)
|
||||
elif code == 'XPST0005':
|
||||
|
|
|
@ -699,9 +699,9 @@ class Parser(object):
|
|||
def infixr(cls, symbol, bp=0):
|
||||
"""Register a token for a symbol that represents an *infixr* binary operator."""
|
||||
def led(self, left):
|
||||
self[:] = left, self.parser.expression(rbp=bp-1)
|
||||
self[:] = left, self.parser.expression(rbp=bp - 1)
|
||||
return self
|
||||
return cls.register(symbol, label='operator', lbp=bp, rbp=bp-1, led=led)
|
||||
return cls.register(symbol, label='operator', lbp=bp, rbp=bp - 1, led=led)
|
||||
|
||||
@classmethod
|
||||
def method(cls, symbol, bp=0):
|
||||
|
|
|
@ -14,7 +14,7 @@ import decimal
|
|||
|
||||
from .compat import PY3, string_base_type
|
||||
from .exceptions import ElementPathSyntaxError, ElementPathTypeError, ElementPathNameError, \
|
||||
ElementPathMissingContextError
|
||||
MissingContextError
|
||||
from .datatypes import UntypedAtomic, DayTimeDuration, YearMonthDuration, XSD_BUILTIN_TYPES
|
||||
from .xpath_context import XPathSchemaContext
|
||||
from .tdop_parser import Parser, MultiLabel
|
||||
|
@ -78,7 +78,7 @@ class XPath1Parser(Parser):
|
|||
|
||||
DEFAULT_NAMESPACES = XPATH_1_DEFAULT_NAMESPACES
|
||||
"""
|
||||
The default prefix-to-namespace associations of the XPath class. Those namespaces are updated
|
||||
The default prefix-to-namespace associations of the XPath class. Those namespaces are updated
|
||||
in the instance with the ones passed with the *namespaces* argument.
|
||||
"""
|
||||
|
||||
|
@ -115,7 +115,7 @@ class XPath1Parser(Parser):
|
|||
@classmethod
|
||||
def axis(cls, symbol, bp=80):
|
||||
"""Register a token for a symbol that represents an XPath *axis*."""
|
||||
def nud_(self):
|
||||
def nud_(self):
|
||||
self.parser.advance('::')
|
||||
self.parser.next_token.expected(
|
||||
'(name)', '*', 'text', 'node', 'document-node', 'comment', 'processing-instruction',
|
||||
|
@ -187,7 +187,7 @@ class XPath1Parser(Parser):
|
|||
|
||||
def next_is_path_step_token(self):
|
||||
return self.next_token.label == 'axis' or self.next_token.symbol in {
|
||||
'(integer)', '(string)', '(float)', '(decimal)', '(name)', 'node', 'text', '*',
|
||||
'(integer)', '(string)', '(float)', '(decimal)', '(name)', 'node', 'text', '*',
|
||||
'@', '..', '.', '(', '{'
|
||||
}
|
||||
|
||||
|
@ -195,7 +195,7 @@ class XPath1Parser(Parser):
|
|||
root_token = super(XPath1Parser, self).parse(source)
|
||||
try:
|
||||
root_token.evaluate() # Static context evaluation
|
||||
except ElementPathMissingContextError:
|
||||
except MissingContextError:
|
||||
pass
|
||||
return root_token
|
||||
|
||||
|
@ -469,7 +469,7 @@ def select(self, context=None):
|
|||
context.item = item
|
||||
yield item
|
||||
elif context is None:
|
||||
raise ElementPathMissingContextError("Context required to evaluate `*`")
|
||||
self.missing_context()
|
||||
else:
|
||||
# Wildcard literal
|
||||
for item in context.iter_children_or_self():
|
||||
|
@ -483,7 +483,7 @@ def select(self, context=None):
|
|||
@method(nullary('.'))
|
||||
def select(self, context=None):
|
||||
if context is None:
|
||||
raise ElementPathMissingContextError("Context required to evaluate `.`")
|
||||
self.missing_context()
|
||||
elif context.item is not None:
|
||||
yield context.item
|
||||
elif is_document_node(context.root):
|
||||
|
@ -493,8 +493,7 @@ def select(self, context=None):
|
|||
@method(nullary('..'))
|
||||
def select(self, context=None):
|
||||
if context is None:
|
||||
raise ElementPathMissingContextError("Context required to evaluate `..`")
|
||||
|
||||
self.missing_context()
|
||||
else:
|
||||
try:
|
||||
parent = context.parent_map[context.item]
|
||||
|
@ -833,7 +832,7 @@ def nud(self):
|
|||
@method(axis('attribute'))
|
||||
def select(self, context=None):
|
||||
if context is None:
|
||||
raise ElementPathMissingContextError("Context required to evaluate `@`")
|
||||
self.missing_context()
|
||||
|
||||
for _ in context.iter_attributes():
|
||||
for result in self[0].select(context):
|
||||
|
|
|
@ -19,7 +19,7 @@ import math
|
|||
import operator
|
||||
|
||||
from .compat import MutableSequence, urlparse
|
||||
from .exceptions import ElementPathError, ElementPathTypeError, ElementPathMissingContextError
|
||||
from .exceptions import ElementPathError, ElementPathTypeError, MissingContextError
|
||||
from .namespaces import XSD_NAMESPACE, XPATH_FUNCTIONS_NAMESPACE, XPATH_2_DEFAULT_NAMESPACES, \
|
||||
XSD_NOTATION, XSD_ANY_ATOMIC_TYPE, get_namespace, qname_to_prefixed, prefixed_to_qname
|
||||
from .datatypes import XSD_BUILTIN_TYPES
|
||||
|
@ -64,7 +64,7 @@ class XPath2Parser(XPath1Parser):
|
|||
|
||||
# Value comparison operators
|
||||
'eq', 'ne', 'lt', 'le', 'gt', 'ge',
|
||||
|
||||
|
||||
# Node comparison operators
|
||||
'is', '<<', '>>',
|
||||
|
||||
|
@ -258,7 +258,7 @@ class XPath2Parser(XPath1Parser):
|
|||
raise self.error('FORG0006', str(err))
|
||||
|
||||
def cast(value):
|
||||
raise NotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
pattern = r'\b%s(?=\s*\(|\s*\(\:.*\:\)\()' % symbol
|
||||
token_class = cls.register(symbol, pattern=pattern, label='constructor', lbp=bp, rbp=bp,
|
||||
|
@ -282,7 +282,7 @@ class XPath2Parser(XPath1Parser):
|
|||
|
||||
try:
|
||||
self_.value = self_.evaluate() # Static context evaluation
|
||||
except ElementPathMissingContextError:
|
||||
except MissingContextError:
|
||||
self_.value = None
|
||||
return self_
|
||||
|
||||
|
@ -314,7 +314,7 @@ class XPath2Parser(XPath1Parser):
|
|||
|
||||
def next_is_path_step_token(self):
|
||||
return self.next_token.label in ('axis', 'function') or self.next_token.symbol in {
|
||||
'(integer)', '(string)', '(float)', '(decimal)', '(name)', '*', '@', '..', '.', '(', '{'
|
||||
'(integer)', '(string)', '(float)', '(decimal)', '(name)', '*', '@', '..', '.', '(', '{'
|
||||
}
|
||||
|
||||
def next_is_sequence_type_token(self):
|
||||
|
@ -334,7 +334,7 @@ class XPath2Parser(XPath1Parser):
|
|||
context = None if self.schema is None else self.schema.get_context()
|
||||
try:
|
||||
root_token.evaluate(context) # Static context evaluation
|
||||
except ElementPathMissingContextError:
|
||||
except MissingContextError:
|
||||
pass
|
||||
return root_token
|
||||
|
||||
|
|
|
@ -99,5 +99,3 @@ class Selector(object):
|
|||
"""
|
||||
context = XPathContext(root)
|
||||
return self.root_token.select(context)
|
||||
|
||||
# 45-48, 74, 81
|
2
setup.py
2
setup.py
|
@ -15,7 +15,7 @@ with open("README.rst") as readme:
|
|||
|
||||
setup(
|
||||
name='elementpath',
|
||||
version='1.1.7',
|
||||
version='1.1.8',
|
||||
packages=['elementpath'],
|
||||
author='Davide Brunato',
|
||||
author_email='brunato@sissa.it',
|
||||
|
|
|
@ -619,7 +619,10 @@ class TimezoneTypeTest(unittest.TestCase):
|
|||
self.assertNotEqual(Timezone.fromstring('+05:00'), Timezone.fromstring('+06:00'))
|
||||
|
||||
def test_hashing(self):
|
||||
self.assertEqual(hash(Timezone.fromstring('+05:00')), 1289844826723787395)
|
||||
if sys.version_info < (3, 8):
|
||||
self.assertEqual(hash(Timezone.fromstring('+05:00')), 1289844826723787395)
|
||||
else:
|
||||
self.assertEqual(hash(Timezone.fromstring('+05:00')), 7009945331308913293)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
# @author Davide Brunato <brunato@sissa.it>
|
||||
#
|
||||
import unittest
|
||||
import lxml.etree
|
||||
try:
|
||||
import lxml.etree as lxml_etree
|
||||
except ImportError:
|
||||
lxml_etree = None
|
||||
|
||||
from elementpath import *
|
||||
from elementpath.compat import PY3
|
||||
from elementpath.namespaces import XML_LANG, XSD_NAMESPACE
|
||||
|
||||
try:
|
||||
|
@ -231,17 +235,17 @@ class XPath2ParserXMLSchemaTest(test_xpath2_parser.XPath2ParserTest):
|
|||
def test_cast_expression(self):
|
||||
self.check_value("5 cast as xs:integer", 5)
|
||||
self.check_value("'5' cast as xs:integer", 5)
|
||||
self.check_value("'hello' cast as xs:integer", ElementPathValueError)
|
||||
self.check_value("('5', '6') cast as xs:integer", ElementPathTypeError)
|
||||
self.check_value("() cast as xs:integer", ElementPathValueError)
|
||||
self.check_value("'hello' cast as xs:integer", ValueError)
|
||||
self.check_value("('5', '6') cast as xs:integer", TypeError)
|
||||
self.check_value("() cast as xs:integer", ValueError)
|
||||
self.check_value("() cast as xs:integer?", [])
|
||||
self.check_value('"1" cast as xs:boolean', True)
|
||||
self.check_value('"0" cast as xs:boolean', False)
|
||||
|
||||
|
||||
@unittest.skipIf(xmlschema is None, "xmlschema library >= v1.0.7 required.")
|
||||
@unittest.skipIf(xmlschema is None or lxml_etree is None, "both xmlschema and lxml required")
|
||||
class LxmlXPath2ParserXMLSchemaTest(XPath2ParserXMLSchemaTest):
|
||||
etree = lxml.etree
|
||||
etree = lxml_etree
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -28,7 +28,11 @@ import pickle
|
|||
from decimal import Decimal
|
||||
from collections import namedtuple
|
||||
from xml.etree import ElementTree
|
||||
import lxml.etree
|
||||
|
||||
try:
|
||||
import lxml.etree as lxml_etree
|
||||
except ImportError:
|
||||
lxml_etree = None
|
||||
|
||||
from elementpath import *
|
||||
from elementpath.namespaces import XML_NAMESPACE, XSD_NAMESPACE, XSI_NAMESPACE, XPATH_FUNCTIONS_NAMESPACE
|
||||
|
@ -206,16 +210,16 @@ class XPath1ParserTest(unittest.TestCase):
|
|||
|
||||
# Wrong XPath expression checker shortcuts
|
||||
def wrong_syntax(self, path):
|
||||
self.assertRaises(ElementPathSyntaxError, self.parser.parse, path)
|
||||
self.assertRaises(SyntaxError, self.parser.parse, path)
|
||||
|
||||
def wrong_value(self, path):
|
||||
self.assertRaises(ElementPathValueError, self.parser.parse, path)
|
||||
self.assertRaises(ValueError, self.parser.parse, path)
|
||||
|
||||
def wrong_type(self, path):
|
||||
self.assertRaises(ElementPathTypeError, self.parser.parse, path)
|
||||
self.assertRaises(TypeError, self.parser.parse, path)
|
||||
|
||||
def wrong_name(self, path):
|
||||
self.assertRaises(ElementPathNameError, self.parser.parse, path)
|
||||
self.assertRaises(NameError, self.parser.parse, path)
|
||||
|
||||
#
|
||||
# Test methods
|
||||
|
@ -870,7 +874,7 @@ class XPath1ParserTest(unittest.TestCase):
|
|||
context = XPathContext(root, variables={'alpha': 10, 'id': '19273222'})
|
||||
self.check_value("$alpha", None) # Do not raise if the dynamic context is None
|
||||
self.check_value("$alpha", 10, context=context)
|
||||
self.check_value("$beta", ElementPathNameError, context=context)
|
||||
self.check_value("$beta", NameError, context=context)
|
||||
self.check_value("$id", '19273222', context=context)
|
||||
self.wrong_syntax("$id()")
|
||||
|
||||
|
@ -897,7 +901,7 @@ class XPath1ParserTest(unittest.TestCase):
|
|||
self.check_selector('/A/.', root, [root])
|
||||
self.check_selector('/A/B1/.', root, [root[0]])
|
||||
self.check_selector('/A/B1/././.', root, [root[0]])
|
||||
self.check_selector('1/.', root, ElementPathTypeError)
|
||||
self.check_selector('1/.', root, TypeError)
|
||||
|
||||
def test_self_axis(self):
|
||||
root = self.etree.XML('<A>A text<B1>B1 text</B1><B2/><B3>B3 text</B3></A>')
|
||||
|
@ -1062,8 +1066,9 @@ class XPath1ParserTest(unittest.TestCase):
|
|||
self.check_selector("name(B1)", root, '', namespaces={'': "http://xpath.test/ns"})
|
||||
|
||||
|
||||
@unittest.skipIf(lxml_etree is None, "The lxml library is not installed")
|
||||
class LxmlXPath1ParserTest(XPath1ParserTest):
|
||||
etree = lxml.etree
|
||||
etree = lxml_etree
|
||||
|
||||
def check_selector(self, path, root, expected, namespaces=None, **kwargs):
|
||||
"""Check using the selector API (the *select* function of the package)."""
|
||||
|
|
|
@ -23,10 +23,15 @@
|
|||
import unittest
|
||||
import datetime
|
||||
import io
|
||||
import locale
|
||||
import math
|
||||
import time
|
||||
from decimal import Decimal
|
||||
import lxml.etree
|
||||
|
||||
try:
|
||||
import lxml.etree as lxml_etree
|
||||
except ImportError:
|
||||
lxml_etree = None
|
||||
|
||||
from elementpath import *
|
||||
from elementpath.namespaces import XSI_NAMESPACE
|
||||
|
@ -204,7 +209,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
|
|||
def test_boolean_functions2(self):
|
||||
root = self.etree.XML('<A><B1/><B2/><B3/></A>')
|
||||
self.check_selector("boolean(/A)", root, True)
|
||||
self.check_selector("boolean((-10, 35))", root, ElementPathTypeError) # Sequence with two numeric values
|
||||
self.check_selector("boolean((-10, 35))", root, TypeError) # Sequence with two numeric values
|
||||
self.check_selector("boolean((/A, 35))", root, True)
|
||||
|
||||
def test_numerical_expressions2(self):
|
||||
|
@ -350,7 +355,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
|
|||
self.check_value(u"fn:compare('Strasse', 'Straße', 'de_DE')", -1)
|
||||
self.check_value(u"fn:compare('Strasse', 'Straße', 'deutsch')", -1)
|
||||
|
||||
with self.assertRaises(ElementPathLocaleError):
|
||||
with self.assertRaises(locale.Error):
|
||||
self.check_value(u"fn:compare('Strasse', 'Straße', 'invalid_collation')")
|
||||
self.wrong_type(u"fn:compare('Strasse', 111)")
|
||||
|
||||
|
@ -1113,7 +1118,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
|
|||
'<B2 /><B3>simple text</B3></A>' % XSI_NAMESPACE)
|
||||
self.check_selector("node-name(.)", root, 'A')
|
||||
self.check_selector("node-name(/A/B1)", root, 'B1')
|
||||
self.check_selector("node-name(/A/*)", root, ElementPathTypeError) # Not allowed more than one item!
|
||||
self.check_selector("node-name(/A/*)", root, TypeError) # Not allowed more than one item!
|
||||
self.check_selector("nilled(./B1/C1)", root, False)
|
||||
self.check_selector("nilled(./B1/C2)", root, True)
|
||||
|
||||
|
@ -1185,7 +1190,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
|
|||
)
|
||||
self.check_selector(
|
||||
'/transactions/purchase[parcel="10-639"] >> /transactions/sale[parcel="33-870"]',
|
||||
root, ElementPathTypeError
|
||||
root, TypeError
|
||||
)
|
||||
|
||||
def test_adjust_datetime_to_timezone_function(self):
|
||||
|
@ -1278,8 +1283,9 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
|
|||
self.check_value('fn:concat($unknown, fn:lower-case(10))', TypeError)
|
||||
|
||||
|
||||
@unittest.skipIf(lxml_etree is None, "The lxml library is not installed")
|
||||
class LxmlXPath2ParserTest(XPath2ParserTest):
|
||||
etree = lxml.etree
|
||||
etree = lxml_etree
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
32
tox.ini
32
tox.ini
|
@ -4,7 +4,7 @@
|
|||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = py27, py34, py35, py36, py37
|
||||
envlist = py27, py35, py36, py37, py38, docs, flake8, coverage
|
||||
skip_missing_interpreters = true
|
||||
toxworkdir = {homedir}/.tox/elementpath
|
||||
|
||||
|
@ -12,20 +12,34 @@ toxworkdir = {homedir}/.tox/elementpath
|
|||
deps =
|
||||
lxml
|
||||
xmlschema~=1.0.9
|
||||
docs: Sphinx
|
||||
flake8: flake8
|
||||
coverage: coverage
|
||||
commands = python -m unittest
|
||||
whitelist_externals = make
|
||||
|
||||
[testenv:py27]
|
||||
deps =
|
||||
lxml
|
||||
xmlschema~=1.0.9
|
||||
commands = python tests/test_elementpath.py
|
||||
|
||||
[testenv:py37]
|
||||
[testenv:py38]
|
||||
deps = xmlschema~=1.0.9
|
||||
commands = python -m unittest
|
||||
|
||||
[testenv:docs]
|
||||
commands =
|
||||
make -C doc html
|
||||
make -C doc latexpdf
|
||||
make -C doc doctest
|
||||
|
||||
[flake8]
|
||||
max-line-length = 119
|
||||
|
||||
[testenv:flake8]
|
||||
commands =
|
||||
flake8 --ignore=F401,F811,F821 elementpath
|
||||
|
||||
[testenv:coverage]
|
||||
commands =
|
||||
coverage run -p setup.py test -q
|
||||
coverage combine
|
||||
coverage report -m
|
||||
deps =
|
||||
lxml
|
||||
xmlschema~=1.0.9
|
||||
coverage
|
||||
|
|
Loading…
Reference in New Issue