Source code for masci_tools.io.fleur_xml

###############################################################################
# Copyright (c), Forschungszentrum Jülich GmbH, IAS-1/PGI-1, Germany.         #
#                All rights reserved.                                         #
# This file is part of the Masci-tools package.                               #
# (Material science tools)                                                    #
#                                                                             #
# The code is hosted on GitHub at https://github.com/judftteam/masci-tools.   #
# For further information on the license, see the LICENSE.txt file.           #
# For further information please visit http://judft.de/.                      #
#                                                                             #
###############################################################################
"""
This module provides easy functions for loading a input/output xml file of
fleur and providing a parsed xml etree together with its corresponding schema dict
"""
from __future__ import annotations
import logging

from lxml import etree
import warnings
import os
from pathlib import Path
from functools import partial
from logging import Logger
from typing import Callable, Any, Generator
from contextlib import _GeneratorContextManager, contextmanager

from masci_tools.io.parsers import fleur_schema
from masci_tools.util.typing import XMLFileLike, XMLLike
from masci_tools.util.xml.common_functions import eval_xpath_one, normalize_xmllike

__all__ = ('load_inpxml', 'load_outxml', 'FleurXMLContext', 'get_constants', 'load_outxml_and_check_for_broken_xml',
           '_EvalContext')


[docs]def load_inpxml(inpxmlfile: XMLFileLike, logger: Logger | None = None, base_url: str | Path | None = None, **kwargs: Any) -> tuple[etree._ElementTree, fleur_schema.InputSchemaDict]: """ Loads a inp.xml file for fleur together with its corresponding schema dictionary :param inpxmlfile: either path to the inp.xml file, opened file handle (in bytes modes i.e. rb) or a xml etree to be parsed :returns: parsed xmltree of the inpxmlfile and the schema dictionary for the corresponding input version """ from masci_tools.io.parsers.fleur_schema import InputSchemaDict xml_parse_func: Callable = etree.parse if isinstance(inpxmlfile, (str, bytes, Path)) and not os.path.isfile(inpxmlfile): xml_parse_func = etree.fromstring if base_url is None: warnings.warn('You provided a string of content but no base_url argument.' 'Setting it to the current working directory.' 'If the tree contains xinclude tags these could fail') base_url = os.getcwd() elif isinstance(base_url, Path): base_url = os.fspath(base_url) xml_parse_func = partial(xml_parse_func, base_url=base_url) #If the XML text is passed in as a string the XML parser #will complain since the XML declaration with encoding #options is contained in the string. We reencode it as utf-8 here #Since we do not expect fragments of a file here we should be fine if isinstance(inpxmlfile, str): inpxmlfile = inpxmlfile.encode('utf-8') if isinstance(inpxmlfile, etree._ElementTree): xmltree = inpxmlfile else: parser = etree.XMLParser(attribute_defaults=True, encoding='utf-8', **kwargs) try: xmltree = xml_parse_func(inpxmlfile, parser) except etree.XMLSyntaxError as msg: if logger is not None: logger.exception('Failed to parse input file') raise ValueError(f'Failed to parse input file: {msg}') from msg if etree.iselement(xmltree): xmltree = xmltree.getroottree() if xmltree is None: if logger is not None: logger.error('No XML tree generated. Check that the given file exists') raise ValueError('No XML tree generated. Check that the given file exists') version = eval_xpath_one(xmltree, '//@fleurInputVersion', str) if logger is not None: logger.info('Got Fleur input file with file version %s', version) schema_dict = InputSchemaDict.fromVersion(version, logger=logger) return xmltree, schema_dict
[docs]def load_outxml(outxmlfile: XMLFileLike, logger: Logger | None = None, base_url: str | Path | None = None, **kwargs: Any) -> tuple[etree._ElementTree, fleur_schema.OutputSchemaDict]: """ Loads a out.xml file for fleur together with its corresponding schema dictionary. Also returns whether the XML file had to be parsed with `recover=True` :param outxmlfile: either path to the out.xml file, opened file handle (in bytes modes i.e. rb) or a xml etree to be parsed :returns: parsed xmltree of the outxmlfile and the schema dictionary for the corresponding output version """ xmltree, schema_dict, _ = load_outxml_and_check_for_broken_xml(outxmlfile, logger, base_url, **kwargs) return xmltree, schema_dict
[docs]def load_outxml_and_check_for_broken_xml( outxmlfile: XMLFileLike, logger: Logger | None = None, base_url: str | Path | None = None, **kwargs: Any) -> tuple[etree._ElementTree, fleur_schema.OutputSchemaDict, bool]: """ Loads a out.xml file for fleur together with its corresponding schema dictionary. Also returns whether the XML file had to be parsed with `recover=True` :param outxmlfile: either path to the out.xml file, opened file handle (in bytes modes i.e. rb) or a xml etree to be parsed :returns: parsed xmltree of the outxmlfile and the schema dictionary for the corresponding output version and bool indicating whether the outxml is broken """ from masci_tools.io.parsers.fleur_schema import OutputSchemaDict xml_parse_func: Callable = etree.parse if isinstance(outxmlfile, (str, bytes, Path)) and not os.path.isfile(outxmlfile): xml_parse_func = etree.fromstring if base_url is None: warnings.warn('You provided a string of content but no base_url argument.' 'Setting it to the current working directory.' 'If the tree contains xinclude tags these could fail') base_url = os.getcwd() elif isinstance(base_url, Path): base_url = os.fspath(base_url) xml_parse_func = partial(xml_parse_func, base_url=base_url) #If the XML text is passed in as a string the XML parser #will complain since the XML declaration with encoding #options is contained in the string. We reencode it as utf-8 here #Since we do not expect fragments of a file here we should be fine if isinstance(outxmlfile, str): outxmlfile = outxmlfile.encode('utf-8') outfile_broken = False if isinstance(outxmlfile, etree._ElementTree): xmltree = outxmlfile else: parser = etree.XMLParser(attribute_defaults=True, recover=False, encoding='utf-8', **kwargs) try: xmltree = xml_parse_func(outxmlfile, parser) except etree.XMLSyntaxError: outfile_broken = True if logger is None: warnings.warn('The out.xml file is broken I try to repair it.') else: logger.warning('The out.xml file is broken I try to repair it.') if outfile_broken: # repair xmlfile and try to parse what is possible. parser = etree.XMLParser(attribute_defaults=True, recover=True, encoding='utf-8', **kwargs) try: xmltree = xml_parse_func(outxmlfile, parser) except etree.XMLSyntaxError as err: raise ValueError('Skipping the parsing of the XML file. Repairing was not possible.') from err if etree.iselement(xmltree): xmltree = xmltree.getroottree() if xmltree is None: if logger is not None: logger.error('No XML tree generated. Check that the given file exists') raise ValueError('No XML tree generated. Check that the given file exists') out_version = eval_xpath_one(xmltree, '//@fleurOutputVersion', str) if out_version == '0.27': program_version = eval_xpath_one(xmltree, '//programVersion/@version', str) if program_version == 'fleur 32': #Max5 release (before bugfix) out_version = '0.33' inp_version = '0.33' if logger is not None: logger.warning("Ignoring '0.27' outputVersion for MaX5.0 release") else: warnings.warn("Ignoring '0.27' outputVersion for MaX5.0 release") elif program_version == 'fleur 31': #Max4 release out_version = '0.31' inp_version = '0.31' if logger is not None: logger.warning("Ignoring '0.27' outputVersion for MaX4.0 release") else: warnings.warn("Ignoring '0.27' outputVersion for MaX4.0 release") elif program_version == 'fleur 30': #Max3.1 release out_version = '0.30' inp_version = '0.30' if logger is not None: logger.warning("Ignoring '0.27' outputVersion for MaX3.1 release") else: warnings.warn("Ignoring '0.27' outputVersion for MaX3.1 release") elif program_version == 'fleur 27': #Max3.1 release out_version = '0.29' inp_version = '0.29' if logger is not None: logger.warning("Found version before MaX3.1 release falling back to file version '0.29'") warnings.warn( 'out.xml files before the MaX3.1 release are not explicitly supported.' ' No guarantee is given that the parser will work without error', UserWarning) else: if logger is not None: logger.error("Unknown fleur version: File-version '%s' Program-version '%s'", out_version, program_version) raise ValueError(f"Unknown fleur version: File-version '{out_version}' Program-version '{program_version}'") else: inp_version = eval_xpath_one(xmltree, '//@fleurInputVersion', str) schema_dict = OutputSchemaDict.fromVersion(out_version, inp_version=inp_version, logger=logger) return xmltree, schema_dict, outfile_broken
[docs]class _EvalContext: """ Contextmanager to hold the relevant datastructures for evaluating values from XML files This holds: :param etree_or_element: The XML tree of the file :param schema_dict: The corresponding SchemaDict :param constants: The dictionary containing the defined mathematical constants :param logger: The configured logger instance """ def __init__(self, etree_or_element: XMLLike | etree.XPathElementEvaluator, schema_dict: fleur_schema.InputSchemaDict | fleur_schema.OutputSchemaDict, constants: dict[str, float] | None = None, logger: logging.Logger | None = None) -> None: self.node: etree._Element | etree.XPathElementEvaluator if not isinstance(etree_or_element, etree.XPathElementEvaluator): self.node = normalize_xmllike(etree_or_element) else: self.node = etree_or_element self.schema_dict = schema_dict self.logger = logger self.constants = constants or get_constants(self.node, self.schema_dict, self.logger)
[docs] def attribute(self, name: str, default: Any | None = None, **kwargs: Any) -> Any: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.evaluate_attribute()` :param name: str, name of the attribute :param default: value to return, if there are no values found :param complex_xpath: an optional xpath to use instead of the simple xpath for the evaluation :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param tag_name: str, name of the tag where the attribute should be parsed :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param exclude: list of str, here specific types of attributes can be excluded valid values are: settable, settable_contains, other :param list_return: if True, the returned quantity is always a list even if only one element is in it :param optional: bool, if True and no logger given none or an empty list is returned :returns: list or single value, converted in convert_xml_attribute """ from masci_tools.util.schema_dict_util import evaluate_attribute if default is not None: kwargs['optional'] = True res = evaluate_attribute(self.node, self.schema_dict, name, logger=self.logger, constants=self.constants, **kwargs) if res is None and default is not None: return default return res
[docs] def text(self, name: str, **kwargs: Any) -> Any: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.evaluate_text()` :param name: str, name of the tag :param complex_xpath: an optional xpath to use instead of the simple xpath for the evaluation :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param list_return: if True, the returned quantity is always a list even if only one element is in it :param optional: bool, if True and no logger given none or an empty list is returned :returns: list or single value, converted in convert_xml_text """ from masci_tools.util.schema_dict_util import evaluate_text return evaluate_text(self.node, self.schema_dict, name, logger=self.logger, constants=self.constants, **kwargs)
[docs] def all_attributes(self, name: str, **kwargs: Any) -> Any: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.evaluate_tag()` :param name: str, name of the tag :param subtags: optional bool, if True the subtags of the given tag are evaluated :param text: optional bool, if True the text of the tag is also parsed :param complex_xpath: an optional xpath to use instead of the simple xpath for the evaluation :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param only_required: bool (optional, default False), if True only required attributes are parsed :param ignore: list of str (optional), attributes not to parse :param list_return: if True, the returned quantity is always a list even if only one element is in it :param strict_missing_error: if True, and no logger is given an error is raised if any attribute is not found """ from masci_tools.util.schema_dict_util import evaluate_tag return evaluate_tag(self.node, self.schema_dict, name, logger=self.logger, constants=self.constants, **kwargs)
[docs] def parent_attributes(self, name: str, **kwargs: Any) -> Any: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.evaluate_parent_tag()` :param name: str, name of the tag :param complex_xpath: an optional xpath to use instead of the simple xpath for the evaluation :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param only_required: bool (optional, default False), if True only required attributes are parsed :param ignore: list of str (optional), attributes not to parse :param list_return: if True, the returned quantity is always a list even if only one element is in it :param strict_missing_error: if True, and no logger is given an error is raised if any attribute is not found :returns: dict, with attribute values converted via convert_xml_attribute """ from masci_tools.util.schema_dict_util import evaluate_parent_tag return evaluate_parent_tag(self.node, self.schema_dict, name, logger=self.logger, constants=self.constants, **kwargs)
[docs] def single_value(self, name: str, **kwargs: Any) -> Any: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.evaluate_single_value_tag()` :param name: str, name of the tag :param complex_xpath: an optional xpath to use instead of the simple xpath for the evaluation Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param only_required: bool (optional, default False), if True only required attributes are parsed :param ignore: list of str (optional), attributes not to parse :param list_return: if True, the returned quantity is always a list even if only one element is in it :param strict_missing_error: if True, and no logger is given an error is raised if any attribute is not found :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details :returns: value and unit, both converted in convert_xml_attribute """ from masci_tools.util.schema_dict_util import evaluate_single_value_tag return evaluate_single_value_tag(self.node, self.schema_dict, name, logger=self.logger, constants=self.constants, **kwargs)
[docs] def tag_exists(self, name: str, **kwargs: Any) -> bool: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.tag_exists()` :param name: str, name of the tag :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :returns: bool, True if any tag exists """ from masci_tools.util.schema_dict_util import tag_exists return tag_exists(self.node, self.schema_dict, name, logger=self.logger, **kwargs)
[docs] def number_nodes(self, name: str, **kwargs: Any) -> int: """ Alias of :py:func:`~masci_tools.util.schema_dict_util.get_number_of_nodes()` :param name: str, name of the tag Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details :returns: number of nodes of the given tag """ from masci_tools.util.schema_dict_util import get_number_of_nodes return get_number_of_nodes(self.node, self.schema_dict, name, logger=self.logger, **kwargs)
[docs] def attribute_exists(self, name: str, **kwargs: Any) -> bool: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.attrib_exists()` :param name: str, name of the attribute :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param tag_name: str, name of the tag where the attribute should be parsed :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :param exclude: list of str, here specific types of attributes can be excluded valid values are: settable, settable_contains, other :returns: bool, True if any tag with the attribute exists """ from masci_tools.util.schema_dict_util import attrib_exists return attrib_exists(self.node, self.schema_dict, name, logger=self.logger, **kwargs)
[docs] def simple_xpath(self, name: str, **kwargs: Any) -> list[etree._Element] | etree._Element: """ Alias for :py:func:`~masci_tools.util.schema_dict_util.eval_simple_xpath()` :param name: str, name of the tag :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details :param list_return: bool, if True a list is always returned Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path :returns: etree Elements obtained via the simple xpath expression """ from masci_tools.util.schema_dict_util import eval_simple_xpath return eval_simple_xpath(self.node, self.schema_dict, name, logger=self.logger, **kwargs)
[docs] def find(self, name: str, **kwargs: Any) -> _GeneratorContextManager[_EvalContext]: #pylint: disable=unsubscriptable-object """ Finds the first element for the given name and constraints and gives a nested context for this element, i.e. inheriting the schema_dict, constants and logger Example:: with FleurXMLContext(xmltree, schema_dict) as root: #Operations happen on the root here jspins = root.attribute('jspins') with root.find('species') as species: #Operations in this block happen on the first species node radius = species.attribute('radius') :param name: str, name of the tag :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path """ if 'list_return' in kwargs: raise ValueError('The argument list_return is not allowed in find()') nodes = self.simple_xpath(name, **kwargs, list_return=True) if nodes: return self.nested(nodes[0]) raise ValueError(f'No nodes found for name {name}')
[docs] @contextmanager def nested(self, etree_or_element: XMLLike) -> Generator[_EvalContext, None, None]: """ Create a nested context from the current one inheriting the schema_dict, constants and logger only replacing the XML element Example of explicit usage:: with FleurXMLContext(xmltree, schema_dict) as root: #Operations happen on the root here jspins = root.attribute('jspins') all_species_tag = root.simple_xpath('atomspecies') with root.nested(all_species_tag) as all_species: #Operations happen on the 'atomSpecies' tag mt_radii = all_species.attribute('radius') :param etree_or_element: Element to use for evaluation in the nested context """ yield _EvalContext(etree_or_element, self.schema_dict, self.constants, logger=self.logger)
[docs] def iter(self, name: str, **kwargs: Any) -> Generator[_EvalContext, None, None]: """ Finds all elements for the given name and constraints and gives nested contexts for these elements to be iterated over, i.e. inheriting the schema_dict, constants and logger Example:: with FleurXMLContext(xmltree, schema_dict) as root: #Operations happen on the root here jspins = root.attribute('jspins') for species in root.iter('species'): #Operations happen on one species node in each iteration of the #for loop. The order is the same as they appear in the XMl file radius = species.attribute('radius') :param name: str, name of the tag :param iteration_path: bool if True and the SchemaDict is of an output schema an absolute path into the iteration element is constructed :param filters: Dict specifying constraints to apply on the xpath. See :py:class:`~masci_tools.util.xml.xpathbuilder.XPathBuilder` for details Kwargs: :param contains: str, this string has to be in the final path :param not_contains: str, this string has to NOT be in the final path """ if 'list_return' in kwargs: raise ValueError('The argument list_return is not allowed in iter()') nodes = self.simple_xpath(name, **kwargs, list_return=True) for node in nodes: with self.nested(node) as ctx: yield ctx
[docs]@contextmanager def FleurXMLContext(etree_or_element: XMLLike | etree.XPathElementEvaluator, schema_dict: fleur_schema.InputSchemaDict | fleur_schema.OutputSchemaDict, constants: dict[str, float] | None = None, logger: logging.Logger | None = None) -> Generator[_EvalContext, None, None]: """ Contextmanager to hold the relevant datastructures for evaluating values from XML files This holds: :param etree_or_element: The XML tree of the file :param schema_dict: The corresponding SchemaDict :param constants: The dictionary containing the defined mathematical constants :param logger: The configured logger instance The following methods are available: - :py:meth:`_EvalContext.attribute()`: Evaluate attribute values (has an additional argument `default` to provide values for missing attributes) - :py:meth:`_EvalContext.text()`: Evaluate text of tags - :py:meth:`_EvalContext.all_attributes()`: Evaluate all attributes of a tag - :py:meth:`_EvalContext.parent_attributes()`: Evaluate attribute values of parent of given tag - :py:meth:`_EvalContext.single_value()`: Evaluate `value` and `unit` attribute of tag - :py:meth:`_EvalContext.tag_exists()`: Evaluate whether a given tag exists - :py:meth:`_EvalContext.number_nodes()`: Evaluate how many elements of the tag are present - :py:meth:`_EvalContext.attribute_exists()`: Evaluate whether an attribute exists - :py:meth:`_EvalContext.simple_xpath()`: Evaluate the simple xpath expression for a given tag - *Nested Context* :py:meth:`_EvalContext.find()`: Find the first occurrence of the tag and provide a nested context to that element - *Nested Context* :py:meth:`_EvalContext.iter()`: Find all occurrences of the tag and provide a nested context to these elements when iterated over - *Nested Context* :py:meth:`_EvalContext.nested()`: Explicitly inherit all the context except the XML tree to a new context with the XML element replaced by the given argument Example Usage:: from masci_tools.io.fleur_xml import load_inpxml, FleurXMLContext xmltree, schema_dict = load_inpxml('/path/to/inp.xml') with FleurXMLContext(xmltree, schema_dict) as root: spins = root.attribute('jspins') noco = root.attribute('l_noco', default=False) #Not nesting the context we need to specify which elements are meant mt_radii = root.attribute('radius', contains='species') #Nesting using find with root.find('atomspecies') as all_species: mt_radii = all_species.attribute('radius') #Nesting using iter mt_radii = [] for species in root.iter('species'): mt_radii.append(species.attribute('radius')) """ yield _EvalContext(etree_or_element, schema_dict, constants=constants, logger=logger)
[docs]def get_constants(xmltree: XMLLike | etree.XPathElementEvaluator, schema_dict: fleur_schema.InputSchemaDict | fleur_schema.OutputSchemaDict, logger: Logger | None = None) -> dict[str, float]: """ Reads in the constants defined in the inp.xml and returns them combined with the predefined constants from fleur as a dictionary :param root: root of the etree of the inp.xml file :param schema_dict: schema_dictionary of the version of the file to read (inp.xml or out.xml) :param logger: logger object for logging warnings, errors :return: a python dictionary with all defined constants """ from masci_tools.util.constants import FLEUR_DEFINED_CONSTANTS from masci_tools.io.parsers.fleur_schema import NoPathFound import copy defined_constants = copy.deepcopy(FLEUR_DEFINED_CONSTANTS) with FleurXMLContext(xmltree, schema_dict, logger=logger, constants=defined_constants) as root: try: root.tag_exists('constant') except NoPathFound: warnings.warn('Cannot extract custom constants for the given root. Assuming defaults') return defined_constants if not root.tag_exists('constant'): #Avoid warnings for empty constants return defined_constants constants = root.all_attributes('constant') if constants['name'] is not None: if not isinstance(constants['name'], list): constants = {key: [val] for key, val in constants.items()} for name, value in zip(constants['name'], constants['value']): if name not in defined_constants: defined_constants[name] = value else: if logger is not None: logger.error('Ambiguous definition of constant %s', name) raise KeyError(f'Ambiguous definition of constant {name}') return defined_constants