From 89c493309f612e0c717902dcf88215568b13f9a0 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Tue, 14 May 2024 17:49:49 +0200 Subject: [PATCH 1/3] Fix XPath find on schemas - Create XPath namespaces dict without default namespace - Add _xsd_find() helper method - Add default_namespace property to XmlDocument --- docs/conf.py | 4 +- qeschema/__init__.py | 2 +- qeschema/documents.py | 55 +- qeschema/schemas/releases/qes_test_240411.xsd | 1391 +++++++++++++++++ qeschema/utils.py | 11 + requirements-dev.txt | 2 +- setup.py | 4 +- tests/test_documents.py | 1 + tox.ini | 2 +- 9 files changed, 1441 insertions(+), 31 deletions(-) create mode 100644 qeschema/schemas/releases/qes_test_240411.xsd diff --git a/docs/conf.py b/docs/conf.py index e861a6c..1033646 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'qeschema' -copyright = '2015-2023, Quantum Espresso Foundation and SISSA' +copyright = '2015-2024, Quantum Espresso Foundation and SISSA' author = 'Davide Brunato, Pietro Delugas' # The full version, including alpha/beta/rc tags -release = '1.5.1' +release = '1.5.2' # -- General configuration --------------------------------------------------- diff --git a/qeschema/__init__.py b/qeschema/__init__.py index 85684f6..a186406 100644 --- a/qeschema/__init__.py +++ b/qeschema/__init__.py @@ -15,7 +15,7 @@ from .exceptions import QESchemaError, XmlDocumentError from .utils import set_logger -__version__ = '1.5.1' +__version__ = '1.5.2' __all__ = [ 'XmlDocument', 'QeDocument', 'PwDocument', 'PhononDocument', 'NebDocument', diff --git a/qeschema/documents.py b/qeschema/documents.py index 96ffd0a..d16260c 100644 --- a/qeschema/documents.py +++ b/qeschema/documents.py @@ -14,8 +14,7 @@ from functools import wraps from xml.etree import ElementTree import xmlschema -from xmlschema import etree_tostring - +from xmlschema import etree_tostring, XsdElement try: import yaml @@ -27,7 +26,7 @@ NebInputConverter, TdInputConverter, TdSpectrumInputConverter, \ XSpectraInputConverter, EPWInputConverter from .exceptions import XmlDocumentError -from .utils import etree_iter_path +from .utils import etree_iter_path, get_target_prefix logger = logging.getLogger('qeschema') @@ -48,7 +47,7 @@ def removeprefix(s, prefix): return s[len(prefix):] if s.startswith(prefix) else s -class XmlDocument(object): +class XmlDocument: """ Base class for a generic XML document based on an XSD schema. The schema associated is used for checking types, validation of the XML data and for @@ -118,11 +117,15 @@ def __init__(self, source=None, schema=None): def namespaces(self): """ XML data namespaces map, a dictionary that maps prefixes to URI. An empty - dictionary if the XML data file is not loaded or it doesn't contain any + dictionary if the XML data file is not loaded or if it doesn't contain any namespace declaration. """ return {k: v for k, v in self._namespaces.items()} + @property + def default_namespace(self): + return self._namespaces.get('', '') + @classmethod def fetch_schema(cls, filename): filename = filename.strip() @@ -442,20 +445,25 @@ def __init__(self, source=None, schema=None, input_builder=None): self.input_builder = self.DEFAULT_INPUT_BUILDER elif not isinstance(input_builder, type) or \ not issubclass(input_builder, RawInputConverter): - msg = "3rd argument must be a {!r} subclass" - raise XmlDocumentError(msg.format(RawInputConverter)) + raise XmlDocumentError(f"3rd argument must be a {RawInputConverter!r} subclass") else: self.input_builder = input_builder - self.default_namespace = self.schema.target_namespace - qe_prefixes = ['qes', 'neb', 'qes_ph', 'qes_lr', 'qes_spectrum', - 'qes_xspectra', 'epw'] - qe_nslist = list(map(self.schema.namespaces.get, qe_prefixes)) - if self.default_namespace not in qe_nslist: + self._xpath_namespaces = {k: v for k, v in self.schema.namespaces.items() if k} + + prefix = get_target_prefix(self.schema.namespaces, self.schema.target_namespace) + if prefix not in ('qes', 'neb', 'qes_ph', 'qes_lr', + 'qes_spectrum', 'qes_xspectra', 'epw'): raise NotImplementedError( - "Converter not implemented for this schema {}".format(self.default_namespace) + f"Converter not implemented for namespace {self.schema.target_namespace!r}" ) + def _xsd_find(self, path, xsd_element=None): + if xsd_element is None: + return self.schema.find(path, self._xpath_namespaces) + else: + return xsd_element.find(path, self._xpath_namespaces) + @property def input_path(self): """The path to XML input section.""" @@ -491,7 +499,7 @@ def get_fortran_input(self, use_defaults=True): raise XmlDocumentError("Missing input {!r} in XML data!".format(input_path)) for schema_root in self.schema.elements.values(): - if schema_root.find(input_path): + if self._xsd_find(input_path, schema_root): break else: raise XmlDocumentError("Missing input element in XSD schema!") @@ -499,7 +507,7 @@ def get_fortran_input(self, use_defaults=True): # Extract values from input's subtree of the XML document for elem, path in etree_iter_path(input_root, path=input_path): rel_path = path.replace(input_path, '.') - xsd_element = schema_root.find(path) + xsd_element = self._xsd_find(path, schema_root) if xsd_element is None: logger.error("%r doesn't match any element!", path) continue @@ -546,7 +554,7 @@ def get_atomic_positions(self): path = './/output//atomic_positions' elem = self.find(path) if elem is not None: - atomic_positions = self.schema.find(path).decode(elem) + atomic_positions = self._xsd_find(path).decode(elem) atoms = atomic_positions.get('atom') if not isinstance(atoms, list): atoms = [atoms] @@ -564,7 +572,7 @@ def get_cell_parameters(self): path = './/output//cell' elem = self.find(path) if elem is not None: - cell = self.schema.find(path).decode(elem) + cell = self._xsd_find(path).decode(elem) return [cell['a1'], cell['a2'], cell['a3']] @requires_xml_data @@ -577,7 +585,7 @@ def get_stress(self): path = './/output//stress' elem = self.find(path) if elem is not None: - stress = self.schema.find(path).decode(elem) + stress = self._xsd_find(path).decode(elem) try: stress = stress['$'] except TypeError: @@ -595,10 +603,9 @@ def get_forces(self): path = './/output/forces' elem = self.find(path) if elem is not None: - forces = self.schema.find(path).decode(elem) + forces = self._xsd_find(path).decode(elem) path = './/output//atomic_positions' - breakpoint() - atomic_positions = self.schema.find(path).decode(self.find(path)) + atomic_positions = self._xsd_find(path).decode(self.find(path)) atoms = atomic_positions.get('atom', []) if not isinstance(atoms, list): atoms = [atoms] @@ -616,7 +623,7 @@ def get_k_points(self): :return: nested list with k_points """ path = './/output//ks_energies/k_point' - return [self.schema.find(path).decode(e)['$'] for e in self.findall(path)] + return [self._xsd_find(path).decode(e)['$'] for e in self.findall(path)] @requires_xml_data def get_ks_eigenvalues(self): @@ -628,7 +635,7 @@ def get_ks_eigenvalues(self): path = './/output//ks_energies/eigenvalues' eigenvalues = [] for e in self.findall(path): - obj = self.schema.find(path).decode(e) + obj = self._xsd_find(path).decode(e) if isinstance(obj, dict): eigenvalues.append(obj['$']) # pragma: no cover else: @@ -644,7 +651,7 @@ def get_total_energy(self): :return: total energy in Hartree Units """ path = './/output//etot' - return self.schema.find(path).decode(self.find(path)) + return self._xsd_find(path).decode(self.find(path)) class PhononDocument(QeDocument): diff --git a/qeschema/schemas/releases/qes_test_240411.xsd b/qeschema/schemas/releases/qes_test_240411.xsd new file mode 100644 index 0000000..e7ec7e8 --- /dev/null +++ b/qeschema/schemas/releases/qes_test_240411.xsd @@ -0,0 +1,1391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qeschema/utils.py b/qeschema/utils.py index e95c7f7..9515564 100644 --- a/qeschema/utils.py +++ b/qeschema/utils.py @@ -94,6 +94,17 @@ def to_fortran(value): return str(value) +def get_target_prefix(namespaces, target_namespace): + """Returns the longest prefix that maps the target namespace.""" + prefix = None + for k, v in namespaces.items(): + if v == target_namespace: + if prefix is None or len(k) > len(prefix): + prefix = k + else: + return prefix + + class BiunivocalMap(MutableMapping): """ A dictionary that implements a bijective correspondence, namely with constraints diff --git a/requirements-dev.txt b/requirements-dev.txt index 65b12b7..41ed651 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ setuptools tox>=4.0 flake8 coverage -xmlschema>=1.6.4<=2.3.99 +xmlschema>=1.6.4, <4.0.0 pyyaml numpy h5py diff --git a/setup.py b/setup.py index a4c5f62..44c61a9 100644 --- a/setup.py +++ b/setup.py @@ -15,8 +15,8 @@ setup( name='qeschema', - version='1.5.1', - install_requires=['xmlschema>=1.6.4,<2.4.0', 'numpy'], + version='1.5.2', + install_requires=['xmlschema>=1.6.4,<4.0.0', 'numpy'], extras_require={ 'HDF5': ['h5py'], 'YAML': ['pyyaml'], diff --git a/tests/test_documents.py b/tests/test_documents.py index c6670cd..7b20be1 100755 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -731,6 +731,7 @@ def test_pw_get_stress(self): source = os.path.join(self.test_dir, 'resources/pw/Si.xml') document = PwDocument(source) + self.assertIsNotNone(document.schema) stress = document.get_stress() self.assertListEqual( stress, diff --git a/tox.ini b/tox.ini index a3a9cce..0bd5808 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ skip_missing_interpreters = true [testenv] deps = - xmlschema>=1.6.4,~=2.3 + xmlschema>=1.6.4,<4.0.0 pyyaml numpy h5py From c59ed6bc215d353ee7af32dc0f0426dfa35d1032 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Tue, 14 May 2024 23:22:22 +0200 Subject: [PATCH 2/3] Update Tox tests and dependencies --- .github/workflows/test.yml | 6 +++--- qeschema/cards.py | 2 +- qeschema/documents.py | 2 +- requirements-dev.txt | 2 +- setup.py | 3 ++- tox.ini | 25 +++++++++++++++++++++++-- 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38882ba..395e833 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Display Python version diff --git a/qeschema/cards.py b/qeschema/cards.py index 53b6560..e88f12f 100644 --- a/qeschema/cards.py +++ b/qeschema/cards.py @@ -249,7 +249,7 @@ def labnl(nnum, lnum): label = labnl(value['n2_number'], value['l2_number']) if 'two' in background: - label = f"{label}-{labnl(value['n3_number'],value['l3_number'])}" + label = f"{label}-{labnl(value['n3_number'], value['l3_number'])}" lines.append(f"U {specie}-{label} {value['$']:8.3f}") elif tag == 'V': speclab1 = f"{value['@specie1']}-{value['@label1']}" diff --git a/qeschema/documents.py b/qeschema/documents.py index d16260c..8345d4c 100644 --- a/qeschema/documents.py +++ b/qeschema/documents.py @@ -14,7 +14,7 @@ from functools import wraps from xml.etree import ElementTree import xmlschema -from xmlschema import etree_tostring, XsdElement +from xmlschema import etree_tostring try: import yaml diff --git a/requirements-dev.txt b/requirements-dev.txt index 41ed651..8b05297 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ setuptools tox>=4.0 flake8 coverage -xmlschema>=1.6.4, <4.0.0 +xmlschema>=2.5.1, <4.0.0 pyyaml numpy h5py diff --git a/setup.py b/setup.py index 44c61a9..15cbcd6 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( name='qeschema', version='1.5.2', - install_requires=['xmlschema>=1.6.4,<4.0.0', 'numpy'], + install_requires=['xmlschema>=2.5.1,<4.0.0', 'numpy'], extras_require={ 'HDF5': ['h5py'], 'YAML': ['pyyaml'], @@ -43,6 +43,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Scientific/Engineering :: Physics', 'Topic :: Utilities', diff --git a/tox.ini b/tox.ini index 0bd5808..f910396 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,12 @@ # and then run "tox" from this directory. [tox] -envlist = py{37,38,39,310,311}, docs, flake8, coverage +envlist = py{37,38,39,310,311,312,312}, xmlschema{251,301,331}, docs, flake8, coverage skip_missing_interpreters = true [testenv] deps = - xmlschema>=1.6.4,<4.0.0 + xmlschema>=2.5.1,<4.0.0 pyyaml numpy h5py @@ -19,6 +19,27 @@ deps = commands = python -m unittest allowlist_externals = make +[testenv:xmlschema251] +deps = + xmlschema==2.5.1 + pyyaml + numpy + h5py + +[testenv:xmlschema301] +deps = + xmlschema==3.0.1 + pyyaml + numpy + h5py + +[testenv:xmlschema331] +deps = + xmlschema==3.3.1 + pyyaml + numpy + h5py + [testenv:docs] commands = make -C docs html From 19aaef8d7e5faa12e19466742ac79a3ba5283e46 Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Wed, 15 May 2024 08:03:12 +0200 Subject: [PATCH 3/3] Remove Python 37 from CI configuration --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 395e833..1d2a42c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - name: Set up Python @@ -29,7 +29,6 @@ jobs: python -m pip install --upgrade pip pip install -r requirements-dev.txt - name: Lint with flake8 - if: ${{ matrix.python-version != '3.7' }} run: | flake8 qeschema --max-line-length=100 --statistics - name: Run tests