"""
Load an XML :ref:`configuration-file`.
"""
from __future__ import annotations
import os
from typing import Any
from typing import BinaryIO
from typing import TYPE_CHECKING
from typing import TextIO
from typing import Union
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from .utils import convert_to_primitive
from .utils import logger
if TYPE_CHECKING:
from .database import Database
XMLType = Union[str, bytes, os.PathLike, BinaryIO, TextIO]
"""An XML-document type that can be parsed."""
[docs]
class Config:
GPIB_LIBRARY: str = ''
"""The path to a GPIB library file.
Setting this attribute is necessary only if you want to communicate with
a GPIB device and the file is not automatically found or you want to
use a different file than the default file.
"""
PyVISA_LIBRARY: str = '@ivi'
"""The PyVISA backend :ref:`library <intro-configuring>` to use."""
DEMO_MODE: bool = False
"""Whether to open connections in demo mode.
If enabled then the equipment does not need to be physically connected
to a computer and the connection is simulated.
"""
PATH: list[str] = []
"""Paths are also appended to :data:`os.environ['PATH'] <os.environ>`."""
def __init__(self, source: XMLType) -> None:
r"""Load an XML :ref:`configuration-file`.
The purpose of the :ref:`configuration-file` is to define parameters
that may be required during data acquisition and to access
:class:`.EquipmentRecord`'s from an :ref:`equipment-database` and
:class:`.ConnectionRecord`'s from a :ref:`connections-database`.
The following table summarizes the XML elements that are used by
MSL-Equipment which may be defined in a :ref:`configuration-file`:
+----------------+----------------------------+---------------------------------------------------+
| XML Tag | Example Values | Description |
+================+============================+===================================================+
| demo_mode | true, false, True, False | Whether to open connections in demo mode. The |
| | | value will set :attr:`.DEMO_MODE`. |
+----------------+----------------------------+---------------------------------------------------+
| gpib_library | /opt/gpib/libgpib.so.0 | The path to a GPIB library file. Required only |
| | | if you want to use a specific file. The value |
| | | will set :attr:`.GPIB_LIBRARY`. |
+----------------+----------------------------+---------------------------------------------------+
| path | C:\\Program Files\\Company | A path that contains additional resources. |
| | | Accepts a *recursive="true"* attribute. The |
| | | path(s) are appended to :attr:`.PATH` and to |
| | | :data:`os.environ['PATH'] <os.environ>`. A |
| | | *<path>* element may be specified multiple times. |
+----------------+----------------------------+---------------------------------------------------+
| pyvisa_library | @ivi, @py, | The PyVISA :ref:`library <intro-configuring>` to |
| | /opt/ni/libvisa.so.7 | use. The value will set :attr:`.PyVISA_LIBRARY`. |
+----------------+----------------------------+---------------------------------------------------+
You are also encouraged to define your own application-specific elements
within your :ref:`configuration-file`.
:param source: A filename or file object containing XML data.
"""
logger.debug('loading %s', source)
self._source = source
self._database: Database | None = None
self._root: Element = ElementTree.parse(source).getroot()
element = self.find('gpib_library')
if element is not None:
Config.GPIB_LIBRARY = element.text
logger.debug('update Config.GPIB_LIBRARY = %s', Config.GPIB_LIBRARY)
element = self.find('pyvisa_library')
if element is not None:
Config.PyVISA_LIBRARY = element.text
logger.debug('update Config.PyVISA_LIBRARY = %s', Config.PyVISA_LIBRARY)
element = self.find('demo_mode')
if element is not None:
Config.DEMO_MODE = element.text.lower() == 'true'
logger.debug('update Config.DEMO_MODE = %s', Config.DEMO_MODE)
for element in self.findall('path'):
path = element.text
if not os.path.isdir(path):
logger.warning('cannot append to Config.PATH, %r is not a directory', path)
elif element.attrib.get('recursive', 'false').lower() == 'true':
for root, _, _ in os.walk(path):
if root not in Config.PATH:
Config.PATH.append(root)
os.environ['PATH'] += os.pathsep + root
logger.debug('append %r to Config.PATH', root)
elif path not in Config.PATH:
Config.PATH.append(path)
os.environ['PATH'] += os.pathsep + path
logger.debug('append %r to Config.PATH', path)
def __repr__(self) -> str:
return f'{self.__class__.__name__}(path={self.path!r})'
[docs]
def attrib(self, tag_or_path: str) -> dict[str, Any]:
"""Get the attributes of the first matching element by tag name or path.
The values are converted to the appropriate data type if possible. For
example, if the text of the element is ``true`` it will be converted
to :data:`True`, otherwise the value will be kept as a :class:`str`.
:param tag_or_path: Either an element tag name or an XPath.
:return: The attributes of the matching element.
"""
element = self.find(tag_or_path)
if element is None:
return {}
return dict((k, convert_to_primitive(v)) for k, v in element.attrib.items())
[docs]
def database(self) -> Database:
"""A reference to the equipment and connection records in the database(s)."""
if self._database is None:
from .database import Database # avoid circular import errors
self._database = Database(self._source)
return self._database
[docs]
def find(self, tag_or_path: str) -> Element | None:
"""Find the first matching element by tag name or path.
:param tag_or_path: Either an element tag name or an XPath.
:return: The element or :data:`None` if no element was found.
"""
return self._root.find(tag_or_path)
[docs]
def findall(self, tag_or_path: str) -> list[Element]:
"""Find all matching sub-elements by tag name or path.
:param tag_or_path: Either an element tag name or an XPath.
:return: All matching elements in document order.
"""
return self._root.findall(tag_or_path)
@property
def path(self) -> str:
"""The path to the configuration file."""
try:
return os.fsdecode(self._source)
except TypeError:
return f'<{self._source.__class__.__name__}>'
@property
def root(self) -> Element:
"""The root element (the first node) in the XML document."""
return self._root
[docs]
def value(self, tag_or_path: str, default: Any = None) -> Any:
"""Gets the value (text) associated with the first matching element.
The value is converted to the appropriate data type if possible. For
example, if the text of the element is ``true`` it will be converted
to :data:`True`.
:param tag_or_path: Either an element tag name or an XPath.
:param default: The default value if an element cannot be found.
:return: The value of the element or `default` if no element was found.
"""
element = self.find(tag_or_path)
if element is None:
return default
return convert_to_primitive(element.text)