From 82b14ad336c24cea8cb84d2327c373d5578c6a26 Mon Sep 17 00:00:00 2001 From: Bernhard Arnold Date: Thu, 30 Jul 2026 12:24:36 +0200 Subject: [PATCH 1/4] chore: modernize codebase and require Python 3.11+ --- .gitignore | 107 ++++++++++-- pyproject.toml | 5 +- src/comet/driver/corvus/venus1.py | 2 +- src/comet/driver/cts/itc.py | 42 +++-- src/comet/driver/ers/ac3.py | 11 +- src/comet/driver/factory.py | 3 +- src/comet/driver/generic/__init__.py | 24 +-- src/comet/driver/generic/dmm.py | 2 +- src/comet/driver/generic/electrometer.py | 2 +- src/comet/driver/generic/instrument.py | 20 +-- src/comet/driver/generic/lcr_meter.py | 1 - src/comet/driver/generic/light_source.py | 1 - src/comet/driver/generic/motion_controller.py | 6 +- src/comet/driver/generic/oscilloscope.py | 5 +- src/comet/driver/generic/power_supply.py | 5 +- src/comet/driver/generic/switching_matrix.py | 3 +- src/comet/driver/hephy/brandbox.py | 14 +- src/comet/driver/hephy/corvuscontroller.py | 8 +- src/comet/driver/hephy/environbox.py | 29 ++-- src/comet/driver/hephy/shuntbox.py | 12 +- src/comet/driver/itk/corvustt.py | 10 +- src/comet/driver/itk/hydra.py | 12 +- src/comet/driver/keithley/k237.py | 8 +- src/comet/driver/keithley/k2400.py | 8 +- src/comet/driver/keithley/k2410.py | 2 +- src/comet/driver/keithley/k2470.py | 7 +- src/comet/driver/keithley/k2657a.py | 7 +- src/comet/driver/keithley/k2700.py | 12 +- src/comet/driver/keithley/k6510.py | 3 +- src/comet/driver/keithley/k6514.py | 4 +- src/comet/driver/keithley/k6517b.py | 4 +- src/comet/driver/keithley/k707b.py | 12 +- src/comet/driver/keithley/k708b.py | 2 +- src/comet/driver/keysight/e4980a.py | 4 +- src/comet/driver/marzhauser/tango.py | 10 +- src/comet/driver/marzhauser/venus.py | 10 +- src/comet/driver/mbi/tablecontrol.py | 8 +- src/comet/driver/nkt_photonics/pilas.py | 6 +- src/comet/driver/rohde_schwarz/nge100.py | 11 +- src/comet/driver/rohde_schwarz/rtp164.py | 17 +- src/comet/driver/rohde_schwarz/sma100b.py | 3 +- src/comet/driver/smc/corvus.py | 11 +- src/comet/driver/thorlabs/pm100.py | 3 +- src/comet/emulator/__init__.py | 10 +- src/comet/emulator/__main__.py | 10 +- src/comet/emulator/cts/itc.py | 40 +++-- src/comet/emulator/emulator.py | 30 ++-- src/comet/emulator/ers/ac3.py | 4 +- src/comet/emulator/hephy/brandbox.py | 8 +- src/comet/emulator/hephy/environbox.py | 6 +- src/comet/emulator/hephy/shuntbox.py | 6 +- src/comet/emulator/iec60488.py | 3 +- src/comet/emulator/itk/corvustt.py | 20 ++- src/comet/emulator/itk/hydra.py | 20 +-- src/comet/emulator/keithley/k2400.py | 74 ++++++--- src/comet/emulator/keithley/k2410.py | 3 +- src/comet/emulator/keithley/k2470.py | 50 +++--- src/comet/emulator/keithley/k2657a.py | 76 +++++---- src/comet/emulator/keithley/k2700.py | 41 +++-- src/comet/emulator/keithley/k4215cvu.py | 49 +++--- src/comet/emulator/keithley/k6510.py | 1 - src/comet/emulator/keithley/k6514.py | 13 +- src/comet/emulator/keithley/k6517b.py | 14 +- src/comet/emulator/keithley/k707b.py | 15 +- src/comet/emulator/keithley/k708b.py | 3 +- src/comet/emulator/keysight/e4980a.py | 17 +- src/comet/emulator/marzhauser/tango.py | 20 ++- src/comet/emulator/nkt_photonics/pilas.py | 8 +- src/comet/emulator/photonic/f3000.py | 3 +- src/comet/emulator/resource.py | 28 ++-- src/comet/emulator/response.py | 8 +- src/comet/emulator/rohde_schwarz/nge100.py | 20 ++- src/comet/emulator/rohde_schwarz/rtp164.py | 16 +- src/comet/emulator/rohde_schwarz/sma100b.py | 10 +- src/comet/emulator/tcpserver.py | 28 ++-- src/comet/emulator/thorlabs/pm100.py | 3 +- src/comet/emulator/utils.py | 22 +-- src/comet/estimate.py | 2 +- src/comet/filters.py | 9 +- src/comet/functions.py | 8 +- src/comet/parameter.py | 51 +++--- src/comet/station.py | 152 ++++++++++++------ src/comet/utils.py | 30 ++-- tests/helpers.py | 28 ++-- tests/test_driver_corvus_venus1.py | 112 +++++++++---- tests/test_driver_cts_itc.py | 38 +++-- tests/test_driver_ers_ac3.py | 2 + tests/test_driver_hephy_environbox.py | 34 ++-- tests/test_driver_keithley_k237.py | 10 +- tests/test_driver_keithley_k2400.py | 18 ++- tests/test_driver_keithley_k2470.py | 18 ++- tests/test_driver_keithley_k2657a.py | 50 +++--- tests/test_driver_keithley_k2700.py | 18 ++- tests/test_driver_keithley_k6510.py | 4 +- tests/test_driver_keithley_k6514.py | 4 +- tests/test_driver_keithley_k6517b.py | 4 +- tests/test_driver_keithley_k707b.py | 18 +-- tests/test_driver_keithley_k708b.py | 18 +-- tests/test_driver_keysight_e4980a.py | 6 +- tests/test_driver_nkt_photonics_pilas.py | 1 - tests/test_driver_rohde_schwarz_rtp164.py | 2 +- tests/test_emulator_emulator.py | 2 +- tests/test_emulator_hephy_environbox.py | 16 +- tests/test_emulator_itk_corvustt.py | 12 +- tests/test_emulator_keithley_k2400.py | 37 +++-- tests/test_emulator_keithley_k2470.py | 41 ++++- tests/test_emulator_keithley_k2657a.py | 4 +- tests/test_emulator_keithley_k2700.py | 2 +- tests/test_emulator_keithley_k4215cvu.py | 16 +- tests/test_emulator_keithley_k6510.py | 4 +- tests/test_emulator_keithley_k6517b.py | 11 +- tests/test_emulator_keithley_k707b.py | 26 ++- tests/test_emulator_keithley_k708b.py | 72 ++++++++- tests/test_emulator_keysight_e4980a.py | 19 ++- tests/test_emulator_marzhauser_tango.py | 5 +- tests/test_emulator_resource.py | 4 +- tests/test_emulator_response.py | 12 +- tests/test_emulator_rohde_schwarz_nge100.py | 28 ++-- tests/test_emulator_rohde_schwarz_rtp164.py | 2 +- tests/test_emulator_tcpserver.py | 136 +++++++++------- tests/test_estimate.py | 2 + tests/test_functions.py | 46 +++--- tests/test_parameter.py | 14 +- tests/test_station.py | 95 ++++++++--- tests/test_utils.py | 36 +++-- tox.ini | 23 ++- 126 files changed, 1530 insertions(+), 927 deletions(-) diff --git a/.gitignore b/.gitignore index d828b9c..d99a8b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -20,7 +20,6 @@ parts/ sdist/ var/ wheels/ -pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -28,11 +27,10 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec -!pyinstaller.spec # Installer logs pip-log.txt @@ -48,9 +46,10 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -73,6 +72,7 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -83,27 +83,73 @@ profile_default/ ipython_config.py # pyenv -.python-version +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -129,3 +175,44 @@ dmypy.json # Pyre type checker .pyre/ +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/pyproject.toml b/pyproject.toml index 1ad24e3..c50df26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ ] readme = "README.md" license = {text = "GPLv3"} -requires-python = ">=3.9" +requires-python = ">=3.11" dependencies = [ "PyVISA", "PyVISA-py", @@ -33,3 +33,6 @@ build-backend = "hatchling.build" [tool.hatch.version] path = "src/comet/__init__.py" + +[tool.ruff.lint] +extend-ignore = ["BLE001", "RUF022"] diff --git a/src/comet/driver/corvus/venus1.py b/src/comet/driver/corvus/venus1.py index aca83b8..98b71ec 100644 --- a/src/comet/driver/corvus/venus1.py +++ b/src/comet/driver/corvus/venus1.py @@ -8,7 +8,7 @@ "'comet.driver.corvus.venus1' is deprecated and will be removed in a future version. " "Use 'comet.driver.itk.corvustt' or 'comet.driver.smc.corvus' instead.", DeprecationWarning, - stacklevel=1 + stacklevel=1, ) diff --git a/src/comet/driver/cts/itc.py b/src/comet/driver/cts/itc.py index ba36220..ddc60be 100644 --- a/src/comet/driver/cts/itc.py +++ b/src/comet/driver/cts/itc.py @@ -1,6 +1,6 @@ -import datetime from collections import namedtuple -from typing import Union +from datetime import UTC, datetime +from typing import ClassVar from comet.driver import Driver @@ -10,7 +10,7 @@ class ITCDriver(Driver): """ITC driver base class.""" - def query_bytes(self, message: Union[str, bytes], count: int) -> str: + def query_bytes(self, message: str | bytes, count: int) -> str: """Raw query for bytes. >>> instr.query_bytes("P", 4) @@ -23,7 +23,7 @@ def query_bytes(self, message: Union[str, bytes], count: int) -> str: class AnalogChannel(ITCDriver): - CHANNELS: dict[int, bytes] = { + CHANNELS: ClassVar[dict[int, bytes]] = { 1: b"A0", 2: b"A1", 3: b"A2", @@ -63,7 +63,9 @@ def __setitem__(self, index: int, value: float) -> None: """ if not 1 <= index <= 7: raise ValueError(f"invalid channel number: {index}") - code = type(self).CHANNELS[index].lower().decode() # write requires lower case 'a' + code = ( + type(self).CHANNELS[index].lower().decode() + ) # write requires lower case 'a' result = self.query_bytes(f"{code} {value:05.1f}", 1) if result != "a": raise RuntimeError(f"failed to set target for channel {index}") @@ -72,7 +74,7 @@ def __setitem__(self, index: int, value: float) -> None: class ITC(ITCDriver): """Interface for CTS Climate Chambers.""" - WARNING_MESSAGES: dict[str, str] = { + WARNING_MESSAGES: ClassVar[dict[str, str]] = { "\x01": "Wassernachfüllen", "\x02": "Temp. Toleranzband Oben", "\x03": "Temp. Toleranzband Unten", @@ -82,7 +84,7 @@ class ITC(ITCDriver): } """Warning messages.""" - ERROR_MESSAGES: dict[str, str] = { + ERROR_MESSAGES: ClassVar[dict[str, str]] = { "\x31": "Temperatur Grenze Min 08-B1", "\x32": "Temperatur Grenze Max 08-B1", "\x33": "Temp. Begrenzer Pruefr. 01-F1.1", @@ -119,28 +121,28 @@ def __init__(self, resource) -> None: def identify(self) -> str: """Returns instrument identification.""" - self.time # perform device access + _ = self.time # perform device access return "ITC climate chamber" @property - def time(self) -> datetime.datetime: + def time(self) -> datetime: """Returns current date and time of device as datetime object. >>> instr.time datetime.datetime(2019, 6, 12, 13, 01, 21) """ result = self.query_bytes("T", 13) - return datetime.datetime.strptime(result, "T%d%m%y%H%M%S") + return datetime.strptime(result, "T%d%m%y%H%M%S").replace(tzinfo=UTC) @time.setter - def time(self, dt: datetime.datetime) -> None: + def time(self, dt: datetime) -> None: """Update device date and time, returns updated data and time as datetime object. >>> instr.time = datetime.datetime.now() """ datetime_format = "t%d%m%y%H%M%S" result = self.query_bytes(dt.strftime(datetime_format), 13) - if dt != datetime.datetime.strptime(result, datetime_format): + if dt != datetime.strptime(result, datetime_format).replace(tzinfo=UTC): raise RuntimeError("failed to set date and time") @property @@ -155,10 +157,20 @@ def status(self) -> Status: result = self.query_bytes("S", 10) running = bool(int(result[1])) is_error = bool(int(result[2])) - channels = {channel: bool(int(state)) for channel, state in enumerate(result[3:9])} + channels = { + channel: bool(int(state)) for channel, state in enumerate(result[3:9]) + } error_nr = result[9] - warning = type(self).WARNING_MESSAGES[error_nr] if is_error and error_nr in type(self).WARNING_MESSAGES else None - error = type(self).ERROR_MESSAGES[error_nr] if is_error and error_nr in type(self).ERROR_MESSAGES else None + warning = ( + type(self).WARNING_MESSAGES[error_nr] + if is_error and error_nr in type(self).WARNING_MESSAGES + else None + ) + error = ( + type(self).ERROR_MESSAGES[error_nr] + if is_error and error_nr in type(self).ERROR_MESSAGES + else None + ) return type(self).Status(running, warning, error, channels) @property diff --git a/src/comet/driver/ers/ac3.py b/src/comet/driver/ers/ac3.py index 5780f0c..56bf330 100644 --- a/src/comet/driver/ers/ac3.py +++ b/src/comet/driver/ers/ac3.py @@ -1,7 +1,8 @@ """Driver for ECR AC3 thermal chuck""" +from typing import ClassVar + from comet.driver.generic import Instrument, InstrumentError -from typing import Optional __all__ = ["AC3"] @@ -20,7 +21,7 @@ class AC3(Instrument): STATUS_COOLING: int = 2 STATUS_ERROR: int = 8 - ERROR_MESSAGES = { + ERROR_MESSAGES: ClassVar[dict[int, str]] = { 1: "OVERTEMP: The Chuck temperature has passed the maximum temperature limit by more than 2°C.", 3: "CHUCKCABLE: Analog-digital-converter error", 4: "CHUCKCABLE: The Chuck's sensor cable or the Chuck sensor is defective.", @@ -55,7 +56,7 @@ def reset(self) -> None: ... # not supported def clear(self) -> None: ... # not supported - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code = int(self._query("RE")[1:]) if code: @@ -100,7 +101,7 @@ def target_temperature(self, value: float) -> None: """Set temperature setpoint in °C.""" if value > 300 or value < -70: - raise ValueError("Temperature {} is out of range -70 to 300C".format(value)) + raise ValueError(f"Temperature {value} is out of range -70 to 300C") # Convert to 1/10°C with sign temp = int(value * 10) @@ -120,7 +121,7 @@ def operating_mode(self) -> int: @operating_mode.setter def operating_mode(self, mode: int) -> None: if mode not in range(1, 5): - raise ValueError("Invalid mode: {}".format(mode)) + raise ValueError(f"Invalid mode: {mode}") """Set operating mode.""" self._query(f"SO{mode}") diff --git a/src/comet/driver/factory.py b/src/comet/driver/factory.py index 7ed4836..f22f26c 100644 --- a/src/comet/driver/factory.py +++ b/src/comet/driver/factory.py @@ -11,7 +11,8 @@ def find_drivers(module: types.ModuleType) -> list[type[Driver]]: """Return all classes of a module derving from class Driver.""" return [ - obj for _, obj in inspect.getmembers(module, inspect.isclass) + obj + for _, obj in inspect.getmembers(module, inspect.isclass) if issubclass(obj, Driver) and obj is not Driver ] diff --git a/src/comet/driver/generic/__init__.py b/src/comet/driver/generic/__init__.py index b8b8ff0..1bdca40 100644 --- a/src/comet/driver/generic/__init__.py +++ b/src/comet/driver/generic/__init__.py @@ -1,36 +1,36 @@ +from .dmm import DigitalMultiMeter +from .electrometer import Electrometer from .instrument import ( BeeperMixin, ErrorQueueMixin, - RouteTerminalMixin, - InstrumentError, Instrument, + InstrumentError, + RouteTerminalMixin, ) -from .dmm import DigitalMultiMeter -from .electrometer import Electrometer from .lcr_meter import LCRMeter from .light_source import LightSource -from .source_meter_unit import SourceMeterUnit -from .switching_matrix import SwitchingMatrix from .motion_controller import ( MotionController, MotionControllerAxis, ) from .oscilloscope import Oscilloscope, OscilloscopeChannel +from .source_meter_unit import SourceMeterUnit +from .switching_matrix import SwitchingMatrix __all__ = [ "BeeperMixin", - "ErrorQueueMixin", - "RouteTerminalMixin", - "InstrumentError", - "Instrument", "DigitalMultiMeter", "Electrometer", + "ErrorQueueMixin", + "Instrument", + "InstrumentError", "LCRMeter", "LightSource", - "SourceMeterUnit", - "SwitchingMatrix", "MotionController", "MotionControllerAxis", "Oscilloscope", "OscilloscopeChannel", + "RouteTerminalMixin", + "SourceMeterUnit", + "SwitchingMatrix", ] diff --git a/src/comet/driver/generic/dmm.py b/src/comet/driver/generic/dmm.py index 7652c7c..1cb2bf1 100644 --- a/src/comet/driver/generic/dmm.py +++ b/src/comet/driver/generic/dmm.py @@ -1,11 +1,11 @@ from abc import abstractmethod + from .instrument import Instrument __all__ = ["DigitalMultiMeter"] class DigitalMultiMeter(Instrument): - # Measurements @abstractmethod diff --git a/src/comet/driver/generic/electrometer.py b/src/comet/driver/generic/electrometer.py index ab7c5d3..dc83558 100644 --- a/src/comet/driver/generic/electrometer.py +++ b/src/comet/driver/generic/electrometer.py @@ -1,11 +1,11 @@ from abc import abstractmethod + from .instrument import Instrument __all__ = ["Electrometer"] class Electrometer(Instrument): - # Measurements @abstractmethod diff --git a/src/comet/driver/generic/instrument.py b/src/comet/driver/generic/instrument.py index b5b4b1e..9088e33 100644 --- a/src/comet/driver/generic/instrument.py +++ b/src/comet/driver/generic/instrument.py @@ -1,22 +1,20 @@ from abc import ABC, abstractmethod -from typing import Optional from ..driver import Driver __all__ = [ - "InstrumentError", - "IdentifyMixin", - "ResetMixin", + "BeeperMixin", "ClearMixin", "ErrorQueueMixin", - "BeeperMixin", - "RouteTerminalMixin", + "IdentifyMixin", "Instrument", + "InstrumentError", + "ResetMixin", + "RouteTerminalMixin", ] class InstrumentError: - def __init__(self, code: int, message: str) -> None: self.code: int = code self.message: str = message @@ -27,31 +25,26 @@ def __repr__(self) -> str: class IdentifyMixin(ABC): - @abstractmethod def identify(self) -> str: ... class ResetMixin(ABC): - @abstractmethod def reset(self) -> None: ... class ClearMixin(ABC): - @abstractmethod def clear(self) -> None: ... class ErrorQueueMixin(ABC): - @abstractmethod - def next_error(self) -> Optional[InstrumentError]: ... + def next_error(self) -> InstrumentError | None: ... class BeeperMixin(ABC): - BEEPER_ON: bool = True BEEPER_OFF: bool = False @@ -65,7 +58,6 @@ def beeper(self, value: bool) -> None: ... class RouteTerminalMixin(ABC): - ROUTE_TERMINAL_FRONT: str = "front" ROUTE_TERMINAL_REAR: str = "rear" diff --git a/src/comet/driver/generic/lcr_meter.py b/src/comet/driver/generic/lcr_meter.py index 9945149..fcb3302 100644 --- a/src/comet/driver/generic/lcr_meter.py +++ b/src/comet/driver/generic/lcr_meter.py @@ -6,7 +6,6 @@ class LCRMeter(Instrument): - @property @abstractmethod def function(self) -> str: ... diff --git a/src/comet/driver/generic/light_source.py b/src/comet/driver/generic/light_source.py index fafa023..f23ec53 100644 --- a/src/comet/driver/generic/light_source.py +++ b/src/comet/driver/generic/light_source.py @@ -6,7 +6,6 @@ class LightSource(Driver): - @property @abstractmethod def brightness(self) -> int: ... diff --git a/src/comet/driver/generic/motion_controller.py b/src/comet/driver/generic/motion_controller.py index aa63695..2564b6e 100644 --- a/src/comet/driver/generic/motion_controller.py +++ b/src/comet/driver/generic/motion_controller.py @@ -1,15 +1,14 @@ from abc import abstractmethod -from typing import Iterable +from collections.abc import Iterable from .instrument import Driver, Instrument -__all__ = ["MotionControllerAxis", "MotionController"] +__all__ = ["MotionController", "MotionControllerAxis"] Position = Iterable[float] class MotionControllerAxis(Driver): - def __init__(self, resource, index: int) -> None: super().__init__(resource) self.index: int = index @@ -40,7 +39,6 @@ def is_moving(self) -> bool: ... class MotionController(Instrument): - @abstractmethod def __getitem__(self, index: int) -> MotionControllerAxis: ... diff --git a/src/comet/driver/generic/oscilloscope.py b/src/comet/driver/generic/oscilloscope.py index d3901e4..1428c66 100644 --- a/src/comet/driver/generic/oscilloscope.py +++ b/src/comet/driver/generic/oscilloscope.py @@ -1,6 +1,5 @@ from abc import abstractmethod - -from typing import Iterator +from collections.abc import Iterator from .instrument import Driver, Instrument @@ -8,7 +7,6 @@ class OscilloscopeChannel(Driver): - def __init__(self, resource, channel: int) -> None: super().__init__(resource) self.channel: int = channel @@ -29,7 +27,6 @@ def acquire_waveform(self) -> list[float]: ... class Oscilloscope(Instrument): - @abstractmethod def __getitem__(self, channel: int) -> OscilloscopeChannel: ... diff --git a/src/comet/driver/generic/power_supply.py b/src/comet/driver/generic/power_supply.py index 55f8b89..77fd33d 100644 --- a/src/comet/driver/generic/power_supply.py +++ b/src/comet/driver/generic/power_supply.py @@ -1,6 +1,5 @@ from abc import abstractmethod - -from typing import Iterator +from collections.abc import Iterator from .instrument import Driver, Instrument @@ -8,7 +7,6 @@ class PowerSupplyChannel(Driver): - def __init__(self, resource, channel: int) -> None: super().__init__(resource) self.channel: int = channel @@ -57,7 +55,6 @@ def measure_power(self) -> float: ... class PowerSupply(Instrument): - @abstractmethod def __getitem__(self, channel: int) -> PowerSupplyChannel: ... diff --git a/src/comet/driver/generic/switching_matrix.py b/src/comet/driver/generic/switching_matrix.py index 234e7fe..4d2e143 100644 --- a/src/comet/driver/generic/switching_matrix.py +++ b/src/comet/driver/generic/switching_matrix.py @@ -1,4 +1,5 @@ from abc import abstractmethod +from typing import ClassVar from .instrument import Instrument @@ -6,7 +7,7 @@ class SwitchingMatrix(Instrument): - CHANNELS: list[str] = [] + CHANNELS: ClassVar[tuple[str, ...]] = () @property @abstractmethod diff --git a/src/comet/driver/hephy/brandbox.py b/src/comet/driver/hephy/brandbox.py index 54ce180..f49b85e 100644 --- a/src/comet/driver/hephy/brandbox.py +++ b/src/comet/driver/hephy/brandbox.py @@ -1,5 +1,5 @@ import re -from typing import Optional +from collections.abc import Iterable from comet.driver.generic import InstrumentError from comet.driver.generic.switching_matrix import SwitchingMatrix @@ -14,11 +14,11 @@ def split_channels(channels: str) -> list[str]: return [channel.strip() for channel in channels.split(",") if channel.strip()] -def join_channels(channels: list[str]) -> str: +def join_channels(channels: Iterable[str]) -> str: return ",".join([format(channel).strip() for channel in channels]) -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: m = re.match(r"^err(\d+)", response.lower()) if m: code = int(m.group(1)) @@ -28,9 +28,11 @@ def parse_error(response: str) -> Optional[InstrumentError]: class BrandBox(SwitchingMatrix): - CHANNELS: list[str] = combine_matrix("ABC", "12") + CHANNELS = tuple(combine_matrix("ABC", "12")) - _error_queue: list[InstrumentError] = [] + def __init__(self, resource) -> None: + super().__init__(resource) + self._error_queue: list[InstrumentError] = [] def identify(self) -> str: return self.query("*IDN?") @@ -45,7 +47,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: if self._error_queue: return self._error_queue.pop(0) return None diff --git a/src/comet/driver/hephy/corvuscontroller.py b/src/comet/driver/hephy/corvuscontroller.py index 094b4fa..2fd81e3 100644 --- a/src/comet/driver/hephy/corvuscontroller.py +++ b/src/comet/driver/hephy/corvuscontroller.py @@ -6,13 +6,13 @@ MR=x.xxx,x - Move relative [StepWidth,Axis] """ -from typing import Final, Optional, Protocol +from typing import Final, Protocol from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["CorvusController"] @@ -89,7 +89,7 @@ def reset(self) -> None: ... # Not supported def clear(self) -> None: ... # Not supported - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: return None # Not supported def __getitem__(self, index: int) -> CorvusControllerAxis: diff --git a/src/comet/driver/hephy/environbox.py b/src/comet/driver/hephy/environbox.py index f60e823..8d7cea6 100644 --- a/src/comet/driver/hephy/environbox.py +++ b/src/comet/driver/hephy/environbox.py @@ -1,8 +1,7 @@ import re -from typing import Any, Optional +from typing import Any -from comet.driver.generic import Instrument -from comet.driver.generic import InstrumentError +from comet.driver.generic import Instrument, InstrumentError __all__ = ["EnvironBox"] @@ -23,7 +22,7 @@ def test_bit(value: int, index: int) -> bool: return bool((value >> index) & 1) -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: m = re.match(r"^err(\d+)", response.lower()) if m: code = int(m.group(1)) @@ -85,7 +84,9 @@ def parse_pc_data(response: str) -> dict[str, Any]: class EnvironBox(Instrument): - _error_queue: list[InstrumentError] = [] + def __init__(self, resource) -> None: + super().__init__(resource) + self._error_queue: list[InstrumentError] = [] def identify(self) -> str: return self.query("*IDN?") @@ -100,7 +101,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: if self._error_queue: return self._error_queue.pop(0) return None @@ -130,7 +131,9 @@ def get_pid_control_mode(self) -> str: return {1: self.PID_CONTROL_MODE_HUM, 2: self.PID_CONTROL_MODE_DEW}[value] def set_pid_control_mode(self, mode: str) -> None: - value = {self.PID_CONTROL_MODE_HUM: "HUM", self.PID_CONTROL_MODE_DEW: "DEW"}[mode] + value = {self.PID_CONTROL_MODE_HUM: "HUM", self.PID_CONTROL_MODE_DEW: "DEW"}[ + mode + ] self.write(f"SET:CTRL_MODE {value}") PID_DOOR_STOP_OFF: bool = False @@ -138,7 +141,9 @@ def set_pid_control_mode(self, mode: str) -> None: def get_pid_door_stop(self) -> bool: value = self.query("GET:PID_DOOR_STOP ?") - return {"1": self.PID_DOOR_STOP_OFF, "2": self.PID_DOOR_STOP_ON}[value] # [1=OFF,2=ON] + return {"1": self.PID_DOOR_STOP_OFF, "2": self.PID_DOOR_STOP_ON}[ + value + ] # [1=OFF,2=ON] def set_pid_door_stop(self, state: bool) -> None: value = {self.PID_DOOR_STOP_OFF: "OFF", self.PID_DOOR_STOP_ON: "ON"}[state] @@ -217,7 +222,9 @@ def set_test_led(self, state: bool) -> None: def get_door_auto_light(self) -> bool: """Get state of door automatic light switch.""" value = self.query("GET:DOOR_AUTO_LIGHT ?") - return {"1": self.DOOR_AUTO_LIGHT_OFF, "2": self.DOOR_AUTO_LIGHT_ON}[value] # [1=OFF,2=ON] + return {"1": self.DOOR_AUTO_LIGHT_OFF, "2": self.DOOR_AUTO_LIGHT_ON}[ + value + ] # [1=OFF,2=ON] def set_door_auto_light(self, state: bool) -> None: """Set door automatic light switch state.""" @@ -232,7 +239,9 @@ def get_uptime(self) -> float: """Return Arduino uptime in seconds.""" value = self.query("GET:UPTIME ?") days, hours, minutes, seconds = map(int, value.split(",")) - total_seconds = (days * 24 * 60 * 60) + (hours * 60 * 60) + (minutes * 60) + seconds + total_seconds = ( + (days * 24 * 60 * 60) + (hours * 60 * 60) + (minutes * 60) + seconds + ) return float(total_seconds) # Helper diff --git a/src/comet/driver/hephy/shuntbox.py b/src/comet/driver/hephy/shuntbox.py index 94def6a..ca48180 100644 --- a/src/comet/driver/hephy/shuntbox.py +++ b/src/comet/driver/hephy/shuntbox.py @@ -1,15 +1,13 @@ import re -from typing import Optional -from comet.driver.generic import Instrument -from comet.driver.generic import InstrumentError +from comet.driver.generic import Instrument, InstrumentError __all__ = ["ShuntBox"] ERROR_MESSAGES: dict[int, str] = {99: "Unknown command"} -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: m = re.match(r"^err(\d+)", response.lower()) if m: code = int(m.group(1)) @@ -19,7 +17,9 @@ def parse_error(response: str) -> Optional[InstrumentError]: class ShuntBox(Instrument): - _error_queue: list[InstrumentError] = [] + def __init__(self, resource) -> None: + super().__init__(resource) + self._error_queue: list[InstrumentError] = [] def identify(self) -> str: return self.query("*IDN?") @@ -34,7 +34,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: if self._error_queue: return self._error_queue.pop(0) return None diff --git a/src/comet/driver/itk/corvustt.py b/src/comet/driver/itk/corvustt.py index c03af2f..0a38bc3 100644 --- a/src/comet/driver/itk/corvustt.py +++ b/src/comet/driver/itk/corvustt.py @@ -1,10 +1,10 @@ -from typing import Final, Optional +from typing import Final from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["CorvusTT"] @@ -27,7 +27,7 @@ } -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: if not response.strip().isnumeric(): raise ValueError(f"Invalid error response, not a number: {response!r}") code = int(response) @@ -77,7 +77,7 @@ def reset(self) -> None: ... def clear(self) -> None: ... - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: response = self.resource.query("geterror").strip() return parse_error(response) diff --git a/src/comet/driver/itk/hydra.py b/src/comet/driver/itk/hydra.py index 009a571..d4fb3c0 100644 --- a/src/comet/driver/itk/hydra.py +++ b/src/comet/driver/itk/hydra.py @@ -1,10 +1,10 @@ -from typing import Optional +from typing import Final from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["Hydra"] @@ -26,7 +26,7 @@ } -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: code = int(response) if code: message = ERROR_MESSAGES.get(code, "unknown error") @@ -65,7 +65,7 @@ def is_moving(self) -> bool: class Hydra(MotionController): - AXES: list[int] = [1, 2] + AXES: Final[list[int]] = [1, 2] def identify(self) -> str: return self.resource.query("identify").strip() @@ -74,7 +74,7 @@ def reset(self) -> None: ... def clear(self) -> None: ... - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: response = self.resource.query("ge") return parse_error(response) diff --git a/src/comet/driver/keithley/k237.py b/src/comet/driver/keithley/k237.py index df5a64f..5da263a 100644 --- a/src/comet/driver/keithley/k237.py +++ b/src/comet/driver/keithley/k237.py @@ -1,5 +1,5 @@ import time -from typing import Optional +from typing import ClassVar from comet.driver.generic import InstrumentError from comet.driver.generic.source_meter_unit import SourceMeterUnit @@ -47,7 +47,7 @@ def select_range_index(values: dict[int, float], level: float) -> int: class K237(SourceMeterUnit): WRITE_DELAY: float = 0.250 - VOLTAGE_RANGES: dict[int, float] = { + VOLTAGE_RANGES: ClassVar[dict[int, float]] = { 0: 0.0, 1: 1.1, 2: 11.0, @@ -55,7 +55,7 @@ class K237(SourceMeterUnit): 4: 1100.0, } - CURRENT_RANGES: dict[int, float] = { + CURRENT_RANGES: ClassVar[dict[int, float]] = { 0: 0.0, 1: 1e-09, 2: 1e-08, @@ -81,7 +81,7 @@ def reset(self) -> None: def clear(self) -> None: self.resource.clear() - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: values = self.query("U1X")[3:] for index, value in enumerate(values): if value == "1": diff --git a/src/comet/driver/keithley/k2400.py b/src/comet/driver/keithley/k2400.py index b07afef..5efac53 100644 --- a/src/comet/driver/keithley/k2400.py +++ b/src/comet/driver/keithley/k2400.py @@ -1,8 +1,4 @@ -from typing import Optional - -from comet.driver.generic import BeeperMixin -from comet.driver.generic import RouteTerminalMixin -from comet.driver.generic import InstrumentError +from comet.driver.generic import BeeperMixin, InstrumentError, RouteTerminalMixin from comet.driver.generic.source_meter_unit import SourceMeterUnit __all__ = ["K2400"] @@ -37,7 +33,7 @@ def beeper(self, value: bool) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/keithley/k2410.py b/src/comet/driver/keithley/k2410.py index a086d0b..453a608 100644 --- a/src/comet/driver/keithley/k2410.py +++ b/src/comet/driver/keithley/k2410.py @@ -5,4 +5,4 @@ class K2410(K2400): """Driver for Keithley 2410 source meter unit.""" - ... + diff --git a/src/comet/driver/keithley/k2470.py b/src/comet/driver/keithley/k2470.py index 825e12d..4abbbc4 100644 --- a/src/comet/driver/keithley/k2470.py +++ b/src/comet/driver/keithley/k2470.py @@ -1,7 +1,4 @@ -from typing import Optional - -from comet.driver.generic import InstrumentError -from comet.driver.generic import RouteTerminalMixin +from comet.driver.generic import InstrumentError, RouteTerminalMixin from comet.driver.generic.source_meter_unit import SourceMeterUnit from .k2400 import parse_error @@ -21,7 +18,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/keithley/k2657a.py b/src/comet/driver/keithley/k2657a.py index cdbfd9b..704792e 100644 --- a/src/comet/driver/keithley/k2657a.py +++ b/src/comet/driver/keithley/k2657a.py @@ -1,7 +1,4 @@ -from typing import Optional - -from comet.driver.generic import BeeperMixin -from comet.driver.generic import InstrumentError +from comet.driver.generic import BeeperMixin, InstrumentError from comet.driver.generic.source_meter_unit import SourceMeterUnit __all__ = ["K2657A"] @@ -29,7 +26,7 @@ def beeper(self, value: bool) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = self.tsp_print("errorqueue.next()").split("\t")[:2] if int(float(code)): return InstrumentError(int(float(code)), message.strip("\"' ")) diff --git a/src/comet/driver/keithley/k2700.py b/src/comet/driver/keithley/k2700.py index e782d4d..c1e9d73 100644 --- a/src/comet/driver/keithley/k2700.py +++ b/src/comet/driver/keithley/k2700.py @@ -1,7 +1,4 @@ -from typing import Optional - -from comet.driver.generic import BeeperMixin -from comet.driver.generic import InstrumentError +from comet.driver.generic import BeeperMixin, InstrumentError from comet.driver.generic.dmm import DigitalMultiMeter __all__ = ["K2700"] @@ -14,10 +11,11 @@ def parse_error(response: str) -> tuple[int, str]: class K2700(BeeperMixin, DigitalMultiMeter): """Driver for Keithley 2700 digital multimeter.""" + def __init__(self, resource) -> None: super().__init__(resource) - self._sense_function: Optional[str] = None - self._format_elements: Optional[str] = None + self._sense_function: str | None = None + self._format_elements: str | None = None def identify(self) -> str: return self._query("*IDN?") @@ -45,7 +43,7 @@ def beeper(self, value: bool) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self._query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/keithley/k6510.py b/src/comet/driver/keithley/k6510.py index fdf6724..d232959 100644 --- a/src/comet/driver/keithley/k6510.py +++ b/src/comet/driver/keithley/k6510.py @@ -1,5 +1,4 @@ import warnings -from typing import Optional from comet.driver.generic import InstrumentError, RouteTerminalMixin from comet.driver.generic.dmm import DigitalMultiMeter @@ -26,7 +25,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/keithley/k6514.py b/src/comet/driver/keithley/k6514.py index b5d8ae5..86386bd 100644 --- a/src/comet/driver/keithley/k6514.py +++ b/src/comet/driver/keithley/k6514.py @@ -1,5 +1,3 @@ -from typing import Optional - from comet.driver.generic import InstrumentError from comet.driver.generic.electrometer import Electrometer @@ -23,7 +21,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/keithley/k6517b.py b/src/comet/driver/keithley/k6517b.py index 55f528f..0e6428a 100644 --- a/src/comet/driver/keithley/k6517b.py +++ b/src/comet/driver/keithley/k6517b.py @@ -1,5 +1,3 @@ -from typing import Optional - from comet.driver.generic import InstrumentError from comet.driver.generic.electrometer import Electrometer @@ -23,7 +21,7 @@ def clear(self) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/keithley/k707b.py b/src/comet/driver/keithley/k707b.py index 95846bb..e172752 100644 --- a/src/comet/driver/keithley/k707b.py +++ b/src/comet/driver/keithley/k707b.py @@ -1,5 +1,3 @@ -from typing import Optional - from comet.driver.generic import InstrumentError from comet.driver.generic.switching_matrix import SwitchingMatrix from comet.utils import combine_matrix @@ -16,8 +14,12 @@ def join_channels(channels: list[str]) -> str: class K707B(SwitchingMatrix): - CHANNELS: list[str] = combine_matrix( - "1234", "ABCDEFG", combine_matrix("0", "123456789") + combine_matrix("1", "12") + CHANNELS = tuple( + combine_matrix( + "1234", + "ABCDEFG", + combine_matrix("0", "123456789") + combine_matrix("1", "12"), + ) ) def identify(self) -> str: @@ -41,7 +43,7 @@ def beeper(self, value: bool) -> None: # Error queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = self.tsp_print("errorqueue.next()").split("\t")[:2] if int(code): return InstrumentError(int(code), message.strip("\"' ")) diff --git a/src/comet/driver/keithley/k708b.py b/src/comet/driver/keithley/k708b.py index ea2e910..2cd1d77 100644 --- a/src/comet/driver/keithley/k708b.py +++ b/src/comet/driver/keithley/k708b.py @@ -6,4 +6,4 @@ class K708B(K707B): - CHANNELS: list[str] = combine_matrix("1", "ABCDEFG", "0", "012345678") + CHANNELS = tuple(combine_matrix("1", "ABCDEFG", "0", "012345678")) diff --git a/src/comet/driver/keysight/e4980a.py b/src/comet/driver/keysight/e4980a.py index b72c586..aae46a7 100644 --- a/src/comet/driver/keysight/e4980a.py +++ b/src/comet/driver/keysight/e4980a.py @@ -1,5 +1,3 @@ -from typing import Optional - from comet.driver.generic import InstrumentError from comet.driver.generic.lcr_meter import LCRMeter @@ -53,7 +51,7 @@ def beeper(self, value: bool) -> None: # Error Queue - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = self.query(":SYST:ERR:NEXT?").split(",")[:2] if int(code): return InstrumentError(int(code), message.strip("\"' ")) diff --git a/src/comet/driver/marzhauser/tango.py b/src/comet/driver/marzhauser/tango.py index 3e1c701..c11c8c2 100644 --- a/src/comet/driver/marzhauser/tango.py +++ b/src/comet/driver/marzhauser/tango.py @@ -1,10 +1,8 @@ -from typing import Optional - from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["Tango"] @@ -56,7 +54,7 @@ } -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: code = int(response) if code: message = ERROR_MESSAGES.get(code, "unknown error") @@ -111,7 +109,7 @@ def reset(self) -> None: ... def clear(self) -> None: self.resource.write("!err") # clear error state - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: return parse_error(self.resource.query("?err")) def __getitem__(self, index: int) -> TangoAxis: diff --git a/src/comet/driver/marzhauser/venus.py b/src/comet/driver/marzhauser/venus.py index 34cd6a8..a709aa5 100644 --- a/src/comet/driver/marzhauser/venus.py +++ b/src/comet/driver/marzhauser/venus.py @@ -1,10 +1,10 @@ -from typing import Final, Optional +from typing import Final from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["Venus"] @@ -56,7 +56,7 @@ } -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: code = int(response) if code: message = ERROR_MESSAGES.get(code, "unknown error") @@ -106,7 +106,7 @@ def reset(self) -> None: ... def clear(self) -> None: ... - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: response = self.resource.query("geterror") return parse_error(response) diff --git a/src/comet/driver/mbi/tablecontrol.py b/src/comet/driver/mbi/tablecontrol.py index c9488bc..621d3b0 100644 --- a/src/comet/driver/mbi/tablecontrol.py +++ b/src/comet/driver/mbi/tablecontrol.py @@ -1,10 +1,10 @@ -from typing import Final, Optional +from typing import Final from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["TableControl"] @@ -71,7 +71,7 @@ def reset(self) -> None: ... def clear(self) -> None: self.resource.query("*CLS") - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: response = self.resource.query("SYS:ERR?").strip() code, message = parse_error(response) if code: diff --git a/src/comet/driver/nkt_photonics/pilas.py b/src/comet/driver/nkt_photonics/pilas.py index 64508ac..87a1064 100644 --- a/src/comet/driver/nkt_photonics/pilas.py +++ b/src/comet/driver/nkt_photonics/pilas.py @@ -57,7 +57,7 @@ def tune(self, value: float) -> None: self.tune_mode = self.TUNE_MANUAL - self.write_and_check(f"tune={int(value*10)}") + self.write_and_check(f"tune={int(value * 10)}") @property def frequency(self) -> int: @@ -84,7 +84,7 @@ def get_laser_head_temperature(self) -> float: "Method 'PILAS.get_laser_head_temperature' is deprecated and will be removed in a future version. " "Use property 'PILAS.laser_head_temperature' instead.", DeprecationWarning, - stacklevel=1 + stacklevel=1, ) return self.laser_head_temperature @@ -93,7 +93,7 @@ def get_laser_diode_temperature(self) -> bool: "Method 'PILAS.get_laser_diode_temperature' is deprecated and will be removed in a future version. " "Use property 'PILAS.laser_diode_temperature' instead.", DeprecationWarning, - stacklevel=1 + stacklevel=1, ) return self.laser_diode_temperature diff --git a/src/comet/driver/rohde_schwarz/nge100.py b/src/comet/driver/rohde_schwarz/nge100.py index f24520f..da2de36 100644 --- a/src/comet/driver/rohde_schwarz/nge100.py +++ b/src/comet/driver/rohde_schwarz/nge100.py @@ -1,4 +1,4 @@ -from typing import Optional, Iterator +from collections.abc import Iterator from comet.driver.generic import InstrumentError from comet.driver.generic.power_supply import PowerSupply, PowerSupplyChannel @@ -77,7 +77,7 @@ def reset(self) -> None: def clear(self) -> None: self.write("*CLS") - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = self.query("SYSTem:ERRor?").split(", ") if int(code): return InstrumentError(int(code), message.strip("'")) @@ -98,7 +98,12 @@ def __getitem__(self, channel: int) -> NGE100Channel: return NGE100Channel(self.resource, channel) def __iter__(self) -> Iterator[NGE100Channel]: - return iter([NGE100Channel(self.resource, channel) for channel in range(type(self).N_CHANNELS)]) + return iter( + [ + NGE100Channel(self.resource, channel) + for channel in range(type(self).N_CHANNELS) + ] + ) def __len__(self) -> int: return type(self).N_CHANNELS diff --git a/src/comet/driver/rohde_schwarz/rtp164.py b/src/comet/driver/rohde_schwarz/rtp164.py index 907baa8..2b65ffe 100644 --- a/src/comet/driver/rohde_schwarz/rtp164.py +++ b/src/comet/driver/rohde_schwarz/rtp164.py @@ -1,4 +1,4 @@ -from typing import Optional, Iterator +from collections.abc import Iterator import numpy as np @@ -33,7 +33,9 @@ def acquire_waveform(self) -> list[float]: self.resource.query("*OPC?") source = f"CHAN{self.channel + 1}" - values = self.resource.query_binary_values(f":{source}:DATA?", datatype="f", is_big_endian=False) + values = self.resource.query_binary_values( + f":{source}:DATA?", datatype="f", is_big_endian=False + ) return values @@ -52,10 +54,10 @@ def clear(self) -> None: self.resource.write("*CLS") self.resource.query("*OPC?") - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = self.resource.query("SYST:ERR?").split(",") if int(code): - return InstrumentError(int(code), message.strip().strip("\"")) + return InstrumentError(int(code), message.strip().strip('"')) return None def configure(self) -> None: @@ -76,7 +78,12 @@ def __getitem__(self, channel: int) -> RTP164Channel: return RTP164Channel(self.resource, channel) def __iter__(self) -> Iterator[RTP164Channel]: - return iter([RTP164Channel(self.resource, channel) for channel in range(type(self).N_CHANNELS)]) + return iter( + [ + RTP164Channel(self.resource, channel) + for channel in range(type(self).N_CHANNELS) + ] + ) def __len__(self) -> int: return type(self).N_CHANNELS diff --git a/src/comet/driver/rohde_schwarz/sma100b.py b/src/comet/driver/rohde_schwarz/sma100b.py index ed01a62..d27544b 100644 --- a/src/comet/driver/rohde_schwarz/sma100b.py +++ b/src/comet/driver/rohde_schwarz/sma100b.py @@ -1,5 +1,4 @@ from comet.driver.generic import Instrument, InstrumentError -from typing import Optional __all__ = ["SMA100B"] @@ -24,7 +23,7 @@ def reset(self) -> None: def clear(self) -> None: self.write("*CLS") - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/driver/smc/corvus.py b/src/comet/driver/smc/corvus.py index 6bb3766..02ffdc5 100644 --- a/src/comet/driver/smc/corvus.py +++ b/src/comet/driver/smc/corvus.py @@ -1,10 +1,10 @@ -from typing import Final, Optional +from typing import Final from comet.driver.generic import InstrumentError from comet.driver.generic.motion_controller import ( - Position, - MotionControllerAxis, MotionController, + MotionControllerAxis, + Position, ) __all__ = ["Corvus"] @@ -27,7 +27,7 @@ } -def parse_error(response: str) -> Optional[InstrumentError]: +def parse_error(response: str) -> InstrumentError | None: code = int(response) if code: message = ERROR_MESSAGES.get(code, "unknown error") @@ -69,6 +69,7 @@ def is_moving(self) -> bool: class Corvus(MotionController): """Driver for SMC Corvus motion controller.""" + AXIS_IDS: Final = (1, 2, 3) def identify(self) -> str: @@ -78,7 +79,7 @@ def reset(self) -> None: ... def clear(self) -> None: ... - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: response = self.resource.query("geterror").strip() return parse_error(response) diff --git a/src/comet/driver/thorlabs/pm100.py b/src/comet/driver/thorlabs/pm100.py index e7ece00..2566f01 100644 --- a/src/comet/driver/thorlabs/pm100.py +++ b/src/comet/driver/thorlabs/pm100.py @@ -1,5 +1,4 @@ from comet.driver.generic import Instrument, InstrumentError -from typing import Optional __all__ = ["PM100"] @@ -24,7 +23,7 @@ def reset(self) -> None: def clear(self) -> None: self.write("*CLS") - def next_error(self) -> Optional[InstrumentError]: + def next_error(self) -> InstrumentError | None: code, message = parse_error(self.query(":SYST:ERR:NEXT?")) if code: return InstrumentError(code, message) diff --git a/src/comet/emulator/__init__.py b/src/comet/emulator/__init__.py index 3a9616b..0cc2568 100644 --- a/src/comet/emulator/__init__.py +++ b/src/comet/emulator/__init__.py @@ -1,16 +1,16 @@ from .emulator import Emulator, message -from .response import TextResponse, BinaryResponse, RawResponse from .iec60488 import IEC60488Emulator from .resource import open_emulator +from .response import BinaryResponse, RawResponse, TextResponse from .tcpserver import run __all__ = [ - "Emulator", - "message", - "TextResponse", "BinaryResponse", - "RawResponse", + "Emulator", "IEC60488Emulator", + "RawResponse", + "TextResponse", + "message", "open_emulator", "run", ] diff --git a/src/comet/emulator/__main__.py b/src/comet/emulator/__main__.py index e951f26..ef897a6 100644 --- a/src/comet/emulator/__main__.py +++ b/src/comet/emulator/__main__.py @@ -38,6 +38,8 @@ from .emulator import emulator_factory from .tcpserver import TCPServer, TCPServerContext +logger = logging.getLogger(__name__) + default_config_filenames: list[str] = ["emulators.yaml", "emulators.yml"] default_host: str = "localhost" default_termination: str = "\n" @@ -67,7 +69,7 @@ def normalize_termination(value: str) -> str: schema.Optional("version"): str, "emulators": { str: { - schema.Optional(schema.Or("model", "module")): str, + schema.Optional(schema.Or("model", "module")): str, # type: ignore schema.Optional("host"): str, "port": schema.And( int, @@ -76,7 +78,9 @@ def normalize_termination(value: str) -> str: ), schema.Optional("termination"): schema.And(str, normalize_termination), schema.Optional("request_delay"): schema.And( - schema.Use(float), lambda d: d >= 0, error="request_delay must be >= 0" + schema.Use(float), # type: ignore + lambda d: d >= 0, + error="request_delay must be >= 0", ), schema.Optional("options"): dict, } @@ -93,7 +97,7 @@ def load_config(filename: str) -> dict[str, Any]: if "model" in params and "module" in params: raise KeyError("keys 'model' and 'module' are exclusive") if "module" in params: - logging.warning( + logger.warning( "Emulator %r uses deprecated config key 'module'; " "use 'model' instead. Support exists only for backward compatibility.", name, diff --git a/src/comet/emulator/cts/itc.py b/src/comet/emulator/cts/itc.py index ea2468e..4711b97 100644 --- a/src/comet/emulator/cts/itc.py +++ b/src/comet/emulator/cts/itc.py @@ -1,10 +1,9 @@ """CTS ITC climate chamber emulator.""" -import datetime import random +from datetime import UTC, datetime -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run __all__ = ["ITCEmulator"] @@ -17,7 +16,6 @@ def fake_analog_channel(channel, minimum, maximum): class ITCEmulator(Emulator): - IDENTITY: str = "ITS Climate Chamber, v1.0 (Emulator)" def __init__(self) -> None: @@ -33,68 +31,68 @@ def __init__(self) -> None: @message(r"T$") def get_t(self) -> str: - return datetime.datetime.now().strftime("T%d%m%y%H%M%S") + return datetime.now(tz=UTC).strftime("T%d%m%y%H%M%S") @message(r"(t\d{6}\d{6})$") def set_t(self, value) -> str: - t = datetime.datetime.strptime(value, "t%d%m%y%H%M%S") + t = datetime.strptime(value, "t%d%m%y%H%M%S").replace(tzinfo=UTC) return t.strftime("t%d%m%y%H%M%S") @message(r"(A0)$") def get_a0(self, channel) -> str: - self.current_temp += random.uniform(-.25, +.25) - self.current_temp = min(60., max(20., self.current_temp)) + self.current_temp += random.uniform(-0.25, +0.25) + self.current_temp = min(60.0, max(20.0, self.current_temp)) return f"{channel} {self.current_temp:05.1f} {self.target_temp:05.1f}" @message(r"(A[34])$") def get_a3(self, channel) -> str: - return fake_analog_channel(channel, -45., +185.) + return fake_analog_channel(channel, -45.0, +185.0) @message(r"(A1)$") def get_a1(self, channel) -> str: - self.current_humid += random.uniform(-.25, +.25) - self.current_humid = min(95., max(15., self.current_humid)) + self.current_humid += random.uniform(-0.25, +0.25) + self.current_humid = min(95.0, max(15.0, self.current_humid)) return f"{channel} {self.current_humid:05.1f} {self.target_humid:05.1f}" @message(r"(A2)$") def get_a2(self, channel) -> str: - return fake_analog_channel(channel, +0., +15.) + return fake_analog_channel(channel, +0.0, +15.0) @message(r"(A[56])$") def get_a5(self, channel) -> str: - return fake_analog_channel(channel, +5., +98.) + return fake_analog_channel(channel, +5.0, +98.0) @message(r"(A7)$") def get_a7(self, channel) -> str: - return fake_analog_channel(channel, -50., +150.) + return fake_analog_channel(channel, -50.0, +150.0) @message(r"(A8)$") def get_a8(self, channel) -> str: - return fake_analog_channel(channel, -80., +190.) + return fake_analog_channel(channel, -80.0, +190.0) @message(r"(A9)$") def get_a9(self, channel) -> str: - return fake_analog_channel(channel, -0., +25.) + return fake_analog_channel(channel, -0.0, +25.0) @message(r"(A\:)$") def get_a10(self, channel) -> str: - return fake_analog_channel(channel, -50., +100.) + return fake_analog_channel(channel, -50.0, +100.0) @message(r"(A\;)$") def get_a11(self, channel) -> str: - return fake_analog_channel(channel, -0., +25.) + return fake_analog_channel(channel, -0.0, +25.0) @message(r"(A\<)$") def get_a12(self, channel) -> str: - return fake_analog_channel(channel, +2., +5.) + return fake_analog_channel(channel, +2.0, +5.0) @message(r"(A[\=\>])$") def get_a13(self, channel) -> str: - return fake_analog_channel(channel, -100., +200.) + return fake_analog_channel(channel, -100.0, +200.0) @message(r"(A\?)$") def get_a14(self, channel) -> str: - return fake_analog_channel(channel, -80., +200.) + return fake_analog_channel(channel, -80.0, +200.0) @message(r"a[1-7]\s(-?\d+.\d)$") def set_a15(self, value) -> str: diff --git a/src/comet/emulator/emulator.py b/src/comet/emulator/emulator.py index bd473c4..66a2876 100644 --- a/src/comet/emulator/emulator.py +++ b/src/comet/emulator/emulator.py @@ -1,21 +1,23 @@ +from __future__ import annotations + import importlib import inspect import logging import re -from collections.abc import Mapping -from typing import Any, Callable, Optional, Union +from collections.abc import Callable, Mapping +from typing import Any from ..utils import parse_model_urn from .response import Response, make_response -__all__ = ["emulator_factory", "message", "Emulator"] +__all__ = ["Emulator", "emulator_factory", "message"] logger = logging.getLogger(__name__) -emulator_registry: dict[str, type["Emulator"]] = {} +emulator_registry: dict[str, type[Emulator]] = {} -def emulator_factory(model_urn: str) -> type["Emulator"]: +def emulator_factory(model_urn: str) -> type[Emulator]: """Returns emulator class from model specified by URN.""" module_name: str = parse_model_urn(model_urn) key: str = module_name @@ -32,11 +34,14 @@ def emulator_factory(model_urn: str) -> type["Emulator"]: module = importlib.import_module(key) # Iterate over all module class members (local and imported). for _, cls in inspect.getmembers(module, inspect.isclass): - if issubclass(cls, Emulator) and cls is not Emulator: + if ( + issubclass(cls, Emulator) + and cls is not Emulator # Make sure class is from module, not an imported one. - if key == cls.__module__: - emulator_registry[key] = cls - break + and key == cls.__module__ + ): + emulator_registry[key] = cls + break if key not in emulator_registry: raise RuntimeError(f"Unable to locate emulator module: {module_name}") return emulator_registry[key] @@ -51,7 +56,8 @@ def normalize_route(pattern: str) -> str: class Route: """Route wrapper for message routing.""" - __slots__ = ["route", "pattern", "method"] + + __slots__ = ["method", "pattern", "route"] def __init__(self, route: str, method: Callable[..., Any]) -> None: self.route: str = normalize_route(route) @@ -61,7 +67,7 @@ def __init__(self, route: str, method: Callable[..., Any]) -> None: def __call__(self, *args, **kwargs) -> Any: return self.method(*args, **kwargs) - def match(self, message: str) -> Optional[tuple[str, ...]]: + def match(self, message: str) -> tuple[str, ...] | None: m = self.pattern.match(message) return m.groups() if m else None @@ -100,7 +106,7 @@ def __init__(self) -> None: def load_options(self, options: Mapping[str, Any]) -> None: self.options.update(options) - def __call__(self, message: str) -> Union[None, Response, list[Response]]: + def __call__(self, message: str) -> Response | list[Response] | None: logger.debug("handle message: %s", message) for route in get_routes(type(self)): args = route.match(message) diff --git a/src/comet/emulator/ers/ac3.py b/src/comet/emulator/ers/ac3.py index 49bcdf7..79f1803 100644 --- a/src/comet/emulator/ers/ac3.py +++ b/src/comet/emulator/ers/ac3.py @@ -4,9 +4,7 @@ from dataclasses import dataclass from enum import IntEnum - -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run __all__ = ["AC3Emulator"] diff --git a/src/comet/emulator/hephy/brandbox.py b/src/comet/emulator/hephy/brandbox.py index 6bab797..b7730fd 100644 --- a/src/comet/emulator/hephy/brandbox.py +++ b/src/comet/emulator/hephy/brandbox.py @@ -1,5 +1,4 @@ -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run __all__ = ["BrandBoxEmulator"] @@ -21,9 +20,8 @@ def format_error(code: int) -> str: class BrandBoxEmulator(Emulator): - - CHANNELS: list[str] = ["A1", "A2", "B1", "B2", "C1", "C2"] - MODS: list[str] = ["IV", "CV"] + CHANNELS: tuple[str, ...] = ("A1", "A2", "B1", "B2", "C1", "C2") + MODS: tuple[str, ...] = ("IV", "CV") IDENTITY: str = "BrandBox, v2.0 (Emulator)" SUCCESS: str = "OK" diff --git a/src/comet/emulator/hephy/environbox.py b/src/comet/emulator/hephy/environbox.py index 1af1ed1..f95b2ff 100644 --- a/src/comet/emulator/hephy/environbox.py +++ b/src/comet/emulator/hephy/environbox.py @@ -1,8 +1,7 @@ import random import time -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run from comet.utils import t_dew __all__ = ["EnvironBoxEmulator"] @@ -20,12 +19,11 @@ def split_seconds(delta_seconds: float) -> tuple[int, int, int, int]: class EnvironBoxEmulator(Emulator): - IDENTITY: str = "EnvironBox, v2.0 (Emulator)" VERSION: str = "V2.0" SUCCESS: str = "OK" PC_DATA_SIZE: int = 39 - SENSOR_ADRESSES: list[int] = [40, 41, 42, 43, 44, 45] + SENSOR_ADRESSES: tuple[int, ...] = (40, 41, 42, 43, 44, 45) def __init__(self) -> None: super().__init__() diff --git a/src/comet/emulator/hephy/shuntbox.py b/src/comet/emulator/hephy/shuntbox.py index aca7acd..2ea75e3 100644 --- a/src/comet/emulator/hephy/shuntbox.py +++ b/src/comet/emulator/hephy/shuntbox.py @@ -3,8 +3,7 @@ import random import time -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run __all__ = ["ShuntBoxEmulator"] @@ -14,7 +13,6 @@ def format_error(code: int) -> str: class ShuntBoxEmulator(Emulator): - IDENTITY: str = "ShuntBox, v1.0 (Emulator)" MEMORY_BYTES: int = 4200 CHANNELS: int = 10 @@ -26,7 +24,7 @@ def __init__(self) -> None: @property def uptime(self) -> int: - return int(round(time.time() - self.start_time)) + return round(time.time() - self.start_time) @message(r"\*IDN\?$") def get_idn(self) -> str: diff --git a/src/comet/emulator/iec60488.py b/src/comet/emulator/iec60488.py index e0f6214..05935d2 100644 --- a/src/comet/emulator/iec60488.py +++ b/src/comet/emulator/iec60488.py @@ -1,7 +1,6 @@ import random -from comet.emulator import Emulator -from comet.emulator import message +from comet.emulator import Emulator, message __all__ = ["IEC60488Emulator"] diff --git a/src/comet/emulator/itk/corvustt.py b/src/comet/emulator/itk/corvustt.py index e3da8fa..27b0bf8 100644 --- a/src/comet/emulator/itk/corvustt.py +++ b/src/comet/emulator/itk/corvustt.py @@ -2,9 +2,9 @@ import random import time -from dataclasses import dataclass, astuple from collections.abc import Mapping -from typing import Any, Optional +from dataclasses import astuple, dataclass +from typing import Any from comet.emulator import Emulator, message, run @@ -119,7 +119,7 @@ def get_status(self) -> str: @message(r"pos|p$") def get_pos(self) -> str: - return f'{self.x_pos:.6f} {self.y_pos:.6f} {self.z_pos:.6f}' + return f"{self.x_pos:.6f} {self.y_pos:.6f} {self.z_pos:.6f}" @message(r"(.+)\s+setlimit$") def set_limit(self, value) -> None: @@ -131,13 +131,17 @@ def get_limit(self) -> tuple[str, str, str]: a1, b1, c1, a2, b2, c2 = astuple(self.table_limits) return f"{a1:.6f} {b1:.6f}", f"{c1:.6f} {a2:.6f}", f"{b2:.6f} {c2:.6f}" - @message(r"([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+(?:move|m)$") + @message( + r"([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+(?:move|m)$" + ) def set_move(self, x, y, z) -> None: self.x_pos = max(0.0, float(x)) self.y_pos = max(0.0, float(y)) self.z_pos = max(0.0, float(z)) - @message(r"([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+(?:rmove|r)$") + @message( + r"([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+([+-]?\d+(?:\.\d+)?)\s+(?:rmove|r)$" + ) def set_rmove(self, x, y, z) -> None: self.x_pos = max(0.0, self.x_pos + float(x)) self.y_pos = max(0.0, self.y_pos + float(y)) @@ -151,7 +155,7 @@ def set_randmove(self) -> None: self.z_pos = random.uniform(c1, c2) @message(r"(1|2|3)\s+getcaldone$") - def get_caldone(self, axis) -> Optional[str]: + def get_caldone(self, axis) -> str | None: if axis == "1": return f"{self.getcaldone[0]}" if axis == "2": @@ -161,7 +165,7 @@ def get_caldone(self, axis) -> Optional[str]: return None @message(r"(-1|1|2|3)\s+getaxis$") - def get_axis(self, axis) -> Optional[str]: + def get_axis(self, axis) -> str | None: if axis == "-1": a1, a2, a3 = self.getaxis return f"{a1} {a2} {a3}" @@ -190,7 +194,7 @@ def get_joystick(self) -> str: return f"{self.joystick:d}" @message(r"(-1|1|2|3)\s+getunit$") - def get_unit(self, axis) -> Optional[str]: + def get_unit(self, axis) -> str | None: if axis == "-1": return f"{self.x_unit} {self.y_unit} {self.z_unit} 1" if axis == "1": diff --git a/src/comet/emulator/itk/hydra.py b/src/comet/emulator/itk/hydra.py index 2fc02f5..62069a1 100644 --- a/src/comet/emulator/itk/hydra.py +++ b/src/comet/emulator/itk/hydra.py @@ -79,12 +79,12 @@ def set_reset(self) -> None: ... @message(r"status|st$") def get_status(self) -> int: status = 0 - all_cal = int(all([value & 0x1 for value in self.calibrate.values()])) - all_rm = int(all([value & 0x2 for value in self.calibrate.values()])) - status |= ((self.axes_moving & 0x1) << 0) - status |= ((self.manual_move & 0x1) << 1) - status |= ((all_cal & 0x1) << 3) - status |= ((all_rm & 0x1) << 4) + all_cal = int(all(value & 0x1 for value in self.calibrate.values())) + all_rm = int(all(value & 0x2 for value in self.calibrate.values())) + status |= (self.axes_moving & 0x1) << 0 + status |= (self.manual_move & 0x1) << 1 + status |= (all_cal & 0x1) << 3 + status |= (all_rm & 0x1) << 4 return status @message(r"(1|2)\s+(?:nstatus|nst|est|ast)$") @@ -92,10 +92,10 @@ def get_nstatus(self, axis) -> int: status = 0 cal = int(self.calibrate[axis] & 0x1 == 0x1) rm = int(self.calibrate[axis] & 0x2 == 0x2) - status |= ((self.axes_moving & 0x1) << 0) - status |= ((self.manual_move & 0x1) << 1) - status |= ((cal & 0x1) << 3) - status |= ((rm & 0x1) << 4) + status |= (self.axes_moving & 0x1) << 0 + status |= (self.manual_move & 0x1) << 1 + status |= (cal & 0x1) << 3 + status |= (rm & 0x1) << 4 return status @message(r"(1|2)\s+np$") diff --git a/src/comet/emulator/keithley/k2400.py b/src/comet/emulator/keithley/k2400.py index fdbcf52..92e6f0c 100644 --- a/src/comet/emulator/keithley/k2400.py +++ b/src/comet/emulator/keithley/k2400.py @@ -1,14 +1,14 @@ import random +from typing import ClassVar from comet.emulator import IEC60488Emulator, message, run from comet.emulator.utils import Error class K2400Emulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 2400, 43768438, v1.0 (Emulator)" - DEFAULT_VOLTAGE_PROTECTION_LEVEL: float = 210. + DEFAULT_VOLTAGE_PROTECTION_LEVEL: float = 210.0 def __init__(self) -> None: super().__init__() @@ -18,11 +18,13 @@ def __init__(self) -> None: self.route_terminals: str = "FRON" self.output_state: bool = False self.source_function_mode: str = "VOLT" - self.source_level: dict[str, float] = {"VOLT": 0., "CURR": 0.} - self.source_range: dict[str, float] = {"VOLT": 0., "CURR": 0.} + self.source_level: dict[str, float] = {"VOLT": 0.0, "CURR": 0.0} + self.source_range: dict[str, float] = {"VOLT": 0.0, "CURR": 0.0} self.source_range_auto: dict[str, bool] = {"VOLT": True, "CURR": True} - self.source_voltage_protection_level: float = self.DEFAULT_VOLTAGE_PROTECTION_LEVEL - self.sense_voltage_protection_level: float = 2.1e+1 + self.source_voltage_protection_level: float = ( + self.DEFAULT_VOLTAGE_PROTECTION_LEVEL + ) + self.sense_voltage_protection_level: float = 2.1e1 self.sense_current_protection_level: float = 1.05e-5 self.sense_function = SenseFunction() self.sense_function.add("CURR") @@ -42,8 +44,8 @@ def set_rst(self) -> None: self.route_terminals = "FRON" self.output_state = False self.source_function_mode = "VOLT" - self.source_level.update({"VOLT": 0., "CURR": 0.}) - self.source_range.update({"VOLT": 0., "CURR": 0.}) + self.source_level.update({"VOLT": 0.0, "CURR": 0.0}) + self.source_range.update({"VOLT": 0.0, "CURR": 0.0}) self.source_range_auto.update({"VOLT": True, "CURR": True}) self.source_voltage_protection_level = self.DEFAULT_VOLTAGE_PROTECTION_LEVEL self.sense_function.clear() @@ -80,7 +82,9 @@ def get_system_beeper_state(self) -> str: @message(r":?SYST(?:em)?:BEEP(?:er)?(?::STAT(?:e)?)?\s+(OFF|ON|0|1)$") def set_system_beeper_state(self, state) -> None: - self.system_beeper_state = {"OFF": False, "ON": True, "0": False, "1": True}[state] + self.system_beeper_state = {"OFF": False, "ON": True, "0": False, "1": True}[ + state + ] # Remote sensing @@ -90,7 +94,7 @@ def get_system_rsense(self) -> str: @message(r":?SYST(?:em)?:RSEN(?:se)?\s+(OFF|ON|0|1)$") def set_system_rsense(self, enabled) -> None: - self.system_rsense = {'OFF': False, 'ON': True, '0': False, '1': True}[enabled] + self.system_rsense = {"OFF": False, "ON": True, "0": False, "1": True}[enabled] # Route terminal @@ -106,7 +110,7 @@ def set_route_terminals(self, terminal) -> None: @message(r":?OUTP(?:ut)?(?::STAT(?:e)?)?\?$") def get_output_state(self) -> str: - return {False: '0', True: '1'}[self.output_state] + return {False: "0", True: "1"}[self.output_state] @message(r":?OUTP(?:ut)?(?::STAT(?:e)?)?\s+(.+)$") def set_output_state(self, state) -> None: @@ -132,7 +136,7 @@ def set_source_function_mode(self, function) -> None: @message(r":?SOUR:(VOLT|CURR)(?::LEV)?\?$") def get_source_level(self, function) -> str: - return format(self.source_level[function], 'E') + return format(self.source_level[function], "E") @message(r":?SOUR:(VOLT|CURR)(?::LEV)?\s+(.+)$") def set_source_level(self, function, level) -> None: @@ -164,7 +168,12 @@ def get_source_range_auto(self, function) -> str: @message(r":?SOUR:(VOLT|CURR):RANG:AUTO\s+(.+)$") def set_source_range_auto(self, function, state) -> None: try: - self.source_range_auto[function] = {"ON": True, "OFF": False, "0": False, "1": True}[state] + self.source_range_auto[function] = { + "ON": True, + "OFF": False, + "0": False, + "1": True, + }[state] except ValueError: self.error_queue.append(Error(101, "malformed command")) @@ -237,7 +246,12 @@ def get_sense_function_concurrent(self) -> int: @message(r"(?::?SENS)?:FUNC:CONC\s+(OFF|ON|0|1)$") def set_sense_function_concurrent(self, state) -> None: - self.sense_function_concurrent = {"OFF": False, "ON": True, "0": False, "1": True}[state] + self.sense_function_concurrent = { + "OFF": False, + "ON": True, + "0": False, + "1": True, + }[state] # Average @@ -263,7 +277,9 @@ def get_sense_average_state(self) -> int: @message(r"(?::?SENS)?:AVER(?::STAT)?\s+(OFF|ON|0|1)$") def set_sense_average_state(self, state) -> None: - self.sense_average_state = {"OFF": False, "ON": True, "0": False, "1": True}[state] + self.sense_average_state = {"OFF": False, "ON": True, "0": False, "1": True}[ + state + ] # Integration time @@ -283,7 +299,9 @@ def get_format_elements(self) -> str: @message(r":?FORM:ELEM\s+(.+)$") def set_format_elements(self, elements) -> None: - elements = [element.strip() for element in elements.split(",") if element.strip()] + elements = [ + element.strip() for element in elements.split(",") if element.strip() + ] self.format_elements.clear() self.format_elements.update(elements) @@ -296,8 +314,12 @@ def set_initiate(self) -> None: ... def get_read(self) -> str: result = [] if "VOLT" in self.format_elements._values: - curr_min = float(self.options.get("volt.min", self.source_level.get("VOLT", 0))) - curr_max = float(self.options.get("volt.max", self.source_level.get("VOLT", 0))) + curr_min = float( + self.options.get("volt.min", self.source_level.get("VOLT", 0)) + ) + curr_max = float( + self.options.get("volt.max", self.source_level.get("VOLT", 0)) + ) result.append(format(random.uniform(curr_min, curr_max), "E")) if "CURR" in self.format_elements._values: curr_min = float(self.options.get("curr.min", 1e-6)) @@ -323,9 +345,8 @@ def unknown_message(self) -> None: class SenseFunction: - - ALLOWED_VALUES = ["VOLT:DC", "CURR:DC", "RES"] - ALIAS_VALUES = {"VOLT": "VOLT:DC", "CURR": "CURR:DC"} + ALLOWED_VALUES = ("VOLT:DC", "CURR:DC", "RES") + ALIAS_VALUES: ClassVar = {"VOLT": "VOLT:DC", "CURR": "CURR:DC"} def __init__(self) -> None: self._values: set[str] = set() @@ -350,13 +371,14 @@ def update(self, values) -> None: self.add(value) def __str__(self) -> str: - values = sorted(self._values, key=lambda value: self.ALLOWED_VALUES.index(value)) + values = sorted( + self._values, key=lambda value: self.ALLOWED_VALUES.index(value) + ) return ",".join([f"'{value}'" for value in values]) class FormatElements: - - ALLOWED_VALUES = ["VOLT", "CURR", "RES", "TIME", "STAT"] + ALLOWED_VALUES = ("VOLT", "CURR", "RES", "TIME", "STAT") def __init__(self) -> None: self._values: set[str] = set() @@ -377,7 +399,9 @@ def update(self, values) -> None: self.add(value) def __str__(self) -> str: - values = sorted(self._values, key=lambda value: self.ALLOWED_VALUES.index(value)) + values = sorted( + self._values, key=lambda value: self.ALLOWED_VALUES.index(value) + ) return ",".join(values) diff --git a/src/comet/emulator/keithley/k2410.py b/src/comet/emulator/keithley/k2410.py index 3638a9d..e56c214 100644 --- a/src/comet/emulator/keithley/k2410.py +++ b/src/comet/emulator/keithley/k2410.py @@ -2,10 +2,9 @@ class K2410Emulator(K2400Emulator): - IDENTITY: str = "Keithley Inc., Model 2410, 43768438, v1.0 (Emulator)" - DEFAULT_VOLTAGE_PROTECTION_LEVEL: float = 1100. + DEFAULT_VOLTAGE_PROTECTION_LEVEL: float = 1100.0 if __name__ == "__main__": diff --git a/src/comet/emulator/keithley/k2470.py b/src/comet/emulator/keithley/k2470.py index 271aeb3..150a505 100644 --- a/src/comet/emulator/keithley/k2470.py +++ b/src/comet/emulator/keithley/k2470.py @@ -1,15 +1,14 @@ import random from comet.emulator import IEC60488Emulator, message, run -from comet.emulator.utils import tsp_print, tsp_assign, Error +from comet.emulator.utils import Error, tsp_assign, tsp_print class K2470Emulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 2470, 43768438, v1.0 (Emulator)" LANGUAGE: str = "SCPI" - DEFAULT_VOLTAGE_PROTECTION_LEVEL: float = 1050. + DEFAULT_VOLTAGE_PROTECTION_LEVEL: float = 1050.0 def __init__(self) -> None: super().__init__() @@ -18,10 +17,12 @@ def __init__(self) -> None: self.route_terminals: str = "FRON" self.output_state: bool = False self.source_function_mode: str = "VOLT" - self.source_level: dict[str, float] = {"VOLT": 0., "CURR": 0.} - self.source_range: dict[str, float] = {"VOLT": 0., "CURR": 0.} + self.source_level: dict[str, float] = {"VOLT": 0.0, "CURR": 0.0} + self.source_range: dict[str, float] = {"VOLT": 0.0, "CURR": 0.0} self.source_range_auto: dict[str, bool] = {"VOLT": True, "CURR": True} - self.source_voltage_protection_level: float = self.DEFAULT_VOLTAGE_PROTECTION_LEVEL + self.source_voltage_protection_level: float = ( + self.DEFAULT_VOLTAGE_PROTECTION_LEVEL + ) self.source_voltage_ilimit_level: float = 1.05e-4 self.source_current_vlimit_level: float = 2.1e-1 self.sense_function_on: str = "CURR" @@ -45,8 +46,8 @@ def set_rst(self) -> None: self.route_terminals = "FRON" self.output_state = False self.source_function_mode = "VOLT" - self.source_level.update({"VOLT": 0., "CURR": 0.}) - self.source_range.update({"VOLT": 0., "CURR": 0.}) + self.source_level.update({"VOLT": 0.0, "CURR": 0.0}) + self.source_range.update({"VOLT": 0.0, "CURR": 0.0}) self.source_range_auto.update({"VOLT": True, "CURR": True}) self.source_voltage_protection_level = self.DEFAULT_VOLTAGE_PROTECTION_LEVEL self.source_voltage_ilimit_level = 1.05e-4 @@ -154,7 +155,12 @@ def get_source_range_auto(self, function) -> int: @message(r":?SOUR:(VOLT|CURR):RANG:AUTO\s+(.+)$") def set_source_range_auto(self, function, state) -> None: try: - self.source_range_auto[function] = {"ON": True, "OFF": False, "0": False, "1": True}[state] + self.source_range_auto[function] = { + "ON": True, + "OFF": False, + "0": False, + "1": True, + }[state] except ValueError: self.error_queue.append(Error(101, "malformed command")) @@ -231,7 +237,12 @@ def get_sense_average_state(self, function: str) -> str: @message(r":?SENS:(VOLT|CURR):AVER:STAT[E]?\s+(OFF|ON|0|1)$") def set_sense_average_state(self, function: str, state: str) -> None: - self.sense_average_state[function] = {"OFF": False, "ON": True, "0": False, "1": True}[state] + self.sense_average_state[function] = { + "OFF": False, + "ON": True, + "0": False, + "1": True, + }[state] # Integration time @@ -257,8 +268,7 @@ def get_read_elements(self, _buffer: str) -> str: return f"{sour:E},{read:E}" @message(r":?INIT(?::IMM)?$") - def set_init(self) -> None: - ... + def set_init(self) -> None: ... @message(r":?MEAS:VOLT\?$") def get_measure_voltage(self) -> str: @@ -271,12 +281,10 @@ def get_measure_current(self) -> str: return format(curr, "E") @message(r":?TRAC[E]?:CLE\s+\"([a-zA-Z0-9_]+)\"$") - def set_trace_clear(self, _buffer: str) -> None: - ... + def set_trace_clear(self, _buffer: str) -> None: ... @message(r":?TRAC[E]?:TRIG\s+\"([a-zA-Z0-9_]+)\"$") - def set_trace_trigger(self, _buffer: str) -> None: - ... + def set_trace_trigger(self, _buffer: str) -> None: ... @message(r":?TRAC[E]?:DATA\?\s+1,\s+1,\s+\"([a-zA-Z0-9_]+)\",\s+SOUR,\s+READ$") def get_trace_data(self, _buffer: str) -> str: @@ -306,8 +314,8 @@ def get_errorqueue_count(self) -> int: def get_errorqueue_next(self) -> str: if self.error_queue: error = self.error_queue.pop(0) - return f"{error.code}, \"{error.message}\", 0, 0" - return "0, \"Queue is Empty\", 0, 0" + return f'{error.code}, "{error.message}", 0, 0' + return '0, "Queue is Empty", 0, 0' @message(tsp_print(r"smu\.source\.output")) def get_tsp_source_output(self) -> int: @@ -317,8 +325,10 @@ def get_tsp_source_output(self) -> int: def set_tsp_source_output(self, value) -> None: try: self.smu_source_output = { - "smu.ON": True, "smu.OFF": False, - "0": False, "1": True + "smu.ON": True, + "smu.OFF": False, + "0": False, + "1": True, }[value] except KeyError: self.error_queue.append(Error(101, "malformed command")) diff --git a/src/comet/emulator/keithley/k2657a.py b/src/comet/emulator/keithley/k2657a.py index 9ba4651..948494a 100644 --- a/src/comet/emulator/keithley/k2657a.py +++ b/src/comet/emulator/keithley/k2657a.py @@ -1,11 +1,10 @@ import random from comet.emulator import IEC60488Emulator, message, run -from comet.emulator.utils import tsp_print, tsp_assign, Error +from comet.emulator.utils import Error, tsp_assign, tsp_print class K2657AEmulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 2657A, 43768438, v1.0 (Emulator)" def __init__(self) -> None: @@ -15,15 +14,15 @@ def __init__(self) -> None: self.display_measure_function: int = 1 self.smua_source_output: bool = False self.smua_source_function = "DCVOLTS" - self.smua_source_level: dict[str, float] = {"v": 0., "i": 0.} - self.smua_source_range: dict[str, float] = {"v": 0., "i": 0.} + self.smua_source_level: dict[str, float] = {"v": 0.0, "i": 0.0} + self.smua_source_range: dict[str, float] = {"v": 0.0, "i": 0.0} self.smua_source_autorange: dict[str, bool] = {"v": True, "i": True} - self.smua_source_limit: dict[str, float] = {"v": 0., "i": 0.} + self.smua_source_limit: dict[str, float] = {"v": 0.0, "i": 0.0} self.smua_measure_filter_enable: bool = False self.smua_measure_filter_count: int = 1 self.smua_measure_filter_type: int = 1 self.smua_measure_nplc: float = 1.0 - self.source_protectv: float = 0. + self.source_protectv: float = 0.0 @message(r"reset\(\)$") def set_reset(self): @@ -32,15 +31,15 @@ def set_reset(self): self.display_measure_function = 1 self.smua_source_output = False self.smua_source_function = "DCVOLTS" - self.smua_source_level.update({"v": 0., "i": 0.}) - self.smua_source_range.update({"v": 0., "i": 0.}) + self.smua_source_level.update({"v": 0.0, "i": 0.0}) + self.smua_source_range.update({"v": 0.0, "i": 0.0}) self.smua_source_autorange.update({"v": True, "i": True}) - self.smua_source_limit.update({"v": 0., "i": 0.}) + self.smua_source_limit.update({"v": 0.0, "i": 0.0}) self.smua_measure_filter_enable = False self.smua_measure_filter_count = 1 self.smua_measure_filter_type = 1 self.smua_measure_nplc = 1.0 - self.source_protectv = 0. + self.source_protectv = 0.0 @message(r"status.reset\(\)$") def set_status_reset(self) -> None: @@ -64,7 +63,7 @@ def get_errorqueue_next(self) -> str: error = self.error_queue.pop(0) else: error = Error(0, "Queue Is Empty") - return f"{error.code}\t\"{error.message}\"\t0\t0" + return f'{error.code}\t"{error.message}"\t0\t0' # Beeper @@ -76,8 +75,10 @@ def get_beeper_enabled(self) -> str: def set_beeper_enable(self, enable: str) -> None: try: self.beeper_enable = { - "beeper.ON": True, "beeper.OFF": False, - "0": False, "1": True + "beeper.ON": True, + "beeper.OFF": False, + "0": False, + "1": True, }[enable] except KeyError: self.error_queue.append(Error(110, "malformed command")) @@ -92,10 +93,14 @@ def get_display_measure_function(self) -> str: def set_display_measure_function(self, func) -> None: try: self.display_measure_function = { - "display.MEASURE_DCAMPS": 0, "0": 0, - "display.MEASURE_DCVOLTS": 1, "1": 1, - "display.MEASURE_OHMS": 2, "2": 2, - "display.MEASURE_WATTS": 3, "3": 3, + "display.MEASURE_DCAMPS": 0, + "0": 0, + "display.MEASURE_DCVOLTS": 1, + "1": 1, + "display.MEASURE_OHMS": 2, + "2": 2, + "display.MEASURE_WATTS": 3, + "3": 3, }[func] except KeyError: self.error_queue.append(Error(111, "malformed command")) @@ -110,8 +115,10 @@ def get_source_output(self) -> str: def set_source_output(self, state) -> None: try: self.smua_source_output = { - "smua.OUTPUT_ON": True, "smua.OUTPUT_OFF": False, - "0": False, "1": True + "smua.OUTPUT_ON": True, + "smua.OUTPUT_OFF": False, + "0": False, + "1": True, }[state] except KeyError: self.error_queue.append(Error(111, "malformed command")) @@ -126,8 +133,10 @@ def get_source_function(self) -> str: def set_source_function(self, function) -> None: try: self.smua_source_function = { - "0": "DCAMPS", "smua.OUTPUT_DCAMPS": "DCAMPS", - "1": "DCVOLTS", "smua.OUTPUT_DCVOLTS": "DCVOLTS" + "0": "DCAMPS", + "smua.OUTPUT_DCAMPS": "DCAMPS", + "1": "DCVOLTS", + "smua.OUTPUT_DCVOLTS": "DCVOLTS", }[function] except KeyError: self.error_queue.append(Error(112, "malformed command")) @@ -168,8 +177,10 @@ def get_source_autorange(self, function) -> str: def set_source_autorange(self, function, state) -> None: try: self.smua_source_autorange[function] = { - "0": False, "smua.AUTORANGE_OFF": False, - "1": True, "smua.AUTORANGE_ON": True + "0": False, + "smua.AUTORANGE_OFF": False, + "1": True, + "smua.AUTORANGE_ON": True, }[state] except KeyError: self.error_queue.append(Error(115, "malformed command")) @@ -212,7 +223,9 @@ def get_measure_i(self) -> str: @message(tsp_print(r"smua\.measure\.v\(\)")) def get_measure_v(self) -> str: - return format(self.smua_source_level.get("v", 0) + random.uniform(-.25, +.25), "E") + return format( + self.smua_source_level.get("v", 0) + random.uniform(-0.25, +0.25), "E" + ) # Average @@ -224,8 +237,10 @@ def get_measure_filter_enable(self) -> str: def set_measure_filter_enable(self, enable: str) -> None: try: self.smua_measure_filter_enable = { - "0": False, "smua.FILTER_OFF": False, - "1": True, "smua.FILTER_ON": True + "0": False, + "smua.FILTER_OFF": False, + "1": True, + "smua.FILTER_ON": True, }[enable] except KeyError: self.error_queue.append(Error(118, "malformed command")) @@ -249,9 +264,12 @@ def get_measure_filter_type(self) -> str: def set_measure_filter_type(self, enable: str) -> None: try: self.smua_measure_filter_type = { - "0": 0, "smua.FILTER_MOVING_AVG": 0, - "1": 1, "smua.FILTER_REPEAT_AVG": 1, - "2": 2, "smua.FILTER_MEDIAN": 2 + "0": 0, + "smua.FILTER_MOVING_AVG": 0, + "1": 1, + "smua.FILTER_REPEAT_AVG": 1, + "2": 2, + "smua.FILTER_MEDIAN": 2, }[enable] except KeyError: self.error_queue.append(Error(120, "malformed command")) diff --git a/src/comet/emulator/keithley/k2700.py b/src/comet/emulator/keithley/k2700.py index ce452fa..c0748d8 100644 --- a/src/comet/emulator/keithley/k2700.py +++ b/src/comet/emulator/keithley/k2700.py @@ -1,6 +1,5 @@ import random import time -from typing import Optional from comet.emulator import IEC60488Emulator, message, run from comet.emulator.utils import Error @@ -17,8 +16,8 @@ def __init__(self, reading: float, unit: str) -> None: class FormatElements: - VALID_ELEMENTS: list[str] = ["READ", "CHAN", "UNIT", "RNUM", "TST", "LIM"] - DEFAULT_ELEMENTS: list[str] = ["READ", "UNIT", "RNUM", "TST"] + VALID_ELEMENTS: tuple[str, ...] = ("READ", "CHAN", "UNIT", "RNUM", "TST", "LIM") + DEFAULT_ELEMENTS: tuple[str, ...] = ("READ", "UNIT", "RNUM", "TST") def __init__(self) -> None: self.elements: set[str] = set(self.DEFAULT_ELEMENTS) @@ -29,12 +28,12 @@ def __str__(self) -> str: elements.append(element if element in self.elements else "") return ",".join(elements) - def from_text(self, text: str) -> Optional[Error]: - elements = set([element.strip() for element in text.split(",") if element.strip()]) + def from_text(self, text: str) -> Error | None: + elements = {element.strip() for element in text.split(",") if element.strip()} for element in elements: if element not in self.VALID_ELEMENTS: return Error(-201, "Syntax error") - if elements == set(["UNIT"]): + if elements == {"UNIT"}: return Error(-101, "Invalid character") self.elements = elements return None @@ -68,7 +67,6 @@ def format_reading(self, reading: Reading) -> str: class K2700Emulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 2700, 43768438, v1.0 (Emulator)" def __init__(self) -> None: @@ -117,11 +115,13 @@ def set_format_elements(self, elements) -> None: @message(r":?SENS(?:E)?:FUNC(?:TION)?\?$") def get_sense_function(self) -> str: - return f"\"{self.sense_function}\"" + return f'"{self.sense_function}"' @message(r":?SENS(?:E)?:FUNC(?:TION)\s+\"(VOLT|CURR|VOLT:DC|CURR:DC|TEMP)\"$") def set_sense_function(self, function: str) -> None: - self.sense_function = {"VOLT": "VOLT:DC", "CURR": "CURR:DC"}.get(function, function) + self.sense_function = {"VOLT": "VOLT:DC", "CURR": "CURR:DC"}.get( + function, function + ) @message(r":?SENS(?:E)?:VOLT:AVER:TCON\?$") def get_sense_average_tcontrol(self) -> str: @@ -145,7 +145,12 @@ def get_sense_voltage_average_state(self) -> str: @message(r":?SENS(?:E)?:VOLT:AVER(?::STAT[E]?)?\s+(OFF|ON|0|1)$") def set_sense_voltage_average_state(self, value) -> None: - self.sense_voltage_average_state = {"0": False, "1": True, "OFF": False, "ON": True}[value] + self.sense_voltage_average_state = { + "0": False, + "1": True, + "OFF": False, + "ON": True, + }[value] @message(r":?SYST:ERR\?$") def get_system_error(self) -> str: @@ -161,13 +166,15 @@ def get_beeper_state(self) -> str: @message(r":?SYST:BEEP(?::STAT)? (OFF|ON|0|1)$") def set_beeper_state(self, value) -> None: - self.system_beeper_state = {'0': False, '1': True, 'OFF': False, 'ON': True}[value] + self.system_beeper_state = {"0": False, "1": True, "OFF": False, "ON": True}[ + value + ] @message(r":?INIT(?::IMM)$") def set_init(self) -> None: ... @message(r":?READ\?$") - def get_read(self) -> None: + def get_read(self) -> str: return self._read() @message(r":?FETC[H]?\?$") @@ -199,7 +206,9 @@ def get_trigger_delay_auto(self) -> str: @message(r":?TRIG:DEL:AUTO\s+(OFF|ON|0|1)$") def set_trigger_delay_auto(self, value) -> None: - self.trigger_delay_auto = {'0': False, '1': True, 'OFF': False, 'ON': True}[value] + self.trigger_delay_auto = {"0": False, "1": True, "OFF": False, "ON": True}[ + value + ] @message(r":?TRIG:DEL\?$") def get_trigger_delay(self) -> str: @@ -216,7 +225,7 @@ def set_trigger_delay(self, value: str) -> None: def unknown_message(self) -> None: self.error_queue.append(Error(101, "malformed command")) - def _read(self): + def _read(self) -> str: """Returns formatted reading.""" if self.sense_function == "VOLT:DC": volt_min = float(self.options.get("volt.min", 0)) @@ -230,11 +239,11 @@ def _read(self): temp_min = float(self.options.get("temp.min", 24)) temp_max = float(self.options.get("temp.max", 25)) reading = Reading(random.uniform(temp_min, temp_max), "") # TEMP - time.sleep(random.uniform(.5, 1.0)) # rev B10 ;) + time.sleep(random.uniform(0.5, 1.0)) # rev B10 ;) reading.reading_number = self.reading_number self.reading_number += 1 return self.format_elements.format_reading(reading) -if __name__ == '__main__': +if __name__ == "__main__": run(K2700Emulator()) diff --git a/src/comet/emulator/keithley/k4215cvu.py b/src/comet/emulator/keithley/k4215cvu.py index ff53a7b..a2f91cc 100644 --- a/src/comet/emulator/keithley/k4215cvu.py +++ b/src/comet/emulator/keithley/k4215cvu.py @@ -1,8 +1,8 @@ import math import random import time -from dataclasses import dataclass, astuple -from typing import Optional +from dataclasses import astuple, dataclass +from typing import ClassVar from comet.emulator import IEC60488Emulator, message, run from comet.emulator.utils import Error @@ -18,7 +18,7 @@ class Correction: class K4215CVUEmulator(IEC60488Emulator): IDENTITY: str = "KEITHLEY INSTRUMENTS,KI4200A,1489223,V1.14 (Emulator)" - MODEL_MAP = { + MODEL_MAP: ClassVar[dict[str, int]] = { "ZTHETA": 0, "RPLUSJX": 1, "CPRP": 2, @@ -28,7 +28,7 @@ class K4215CVUEmulator(IEC60488Emulator): "CSD": 5, "YTHETA": 7, } - MODEL_MAP_INV = {v: k for k, v in MODEL_MAP.items()} + MODEL_MAP_INV: ClassVar[dict[int, str]] = {v: k for k, v in MODEL_MAP.items()} def __init__(self) -> None: super().__init__() @@ -71,14 +71,14 @@ def __init__(self) -> None: def _push_error(self, code: int, msg: str) -> None: self.error_queue.append(Error(code, msg)) - def _parse_float(self, s: str) -> Optional[float]: + def _parse_float(self, s: str) -> float | None: try: return float(s) except Exception: self._push_error(-104, "Data type error") return None - def _parse_int(self, s: str) -> Optional[int]: + def _parse_int(self, s: str) -> int | None: try: return int(s) except Exception: @@ -86,7 +86,7 @@ def _parse_int(self, s: str) -> Optional[int]: return None def _clamp(self, x: float, lo: float, hi: float) -> float: - return lo if x < lo else hi if x > hi else x + return lo if x < lo else min(x, hi) def _noise(self, scale: float) -> float: # scale is approximate RMS-ish; keep it simple @@ -139,8 +139,7 @@ def set_cls(self) -> None: self.error_queue.clear() @message(r"BC$") - def set_buffer_clear(self) -> None: - ... + def set_buffer_clear(self) -> None: ... @message(r":?ERROR:LAST:GET$") def get_last_error(self) -> str: @@ -257,7 +256,7 @@ def set_freq(self, frequency: str) -> None: if not (1e3 <= v <= 1e7): self._push_error(-222, "Data out of range") return - self.freq_hz = int(round(v)) + self.freq_hz = round(v) @message(r":CVU:FREQ\?$") def get_freq(self) -> str: @@ -292,7 +291,9 @@ def get_dcv_offset(self) -> str: return f"{self.dcv_offset:.3E}" @message(r":CVU:SPEED\s+(\d+),(.+),(.+),(.+)$") - def set_speed(self, mode: str, delay_factor: str, filter_factor: str, aperture: str) -> None: + def set_speed( + self, mode: str, delay_factor: str, filter_factor: str, aperture: str + ) -> None: m = self._parse_int(mode) d = self._parse_float(delay_factor) f = self._parse_float(filter_factor) @@ -364,8 +365,12 @@ def get_measz(self) -> str: # Base capacitance ~ 100 pF, mild frequency dependence + bias dependence bias = self._effective_bias() c0 = 100e-12 - c_bias = c0 * (1.0 + 0.002 * self._clamp(bias, -30.0, 30.0)) # ~ +/-6% across range - c_freq = c_bias * (1.0 - 0.01 * math.log10(max(f, 1.0) / 1e5)) # slight slope vs freq + c_bias = c0 * ( + 1.0 + 0.002 * self._clamp(bias, -30.0, 30.0) + ) # ~ +/-6% across range + c_freq = c_bias * ( + 1.0 - 0.01 * math.log10(max(f, 1.0) / 1e5) + ) # slight slope vs freq # Loss tangent / dissipation factor (small) d0 = 0.01 + 0.002 * abs(bias) / 30.0 # 0.01..~0.012 @@ -388,13 +393,19 @@ def get_measz(self) -> str: x = x_l + x_c # Add some noise + drift - r_meas = r * (1.0 + 0.01 * self._drift()) + self._noise(abs(r) * 0.02 + noise_scale) - x_meas = x * (1.0 + 0.01 * self._drift()) + self._noise(abs(x) * 0.02 + noise_scale) + r_meas = r * (1.0 + 0.01 * self._drift()) + self._noise( + abs(r) * 0.02 + noise_scale + ) + x_meas = x * (1.0 + 0.01 * self._drift()) + self._noise( + abs(x) * 0.02 + noise_scale + ) # Apply "corrections" in a naive way: # if open/short/load enabled, reduce systematic error a bit. open_val, short_val, load_val = astuple(self.cvu_correction) - corr_strength = 1.0 - 0.03 * (open_val + short_val + load_val) # up to ~9% improvement + corr_strength = 1.0 - 0.03 * ( + open_val + short_val + load_val + ) # up to ~9% improvement r_meas *= corr_strength x_meas *= corr_strength @@ -413,7 +424,7 @@ def get_measz(self) -> str: elif model in (self.MODEL_MAP["CPRP"], self.MODEL_MAP["CPGP"]): # Parallel capacitance / parallel resistance approximation: # From admittance Y = 1/Z = G + jB, C_p = B / w, R_p = 1/G - denom = (r_meas * r_meas + x_meas * x_meas) + denom = r_meas * r_meas + x_meas * x_meas g = r_meas / max(denom, 1e-30) b = -x_meas / max(denom, 1e-30) c_p = b / max(w, 1e-30) @@ -428,7 +439,7 @@ def get_measz(self) -> str: elif model == self.MODEL_MAP["CPD"]: # Cp, D (D ~ ESR*w*C for series -> rough; for parallel, D ~ G/B) - denom = (r_meas * r_meas + x_meas * x_meas) + denom = r_meas * r_meas + x_meas * x_meas g = r_meas / max(denom, 1e-30) bb = -x_meas / max(denom, 1e-30) c_p = bb / max(w, 1e-30) @@ -443,7 +454,7 @@ def get_measz(self) -> str: elif model == self.MODEL_MAP["YTHETA"]: # Y magnitude and phase (degrees) - denom = (r_meas * r_meas + x_meas * x_meas) + denom = r_meas * r_meas + x_meas * x_meas g = r_meas / max(denom, 1e-30) bb = -x_meas / max(denom, 1e-30) y = math.hypot(g, bb) diff --git a/src/comet/emulator/keithley/k6510.py b/src/comet/emulator/keithley/k6510.py index 3eae475..b7c0ebb 100644 --- a/src/comet/emulator/keithley/k6510.py +++ b/src/comet/emulator/keithley/k6510.py @@ -7,7 +7,6 @@ class K6510Emulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model DAQ6510, 54313645, v1.0 (Emulator)" def __init__(self) -> None: diff --git a/src/comet/emulator/keithley/k6514.py b/src/comet/emulator/keithley/k6514.py index 0e6d1ba..98c5d17 100644 --- a/src/comet/emulator/keithley/k6514.py +++ b/src/comet/emulator/keithley/k6514.py @@ -6,7 +6,6 @@ class K6514Emulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 5614, 43768438, v1.0 (Emulator)" def __init__(self) -> None: @@ -60,7 +59,7 @@ def get_format_elements(self) -> str: @message(r":?INIT(?::IMM)?$") def set_init(self) -> None: - time.sleep(random.uniform(.5, 1.0)) + time.sleep(random.uniform(0.5, 1.0)) def _reading(self) -> float: if self.sense_function == "CURR": @@ -79,7 +78,7 @@ def get_fetch(self) -> str: @message(r":?READ\?$") def get_read(self) -> str: - time.sleep(random.uniform(.25, 1.0)) + time.sleep(random.uniform(0.25, 1.0)) return format(self._reading(), "E") @message(r":?SYST:ZCH\s+(0|1|ON|OFF)$") @@ -104,7 +103,7 @@ def set_sense_function(self, value: str) -> None: @message(r":?SENS:FUNC\?$") def get_sense_function(self) -> str: - return f"\"{self.sense_function}:DC\"" + return f'"{self.sense_function}:DC"' # Average @@ -154,12 +153,10 @@ def set_sense_current_range_auto(self, state: str) -> None: self.sense_current_range_auto = {"OFF": 0, "ON": 1, "0": 0, "1": 1}[state] @message(r"(?::?SENS)?:CURR:RANG:AUTO:ULIM\s+(.+)$") - def set_sense_current_range_auto_ulimit(self, value: str) -> None: - ... # TODO + def set_sense_current_range_auto_ulimit(self, value: str) -> None: ... # TODO @message(r"(?::?SENS)?:CURR:RANG:AUTO:LLIM\s+(.+)$") - def set_sense_current_range_auto_llimit(self, value: str) -> None: - ... # TODO + def set_sense_current_range_auto_llimit(self, value: str) -> None: ... # TODO # NPLC (coupled commands) diff --git a/src/comet/emulator/keithley/k6517b.py b/src/comet/emulator/keithley/k6517b.py index 5eee87c..4b849b2 100644 --- a/src/comet/emulator/keithley/k6517b.py +++ b/src/comet/emulator/keithley/k6517b.py @@ -6,7 +6,6 @@ class K6517BEmulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 6517B, 43768438, v1.0 (Emulator)" def __init__(self) -> None: @@ -136,7 +135,12 @@ def get_source_current_limit_state(self) -> str: @message(r":?SOUR:VOLT:MCON\s+(OFF|ON|0|1)$") def set_source_voltage_mconnect(self, value: str) -> None: - self.source_voltage_mconnect = {"0": False, "1": True, "OFF": False, "ON": True}[value] + self.source_voltage_mconnect = { + "0": False, + "1": True, + "OFF": False, + "ON": True, + }[value] @message(r":?SOUR:VOLT:MCON\?$") def get_source_voltage_mconnect(self) -> str: @@ -227,12 +231,10 @@ def set_sense_current_range_auto(self, state: str) -> None: self.sense_current_range_auto = {"OFF": 0, "ON": 1, "0": 0, "1": 1}[state] @message(r"(?::?SENS)?:CURR:RANG:AUTO:ULIM\s+(.+)$") - def set_sense_current_range_auto_ulimit(self, value: str) -> None: - ... # TODO + def set_sense_current_range_auto_ulimit(self, value: str) -> None: ... # TODO @message(r"(?::?SENS)?:CURR:RANG:AUTO:LLIM\s+(.+)$") - def set_sense_current_range_auto_llimit(self, value: str) -> None: - ... # TODO + def set_sense_current_range_auto_llimit(self, value: str) -> None: ... # TODO # NPLC (coupled commands) diff --git a/src/comet/emulator/keithley/k707b.py b/src/comet/emulator/keithley/k707b.py index 7f5e393..6bfe8f8 100644 --- a/src/comet/emulator/keithley/k707b.py +++ b/src/comet/emulator/keithley/k707b.py @@ -1,12 +1,15 @@ +from typing import ClassVar + from comet.emulator import IEC60488Emulator, message, run -from comet.emulator.utils import tsp_print, Error +from comet.emulator.utils import Error, tsp_print from comet.utils import combine_matrix class K707BEmulator(IEC60488Emulator): - IDENTITY: str = "Keithley Inc., Model 707B, 43768438, v1.0 (Emulator)" - CHANNELS: list[str] = combine_matrix("1234", "ABCDEFGH", (format(i, "02d") for i in range(1, 13))) + CHANNELS: ClassVar[list[str]] = combine_matrix( + "1234", "ABCDEFGH", (format(i, "02d") for i in range(1, 13)) + ) def __init__(self) -> None: super().__init__() @@ -35,7 +38,7 @@ def get_errorqueue_next(self) -> str: error = self.error_queue.pop(0) else: error = Error(0, "Queue is Empty") - return f"{error.code}\t\"{error.message}\"\t0\t0" + return f'{error.code}\t"{error.message}"\t0\t0' @message(tsp_print(r"channel\.getclose\(([^\)]+)\)")) def get_channel_getclose(self, channels: str) -> str: @@ -45,12 +48,12 @@ def get_channel_getclose(self, channels: str) -> str: @message(r"channel\.close\(([^\)]+)\)$") def set_channel_close(self, channels: str) -> None: - channels_ = channels.strip('"').split(',') + channels_ = channels.strip('"').split(",") self.closed_channels.update(channels_) @message(r"channel\.open\(([^\)]+)\)$") def set_channel_open(self, channels: str) -> None: - channels_ = channels.strip('"').split(',') + channels_ = channels.strip('"').split(",") if "allslots" in channels_: self.closed_channels.clear() else: diff --git a/src/comet/emulator/keithley/k708b.py b/src/comet/emulator/keithley/k708b.py index cdb44b2..2b7b754 100644 --- a/src/comet/emulator/keithley/k708b.py +++ b/src/comet/emulator/keithley/k708b.py @@ -5,9 +5,8 @@ class K708BEmulator(K707BEmulator): - IDENTITY: str = "Keithley Inc., Model 708B, 43768438, v1.0 (Emulator)" - CHANNELS: list[str] = combine_matrix("1", "ABCDEFGH", "0", "12345678") + CHANNELS = combine_matrix("1", "ABCDEFGH", "0", "12345678") if __name__ == "__main__": diff --git a/src/comet/emulator/keysight/e4980a.py b/src/comet/emulator/keysight/e4980a.py index 5c6ec7c..02ab1ee 100644 --- a/src/comet/emulator/keysight/e4980a.py +++ b/src/comet/emulator/keysight/e4980a.py @@ -6,7 +6,6 @@ class E4980AEmulator(IEC60488Emulator): - IDENTITY: str = "Keysight Inc., Model E4980A, v1.0 (Emulator)" CORRECTION_OPEN_DELAY: float = 4.0 @@ -20,7 +19,7 @@ def __init__(self) -> None: self.correction_method: str = "SING" self.correction_channel: int = 0 self.correction_length: int = 4 - self.bias_voltage_level: float = 0. + self.bias_voltage_level: float = 0.0 self.bias_state: bool = False @message(r"\*RST$") @@ -32,7 +31,7 @@ def set_rst(self) -> None: self.correction_method = "SING" self.correction_channel = 0 self.correction_length = 4 - self.bias_voltage_level = 0. + self.bias_voltage_level = 0.0 self.bias_state = False @message(r"\*CLS$") @@ -43,7 +42,7 @@ def set_cls(self) -> None: def get_system_error_next(self) -> str: if self.error_queue: error = self.error_queue.pop(0) - return f"{error.code:+d},\"{error.message}\"" + return f'{error.code:+d},"{error.message}"' return '+0,"No error"' @message(r":?FUNC:IMP:TYPE\?$") @@ -61,7 +60,9 @@ def get_correction_open_state(self) -> str: @message(r":?CORR:OPEN:STAT\s+(OFF|ON|0|1)$") def set_correction_open_state(self, state: str) -> None: - self.correction_open_state = {"0": False, "1": True, "OFF": False, "ON": True}[state] + self.correction_open_state = {"0": False, "1": True, "OFF": False, "ON": True}[ + state + ] @message(r":?CORR:OPEN$") def get_correction_open(self) -> None: @@ -105,15 +106,15 @@ def get_fetch(self) -> str: rp_max = float(self.options.get("rp.max", 120)) prim = random.uniform(cp_min, cp_max) sec = random.uniform(rp_min, rp_max) - return '{:E},{:E},{:+d}'.format(prim, sec, 0) + return f"{prim:E},{sec:E},{0:+d}" @message(r":?BIAS:POL:CURR(\::LEV)?\?$") def get_bias_polarity_current_level(self) -> str: - return format(random.random() / 1000., "E") + return format(random.random() / 1000.0, "E") @message(r":?BIAS:POL:VOLT(\::LEV)?\?$") def get_bias_polarity_voltage_level(self) -> str: - return format(random.random() / 100., "E") + return format(random.random() / 100.0, "E") @message(r":?BIAS:VOLT(?::LEV)?\?$") def get_bias_voltage_level(self) -> str: diff --git a/src/comet/emulator/marzhauser/tango.py b/src/comet/emulator/marzhauser/tango.py index 17ac6aa..2d41da4 100644 --- a/src/comet/emulator/marzhauser/tango.py +++ b/src/comet/emulator/marzhauser/tango.py @@ -1,7 +1,5 @@ """TANGO emulator.""" -from typing import Optional - from comet.emulator import Emulator, message, run __all__ = ["TangoEmulator"] @@ -41,7 +39,7 @@ def get_error(self) -> str: # Calibration @message(r"\!?cal$") - def set_cal(self) -> Optional[str]: + def set_cal(self) -> str | None: self.calst["x"] |= 0x1 self.calst["y"] |= 0x1 self.calst["z"] |= 0x1 @@ -50,14 +48,14 @@ def set_cal(self) -> Optional[str]: return None @message(r"\!?cal\s+(x|y|z)$") - def set_cal_xyz(self, axes) -> Optional[str]: + def set_cal_xyz(self, axes) -> str | None: self.calst[axes] |= 0x1 if self.autostatus: return "A" return None @message(r"\!?rm$") - def set_rm(self) -> Optional[str]: + def set_rm(self) -> str | None: self.calst["x"] |= 0x2 self.calst["y"] |= 0x2 self.calst["z"] |= 0x2 @@ -66,7 +64,7 @@ def set_rm(self) -> Optional[str]: return None @message(r"\!?rm\s+(x|y|z)$") - def set_rm_xyz(self, axes) -> Optional[str]: + def set_rm_xyz(self, axes) -> str | None: self.calst[axes] &= 0x1 if self.autostatus: return "D" @@ -99,7 +97,7 @@ def get_pos_xyz(self, axis) -> str: return f"{value:.3f}" @message(r"\!?moa\s+([^\sxyza]+)\s+([^\s]+)\s+([^\s]+)$") - def set_move_absolute(self, x, y, z) -> Optional[str]: + def set_move_absolute(self, x, y, z) -> str | None: self.position["x"] = float(x) self.position["y"] = float(y) self.position["z"] = float(z) @@ -108,14 +106,14 @@ def set_move_absolute(self, x, y, z) -> Optional[str]: return None @message(r"\!?moa\s+(x|y|z)\s+([^\s]+)$") - def set_move_absolute_xyz(self, axis, value) -> Optional[str]: + def set_move_absolute_xyz(self, axis, value) -> str | None: self.position[axis] = float(value) if self.autostatus: return "@@@-." return None @message(r"\!?mor\s+([^\sxyza]+)\s+([^\s]+)\s+([^\s]+)$") - def set_move_relative(self, x, y, z) -> Optional[str]: + def set_move_relative(self, x, y, z) -> str | None: self.position["x"] = float(x) self.position["y"] = float(y) self.position["z"] = float(z) @@ -124,7 +122,7 @@ def set_move_relative(self, x, y, z) -> Optional[str]: return None @message(r"\!?mor\s+(x|y|z)\s+([^\s]+)$") - def set_move_relative_xyz(self, axis, value) -> Optional[str]: + def set_move_relative_xyz(self, axis, value) -> str | None: self.position[axis] = float(value) if self.autostatus: return "@@@-." @@ -156,7 +154,7 @@ def get_vel_xyz(self, axis) -> str: @message(r"!vel\s+(x|y|z)\s+([^\s]+)$") def set_vel_xyz(self, axis, value) -> str: - self.velocity[axis]= float(value) + self.velocity[axis] = float(value) return "@@@-." # System configuration diff --git a/src/comet/emulator/nkt_photonics/pilas.py b/src/comet/emulator/nkt_photonics/pilas.py index af88eaf..aee1280 100644 --- a/src/comet/emulator/nkt_photonics/pilas.py +++ b/src/comet/emulator/nkt_photonics/pilas.py @@ -1,7 +1,6 @@ """NKT Photonics PILAS picosecond pulsed diode laser emulator""" -from comet.emulator import Emulator, TextResponse -from comet.emulator import message, run +from comet.emulator import Emulator, TextResponse, message, run __all__ = ["PILASEmulator"] @@ -71,7 +70,10 @@ def set_frequency(self, frequency: int) -> str: @message(r"lht\?$") def get_laser_head_temperature(self) -> TextResponse: - return TextResponse(f"laser head temp.:\t {self.laser_head_temperature} °C", encoding="latin-1") + return TextResponse( + f"laser head temp.:\t {self.laser_head_temperature} °C", + encoding="latin-1", + ) @message(r"ldtemp\?$") def get_laser_diode_temperature(self) -> str: diff --git a/src/comet/emulator/photonic/f3000.py b/src/comet/emulator/photonic/f3000.py index 689cd36..871a0a3 100644 --- a/src/comet/emulator/photonic/f3000.py +++ b/src/comet/emulator/photonic/f3000.py @@ -1,7 +1,6 @@ """Photonic F3000 LED light source emulator""" -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run __all__ = ["F3000Emulator"] diff --git a/src/comet/emulator/resource.py b/src/comet/emulator/resource.py index 1b9d75c..14b9460 100644 --- a/src/comet/emulator/resource.py +++ b/src/comet/emulator/resource.py @@ -6,15 +6,17 @@ """ +from __future__ import annotations + import time -from typing import Optional, TypeVar +from typing import Self, TypeVar from .emulator import Emulator, emulator_factory T = TypeVar("T") -def open_emulator(module_name: str, options: Optional[dict] = None) -> "EmulatorResource": +def open_emulator(module_name: str, options: dict | None = None) -> EmulatorResource: emulator = emulator_factory(module_name)() if options: emulator.options.update(options) @@ -32,13 +34,17 @@ def __init__(self, emulator: Emulator) -> None: self.emulator: Emulator = emulator self.buffer: list = [] - def __enter__(self: T) -> T: + def __enter__(self) -> Self: return self - def __exit__(self, *args) -> None: - ... + def __exit__(self, *args) -> None: ... - def write(self, message: str, termination: Optional[str] = None, encoding: Optional[str] = None) -> int: + def write( + self, + message: str, + termination: str | None = None, + encoding: str | None = None, + ) -> int: termination = self.termination if termination is None else termination response = self.emulator(message) if response: @@ -49,14 +55,18 @@ def write(self, message: str, termination: Optional[str] = None, encoding: Optio self.buffer.append(response) return len(message + termination) - def read(self, termination: Optional[str] = None, encoding: Optional[str] = None,) -> str: + def read( + self, + termination: str | None = None, + encoding: str | None = None, + ) -> str: encoding = self.encoding if encoding is None else encoding if not self.buffer: raise EmptyBufferError("Read buffer is empty.") return bytes(self.buffer.pop(0)).decode(encoding) - def query(self, message: str, delay: Optional[float] = None) -> str: + def query(self, message: str, delay: float | None = None) -> str: self.write(message) - if delay: + if delay is not None: time.sleep(delay) return self.read() diff --git a/src/comet/emulator/response.py b/src/comet/emulator/response.py index c4d1b04..b72456c 100644 --- a/src/comet/emulator/response.py +++ b/src/comet/emulator/response.py @@ -4,7 +4,7 @@ import numpy as np -__all__ = ["TextResponse", "BinaryResponse", "RawResponse"] +__all__ = ["BinaryResponse", "RawResponse", "TextResponse"] @dataclass @@ -18,6 +18,7 @@ def __bytes__(self) -> bytes: ... @dataclass(repr=False) class TextResponse(Response): """SCPI text response with optional encoding.""" + text: str encoding: str = "ascii" # SCPI default is ascii @@ -51,6 +52,7 @@ def __eq__(self, other: object) -> bool: @dataclass(repr=False) class RawResponse(Response): """Generic bytes response.""" + data: bytes def __repr__(self) -> str: @@ -119,9 +121,7 @@ def make_response(response: Any) -> Response: """ if isinstance(response, Response): return response - elif isinstance(response, int): - return TextResponse(format(response)) - elif isinstance(response, float): + elif isinstance(response, (int, float)): return TextResponse(format(response)) elif isinstance(response, str): return TextResponse(response) diff --git a/src/comet/emulator/rohde_schwarz/nge100.py b/src/comet/emulator/rohde_schwarz/nge100.py index 59e7fd1..7e92a6c 100644 --- a/src/comet/emulator/rohde_schwarz/nge100.py +++ b/src/comet/emulator/rohde_schwarz/nge100.py @@ -2,9 +2,7 @@ import math -from comet.emulator import Emulator -from comet.emulator import message, run - +from comet.emulator import Emulator, message, run __all__ = ["NGE100Emulator"] @@ -59,21 +57,29 @@ def set_enabled(self, enabled: int) -> None: def get_enabled(self) -> str: return str(int(self.enabled_channels[self.selected_channel])) - @message(r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$") + @message( + r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$" + ) def set_voltage_level(self, voltage_level: float) -> None: voltage_level = min(max(0, float(voltage_level)), 32) self.voltage_levels[self.selected_channel] = voltage_level - @message(r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$") + @message( + r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$" + ) def get_voltage_level(self) -> str: return str(self.voltage_levels[self.selected_channel]) - @message(r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$") + @message( + r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$" + ) def set_current_limit(self, current_limit: float) -> None: current_limit = min(max(0, float(current_limit)), 3) self.current_limits[self.selected_channel] = current_limit - @message(r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$") + @message( + r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$" + ) def get_current_limit(self) -> str: return str(self.current_limits[self.selected_channel]) diff --git a/src/comet/emulator/rohde_schwarz/rtp164.py b/src/comet/emulator/rohde_schwarz/rtp164.py index 9f7d8bf..9f6083b 100644 --- a/src/comet/emulator/rohde_schwarz/rtp164.py +++ b/src/comet/emulator/rohde_schwarz/rtp164.py @@ -1,8 +1,7 @@ """Rohde Schwarz RTP164 oscilloscope emulator""" -from comet.emulator import IEC60488Emulator -from comet.emulator import BinaryResponse, message, run -from comet.emulator.utils import SCPIError, scpi_parse_bool, generate_waveform +from comet.emulator import BinaryResponse, IEC60488Emulator, message, run +from comet.emulator.utils import SCPIError, generate_waveform, scpi_parse_bool __all__ = ["RTP164Emulator"] @@ -53,14 +52,13 @@ def get_format_data(self) -> str: return self.format_data @message(r":?FORM(?:AT)?(?::DATA)?\s+(ASC|ASC,0|REAL,32|INT,8|INT,16)$") - def set_format_data(self, format_length) -> None: + def set_format_data(self, format_length: str) -> None: if format_length == "ASC": - format_length == "ASC,0" + format_length = "ASC,0" self.format_data = format_length @message(r":?SING(?:LE)?$") - def set_single(self) -> None: - ... + def set_single(self) -> None: ... @message(r":?CHAN([1-4]):STAT\?$") def get_channel_state(self, channel) -> str: @@ -76,7 +74,9 @@ def get_channel_waveform_data_header(self, channel, waveform) -> str: @message(r":?CHAN([1-4])(?::WAV([1-3]))?:DATA(?::VAL)?\?$") def get_channel_waveform_data(self, channel, waveform) -> BinaryResponse: - _, y = generate_waveform(self.num_samples, duration=self.duration, noise_std=0.01) # TODO + _, y = generate_waveform( + self.num_samples, duration=self.duration, noise_std=0.01 + ) # TODO big_endian = self.format_border == "MSBF" return BinaryResponse.pack_real32(y, big_endian=big_endian) diff --git a/src/comet/emulator/rohde_schwarz/sma100b.py b/src/comet/emulator/rohde_schwarz/sma100b.py index c53473d..135ed11 100644 --- a/src/comet/emulator/rohde_schwarz/sma100b.py +++ b/src/comet/emulator/rohde_schwarz/sma100b.py @@ -1,10 +1,8 @@ """Rohde Schwarz SMA100B signal generator emulator""" -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run from comet.emulator.utils import Error - __all__ = ["SMA100BEmulator"] @@ -54,7 +52,9 @@ def set_frequency_mode(self, mode: str) -> None: def get_frequency(self) -> float: return self.frequency - @message(r"(?:SOUR(?:ce)?1)?:FREQuency:(?:CW|FIX(?:ed)?) ([\d.]+(?:[eE][+-]?\d+)?)$") + @message( + r"(?:SOUR(?:ce)?1)?:FREQuency:(?:CW|FIX(?:ed)?) ([\d.]+(?:[eE][+-]?\d+)?)$" + ) def set_frequency(self, frequency) -> None: frequency = float(frequency) if frequency < 8e3 or frequency > 12.75e9: @@ -80,7 +80,7 @@ def get_output(self) -> str: @message(r"(?:SOUR(?:ce)?1:)?OUTP(?:ut)?:STAT(?:e)?\s+(ON|OFF)$") def set_output(self, state) -> None: - self.output = True if state == "ON" else False + self.output = state == "ON" if __name__ == "__main__": diff --git a/src/comet/emulator/tcpserver.py b/src/comet/emulator/tcpserver.py index 2dc4068..dbbecc1 100644 --- a/src/comet/emulator/tcpserver.py +++ b/src/comet/emulator/tcpserver.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import asyncio import contextlib @@ -5,8 +7,8 @@ import logging import re import signal +from collections.abc import Iterable from dataclasses import dataclass -from typing import Iterable, Optional, Union from .emulator import Emulator from .response import Response @@ -18,9 +20,9 @@ class TCPRequestHandler: async def read_messages( self, reader: asyncio.StreamReader, - context: "TCPServerContext", + context: TCPServerContext, rx_buffer: bytearray, - ) -> Optional[list[bytes]]: + ) -> list[bytes] | None: data = await reader.read(4096) if not data: return None @@ -41,8 +43,8 @@ async def read_messages( async def send_messages( self, writer: asyncio.StreamWriter, - context: "TCPServerContext", - response: Union[Response, Iterable[Response]], + context: TCPServerContext, + response: Response | Iterable[Response], ) -> None: termination_bytes = context.termination @@ -66,7 +68,7 @@ async def handle( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, - context: "TCPServerContext", + context: TCPServerContext, ) -> None: rx_buffer = bytearray() @@ -80,8 +82,6 @@ async def handle( response = await context.handle_message(str(message, "utf-8")) if response is not None: await self.send_messages(writer, context, response) - except asyncio.CancelledError: - raise finally: writer.close() with contextlib.suppress(Exception): @@ -99,7 +99,7 @@ class TCPServerContext: async def handle_message( self, message: str, - ) -> Union[None, Response, Iterable[Response]]: + ) -> Response | Iterable[Response] | None: response = self.emulator(message) if response is not None: await asyncio.sleep(self.request_delay) @@ -110,7 +110,7 @@ class TCPServer: def __init__(self, address: tuple[str, int], context: TCPServerContext) -> None: self.address = address self.context = context - self._server: Optional[asyncio.base_events.Server] = None + self._server: asyncio.base_events.Server | None = None self._handler = TCPRequestHandler() self._shutdown_lock = asyncio.Lock() self._shutdown_started = False @@ -163,8 +163,12 @@ def option_type(value: str) -> tuple[str, str]: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() - parser.add_argument("--host", default="localhost", help="host, default is 'localhost'") - parser.add_argument("-p", "--port", type=int, default=10000, help="port, default is 10000") + parser.add_argument( + "--host", default="localhost", help="host, default is 'localhost'" + ) + parser.add_argument( + "-p", "--port", type=int, default=10000, help="port, default is 10000" + ) parser.add_argument( "-t", "--termination", diff --git a/src/comet/emulator/thorlabs/pm100.py b/src/comet/emulator/thorlabs/pm100.py index 84555fe..a2e0eab 100644 --- a/src/comet/emulator/thorlabs/pm100.py +++ b/src/comet/emulator/thorlabs/pm100.py @@ -2,8 +2,7 @@ import random -from comet.emulator import Emulator -from comet.emulator import message, run +from comet.emulator import Emulator, message, run from comet.emulator.utils import Error __all__ = ["PM100Emulator"] diff --git a/src/comet/emulator/utils.py b/src/comet/emulator/utils.py index 8af8b01..aa81451 100644 --- a/src/comet/emulator/utils.py +++ b/src/comet/emulator/utils.py @@ -5,16 +5,17 @@ __all__ = [ "Error", "SCPIError", - "tsp_print", - "tsp_assign", - "scpi_parse_bool", "generate_waveform", + "scpi_parse_bool", + "tsp_assign", + "tsp_print", ] @dataclass class Error: """Generic error message container.""" + code: int message: str @@ -22,8 +23,9 @@ class Error: @dataclass class SCPIError(Error): """Generic SCPI error message container.""" + def __str__(self) -> str: - return f"{self.code},\"{self.message}\"" + return f'{self.code},"{self.message}"' def tsp_print(route: str) -> str: @@ -45,12 +47,12 @@ def scpi_parse_bool(s: str) -> bool: def generate_waveform( n_points=1000, - duration=1e-3, # 1 ms total time - baseline=0.0, # DC baseline level - spike_time=0.5e-3, # spike occurs at 0.5 ms - spike_width=5e-6, # spike duration 5 µs - spike_amplitude=2.0, # spike height - noise_std=0.0 # optional Gaussian noise + duration=1e-3, # 1 ms total time + baseline=0.0, # DC baseline level + spike_time=0.5e-3, # spike occurs at 0.5 ms + spike_width=5e-6, # spike duration 5 µs + spike_amplitude=2.0, # spike height + noise_std=0.0, # optional Gaussian noise ): """Create a waveform with a DC baseline and one spike for testing.""" # Time axis diff --git a/src/comet/estimate.py b/src/comet/estimate.py index 3cc459b..108e414 100644 --- a/src/comet/estimate.py +++ b/src/comet/estimate.py @@ -1,8 +1,8 @@ """Estimate remaining time.""" +from collections.abc import Callable from datetime import timedelta from time import monotonic -from typing import Callable __all__ = ["Estimate"] diff --git a/src/comet/filters.py b/src/comet/filters.py index 0913353..1bdeb84 100644 --- a/src/comet/filters.py +++ b/src/comet/filters.py @@ -1,4 +1,4 @@ -from typing import Iterable +from collections.abc import Iterable import numpy as np @@ -11,10 +11,11 @@ def std_mean_filter(values: Iterable[float], threshold: float) -> bool: >>> std_mean_filter([0.250, 0.249], threshold=0.005) True """ - mean = np.mean(np.array(values)) + arr = np.asarray(values) + mean = np.mean(arr) # Sample standard deviation with ddof=1 (not population standard deviation) # http://stackoverflow.com/questions/34050491/ddg#34050706 # https://www.sharpsightlabs.com/blog/numpy-standard-deviation/ - sample_std_dev = np.std(np.array(values), ddof=1) + sample_std_dev = np.std(arr, ddof=1) ratio = sample_std_dev / mean - return ratio < threshold + return bool(ratio < threshold) diff --git a/src/comet/functions.py b/src/comet/functions.py index 8bbc1d9..ea731ea 100644 --- a/src/comet/functions.py +++ b/src/comet/functions.py @@ -1,7 +1,7 @@ """Functions module.""" +from collections.abc import Iterator from decimal import Context, Decimal -from typing import Iterator __all__ = ["LinearRange"] @@ -22,11 +22,7 @@ class LinearRange: [0.0, 1.0, 2.0, 3.0, 4.0] """ - __slots__ = ( - "begin", - "end", - "step" - ) + __slots__ = ("begin", "end", "step") def __init__(self, begin: float, end: float, step: float) -> None: self.begin: float = begin diff --git a/src/comet/parameter.py b/src/comet/parameter.py index 5c995dd..3216920 100644 --- a/src/comet/parameter.py +++ b/src/comet/parameter.py @@ -1,30 +1,30 @@ -from typing import Any, Optional, Type +from __future__ import annotations + +from typing import Any from .utils import to_unit -__all__ = ["inspect_parameters", "Parameter", "ParameterBase"] +__all__ = ["Parameter", "ParameterBase", "inspect_parameters"] -ParameterBaseType = Type["ParameterBase"] +ParameterBaseType = type["ParameterBase"] ParameterValues = dict[str, Any] -def inspect_parameters(cls: ParameterBaseType) -> dict[str, "Parameter"]: +def inspect_parameters(cls: ParameterBaseType) -> dict[str, Parameter]: """Retrun dictionary of assigned class parameters.""" parameters = {} for mro_cls in cls.__mro__: for key, value in mro_cls.__dict__.items(): - if key not in parameters: - if isinstance(value, Parameter): - parameters[key] = value + if key not in parameters and isinstance(value, Parameter): + parameters[key] = value return parameters def validate_parameters(cls: ParameterBaseType, values: ParameterValues) -> None: """Validates a dictionary containing parameter values.""" for key, parameter in inspect_parameters(cls).items(): - if parameter.required: - if key not in values: - raise KeyError(f"missing required parameter: {key!r}") + if parameter.required and key not in values: + raise KeyError(f"missing required parameter: {key!r}") if key in values: parameter.validate(values.get(key)) @@ -32,8 +32,17 @@ def validate_parameters(cls: ParameterBaseType, values: ParameterValues) -> None class Parameter: """Class parameter specification.""" - def __init__(self, default=None, *, type = None, minimum=None, maximum=None, - choice=None, unit=None, constraint=None) -> None: + def __init__( + self, + default=None, + *, + type=None, + minimum=None, + maximum=None, + choice=None, + unit=None, + constraint=None, + ) -> None: self.type = type self.minimum = minimum self.maximum = maximum @@ -49,9 +58,10 @@ def required(self) -> bool: return self.default is None def validate(self, value: Any) -> Any: - if self.choice is not None: - if value not in self.choice: - raise ValueError(f"value not allowed: {value!r}, musst be one of: {self.choice!r}") + if self.choice is not None and value not in self.choice: + raise ValueError( + f"value not allowed: {value!r}, musst be one of: {self.choice!r}" + ) if self.unit is not None: value = to_unit(value, self.unit) if self.type is not None: @@ -68,23 +78,22 @@ def validate(self, value: Any) -> Any: maximum = to_unit(maximum, self.unit) if value > maximum: raise ValueError(f"value out of bounds: {value!r}") - if self.constraint is not None: - if not self.constraint(self, value): - raise ValueError(f"failed value constraint check: {value!r}") + if self.constraint is not None and not self.constraint(self, value): + raise ValueError(f"failed value constraint check: {value!r}") return value class ParameterBase: """Base class for parameters.""" - def __init__(self, values: Optional[ParameterValues] = None) -> None: + def __init__(self, values: ParameterValues | None = None) -> None: self.__values: dict[str, Any] = {} self.update_parameters(values or {}) def __getattribute__(self, name): parameters = inspect_parameters(type(self)) if name in parameters: - default = parameters.get(name).default + default = parameters[name].default return self.__values.get(name, default) return super().__getattribute__(name) @@ -97,7 +106,7 @@ def __setattr__(self, name, value): def parameters(self) -> ParameterValues: """Retrun dictionary containing all parameter values.""" values = {} - for key, value in inspect_parameters(type(self)).items(): + for key in inspect_parameters(type(self)): values[key] = getattr(self, key) return values diff --git a/src/comet/station.py b/src/comet/station.py index fe5616a..96f449a 100644 --- a/src/comet/station.py +++ b/src/comet/station.py @@ -1,35 +1,40 @@ -import os +from __future__ import annotations + import logging -from collections.abc import Mapping -from contextlib import ExitStack -from typing import Any, Callable, ContextManager, Optional, TextIO, Union +import os +from collections.abc import Callable, Mapping +from contextlib import AbstractContextManager, ExitStack +from pathlib import Path +from typing import Any, Self, TextIO import pyvisa import yaml -from pathlib import Path -from schema import Schema, SchemaError, And, Optional as Opt, Use +from pyvisa.resources.resource import Resource +from schema import And, Optional, Schema, SchemaError, Use -from comet.driver import driver_factory, Driver +from comet.driver import Driver, driver_factory __all__ = ["Station"] Config = dict[str, Any] -ResourceFactory = Callable[[Config], ContextManager[Any]] +ResourceFactory = Callable[[Config], AbstractContextManager[Any]] logger = logging.getLogger(__name__) -INSTRUMENT_SCHEMA: Schema = Schema({ - Opt("model"): And(str, lambda s: len(s) > 0), - "resource_name": And(str, lambda s: len(s) > 0), - Opt("termination"): And(str, lambda s: len(s) > 0), - Opt("timeout"): And(Use(float), lambda t: t > 0), - Opt("visa_library"): str, -}) +INSTRUMENT_SCHEMA: Schema = Schema( + { + Optional("model"): And(str, lambda s: len(s) > 0), + "resource_name": And(str, lambda s: len(s) > 0), + Optional("termination"): And(str, lambda s: len(s) > 0), + Optional("timeout"): And(Use(float), lambda t: t > 0), # type: ignore + Optional("visa_library"): str, + } +) DEFAULT_CONFIG_FILES: list[str] = ["station.yaml", "station.yml", "station.json"] -def default_resource_factory(config: Config) -> pyvisa.Resource: +def default_resource_factory(config: Config) -> Resource: visa_library = config.get("visa_library", "@py") rm = pyvisa.ResourceManager(visa_library) resource_name = config["resource_name"] @@ -39,25 +44,33 @@ def default_resource_factory(config: Config) -> pyvisa.Resource: resource_name, read_termination=termination, write_termination=termination, - timeout=timeout_ms + timeout=timeout_ms, ) def find_filenames(default_filenames: list[str]) -> list[str]: """Lookup an orderd list of files and return absolut paths to existing ones.""" - return [os.path.abspath(filename) for filename in default_filenames if os.path.isfile(filename)] + return [ + os.path.abspath(filename) + for filename in default_filenames + if os.path.isfile(filename) + ] class Station(Mapping): - def __init__(self, *, resource_factory: Optional[ResourceFactory] = None) -> None: + def __init__(self, *, resource_factory: ResourceFactory | None = None) -> None: """Create an empty Station instance.""" self.instruments_config: Config = {} self._instruments: dict[str, Any] = {} - self._stack: Optional[ExitStack] = None - self.resource_factory: ResourceFactory = resource_factory or default_resource_factory + self._stack: ExitStack | None = None + self.resource_factory: ResourceFactory = ( + resource_factory or default_resource_factory + ) @classmethod - def from_config(cls, config: Config, *, resource_factory: Optional[ResourceFactory] = None) -> "Station": + def from_config( + cls, config: Config, *, resource_factory: ResourceFactory | None = None + ) -> Station: """ Create a Station instance from a config dictionary. @@ -81,14 +94,21 @@ def from_config(cls, config: Config, *, resource_factory: Optional[ResourceFacto validated = INSTRUMENT_SCHEMA.validate(conf) validated_configs[name] = validated except SchemaError as exc: - raise ValueError(f"Invalid configuration for instrument {name!r}: {exc}") + raise ValueError( + f"Invalid configuration for instrument {name!r}: {exc}" + ) station = cls(resource_factory=resource_factory) station.instruments_config = validated_configs return station @classmethod - def from_file(cls, config_file: Optional[Union[str, Path, TextIO]] = None, *, resource_factory: Optional[Callable[[dict[str, Any]], Any]] = None) -> "Station": + def from_file( + cls, + config_file: str | Path | TextIO | None = None, + *, + resource_factory: Callable[[dict[str, Any]], Any] | None = None, + ) -> Station: """ Create a Station instance from a config file. @@ -103,16 +123,25 @@ def from_file(cls, config_file: Optional[Union[str, Path, TextIO]] = None, *, re if found_config_files: config_file = found_config_files[0] if len(found_config_files) > 1: - logger.warning("Found multiple config files with supported names: %s", ", ".join(found_config_files)) + logger.warning( + "Found multiple config files with supported names: %s", + ", ".join(found_config_files), + ) logger.warning("Using %s", config_file) if config_file is None: - default_file_list = ", ".join([f"{file_name!r}" for file_name in DEFAULT_CONFIG_FILES]) - raise ValueError(f"No default config file found, must be one of: {default_file_list}") + default_file_list = ", ".join( + [f"{file_name!r}" for file_name in DEFAULT_CONFIG_FILES] + ) + raise ValueError( + f"No default config file found, must be one of: {default_file_list}" + ) with ExitStack() as stack: if isinstance(config_file, (str, os.PathLike, Path)): - file_obj: Any = stack.enter_context(open(config_file, "r", encoding="utf-8")) + file_obj: Any = stack.enter_context( + open(config_file, "r", encoding="utf-8") + ) elif hasattr(config_file, "read"): file_obj = config_file else: @@ -150,37 +179,30 @@ def update_instrument(self, name: str, /, **kwargs): def enter_context(self, cm: Any) -> Any: """Enter an context manager and attach it to the station's lifecycle.""" if not self._stack: - raise RuntimeError(f"{type(self).__name__!r} context is not active, enter context first.") + raise RuntimeError( + f"{type(self).__name__!r} context is not active, enter context first." + ) return self._stack.enter_context(cm) - def __enter__(self) -> "Station": - self._stack = ExitStack() - for name, config in self.instruments_config.items(): - # Create resource via factory. - res_cm = self.resource_factory(config) - # Open the resource and instantiate the driver. - res = self._stack.enter_context(res_cm) - driver_cls = driver_factory(config["model"]) if "model" in config else Driver - self._instruments[name] = driver_cls(res) - # Attach as a read-only attribute. - object.__setattr__(self, name, self._instruments[name]) - return self - - def __exit__(self, exc_type, exc_value, traceback) -> None: - """Close all instrument resources.""" - if self._stack: - self._stack.close() - for name in self._instruments: - if hasattr(self, name): - object.__delattr__(self, name) - self._instruments = {} - def __setattr__(self, name, value): """Prevent modifications to instrument attributes once they are set.""" - if "_instruments" in self.__dict__ and name in self.__dict__.get("_instruments", {}): - raise AttributeError(f"Cannot modify read-only instrument attribute {name!r}") + if "_instruments" in self.__dict__ and name in self.__dict__.get( + "_instruments", {} + ): + raise AttributeError( + f"Cannot modify read-only instrument attribute {name!r}" + ) object.__setattr__(self, name, value) + def __getattr__(self, name: str) -> Any: + instruments = self.__dict__.get("_instruments", {}) + try: + return instruments[name] + except KeyError: + raise AttributeError( + f"{type(self).__name__!r} has no attribute {name!r}" + ) from None + def __getitem__(self, name): return self._instruments[name] @@ -192,3 +214,27 @@ def __iter__(self): def __len__(self): return len(self._instruments) + + def __enter__(self) -> Self: + self._stack = ExitStack() + + for name, config in self.instruments_config.items(): + resource = self._stack.enter_context(self.resource_factory(config)) + driver_cls = ( + driver_factory(config["model"]) if "model" in config else Driver + ) + self._instruments[name] = driver_cls(resource) + + return self + + def __exit__( + self, + exc_type: object, + exc_value: object, + traceback: object, + ) -> None: + if self._stack is not None: + self._stack.close() + + self._stack = None + self._instruments.clear() diff --git a/src/comet/utils.py b/src/comet/utils.py index 21dbc53..490e9b1 100644 --- a/src/comet/utils.py +++ b/src/comet/utils.py @@ -1,27 +1,27 @@ -import datetime import re import warnings +from collections.abc import Iterable +from datetime import UTC, datetime from math import log -from typing import Iterable, Optional, Union -from pint import UnitRegistry, Quantity +from pint import Quantity, UnitRegistry __all__ = [ - "ureg", - "to_unit", "auto_scale", "combine_matrix", "inverse_square", - "t_dew", "make_iso", - "safe_filename", "parse_model_urn", + "safe_filename", + "t_dew", + "to_unit", + "ureg", ] ureg: UnitRegistry = UnitRegistry() -def to_unit(value: Union[float, str, Quantity], unit: str) -> float: +def to_unit(value: float | str | Quantity, unit: str) -> float: """Convert value or string representation with or without unit to another unit.""" if isinstance(value, Quantity): @@ -58,7 +58,7 @@ def auto_scale(value: float) -> tuple[float, str, str]: def combine_matrix(a: Iterable, b: Iterable, *args: Iterable) -> list[str]: - c = ["".join((x, y)) for x in a for y in b] + c = [f"{x}{y}" for x in a for y in b] if args: return combine_matrix(c, *args) return c @@ -79,8 +79,8 @@ def t_dew(t: float, rh: float) -> float: return (b * m) / (a - m) -def make_iso(dt: Optional[Union[float, datetime.datetime]] = None) -> str: - """Return filesystem safe ISO date time. +def make_iso(dt: float | datetime | None = None) -> str: + """Return filesystem safe ISO date time (default UTC). >>> make_iso() '2019-12-24T12-21-42' @@ -89,10 +89,10 @@ def make_iso(dt: Optional[Union[float, datetime.datetime]] = None) -> str: '2015-02-09T05-39-49' """ if dt is None: - dt = datetime.datetime.now() - if not isinstance(dt, datetime.datetime): - dt = datetime.datetime.fromtimestamp(dt) - return dt.replace(microsecond=0).isoformat().replace(":", "-") + dt = datetime.now(tz=UTC) + if not isinstance(dt, datetime): + dt = datetime.fromtimestamp(dt, tz=UTC) + return dt.strftime("%Y-%m-%dT%H-%M-%S") def safe_filename(filename: str) -> str: diff --git a/tests/helpers.py b/tests/helpers.py index fc92fbf..62f47f4 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,24 +7,25 @@ ] -def pack_binary_values(values) -> bytes: - payload = struct.pack("<" + "f" * len(values), *values) +def pack_binary_values(values, is_big_endian: bool = False) -> bytes: + endian = ">" if is_big_endian else "<" + payload = struct.pack(endian + "f" * len(values), *values) header = f"#{len(str(len(payload)))}{len(payload)}".encode("ascii") return header + payload -def unpack_binary_values(data: bytes) -> list[float]: +def unpack_binary_values(data: bytes, is_big_endian: bool = False) -> list[float]: if not data.startswith(b"#"): raise ValueError("Invalid block: must start with '#'") # Read header n_digits = int(chr(data[1])) # how many digits in the length field - length_str = data[2:2+n_digits].decode("ascii") + length_str = data[2 : 2 + n_digits].decode("ascii") payload_len = int(length_str) # Slice out the binary payload start = 2 + n_digits - payload = data[start:start+payload_len] + payload = data[start : start + payload_len] if len(payload) != payload_len: raise ValueError("Invalid block: payload length mismatch") @@ -34,20 +35,18 @@ def unpack_binary_values(data: bytes) -> list[float]: raise ValueError("Invalid block: payload not multiple of 4 bytes") count = payload_len // 4 - floats = struct.unpack("<" + "f"*count, payload) + endian = ">" if is_big_endian else "<" + floats = struct.unpack(endian + "f" * count, payload) return list(floats) - class MockResource: - def __init__(self) -> None: self.buffer = [] - def clear(self) -> None: - ... # VISA bus clear + def clear(self) -> None: ... # VISA bus clear - def read(self, encoding=None) -> str: + def read(self, encoding: str = "utf-8") -> str: result = self.buffer.pop(0) if isinstance(result, bytes): return result.decode(encoding) @@ -66,7 +65,10 @@ def read_bytes(self, count: int) -> bytes: def write_raw(self, message: bytes) -> int: self.buffer.append(message) + return len(message) - def query_binary_values(self, message: str, *, datatype="f", is_big_endian=False): + def query_binary_values( + self, message: str, *, datatype: str = "f", is_big_endian: bool = False + ) -> list[float]: self.write(message) - return unpack_binary_values(self.buffer.pop(0)) + return unpack_binary_values(self.buffer.pop(0), is_big_endian=is_big_endian) diff --git a/tests/test_driver_corvus_venus1.py b/tests/test_driver_corvus_venus1.py index dc8f171..0547867 100644 --- a/tests/test_driver_corvus_venus1.py +++ b/tests/test_driver_corvus_venus1.py @@ -98,7 +98,7 @@ def test_ipadr(driver, resource): def test_vel(driver, resource): - for value in 0., 60., 90.: + for value in 0.0, 60.0, 90.0: resource.buffer = [f"{value:.6f}"] assert driver.vel == value assert resource.buffer == ["getvel"] @@ -109,7 +109,7 @@ def test_vel(driver, resource): def test_accel(driver, resource): - for value in 0., 120., 200.: + for value in 0.0, 120.0, 200.0: resource.buffer = [f"{value:.6f}"] assert driver.accel == value assert resource.buffer == ["getaccel"] @@ -131,7 +131,7 @@ def test_accelfunc(driver, resource): def test_manaccel(driver, resource): - for value in 0., 100., 2400.: + for value in 0.0, 100.0, 2400.0: resource.buffer = [f"{value:.6f}"] assert driver.manaccel == value assert resource.buffer == ["getmanaccel"] @@ -208,7 +208,9 @@ def test_limit(driver, resource): resource.buffer = [] driver.limit = (1, 2), (3, 4), (5, 6) - assert resource.buffer == ["1.000000 3.000000 5.000000 2.000000 4.000000 6.000000 setlimit"] + assert resource.buffer == [ + "1.000000 3.000000 5.000000 2.000000 4.000000 6.000000 setlimit" + ] def test_abort(driver, resource): @@ -310,26 +312,30 @@ def test_joyspeed(driver, resource): def test_joybspeed(driver, resource): resource.buffer = ["0.123456"] - assert driver.joybspeed == .123456 + assert driver.joybspeed == 0.123456 assert resource.buffer == ["getjoybspeed"] resource.buffer = [] - driver.joybspeed = .123456 + driver.joybspeed = 0.123456 assert resource.buffer == ["0.123456 setjoybspeed"] def test_axis_pitch(driver, resource): resource.buffer = ["0.1", "0.2", "0.3"] - assert driver.x.pitch == .1 - assert driver.y.pitch == .2 - assert driver.z.pitch == .3 + assert driver.x.pitch == 0.1 + assert driver.y.pitch == 0.2 + assert driver.z.pitch == 0.3 assert resource.buffer == ["1 getpitch", "2 getpitch", "3 getpitch"] resource.buffer = [] - driver.x.pitch = .3 - driver.y.pitch = .2 - driver.z.pitch = .1 - assert resource.buffer == ["0.300000 1 setpitch", "0.200000 2 setpitch", "0.100000 3 setpitch"] + driver.x.pitch = 0.3 + driver.y.pitch = 0.2 + driver.z.pitch = 0.1 + assert resource.buffer == [ + "0.300000 1 setpitch", + "0.200000 2 setpitch", + "0.100000 3 setpitch", + ] def test_axis_unit(driver, resource): @@ -344,7 +350,11 @@ def test_axis_unit(driver, resource): driver.x.unit = value driver.y.unit = value driver.z.unit = value - assert resource.buffer == [f"{value:d} 1 setunit", f"{value:d} 2 setunit", f"{value:d} 3 setunit"] + assert resource.buffer == [ + f"{value:d} 1 setunit", + f"{value:d} 2 setunit", + f"{value:d} 3 setunit", + ] def test_axis_umotmin(driver, resource): @@ -359,7 +369,11 @@ def test_axis_umotmin(driver, resource): driver.x.umotmin = value driver.y.umotmin = value driver.z.umotmin = value - assert resource.buffer == [f"{value:d} 1 setumotmin", f"{value:d} 2 setumotmin", f"{value:d} 3 setumotmin"] + assert resource.buffer == [ + f"{value:d} 1 setumotmin", + f"{value:d} 2 setumotmin", + f"{value:d} 3 setumotmin", + ] def test_axis_umotgrad(driver, resource): @@ -374,7 +388,11 @@ def test_axis_umotgrad(driver, resource): driver.x.umotgrad = value driver.y.umotgrad = value driver.z.umotgrad = value - assert resource.buffer == [f"{value:d} 1 setumotgrad", f"{value:d} 2 setumotgrad", f"{value:d} 3 setumotgrad"] + assert resource.buffer == [ + f"{value:d} 1 setumotgrad", + f"{value:d} 2 setumotgrad", + f"{value:d} 3 setumotgrad", + ] def test_axis_polepairs(driver, resource): @@ -389,7 +407,11 @@ def test_axis_polepairs(driver, resource): driver.x.polepairs = value driver.y.polepairs = value driver.z.polepairs = value - assert resource.buffer == [f"{value:d} 1 setpolepairs", f"{value:d} 2 setpolepairs", f"{value:d} 3 setpolepairs"] + assert resource.buffer == [ + f"{value:d} 1 setpolepairs", + f"{value:d} 2 setpolepairs", + f"{value:d} 3 setpolepairs", + ] def test_axis_enabled(driver, resource): @@ -404,7 +426,11 @@ def test_axis_enabled(driver, resource): driver.x.enabled = value driver.y.enabled = value driver.z.enabled = value - assert resource.buffer == [f"{value:d} 1 setaxis", f"{value:d} 2 setaxis", f"{value:d} 3 setaxis"] + assert resource.buffer == [ + f"{value:d} 1 setaxis", + f"{value:d} 2 setaxis", + f"{value:d} 3 setaxis", + ] def test_axis_phaseares(driver, resource): @@ -419,7 +445,11 @@ def test_axis_phaseares(driver, resource): driver.x.phaseares = value driver.y.phaseares = value driver.z.phaseares = value - assert resource.buffer == [f"{value:d} 1 setphaseares", f"{value:d} 2 setphaseares", f"{value:d} 3 setphaseares"] + assert resource.buffer == [ + f"{value:d} 1 setphaseares", + f"{value:d} 2 setphaseares", + f"{value:d} 3 setphaseares", + ] def test_axis_motiondir(driver, resource): @@ -434,7 +464,11 @@ def test_axis_motiondir(driver, resource): driver.x.motiondir = value driver.y.motiondir = value driver.z.motiondir = value - assert resource.buffer == [f"{value:d} 1 setmotiondir", f"{value:d} 2 setmotiondir", f"{value:d} 3 setmotiondir"] + assert resource.buffer == [ + f"{value:d} 1 setmotiondir", + f"{value:d} 2 setmotiondir", + f"{value:d} 3 setmotiondir", + ] def test_axis_speed(driver, resource): @@ -442,7 +476,11 @@ def test_axis_speed(driver, resource): driver.x.speed(-0.1) driver.y.speed(-0.2) driver.z.speed(-0.3) - assert resource.buffer == [f"{-.1:.6f} 1 speed", f"{-.2:.6f} 2 speed", f"{-.3:.6f} 3 speed"] + assert resource.buffer == [ + f"{-0.1:.6f} 1 speed", + f"{-0.2:.6f} 2 speed", + f"{-0.3:.6f} 3 speed", + ] def test_axis_test(driver, resource): @@ -450,7 +488,11 @@ def test_axis_test(driver, resource): driver.x.test(10) driver.y.test(11) driver.z.test(12) - assert resource.buffer == [f"{10:.6f} 1 test", f"{11:.6f} 2 test", f"{12:.6f} 3 test"] + assert resource.buffer == [ + f"{10:.6f} 1 test", + f"{11:.6f} 2 test", + f"{12:.6f} 3 test", + ] def test_axis_caldone(driver, resource): @@ -470,7 +512,7 @@ def test_axis_sw(driver, resource): def test_calswdist(driver, resource): - for value in 0., 1.: + for value in 0.0, 1.0: resource.buffer = [f"{value:.6f}", f"{value:.6f}", f"{value:.6f}"] assert driver.x.calswdist == value assert driver.y.calswdist == value @@ -481,7 +523,11 @@ def test_calswdist(driver, resource): driver.x.calswdist = value driver.y.calswdist = value driver.z.calswdist = value - assert resource.buffer == [f"{value:.6f} 1 setcalswdist", f"{value:.6f} 2 setcalswdist", f"{value:.6f} 3 setcalswdist"] + assert resource.buffer == [ + f"{value:.6f} 1 setcalswdist", + f"{value:.6f} 2 setcalswdist", + f"{value:.6f} 3 setcalswdist", + ] def test_ncal(driver, resource): @@ -502,9 +548,9 @@ def test_nrm(driver, resource): def test_nlimit(driver, resource): resource.buffer = ["0 1", "2 3", "4 5"] - assert driver.x.nlimit == (0., 1.) - assert driver.y.nlimit == (2., 3.) - assert driver.z.nlimit == (4., 5.) + assert driver.x.nlimit == (0.0, 1.0) + assert driver.y.nlimit == (2.0, 3.0) + assert driver.z.nlimit == (4.0, 5.0) assert resource.buffer == ["1 getnlimit", "2 getnlimit", "3 getnlimit"] @@ -520,7 +566,11 @@ def test_axis_mp(driver, resource): driver.x.mp = value driver.y.mp = value driver.z.mp = value - assert resource.buffer == [f"{value:d} 1 setmp", f"{value:d} 2 setmp", f"{value:d} 3 setmp"] + assert resource.buffer == [ + f"{value:d} 1 setmp", + f"{value:d} 2 setmp", + f"{value:d} 3 setmp", + ] def test_axis_joyspeed(driver, resource): @@ -535,7 +585,11 @@ def test_axis_joyspeed(driver, resource): driver.x.joyspeed = value driver.y.joyspeed = value driver.z.joyspeed = value - assert resource.buffer == [f"{value:.6f} 1 setnjoyspeed", f"{value:.6f} 2 setnjoyspeed", f"{value:.6f} 3 setnjoyspeed"] + assert resource.buffer == [ + f"{value:.6f} 1 setnjoyspeed", + f"{value:.6f} 2 setnjoyspeed", + f"{value:.6f} 3 setnjoyspeed", + ] def test_system(driver, resource): diff --git a/tests/test_driver_cts_itc.py b/tests/test_driver_cts_itc.py index ef3da61..64f7e12 100644 --- a/tests/test_driver_cts_itc.py +++ b/tests/test_driver_cts_itc.py @@ -1,6 +1,7 @@ -from datetime import datetime +from datetime import UTC, datetime import pytest + from comet.driver.cts import ITC @@ -21,21 +22,24 @@ def test_identify(driver): def test_get_time(driver): driver.resource.buffer = [to_bytes("T010120010000")] - assert driver.time == datetime(2020, 1, 1, 1, 0) + assert driver.time == datetime(2020, 1, 1, 1, 0, tzinfo=UTC) assert driver.resource.buffer == [to_bytes("T")] def test_set_time(driver): driver.resource.buffer = [to_bytes("t010120010000")] - driver.time = datetime(2020, 1, 1, 1, 0) + driver.time = datetime(2020, 1, 1, 1, 0, tzinfo=UTC) assert driver.resource.buffer == [to_bytes("t010120010000")] @pytest.mark.parametrize("running", [0, 1]) -@pytest.mark.parametrize("error", [ - (0, "0", None), - (1, "\x3a", "Feuchtesensor 08-B2"), -]) +@pytest.mark.parametrize( + "error", + [ + (0, "0", None), + (1, "\x3a", "Feuchtesensor 08-B2"), + ], +) def test_status(driver, running, error): is_error, error_code, error_msg = error driver.resource.buffer = [to_bytes(f"S{running}{is_error}000101{error_code}")] @@ -43,14 +47,24 @@ def test_status(driver, running, error): assert st.running == bool(running) assert st.error == error_msg assert st.warning is None - assert st.channels == {0: False, 1: False, 2: False, 3: True, 4: False, 5: True} # TODO refactor? + assert st.channels == { + 0: False, + 1: False, + 2: False, + 3: True, + 4: False, + 5: True, + } # TODO refactor? assert driver.resource.buffer == [to_bytes("S")] -@pytest.mark.parametrize("error_message", [ - "", - "Feuchtesensor 08-B2", -]) +@pytest.mark.parametrize( + "error_message", + [ + "", + "Feuchtesensor 08-B2", + ], +) def test_error_message(driver, error_message): driver.resource.buffer = [to_bytes(f"F{error_message:<32}")] assert driver.error_message == error_message diff --git a/tests/test_driver_ers_ac3.py b/tests/test_driver_ers_ac3.py index ab381ca..398725f 100644 --- a/tests/test_driver_ers_ac3.py +++ b/tests/test_driver_ers_ac3.py @@ -1,4 +1,5 @@ import pytest + from comet.driver.ers import AC3 @@ -175,6 +176,7 @@ def test_next_error(resource): resource.buffer = ["E001"] error = device.next_error() + assert error is not None assert error.message == AC3.ERROR_MESSAGES[1] assert error.code == 1 assert resource.buffer == ["RE"] diff --git a/tests/test_driver_hephy_environbox.py b/tests/test_driver_hephy_environbox.py index 60e6b5a..ebd4f2b 100644 --- a/tests/test_driver_hephy_environbox.py +++ b/tests/test_driver_hephy_environbox.py @@ -72,11 +72,15 @@ def test_pid_control_mode(driver, resource): assert driver.set_pid_control_mode(driver.PID_CONTROL_MODE_DEW) is None assert resource.buffer == ["SET:CTRL_MODE DEW"] - resource.buffer = ["0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"] + resource.buffer = [ + "0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + ] assert driver.get_pid_control_mode() == driver.PID_CONTROL_MODE_HUM assert resource.buffer == ["GET:PC_DATA ?"] - resource.buffer = ["0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"] + resource.buffer = [ + "0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + ] assert driver.get_pid_control_mode() == driver.PID_CONTROL_MODE_DEW assert resource.buffer == ["GET:PC_DATA ?"] @@ -181,23 +185,23 @@ def test_parse_pc_data(): assert data["box_temperature"] == 2.34 assert data["box_dewpoint"] == 11.1 assert not data["pid_status"] - assert data["pid_setpoint"] == 0. - assert data["pid_input"] == 0. - assert data["pid_output"] == 0. - assert data["pid_kp_1"] == 0. - assert data["pid_ki_1"] == 0. - assert data["pid_kd_1"] == 0. + assert data["pid_setpoint"] == 0.0 + assert data["pid_input"] == 0.0 + assert data["pid_output"] == 0.0 + assert data["pid_kp_1"] == 0.0 + assert data["pid_ki_1"] == 0.0 + assert data["pid_kd_1"] == 0.0 assert data["pid_min"] == 0 assert data["pid_max"] == 0 assert data["pid_control_mode"] == 1 - assert data["pid_kp_2"] == 0. - assert data["pid_ki_2"] == 0. - assert data["pid_kd_2"] == 0. + assert data["pid_kp_2"] == 0.0 + assert data["pid_ki_2"] == 0.0 + assert data["pid_kd_2"] == 0.0 assert data["parameter_set"] == 0 - assert data["parameter_threshold"] == 0. + assert data["parameter_threshold"] == 0.0 assert data["hum_flow_dir"] == 0 - assert data["pid_threshold"] == 0. - assert data["vac_valve_current"] == 0. + assert data["pid_threshold"] == 0.0 + assert data["vac_valve_current"] == 0.0 assert data["vac_valve_count"] == 0 assert data["power_microscope_ctrl"] assert not data["power_box_light"] @@ -217,7 +221,7 @@ def test_parse_pc_data(): assert data["box_lux"] == 0.21 assert data["pt100_1"] == 0.23 assert data["pt100_2"] == 0.34 - assert data["pid_sample_time"] == 0. + assert data["pid_sample_time"] == 0.0 assert data["pid_prop_mode"] == 1 assert data["pt100_1_enabled"] assert not data["pt100_2_enabled"] diff --git a/tests/test_driver_keithley_k237.py b/tests/test_driver_keithley_k237.py index 07a0351..c7c5112 100644 --- a/tests/test_driver_keithley_k237.py +++ b/tests/test_driver_keithley_k237.py @@ -6,7 +6,7 @@ @pytest.fixture def driver(resource): driver = K237(resource) - driver.WRITE_DELAY = 0. + driver.WRITE_DELAY = 0.0 return driver @@ -66,24 +66,24 @@ def test_function(driver, resource): def test_voltage(driver, resource): - for level in (-2.5, 0., +2.5): + for level in (-2.5, 0.0, +2.5): resource.buffer = [format(level, ".3E")] assert driver.voltage_level == level assert resource.buffer == ["G1,2,0X", "X"] - for level in (-2.5, 0., +2.5): + for level in (-2.5, 0.0, +2.5): resource.buffer = [] driver.voltage_level = level assert resource.buffer == [f"B{level:.3E},,X"] def test_current(driver, resource): - for level in (-2.5e-06, 0., +2.5e-06): + for level in (-2.5e-06, 0.0, +2.5e-06): resource.buffer = [format(level, ".3E")] assert driver.current_level == level assert resource.buffer == ["G1,2,0X", "X"] - for level in (-2.5e-06, 0., +2.5e-06): + for level in (-2.5e-06, 0.0, +2.5e-06): resource.buffer = [] driver.current_level = level assert resource.buffer == [f"B{level:.3E},,X"] diff --git a/tests/test_driver_keithley_k2400.py b/tests/test_driver_keithley_k2400.py index 7436af7..b3f71fa 100644 --- a/tests/test_driver_keithley_k2400.py +++ b/tests/test_driver_keithley_k2400.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" @@ -74,11 +74,21 @@ def test_function(driver, resource): resource.buffer = ["1", "1"] driver.function = driver.FUNCTION_VOLTAGE - assert resource.buffer == [":SOUR:FUNC:MODE VOLT", "*OPC?", ":SENS:FUNC 'CURR'", "*OPC?"] + assert resource.buffer == [ + ":SOUR:FUNC:MODE VOLT", + "*OPC?", + ":SENS:FUNC 'CURR'", + "*OPC?", + ] resource.buffer = ["1", "1"] driver.function = driver.FUNCTION_CURRENT - assert resource.buffer == [":SOUR:FUNC:MODE CURR", "*OPC?", ":SENS:FUNC 'VOLT'", "*OPC?"] + assert resource.buffer == [ + ":SOUR:FUNC:MODE CURR", + "*OPC?", + ":SENS:FUNC 'VOLT'", + "*OPC?", + ] def test_measure_voltage(driver, resource): diff --git a/tests/test_driver_keithley_k2470.py b/tests/test_driver_keithley_k2470.py index 480eae2..dbcc5a1 100644 --- a/tests/test_driver_keithley_k2470.py +++ b/tests/test_driver_keithley_k2470.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" @@ -74,11 +74,21 @@ def test_function(driver, resource): resource.buffer = ["1", "1"] driver.function = driver.FUNCTION_VOLTAGE - assert resource.buffer == [":SOUR:FUNC:MODE VOLT", "*OPC?", ":SENS:FUNC 'CURR'", "*OPC?"] + assert resource.buffer == [ + ":SOUR:FUNC:MODE VOLT", + "*OPC?", + ":SENS:FUNC 'CURR'", + "*OPC?", + ] resource.buffer = ["1", "1"] driver.function = driver.FUNCTION_CURRENT - assert resource.buffer == [":SOUR:FUNC:MODE CURR", "*OPC?", ":SENS:FUNC 'VOLT'", "*OPC?"] + assert resource.buffer == [ + ":SOUR:FUNC:MODE CURR", + "*OPC?", + ":SENS:FUNC 'VOLT'", + "*OPC?", + ] def test_measure_voltage(driver, resource): diff --git a/tests/test_driver_keithley_k2657a.py b/tests/test_driver_keithley_k2657a.py index 72a4089..9d8fa8c 100644 --- a/tests/test_driver_keithley_k2657a.py +++ b/tests/test_driver_keithley_k2657a.py @@ -9,67 +9,67 @@ def driver(resource): def test_basic(driver, resource): - resource.buffer = ['Keithley Model 2657A', '1', '1'] - assert driver.identify() == 'Keithley Model 2657A' + resource.buffer = ["Keithley Model 2657A", "1", "1"] + assert driver.identify() == "Keithley Model 2657A" assert driver.reset() is None assert driver.clear() is None - assert resource.buffer == ['*IDN?', '*RST', '*OPC?', '*CLS', '*OPC?'] + assert resource.buffer == ["*IDN?", "*RST", "*OPC?", "*CLS", "*OPC?"] def test_errors(driver, resource): resource.buffer = ['0\t"no error"\t0\t0'] assert driver.next_error() is None - assert resource.buffer == ['print(errorqueue.next())'] + assert resource.buffer == ["print(errorqueue.next())"] resource.buffer = ['42\t"test error"\t0\t0'] error = driver.next_error() assert error.code == 42 - assert error.message == 'test error' + assert error.message == "test error" def test_output(driver, resource): - resource.buffer = ['0'] + resource.buffer = ["0"] assert driver.output == driver.OUTPUT_OFF - assert resource.buffer == ['print(smua.source.output)'] + assert resource.buffer == ["print(smua.source.output)"] - resource.buffer = ['1'] + resource.buffer = ["1"] assert driver.output == driver.OUTPUT_ON - assert resource.buffer == ['print(smua.source.output)'] + assert resource.buffer == ["print(smua.source.output)"] - resource.buffer = ['1'] + resource.buffer = ["1"] driver.output = driver.OUTPUT_OFF - assert resource.buffer == ['smua.source.output = 0', '*OPC?'] + assert resource.buffer == ["smua.source.output = 0", "*OPC?"] - resource.buffer = ['1'] + resource.buffer = ["1"] driver.output = driver.OUTPUT_ON - assert resource.buffer == ['smua.source.output = 1', '*OPC?'] + assert resource.buffer == ["smua.source.output = 1", "*OPC?"] def test_function(driver, resource): - resource.buffer = ['1'] + resource.buffer = ["1"] assert driver.function == driver.FUNCTION_VOLTAGE - assert resource.buffer == ['print(smua.source.func)'] + assert resource.buffer == ["print(smua.source.func)"] - resource.buffer = ['0'] + resource.buffer = ["0"] assert driver.function == driver.FUNCTION_CURRENT - assert resource.buffer == ['print(smua.source.func)'] + assert resource.buffer == ["print(smua.source.func)"] - resource.buffer = ['1'] + resource.buffer = ["1"] driver.function = driver.FUNCTION_VOLTAGE - assert resource.buffer == ['smua.source.func = 1', '*OPC?'] + assert resource.buffer == ["smua.source.func = 1", "*OPC?"] - resource.buffer = ['1'] + resource.buffer = ["1"] driver.function = driver.FUNCTION_CURRENT - assert resource.buffer == ['smua.source.func = 0', '*OPC?'] + assert resource.buffer == ["smua.source.func = 0", "*OPC?"] def test_measure_voltage(driver, resource): - resource.buffer = ['+4.200000E-03'] + resource.buffer = ["+4.200000E-03"] assert driver.measure_voltage() == 4.2e-03 - assert resource.buffer == ['print(smua.measure.v())'] + assert resource.buffer == ["print(smua.measure.v())"] def test_measure_current(driver, resource): - resource.buffer = ['+4.200000E-06'] + resource.buffer = ["+4.200000E-06"] assert driver.measure_current() == 4.2e-06 - assert resource.buffer == ['print(smua.measure.i())'] + assert resource.buffer == ["print(smua.measure.i())"] diff --git a/tests/test_driver_keithley_k2700.py b/tests/test_driver_keithley_k2700.py index 64459c3..95f774c 100644 --- a/tests/test_driver_keithley_k2700.py +++ b/tests/test_driver_keithley_k2700.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" @@ -31,11 +31,21 @@ def test_measure_voltage(driver, resource): resource.buffer = ["+4.200000E-03", "-4.200000E-03"] assert driver.measure_voltage() == +4.2e-03 assert driver.measure_voltage() == -4.2e-03 - assert resource.buffer == [":SENS:FUNC 'VOLT:DC'", ":FORM:ELEM READ", ":READ?", ":READ?"] + assert resource.buffer == [ + ":SENS:FUNC 'VOLT:DC'", + ":FORM:ELEM READ", + ":READ?", + ":READ?", + ] def test_measure_current(driver, resource): resource.buffer = ["+4.200000E-06", "-4.200000E-06"] assert driver.measure_current() == +4.2e-06 assert driver.measure_current() == -4.2e-06 - assert resource.buffer == [":SENS:FUNC 'CURR:DC'", ":FORM:ELEM READ", ":READ?", ":READ?"] + assert resource.buffer == [ + ":SENS:FUNC 'CURR:DC'", + ":FORM:ELEM READ", + ":READ?", + ":READ?", + ] diff --git a/tests/test_driver_keithley_k6510.py b/tests/test_driver_keithley_k6510.py index a13efbd..066f0bc 100644 --- a/tests/test_driver_keithley_k6510.py +++ b/tests/test_driver_keithley_k6510.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" diff --git a/tests/test_driver_keithley_k6514.py b/tests/test_driver_keithley_k6514.py index e530582..389662b 100644 --- a/tests/test_driver_keithley_k6514.py +++ b/tests/test_driver_keithley_k6514.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" diff --git a/tests/test_driver_keithley_k6517b.py b/tests/test_driver_keithley_k6517b.py index c17b6fe..2ecb826 100644 --- a/tests/test_driver_keithley_k6517b.py +++ b/tests/test_driver_keithley_k6517b.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" diff --git a/tests/test_driver_keithley_k707b.py b/tests/test_driver_keithley_k707b.py index 9bac76e..24e5d55 100644 --- a/tests/test_driver_keithley_k707b.py +++ b/tests/test_driver_keithley_k707b.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0\t\"Queue is Empty\"\t0\t0"] + resource.buffer = ['0\t"Queue is Empty"\t0\t0'] assert driver.next_error() is None assert resource.buffer == ["print(errorqueue.next())"] - resource.buffer = ["42\t\"test error\"\t0\t0"] + resource.buffer = ['42\t"test error"\t0\t0'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" @@ -30,28 +30,28 @@ def test_errors(driver, resource): def test_channels(driver, resource): resource.buffer = [""] assert driver.closed_channels == [] - assert resource.buffer == ["print(channel.getclose(\"allslots\"))"] + assert resource.buffer == ['print(channel.getclose("allslots"))'] resource.buffer = ["1B01"] assert driver.closed_channels == ["1B01"] - assert resource.buffer == ["print(channel.getclose(\"allslots\"))"] + assert resource.buffer == ['print(channel.getclose("allslots"))'] resource.buffer = ["1B01;1A02"] assert driver.closed_channels == ["1A02", "1B01"] - assert resource.buffer == ["print(channel.getclose(\"allslots\"))"] + assert resource.buffer == ['print(channel.getclose("allslots"))'] resource.buffer = ["1"] assert driver.close_channels(["1A02"]) is None - assert resource.buffer == ["channel.close(\"1A02\")", "*OPC?"] + assert resource.buffer == ['channel.close("1A02")', "*OPC?"] resource.buffer = ["1"] assert driver.close_channels(["1A02", "1B01"]) is None - assert resource.buffer == ["channel.close(\"1A02,1B01\")", "*OPC?"] + assert resource.buffer == ['channel.close("1A02,1B01")', "*OPC?"] resource.buffer = ["1"] assert driver.open_channels(["1A02", "1B01"]) is None - assert resource.buffer == ["channel.open(\"1A02,1B01\")", "*OPC?"] + assert resource.buffer == ['channel.open("1A02,1B01")', "*OPC?"] resource.buffer = ["1"] assert driver.open_all_channels() is None - assert resource.buffer == ["channel.open(\"allslots\")", "*OPC?"] + assert resource.buffer == ['channel.open("allslots")', "*OPC?"] diff --git a/tests/test_driver_keithley_k708b.py b/tests/test_driver_keithley_k708b.py index 1eb91a7..c5e51b1 100644 --- a/tests/test_driver_keithley_k708b.py +++ b/tests/test_driver_keithley_k708b.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0\t\"Queue is Empty\"\t0\t0"] + resource.buffer = ['0\t"Queue is Empty"\t0\t0'] assert driver.next_error() is None assert resource.buffer == ["print(errorqueue.next())"] - resource.buffer = ["42\t\"test error\"\t0\t0"] + resource.buffer = ['42\t"test error"\t0\t0'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" @@ -30,28 +30,28 @@ def test_errors(driver, resource): def test_channels(driver, resource): resource.buffer = [""] assert driver.closed_channels == [] - assert resource.buffer == ["print(channel.getclose(\"allslots\"))"] + assert resource.buffer == ['print(channel.getclose("allslots"))'] resource.buffer = ["1B01"] assert driver.closed_channels == ["1B01"] - assert resource.buffer == ["print(channel.getclose(\"allslots\"))"] + assert resource.buffer == ['print(channel.getclose("allslots"))'] resource.buffer = ["1B01;1A02"] assert driver.closed_channels == ["1A02", "1B01"] - assert resource.buffer == ["print(channel.getclose(\"allslots\"))"] + assert resource.buffer == ['print(channel.getclose("allslots"))'] resource.buffer = ["1"] assert driver.close_channels(["1A02"]) is None - assert resource.buffer == ["channel.close(\"1A02\")", "*OPC?"] + assert resource.buffer == ['channel.close("1A02")', "*OPC?"] resource.buffer = ["1"] assert driver.close_channels(["1A02", "1B01"]) is None - assert resource.buffer == ["channel.close(\"1A02,1B01\")", "*OPC?"] + assert resource.buffer == ['channel.close("1A02,1B01")', "*OPC?"] resource.buffer = ["1"] assert driver.open_channels(["1A02", "1B01"]) is None - assert resource.buffer == ["channel.open(\"1A02,1B01\")", "*OPC?"] + assert resource.buffer == ['channel.open("1A02,1B01")', "*OPC?"] resource.buffer = ["1"] assert driver.open_all_channels() is None - assert resource.buffer == ["channel.open(\"allslots\")", "*OPC?"] + assert resource.buffer == ['channel.open("allslots")', "*OPC?"] diff --git a/tests/test_driver_keysight_e4980a.py b/tests/test_driver_keysight_e4980a.py index 77c11f0..ac8c385 100644 --- a/tests/test_driver_keysight_e4980a.py +++ b/tests/test_driver_keysight_e4980a.py @@ -17,11 +17,11 @@ def test_basic(driver, resource): def test_errors(driver, resource): - resource.buffer = ["0,\"no error\""] + resource.buffer = ['0,"no error"'] assert driver.next_error() is None assert resource.buffer == [":SYST:ERR:NEXT?"] - resource.buffer = ["42,\"test error\""] + resource.buffer = ['42,"test error"'] error = driver.next_error() assert error.code == 42 assert error.message == "test error" @@ -76,5 +76,5 @@ def test_correction_length(driver, resource): def test_measure_impedance(driver, resource): resource.buffer = ["1.002000E+00,2.004000E-03"] - assert driver.measure_impedance() == (1.002e+0, 2.004e-3) + assert driver.measure_impedance() == (1.002e0, 2.004e-3) assert resource.buffer == [":FETC:IMP:FORM?"] diff --git a/tests/test_driver_nkt_photonics_pilas.py b/tests/test_driver_nkt_photonics_pilas.py index 5e91f8a..ba96b01 100644 --- a/tests/test_driver_nkt_photonics_pilas.py +++ b/tests/test_driver_nkt_photonics_pilas.py @@ -79,7 +79,6 @@ def test_tune(driver, resource): resource.buffer = ["done", "done"] with pytest.raises(ValueError): - driver.tune = 101 diff --git a/tests/test_driver_rohde_schwarz_rtp164.py b/tests/test_driver_rohde_schwarz_rtp164.py index 6e6e6c4..63e9a30 100644 --- a/tests/test_driver_rohde_schwarz_rtp164.py +++ b/tests/test_driver_rohde_schwarz_rtp164.py @@ -28,7 +28,7 @@ def test_clear(driver, resource): def test_error(driver, resource): - resource.buffer = ["0,\"No error\""] + resource.buffer = ['0,"No error"'] assert driver.next_error() is None assert resource.buffer == ["SYST:ERR?"] diff --git a/tests/test_emulator_emulator.py b/tests/test_emulator_emulator.py index 75b7668..9d4449c 100644 --- a/tests/test_emulator_emulator.py +++ b/tests/test_emulator_emulator.py @@ -2,8 +2,8 @@ import pytest +from comet.emulator.emulator import emulator_factory, get_routes from comet.emulator.keithley.k2410 import K2410Emulator -from comet.emulator.emulator import get_routes, emulator_factory def test_emulator_factory(): diff --git a/tests/test_emulator_hephy_environbox.py b/tests/test_emulator_hephy_environbox.py index 1891678..05cad14 100644 --- a/tests/test_emulator_hephy_environbox.py +++ b/tests/test_emulator_hephy_environbox.py @@ -1,13 +1,15 @@ -import pytest import re +import pytest + from comet.emulator.hephy.environbox import EnvironBoxEmulator @pytest.fixture def emulator(): emulator = EnvironBoxEmulator() - emulator.options.update({ + emulator.options.update( + { "box_temperature.min": 24.0, "box_temperature.max": 24.0, "box_humidity.min": 40.0, @@ -16,7 +18,8 @@ def emulator(): "pt100_1.max": 21.5, "pt100_2.min": 22.5, "pt100_2.max": 22.5, - }) + } + ) return emulator @@ -184,7 +187,7 @@ def test_env(emulator): def test_uptime(emulator): - assert re.match(r'^\d\d,\d\d,\d\d,\d\d$', str(emulator("GET:UPTIME ?"))) is not None + assert re.match(r"^\d\d,\d\d,\d\d,\d\d$", str(emulator("GET:UPTIME ?"))) is not None def test_version(emulator): @@ -192,4 +195,7 @@ def test_version(emulator): def test_pc_data(emulator): - assert emulator("GET:PC_DATA ?") == "2,40.0,24.0,9.58,0,30.0,9.2,49.00,0.250000,0.010000,1.230000,1700.00,10.00,1,22.400000,1.250000,3.560000,1,25.50,0,0.00,2.25,1,0,0,0,0,0,0,0,0,1000,0.0,21.50,NAN,100,0,1,0" + assert ( + emulator("GET:PC_DATA ?") + == "2,40.0,24.0,9.58,0,30.0,9.2,49.00,0.250000,0.010000,1.230000,1700.00,10.00,1,22.400000,1.250000,3.560000,1,25.50,0,0.00,2.25,1,0,0,0,0,0,0,0,0,1000,0.0,21.50,NAN,100,0,1,0" + ) diff --git a/tests/test_emulator_itk_corvustt.py b/tests/test_emulator_itk_corvustt.py index 7eab828..74a15e9 100644 --- a/tests/test_emulator_itk_corvustt.py +++ b/tests/test_emulator_itk_corvustt.py @@ -34,9 +34,17 @@ def test_position(emulator): def test_limits(emulator): # getlimit returns three lines - assert emulator("getlimit") == ['0.000000 0.000000', '0.000000 1000000.000000', '100000.000000 25000.000000'] + assert emulator("getlimit") == [ + "0.000000 0.000000", + "0.000000 1000000.000000", + "100000.000000 25000.000000", + ] assert emulator("1 2 3 4 5 6 setlimit") is None - assert emulator("getlimit") == ['1.000000 2.000000', '3.000000 4.000000', '5.000000 6.000000'] + assert emulator("getlimit") == [ + "1.000000 2.000000", + "3.000000 4.000000", + "5.000000 6.000000", + ] def test_calibration(emulator): diff --git a/tests/test_emulator_keithley_k2400.py b/tests/test_emulator_keithley_k2400.py index 61c226d..5a0c81a 100644 --- a/tests/test_emulator_keithley_k2400.py +++ b/tests/test_emulator_keithley_k2400.py @@ -70,7 +70,12 @@ def test_source_range_auto(emulator): def test_source_voltage_protection_level(emulator): - for command in ("SOUR:VOLT:PROT", ":SOUR:VOLT:PROT", "SOUR:VOLT:PROT:LEV", ":SOUR:VOLT:PROT:LEV"): + for command in ( + "SOUR:VOLT:PROT", + ":SOUR:VOLT:PROT", + "SOUR:VOLT:PROT:LEV", + ":SOUR:VOLT:PROT:LEV", + ): assert float(emulator(f"{command}?")) == 210 assert emulator(f"{command} 60") is None assert float(emulator(f"{command}?")) == 60 @@ -79,12 +84,17 @@ def test_source_voltage_protection_level(emulator): def test_sense_voltage_protection_level(emulator): - for command in (":VOLT:PROT", ":SENS:VOLT:PROT", "SENS:VOLT:PROT:LEV", ":SENS:VOLT:PROT:LEV"): - assert float(emulator(f"{command}?")) == 2.1e+1 + for command in ( + ":VOLT:PROT", + ":SENS:VOLT:PROT", + "SENS:VOLT:PROT:LEV", + ":SENS:VOLT:PROT:LEV", + ): + assert float(emulator(f"{command}?")) == 2.1e1 assert emulator(f"{command} 1.5E-3") is None assert float(emulator(f"{command}?")) == 1.5e-3 assert emulator(f"{command} 2.10E+1") is None - assert float(emulator(f"{command}?")) == 2.1e+1 + assert float(emulator(f"{command}?")) == 2.1e1 def test_sense_voltage_protection_tripped(emulator): @@ -93,7 +103,12 @@ def test_sense_voltage_protection_tripped(emulator): def test_sense_current_protection_level(emulator): - for command in (":CURR:PROT", ":SENS:CURR:PROT", "SENS:CURR:PROT:LEV", ":SENS:CURR:PROT:LEV"): + for command in ( + ":CURR:PROT", + ":SENS:CURR:PROT", + "SENS:CURR:PROT:LEV", + ":SENS:CURR:PROT:LEV", + ): assert float(emulator(f"{command}?")) == 1.05e-5 assert emulator(f"{command} 0.0001") is None assert float(emulator(f"{command}?")) == 0.0001 @@ -102,12 +117,16 @@ def test_sense_current_protection_level(emulator): def test_sense_current_protection_tripped(emulator): - for command in (":CURR:PROT:TRIP", ":SENS:CURR:PROT:TRIP", "SENS:CURR:PROT:TRIP", ): + for command in ( + ":CURR:PROT:TRIP", + ":SENS:CURR:PROT:TRIP", + "SENS:CURR:PROT:TRIP", + ): assert float(emulator(f"{command}?")) == 0 def test_sense_function(emulator): for command in (":FUNC", ":FUNC:ON", "SENS:FUNC:ON", ":SENS:FUNC:ON"): - assert emulator(f"{command}?") == "\'CURR:DC\'" - assert emulator(f"{command} \'CURR\'") is None - assert emulator(f"{command}?") == "\'CURR:DC\'" # TODO + assert emulator(f"{command}?") == "'CURR:DC'" + assert emulator(f"{command} 'CURR'") is None + assert emulator(f"{command}?") == "'CURR:DC'" # TODO diff --git a/tests/test_emulator_keithley_k2470.py b/tests/test_emulator_keithley_k2470.py index 8846ade..b510e37 100644 --- a/tests/test_emulator_keithley_k2470.py +++ b/tests/test_emulator_keithley_k2470.py @@ -71,7 +71,12 @@ def test_source_range_auto(emulator): def test_source_voltage_protection_level(emulator): - for command in ("SOUR:VOLT:PROT", ":SOUR:VOLT:PROT", "SOUR:VOLT:PROT:LEV", ":SOUR:VOLT:PROT:LEV"): + for command in ( + "SOUR:VOLT:PROT", + ":SOUR:VOLT:PROT", + "SOUR:VOLT:PROT:LEV", + ":SOUR:VOLT:PROT:LEV", + ): assert float(emulator(f"{command}?")) == 1050 assert emulator(f"{command} 60") is None assert float(emulator(f"{command}?")) == 60 @@ -80,7 +85,12 @@ def test_source_voltage_protection_level(emulator): def test_source_voltage_ilimit_level(emulator): - for command in ("SOUR:VOLT:ILIM", ":SOUR:VOLT:ILIM", "SOUR:VOLT:ILIM:LEV", ":SOUR:VOLT:ILIM:LEV"): + for command in ( + "SOUR:VOLT:ILIM", + ":SOUR:VOLT:ILIM", + "SOUR:VOLT:ILIM:LEV", + ":SOUR:VOLT:ILIM:LEV", + ): assert float(emulator(f"{command}?")) == 1.05e-4 assert emulator(f"{command} 2.1E-5") is None assert float(emulator(f"{command}?")) == 2.1e-5 @@ -89,12 +99,22 @@ def test_source_voltage_ilimit_level(emulator): def test_source_voltage_ilimit_level_tripped(emulator): - for command in ("SOUR:VOLT:ILIM:TRIP", ":SOUR:VOLT:ILIM:TRIP", "SOUR:VOLT:ILIM:LEV:TRIP", ":SOUR:VOLT:ILIM:LEV:TRIP"): + for command in ( + "SOUR:VOLT:ILIM:TRIP", + ":SOUR:VOLT:ILIM:TRIP", + "SOUR:VOLT:ILIM:LEV:TRIP", + ":SOUR:VOLT:ILIM:LEV:TRIP", + ): assert float(emulator(f"{command}?")) == 0 def test_source_current_vlimit_level(emulator): - for command in ("SOUR:CURR:VLIM", ":SOUR:CURR:VLIM", "SOUR:CURR:VLIM:LEV", ":SOUR:CURR:VLIM:LEV"): + for command in ( + "SOUR:CURR:VLIM", + ":SOUR:CURR:VLIM", + "SOUR:CURR:VLIM:LEV", + ":SOUR:CURR:VLIM:LEV", + ): assert float(emulator(f"{command}?")) == 2.1e-1 assert emulator(f"{command} 0.0001") is None assert float(emulator(f"{command}?")) == 0.0001 @@ -103,7 +123,12 @@ def test_source_current_vlimit_level(emulator): def test_source_current_vlimit_level_tripped(emulator): - for command in ("SOUR:CURR:VLIM:TRIP", ":SOUR:CURR:VLIM:TRIP", "SOUR:CURR:VLIM:LEV:TRIP", ":SOUR:CURR:VLIM:LEV:TRIP"): + for command in ( + "SOUR:CURR:VLIM:TRIP", + ":SOUR:CURR:VLIM:TRIP", + "SOUR:CURR:VLIM:LEV:TRIP", + ":SOUR:CURR:VLIM:LEV:TRIP", + ): assert float(emulator(f"{command}?")) == 0 @@ -121,16 +146,16 @@ def test_read(emulator): def test_read_elements(emulator): - result = emulator(":READ? \"defbuffer1\", SOUR, READ") + result = emulator(':READ? "defbuffer1", SOUR, READ') sour, read = str(result).split(",") assert float(sour), float(read) def test_trace_trigger(emulator): - assert emulator(":TRAC:TRIG \"defbuffer1\"") is None + assert emulator(':TRAC:TRIG "defbuffer1"') is None def test_trace_data(emulator): - result = emulator(":TRAC:DATA? 1, 1, \"defbuffer1\", SOUR, READ") + result = emulator(':TRAC:DATA? 1, 1, "defbuffer1", SOUR, READ') sour, read = str(result).split(",") assert float(sour), float(read) diff --git a/tests/test_emulator_keithley_k2657a.py b/tests/test_emulator_keithley_k2657a.py index 97f6207..d862839 100644 --- a/tests/test_emulator_keithley_k2657a.py +++ b/tests/test_emulator_keithley_k2657a.py @@ -51,7 +51,7 @@ def test_source_levelv(emulator): def test_source_leveli(emulator): assert float(emulator("print(smua.source.leveli)")) == 0 assert emulator("smua.source.leveli = 2.5E-6") is None - assert float(emulator("print(smua.source.leveli)")) == 2.5E-6 + assert float(emulator("print(smua.source.leveli)")) == 2.5e-6 assert emulator("smua.source.leveli = 0") is None assert float(emulator("print(smua.source.leveli)")) == 0 @@ -67,7 +67,7 @@ def test_source_rangev(emulator): def test_source_rangei(emulator): assert float(emulator("print(smua.source.rangei)")) == 0 assert emulator("smua.source.rangei = 2.0E-3") is None - assert float(emulator("print(smua.source.rangei)")) == 2.0E-3 + assert float(emulator("print(smua.source.rangei)")) == 2.0e-3 assert emulator("smua.source.rangei = 0") is None assert float(emulator("print(smua.source.rangei)")) == 0 diff --git a/tests/test_emulator_keithley_k2700.py b/tests/test_emulator_keithley_k2700.py index 7449f42..70f7ed1 100644 --- a/tests/test_emulator_keithley_k2700.py +++ b/tests/test_emulator_keithley_k2700.py @@ -19,7 +19,7 @@ def test_format_elements(emulator): assert emulator(":FORM:ELEM READ") is None assert emulator(":FORM:ELEM?") == "READ,,,,," assert emulator(":FORM:ELEM CHAN, CHAN ,UNIT") is None - assert emulator(":FORM:ELEM?") == ",CHAN,UNIT,,," + assert emulator(":FORM:ELEM?") == ",CHAN,UNIT,,," assert emulator(":FORM:ELEM CHAN,TST,READ,UNIT,LIM,LIM,RNUM") is None assert emulator(":FORM:ELEM?") == "READ,CHAN,UNIT,RNUM,TST,LIM" diff --git a/tests/test_emulator_keithley_k4215cvu.py b/tests/test_emulator_keithley_k4215cvu.py index 262fdd7..039d2cc 100644 --- a/tests/test_emulator_keithley_k4215cvu.py +++ b/tests/test_emulator_keithley_k4215cvu.py @@ -1,4 +1,5 @@ import math + import pytest from comet.emulator.keithley.k4215cvu import K4215CVUEmulator @@ -57,12 +58,15 @@ def test_measz_returns_two_floats_output_on(emulator): assert_two_floats_csv(s) -@pytest.mark.parametrize("cmd,val,query,default", [ - (":CVU:ACV {}", "1.000000E-01", ":CVU:ACV?", 0.1), - (":CVU:FREQ {}", "100000", ":CVU:FREQ?", 100000), - (":CVU:DCV {}", "0.000E+00", ":CVU:DCV?", 0.0), - (":CVU:DCV:OFFSET {}", "0.000E+00", ":CVU:DCV:OFFSET?", 0.0), -]) +@pytest.mark.parametrize( + "cmd,val,query,default", + [ + (":CVU:ACV {}", "1.000000E-01", ":CVU:ACV?", 0.1), + (":CVU:FREQ {}", "100000", ":CVU:FREQ?", 100000), + (":CVU:DCV {}", "0.000E+00", ":CVU:DCV?", 0.0), + (":CVU:DCV:OFFSET {}", "0.000E+00", ":CVU:DCV:OFFSET?", 0.0), + ], +) def test_getters_setters_roundtrip_common(emulator, cmd, val, query, default): # check getter exists and returns something parseable assert emulator(query) == val diff --git a/tests/test_emulator_keithley_k6510.py b/tests/test_emulator_keithley_k6510.py index c0ef194..2d7ab5b 100644 --- a/tests/test_emulator_keithley_k6510.py +++ b/tests/test_emulator_keithley_k6510.py @@ -9,7 +9,9 @@ def emulator(): def test_basic(emulator): - assert emulator("*IDN?") == "Keithley Inc., Model DAQ6510, 54313645, v1.0 (Emulator)" + assert ( + emulator("*IDN?") == "Keithley Inc., Model DAQ6510, 54313645, v1.0 (Emulator)" + ) assert emulator("*RST") is None assert emulator("*OPC?") == "1" diff --git a/tests/test_emulator_keithley_k6517b.py b/tests/test_emulator_keithley_k6517b.py index 4ab4108..b0bc422 100644 --- a/tests/test_emulator_keithley_k6517b.py +++ b/tests/test_emulator_keithley_k6517b.py @@ -28,7 +28,12 @@ def test_output_state(emulator): def test_source_voltage_level_immediate_amplitude(emulator): - for command in ("SOUR:VOLT", ":SOUR:VOLT", ":SOUR:VOLT:LEV", ":SOUR:VOLT:LEV:IMM:AMPL"): + for command in ( + "SOUR:VOLT", + ":SOUR:VOLT", + ":SOUR:VOLT:LEV", + ":SOUR:VOLT:LEV:IMM:AMPL", + ): assert emulator(f"{command}?") == format(0, "E") assert emulator(f"{command} 42.5") is None assert emulator(f"{command}?") == format(42.5, "E") @@ -39,9 +44,9 @@ def test_source_voltage_level_immediate_amplitude(emulator): def test_source_voltage_range(emulator): for command in ("SOUR:VOLT:RANG", ":SOUR:VOLT:RANG"): assert emulator(f"{command}?") == format(100, "E") - assert emulator(f"{command} 101")is None + assert emulator(f"{command} 101") is None assert emulator(f"{command}?") == format(1000, "E") - assert emulator(f"{command} 42")is None + assert emulator(f"{command} 42") is None assert emulator(f"{command}?") == format(100, "E") diff --git a/tests/test_emulator_keithley_k707b.py b/tests/test_emulator_keithley_k707b.py index 2cffa9a..2ede7da 100644 --- a/tests/test_emulator_keithley_k707b.py +++ b/tests/test_emulator_keithley_k707b.py @@ -10,8 +10,18 @@ def emulator(): def test_constants(emulator): channels = [ - "1A01", "1A02", "1A03", "1A04", "1A05", "1A06", - "1A07", "1A08", "1A09", "1A10", "1A11", "1A12", + "1A01", + "1A02", + "1A03", + "1A04", + "1A05", + "1A06", + "1A07", + "1A08", + "1A09", + "1A10", + "1A11", + "1A12", ] assert emulator.CHANNELS == channels @@ -22,17 +32,17 @@ def test_basic(emulator): assert emulator("*CLS") is None assert emulator("*OPC?") == "1" assert emulator("print(errorqueue.count)") == "0" - assert emulator("print(errorqueue.next())") == "0\t\"Queue is Empty\"\t0\t0" + assert emulator("print(errorqueue.next())") == '0\t"Queue is Empty"\t0\t0' def test_channels(emulator): - assert emulator("print(channel.getclose(\"allslots\"))") == "nil" + assert emulator('print(channel.getclose("allslots"))') == "nil" assert emulator("print(errorqueue.count)") == "0" - assert emulator("channel.close(\"1A01,1A07\")") is None + assert emulator('channel.close("1A01,1A07")') is None assert emulator("print(errorqueue.count)") == "0" - assert emulator("print(channel.getclose(\"allslots\"))") == "1A01;1A07" + assert emulator('print(channel.getclose("allslots"))') == "1A01;1A07" assert emulator("print(errorqueue.count)") == "0" - assert emulator("channel.open(\"allslots\")") is None + assert emulator('channel.open("allslots")') is None assert emulator("print(errorqueue.count)") == "0" - assert emulator("print(channel.getclose(\"allslots\"))") == "nil" + assert emulator('print(channel.getclose("allslots"))') == "nil" assert emulator("print(errorqueue.count)") == "0" diff --git a/tests/test_emulator_keithley_k708b.py b/tests/test_emulator_keithley_k708b.py index a47cade..91f3051 100644 --- a/tests/test_emulator_keithley_k708b.py +++ b/tests/test_emulator_keithley_k708b.py @@ -10,14 +10,70 @@ def emulator(): def test_constants(emulator): channels = [ - "1A01", "1A02", "1A03", "1A04", "1A05", "1A06", "1A07", "1A08", - "1B01", "1B02", "1B03", "1B04", "1B05", "1B06", "1B07", "1B08", - "1C01", "1C02", "1C03", "1C04", "1C05", "1C06", "1C07", "1C08", - "1D01", "1D02", "1D03", "1D04", "1D05", "1D06", "1D07", "1D08", - "1E01", "1E02", "1E03", "1E04", "1E05", "1E06", "1E07", "1E08", - "1F01", "1F02", "1F03", "1F04", "1F05", "1F06", "1F07", "1F08", - "1G01", "1G02", "1G03", "1G04", "1G05", "1G06", "1G07", "1G08", - "1H01", "1H02", "1H03", "1H04", "1H05", "1H06", "1H07", "1H08", + "1A01", + "1A02", + "1A03", + "1A04", + "1A05", + "1A06", + "1A07", + "1A08", + "1B01", + "1B02", + "1B03", + "1B04", + "1B05", + "1B06", + "1B07", + "1B08", + "1C01", + "1C02", + "1C03", + "1C04", + "1C05", + "1C06", + "1C07", + "1C08", + "1D01", + "1D02", + "1D03", + "1D04", + "1D05", + "1D06", + "1D07", + "1D08", + "1E01", + "1E02", + "1E03", + "1E04", + "1E05", + "1E06", + "1E07", + "1E08", + "1F01", + "1F02", + "1F03", + "1F04", + "1F05", + "1F06", + "1F07", + "1F08", + "1G01", + "1G02", + "1G03", + "1G04", + "1G05", + "1G06", + "1G07", + "1G08", + "1H01", + "1H02", + "1H03", + "1H04", + "1H05", + "1H06", + "1H07", + "1H08", ] assert emulator.CHANNELS == channels diff --git a/tests/test_emulator_keysight_e4980a.py b/tests/test_emulator_keysight_e4980a.py index 6b49d0e..a5cd4e0 100644 --- a/tests/test_emulator_keysight_e4980a.py +++ b/tests/test_emulator_keysight_e4980a.py @@ -15,9 +15,9 @@ def test_basic(emulator): def test_error(emulator): - assert emulator(":SYST:ERR?") == "+0,\"No error\"" + assert emulator(":SYST:ERR?") == '+0,"No error"' assert emulator("SHRUBBERY?") is None - assert emulator(":SYST:ERR?") == "-113,\"Undefined header\"" + assert emulator(":SYST:ERR?") == '-113,"Undefined header"' def test_correction_method(emulator): @@ -32,10 +32,19 @@ def test_fetch_impedance_format(emulator): def get_types(result): tokens = result.split(",") assert len(tokens) == 3 - a, b, c = tokens - float(a), float(b) + a, b, _ = tokens + a = float(a) + b = float(b) return True - for command in ("FETC", ":FETC", "FETCH:FORM", ":FETCH:FORM", "FETCH:IMP:FORM", ":FETCH:IMP:FORM"): + + for command in ( + "FETC", + ":FETC", + "FETCH:FORM", + ":FETCH:FORM", + "FETCH:IMP:FORM", + ":FETCH:IMP:FORM", + ): assert get_types(str(emulator(f"{command}?"))) diff --git a/tests/test_emulator_marzhauser_tango.py b/tests/test_emulator_marzhauser_tango.py index fe7b027..d35cfd8 100644 --- a/tests/test_emulator_marzhauser_tango.py +++ b/tests/test_emulator_marzhauser_tango.py @@ -11,7 +11,10 @@ def emulator(): def test_basic(emulator): - assert emulator("?version") == "TANGO-MINI3-EMULATOR, Version 1.00, Mar 11 2022, 13:51:01" + assert ( + emulator("?version") + == "TANGO-MINI3-EMULATOR, Version 1.00, Mar 11 2022, 13:51:01" + ) assert emulator("?pos") == "0.000 0.000 0.000" assert emulator("?pos x") == "0.000" assert emulator("?pos y") == "0.000" diff --git a/tests/test_emulator_resource.py b/tests/test_emulator_resource.py index b2ed584..d14e327 100644 --- a/tests/test_emulator_resource.py +++ b/tests/test_emulator_resource.py @@ -4,7 +4,9 @@ def test_resource(): with open_emulator("urn:comet:model:keithley:2410") as res: assert res.encoding == "ascii" - assert res.query("*IDN?") == "Keithley Inc., Model 2410, 43768438, v1.0 (Emulator)" + assert ( + res.query("*IDN?") == "Keithley Inc., Model 2410, 43768438, v1.0 (Emulator)" + ) def test_resource_latin1(): diff --git a/tests/test_emulator_response.py b/tests/test_emulator_response.py index 1cd84f1..1af1dde 100644 --- a/tests/test_emulator_response.py +++ b/tests/test_emulator_response.py @@ -1,9 +1,9 @@ import numpy as np from comet.emulator.response import ( - TextResponse, BinaryResponse, RawResponse, + TextResponse, make_response, ) @@ -12,7 +12,7 @@ def test_text_response(): res = TextResponse("Ni!") assert res == "Ni!" assert res == TextResponse("Ni!") - assert not res == TextResponse("spam") + assert res != TextResponse("spam") assert res.text == "Ni!" assert res.encoding == "ascii" assert bytes(res) == "Ni!".encode("ascii") @@ -31,18 +31,18 @@ def test_text_response_latin1(): def test_text_response_utf8(): res = TextResponse("blancmangé", encoding="utf-8") assert res == "blancmangé" - assert not res == TextResponse("blancmangé") + assert res != TextResponse("blancmangé") assert res == TextResponse("blancmangé", encoding="utf-8") assert res.text == "blancmangé" assert res.encoding == "utf-8" - assert bytes(res) == "blancmangé".encode("utf-8") + assert bytes(res) == "blancmangé".encode("utf-8") # noqa: UP012 def test_binary_response(): res = BinaryResponse("shrubbery".encode("ascii")) assert res == "#19shrubbery".encode("ascii") assert res == BinaryResponse("shrubbery".encode("ascii")) - assert not res == BinaryResponse("spam".encode("ascii")) + assert res != BinaryResponse("spam".encode("ascii")) assert res.data == "shrubbery".encode("ascii") assert bytes(res) == "#19shrubbery".encode("ascii") @@ -91,7 +91,7 @@ def test_raw_response(): res = RawResponse("spam".encode("ascii")) assert res == "spam".encode("ascii") assert res == RawResponse("spam".encode("ascii")) - assert not res == RawResponse("wibble".encode("ascii")) + assert res != RawResponse("wibble".encode("ascii")) assert res.data == "spam".encode("ascii") assert bytes(res) == "spam".encode("ascii") diff --git a/tests/test_emulator_rohde_schwarz_nge100.py b/tests/test_emulator_rohde_schwarz_nge100.py index 1a09049..6502571 100644 --- a/tests/test_emulator_rohde_schwarz_nge100.py +++ b/tests/test_emulator_rohde_schwarz_nge100.py @@ -15,15 +15,15 @@ def test_identify(emulator): def test_channel_selection(emulator): for channel in range(3): - emulator(f"INSTrument {channel+1}") - assert emulator("INSTrument?") == f"{channel+1}" + emulator(f"INSTrument {channel + 1}") + assert emulator("INSTrument?") == f"{channel + 1}" def test_initialization(emulator): for channel in range(3): - emulator(f"INSTrument {channel+1}") - assert emulator("INSTrument?") == f"{channel+1}" + emulator(f"INSTrument {channel + 1}") + assert emulator("INSTrument?") == f"{channel + 1}" assert emulator("OUTPut?") == "0" assert emulator("VOLTage?") == "0.0" assert emulator("CURRent?") == "0.0" @@ -32,17 +32,17 @@ def test_initialization(emulator): def test_enable_channel(emulator): for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("OUTPut 1") assert emulator("OUTPut?") == "1" for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") assert emulator("VOLT?") == "0.0" assert emulator("CURR?") == "0.0" for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("OUTPut 0") assert emulator("OUTPut?") == "0" @@ -50,7 +50,7 @@ def test_enable_channel(emulator): def test_set_voltage(emulator): for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("VOLTage 1.0") assert emulator("VOLTage?") == "1.0" @@ -58,7 +58,7 @@ def test_set_voltage(emulator): def test_set_current(emulator): for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("CURRent 2.0") assert emulator("CURRent?") == "2.0" @@ -66,12 +66,12 @@ def test_set_current(emulator): def test_set_output(emulator): for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("OUTPut 1") assert emulator("OUTPut?") == "1" for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("OUTPut 0") assert emulator("OUTPut?") == "0" @@ -81,7 +81,7 @@ def test_set_voltage_limit(emulator): expected_current = [1.0, 0.001, 0.0] for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("CURR 1.0") emulator("OUTPut 1") emulator("VOLTage 1.0") @@ -91,7 +91,7 @@ def test_set_voltage_limit(emulator): assert emulator("VOLT?") == "1.0" for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") assert emulator("MEAS:VOLT?") == "1.0" assert emulator("MEAS:CURR?") == f"{expected_current[channel]}" @@ -104,7 +104,7 @@ def test_set_current_limit(emulator): voltages_expected = ["0.001", "1.0", "1.0"] for channel in range(3): - emulator(f"INSTrument {channel+1}") + emulator(f"INSTrument {channel + 1}") emulator("VOLTage 1.0") emulator(f"CURR {current_limit}") emulator("OUTPut 1") diff --git a/tests/test_emulator_rohde_schwarz_rtp164.py b/tests/test_emulator_rohde_schwarz_rtp164.py index bfdee49..eb03dfd 100644 --- a/tests/test_emulator_rohde_schwarz_rtp164.py +++ b/tests/test_emulator_rohde_schwarz_rtp164.py @@ -14,7 +14,7 @@ def test_identify(emulator): def test_error(emulator): assert emulator("SHRUBBERY?") is None - assert emulator("SYST:ERR?") == "-113,\"Undefined header\"" + assert emulator("SYST:ERR?") == '-113,"Undefined header"' def test_channe_waveform_data_header(emulator): diff --git a/tests/test_emulator_tcpserver.py b/tests/test_emulator_tcpserver.py index 253d824..b2e9024 100644 --- a/tests/test_emulator_tcpserver.py +++ b/tests/test_emulator_tcpserver.py @@ -1,10 +1,16 @@ import argparse +import asyncio +import inspect import logging import types +from dataclasses import dataclass +from typing import cast import pytest -from comet.emulator import tcpserver +from comet.emulator import Emulator, tcpserver +from comet.emulator.response import Response, TextResponse +from comet.emulator.tcpserver import TCPRequestHandler, TCPServer, TCPServerContext class FakeResponse: @@ -75,12 +81,12 @@ def patch_response(monkeypatch): @pytest.mark.asyncio async def test_read_messages_returns_none_on_eof(): - handler = tcpserver.TCPRequestHandler() - reader = FakeStreamReader([b""]) + handler = TCPRequestHandler() + reader = cast(asyncio.StreamReader, FakeStreamReader([b""])) context = DummyContext() rx_buffer = bytearray() - result = await handler.read_messages(reader, context, rx_buffer) + result = await handler.read_messages(reader, context, rx_buffer) # type: ignore assert result is None assert rx_buffer == bytearray() @@ -89,12 +95,12 @@ async def test_read_messages_returns_none_on_eof(): @pytest.mark.asyncio async def test_read_messages_splits_complete_messages(): - handler = tcpserver.TCPRequestHandler() - reader = FakeStreamReader([b"CMD1\nCMD2\n"]) + handler = TCPRequestHandler() + reader = cast(asyncio.StreamReader, FakeStreamReader([b"CMD1\nCMD2\n"])) context = DummyContext(termination=b"\n") rx_buffer = bytearray() - result = await handler.read_messages(reader, context, rx_buffer) + result = await handler.read_messages(reader, context, rx_buffer) # type: ignore assert result == [b"CMD1", b"CMD2"] assert rx_buffer == bytearray() @@ -103,12 +109,12 @@ async def test_read_messages_splits_complete_messages(): @pytest.mark.asyncio async def test_read_messages_preserves_partial_frame(): - handler = tcpserver.TCPRequestHandler() - context = DummyContext(termination=b"\r\n") + handler = TCPRequestHandler() + context = cast(TCPServerContext, DummyContext(termination=b"\r\n")) rx_buffer = bytearray() result1 = await handler.read_messages( - FakeStreamReader([b"ONE\r\nTWO\r"]), + cast(asyncio.StreamReader, FakeStreamReader([b"ONE\r\nTWO\r"])), context, rx_buffer, ) @@ -116,7 +122,7 @@ async def test_read_messages_preserves_partial_frame(): assert rx_buffer == bytearray(b"TWO\r") result2 = await handler.read_messages( - FakeStreamReader([b"\nTHREE\r\n"]), + cast(asyncio.StreamReader, FakeStreamReader([b"\nTHREE\r\n"])), context, rx_buffer, ) @@ -126,11 +132,11 @@ async def test_read_messages_preserves_partial_frame(): @pytest.mark.asyncio async def test_send_messages_single_response(patch_response): - handler = tcpserver.TCPRequestHandler() + handler = TCPRequestHandler() writer = FakeStreamWriter() context = DummyContext(termination=b"\n") - await handler.send_messages(writer, context, FakeResponse(b"OK")) + await handler.send_messages(writer, context, FakeResponse(b"OK")) # type: ignore assert writer.writes == [b"OK\n"] assert writer.drained == 1 @@ -139,12 +145,12 @@ async def test_send_messages_single_response(patch_response): @pytest.mark.asyncio async def test_send_messages_multiple_responses(patch_response): - handler = tcpserver.TCPRequestHandler() + handler = TCPRequestHandler() writer = FakeStreamWriter() context = DummyContext(termination=b"\r\n") responses = [FakeResponse(b"A"), FakeResponse(b"B")] - await handler.send_messages(writer, context, responses) + await handler.send_messages(writer, context, responses) # type: ignore assert writer.writes == [b"A\r\nB\r\n"] assert writer.drained == 1 @@ -152,18 +158,22 @@ async def test_send_messages_multiple_responses(patch_response): @pytest.mark.asyncio -async def test_handle_reads_dispatches_sends_and_closes_writer(monkeypatch, patch_response): - handler = tcpserver.TCPRequestHandler() +async def test_handle_reads_dispatches_sends_and_closes_writer( + monkeypatch, patch_response +): + handler = TCPRequestHandler() context = DummyContext(handle_result=FakeResponse(b"RSP")) writer = FakeStreamWriter() + results = iter( + [ + [b"PING", b"PONG"], + None, + ] + ) + async def fake_read_messages(reader, context, rx_buffer): - if not hasattr(fake_read_messages, "count"): - fake_read_messages.count = 0 - fake_read_messages.count += 1 - if fake_read_messages.count == 1: - return [b"PING", b"PONG"] - return None + return next(results) sent = [] @@ -173,7 +183,7 @@ async def fake_send_messages(writer, context, response): monkeypatch.setattr(handler, "read_messages", fake_read_messages) monkeypatch.setattr(handler, "send_messages", fake_send_messages) - await handler.handle(object(), writer, context) + await handler.handle(object(), writer, context) # type: ignore assert context.messages == ["PING", "PONG"] assert sent == [b"RSP", b"RSP"] @@ -183,7 +193,7 @@ async def fake_send_messages(writer, context, response): @pytest.mark.asyncio async def test_handle_closes_writer_even_if_handle_message_raises(): - handler = tcpserver.TCPRequestHandler() + handler = TCPRequestHandler() writer = FakeStreamWriter() class ExplodingContext(DummyContext): @@ -198,7 +208,7 @@ async def fake_read_messages(reader, context, rx_buffer): handler.read_messages = fake_read_messages with pytest.raises(RuntimeError, match="boom"): - await handler.handle(object(), writer, context) + await handler.handle(object(), writer, context) # type: ignore assert writer.closed is True assert writer.wait_closed_called is True @@ -211,25 +221,26 @@ async def test_context_handle_message_waits_only_when_response(monkeypatch): async def fake_sleep(delay): sleep_calls.append(delay) - monkeypatch.setattr(tcpserver.asyncio, "sleep", fake_sleep) + monkeypatch.setattr(asyncio, "sleep", fake_sleep) - class DummyEmulator: - def __init__(self, response): + class TestEmulator(Emulator): + def __init__(self, response: Response | list[Response] | None): + super().__init__() self.response = response - def __call__(self, message): + def __call__(self, message: str) -> Response | list[Response] | None: return self.response - ctx1 = tcpserver.TCPServerContext( + ctx1 = TCPServerContext( name="x", - emulator=DummyEmulator("ok"), + emulator=TestEmulator(TextResponse("ok")), termination=b"\n", request_delay=0.25, logger=logging.getLogger("test1"), ) - ctx2 = tcpserver.TCPServerContext( + ctx2 = TCPServerContext( name="x", - emulator=DummyEmulator(None), + emulator=TestEmulator(None), termination=b"\n", request_delay=0.25, logger=logging.getLogger("test2"), @@ -243,43 +254,44 @@ def __call__(self, message): def test_server_address_returns_configured_address_before_start(): - ctx = DummyContext() - server = tcpserver.TCPServer(("127.0.0.1", 5555), ctx) + ctx = cast(TCPServerContext, DummyContext()) + server = TCPServer(("127.0.0.1", 5555), ctx) assert server.server_address == ("127.0.0.1", 5555) def test_server_address_returns_bound_socket_address_after_start(): - ctx = DummyContext() - server = tcpserver.TCPServer(("127.0.0.1", 5555), ctx) + ctx = cast(TCPServerContext, DummyContext()) + server = TCPServer(("127.0.0.1", 5555), ctx) class DummySocket: def getsockname(self): return ("127.0.0.1", 6000, "ignored") - server._server = types.SimpleNamespace(sockets=[DummySocket()]) + server._server = types.SimpleNamespace(sockets=[DummySocket()]) # type: ignore assert server.server_address == ("127.0.0.1", 6000) @pytest.mark.asyncio async def test_server_start_uses_asyncio_start_server(monkeypatch): - ctx = DummyContext() - server = tcpserver.TCPServer(("localhost", 1234), ctx) + ctx = cast(TCPServerContext, DummyContext()) + server = TCPServer(("localhost", 1234), ctx) created = {} + @dataclass class DummyAsyncServer: - sockets = [] + sockets: list async def fake_start_server(callback, host, port, reuse_address): created["callback"] = callback created["host"] = host created["port"] = port created["reuse_address"] = reuse_address - return DummyAsyncServer() + return DummyAsyncServer([]) - monkeypatch.setattr(tcpserver.asyncio, "start_server", fake_start_server) + monkeypatch.setattr(asyncio, "start_server", fake_start_server) await server.start() @@ -293,7 +305,7 @@ async def fake_start_server(callback, host, port, reuse_address): @pytest.mark.asyncio async def test_server_shutdown_closes_server(): ctx = DummyContext() - server = tcpserver.TCPServer(("localhost", 1234), ctx) + server = TCPServer(("localhost", 1234), cast(TCPServerContext, ctx)) class DummyAsyncServer: def __init__(self): @@ -307,7 +319,7 @@ async def wait_closed(self): self.wait_closed_called = True dummy = DummyAsyncServer() - server._server = dummy + server._server = dummy # type: ignore await server.shutdown() @@ -326,13 +338,17 @@ def test_option_type_rejects_invalid_key(): def test_parse_args_defaults(monkeypatch): - monkeypatch.setattr(tcpserver.argparse.ArgumentParser, "parse_args", lambda self: argparse.Namespace( - host="localhost", - port=10000, - termination="\n", - request_delay=0.1, - option=[], - )) + monkeypatch.setattr( + argparse.ArgumentParser, + "parse_args", + lambda self: argparse.Namespace( + host="localhost", + port=10000, + termination="\n", + request_delay=0.1, + option=[], + ), + ) args = tcpserver.parse_args() @@ -345,7 +361,7 @@ def test_parse_args_defaults(monkeypatch): def test_run_rejects_non_emulator(): with pytest.raises(TypeError, match="Emulator must inherit from"): - tcpserver.run(object()) + tcpserver.run(object()) # type: ignore def test_run_configures_options_and_returns_zero(monkeypatch): @@ -369,7 +385,13 @@ def __init__(self): ), ) - monkeypatch.setattr(tcpserver.inspect, "getmodule", lambda cls: types.SimpleNamespace(__spec__=types.SimpleNamespace(name="pkg.tcpserver"))) + monkeypatch.setattr( + inspect, + "getmodule", + lambda cls: types.SimpleNamespace( + __spec__=types.SimpleNamespace(name="pkg.tcpserver") + ), + ) ran = {} @@ -377,9 +399,9 @@ def fake_asyncio_run(coro): ran["called"] = True coro.close() - monkeypatch.setattr(tcpserver.asyncio, "run", fake_asyncio_run) + monkeypatch.setattr(asyncio, "run", fake_asyncio_run) - rc = tcpserver.run(emulator) + rc = tcpserver.run(cast(Emulator, emulator)) assert rc == 0 assert ran["called"] is True diff --git a/tests/test_estimate.py b/tests/test_estimate.py index 9965ef5..266fddd 100644 --- a/tests/test_estimate.py +++ b/tests/test_estimate.py @@ -2,9 +2,11 @@ from comet.estimate import Estimate + class FakeClock: def __init__(self): self.t = 0.0 + def __call__(self): self.t += 1.0 return self.t diff --git a/tests/test_functions.py b/tests/test_functions.py index 894ce53..71878de 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -35,22 +35,22 @@ def test_linear_range(): assert_linear_range(0, 0, 1, []) assert_linear_range(0, 1, 2, [0, 1]) assert_linear_range(0, 1, 1, [0, 1]) - assert_linear_range(1, 0, 1, [1, 0]) # auto step + assert_linear_range(1, 0, 1, [1, 0]) # auto step assert_linear_range(1, 1, 1, []) assert_linear_range(0, 0, 1, []) - assert_linear_range(0, -1, 1, [0, -1]) # auto step + assert_linear_range(0, -1, 1, [0, -1]) # auto step assert_linear_range(-1, 0, 1, [-1, 0]) assert_linear_range(-1, -1, 1, []) assert_linear_range(0, 0, -1, []) - assert_linear_range(0, 1, -1, [0, 1]) # auto step + assert_linear_range(0, 1, -1, [0, 1]) # auto step assert_linear_range(1, 0, -1, [1, 0]) assert_linear_range(1, 1, -1, []) assert_linear_range(0, 0, -1, []) assert_linear_range(0, -1, -1, [0, -1]) - assert_linear_range(-1, 0, -1, [-1, 0]) # auto step + assert_linear_range(-1, 0, -1, [-1, 0]) # auto step assert_linear_range(-1, -1, -1, []) assert_linear_range(0, 0, 0, []) @@ -88,29 +88,39 @@ def test_linear_range(): assert_linear_range(2.5, -2.5, 2.5, [2.5, 0, -2.5]) # auto step assert_linear_range(2.5, -2.5, -2.5, [2.5, 0, -2.5]) - assert_linear_range(0, 1.001e-15, 2e-16, [0, 2e-16, 4e-16, 6e-16, 8e-16, 1e-15, 1.001e-15]) # jump - assert_linear_range(1e-15, 0, -2e-16, [1e-15, 8e-16, 6e-16, 4e-16, 2e-16, 0]) # auto step - assert_linear_range(0, 1, 0.1, [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) - - assert_linear_range(-2.5e-12, 2.5e-12, -2.5e-12, [-2.5e-12, 0, 2.5e-12]) # auto step + assert_linear_range( + 0, 1.001e-15, 2e-16, [0, 2e-16, 4e-16, 6e-16, 8e-16, 1e-15, 1.001e-15] + ) # jump + assert_linear_range( + 1e-15, 0, -2e-16, [1e-15, 8e-16, 6e-16, 4e-16, 2e-16, 0] + ) # auto step + assert_linear_range( + 0, 1, 0.1, [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + ) + + assert_linear_range( + -2.5e-12, 2.5e-12, -2.5e-12, [-2.5e-12, 0, 2.5e-12] + ) # auto step assert_linear_range(-2.5e-12, 2.5e-12, 2.5e-12, [-2.5e-12, 0, 2.5e-12]) assert_linear_range(2.5e-12, -2.5e-12, 2.5e-12, [2.5e-12, 0, -2.5e-12]) # auto step assert_linear_range(2.5e-12, -2.5e-12, -2.5e-12, [2.5e-12, 0, -2.5e-12]) - assert_linear_range(-2.5e+12, 2.5e+12, -2.5e+12, [-2.5e+12, 0, 2.5e+12]) # auto step - assert_linear_range(-2.5e+12, 2.5e+12, 2.5e+12, [-2.5e+12, 0, 2.5e+12]) - assert_linear_range(2.5e+12, -2.5e+12, 2.5e+12, [2.5e+12, 0, -2.5e+12]) # auto step - assert_linear_range(2.5e+12, -2.5e+12, -2.5e+12, [2.5e+12, 0, -2.5e+12]) + assert_linear_range(-2.5e12, 2.5e12, -2.5e12, [-2.5e12, 0, 2.5e12]) # auto step + assert_linear_range(-2.5e12, 2.5e12, 2.5e12, [-2.5e12, 0, 2.5e12]) + assert_linear_range(2.5e12, -2.5e12, 2.5e12, [2.5e12, 0, -2.5e12]) # auto step + assert_linear_range(2.5e12, -2.5e12, -2.5e12, [2.5e12, 0, -2.5e12]) - assert_linear_range(-2.5e-24, 2.5e-24, -2.5e-24, [-2.5e-24, 0, 2.5e-24]) # auto step + assert_linear_range( + -2.5e-24, 2.5e-24, -2.5e-24, [-2.5e-24, 0, 2.5e-24] + ) # auto step assert_linear_range(-2.5e-24, 2.5e-24, 2.5e-24, [-2.5e-24, 0, 2.5e-24]) assert_linear_range(2.5e-24, -2.5e-24, 2.5e-24, [2.5e-24, 0, -2.5e-24]) # auto step assert_linear_range(2.5e-24, -2.5e-24, -2.5e-24, [2.5e-24, 0, -2.5e-24]) - assert_linear_range(-2.5e+24, 2.5e+24, -2.5e+24, [-2.5e+24, 0, 2.5e+24]) # auto step - assert_linear_range(-2.5e+24, 2.5e+24, 2.5e+24, [-2.5e+24, 0, 2.5e+24]) - assert_linear_range(2.5e+24, -2.5e+24, 2.5e+24, [2.5e+24, 0, -2.5e+24]) # auto step - assert_linear_range(2.5e+24, -2.5e+24, -2.5e+24, [2.5e+24, 0, -2.5e+24]) + assert_linear_range(-2.5e24, 2.5e24, -2.5e24, [-2.5e24, 0, 2.5e24]) # auto step + assert_linear_range(-2.5e24, 2.5e24, 2.5e24, [-2.5e24, 0, 2.5e24]) + assert_linear_range(2.5e24, -2.5e24, 2.5e24, [2.5e24, 0, -2.5e24]) # auto step + assert_linear_range(2.5e24, -2.5e24, -2.5e24, [2.5e24, 0, -2.5e24]) assert_linear_range(0, 0, 5, []) assert_linear_range(0, 1, 5, [0, 1]) # limited step diff --git a/tests/test_parameter.py b/tests/test_parameter.py index 7cad529..90e2d6b 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -6,13 +6,13 @@ def test_parameter_empty(): p = Parameter() assert p.default is None - assert p.validate(.42) == .42 + assert p.validate(0.42) == 0.42 def test_parameter_default(): p = Parameter(42) assert p.default == 42 - assert p.validate(.42) == .42 + assert p.validate(0.42) == 0.42 def test_parameter_choice(): @@ -32,15 +32,15 @@ def test_parameter_range(): assert p.maximum == 42 assert p.validate(4.2) == 4.2 with pytest.raises(ValueError): - p.validate(100.) + p.validate(100.0) with pytest.raises(ValueError): - p.validate(0.) + p.validate(0.0) def test_parameter_unit(): p = Parameter("25 mV", unit="V") - assert p.default == .025 + assert p.default == 0.025 assert p.type is None assert p.unit == "V" - assert p.validate(.100) == .100 - assert p.validate("1kV") == 1000. + assert p.validate(0.100) == 0.100 + assert p.validate("1kV") == 1000.0 diff --git a/tests/test_station.py b/tests/test_station.py index f0b3357..7e4c5fe 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -1,8 +1,9 @@ -import pytest from io import StringIO -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +import pytest -from comet.station import default_resource_factory, Station +from comet.station import Station, default_resource_factory @pytest.fixture @@ -12,6 +13,7 @@ def factory(config): mock_resource.__enter__.return_value = mock_resource mock_resource.query.return_value = "Keithley Model 2410" return mock_resource + return factory @@ -23,19 +25,21 @@ def test_default_resource_factory(mock_rm_cls): mock_rm_cls.return_value = mock_rm mock_rm.open_resource.return_value = mock_resource - result = default_resource_factory({ - "visa_library": "@sim", - "resource_name": "GPIB::1::INSTR", - "termination": "\n", - "timeout": 5.0 - }) + result = default_resource_factory( + { + "visa_library": "@sim", + "resource_name": "GPIB::1::INSTR", + "termination": "\n", + "timeout": 5.0, + } + ) mock_rm_cls.assert_called_once_with("@sim") mock_rm.open_resource.assert_called_once_with( "GPIB::1::INSTR", read_termination="\n", write_termination="\n", - timeout=5000 # 5.0 * 1000 + timeout=5000, # 5.0 * 1000 ) assert result is mock_resource @@ -50,17 +54,40 @@ def test_station_from_config(): assert station.instruments_config == {} station = Station.from_config({"instruments": {}}) assert station.instruments_config == {} - station = Station.from_config({"instruments": {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}}}) - assert station.instruments_config == {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}} + station = Station.from_config( + { + "instruments": { + "smu": { + "resource_name": "GPIB::16::INSTR", + "model": "urn:comet:model:keithley:2410", + } + } + } + ) + assert station.instruments_config == { + "smu": { + "resource_name": "GPIB::16::INSTR", + "model": "urn:comet:model:keithley:2410", + } + } def test_station_from_file_json(): station = Station.from_file(StringIO("\n")) assert station.instruments_config == {} - station = Station.from_file(StringIO("{\"instruments\": {}}\n")) + station = Station.from_file(StringIO('{"instruments": {}}\n')) assert station.instruments_config == {} - station = Station.from_file(StringIO("{\"instruments\": {\"smu\": {\"resource_name\": \"GPIB::16::INSTR\", \"model\": \"urn:comet:model:keithley:2410\"}}}\n")) - assert station.instruments_config == {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}} + station = Station.from_file( + StringIO( + '{"instruments": {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}}}\n' + ) + ) + assert station.instruments_config == { + "smu": { + "resource_name": "GPIB::16::INSTR", + "model": "urn:comet:model:keithley:2410", + } + } def test_station_from_file_yaml(): @@ -68,14 +95,30 @@ def test_station_from_file_yaml(): assert station.instruments_config == {} station = Station.from_file(StringIO("instruments: {}\n")) assert station.instruments_config == {} - station = Station.from_file(StringIO("instruments:\n smu:\n resource_name: GPIB::16::INSTR\n model: urn:comet:model:keithley:2410\n")) - assert station.instruments_config == {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}} + station = Station.from_file( + StringIO( + "instruments:\n smu:\n resource_name: GPIB::16::INSTR\n model: urn:comet:model:keithley:2410\n" + ) + ) + assert station.instruments_config == { + "smu": { + "resource_name": "GPIB::16::INSTR", + "model": "urn:comet:model:keithley:2410", + } + } def test_station_context(mock_resource_factory): with Station(resource_factory=mock_resource_factory) as station: assert station._instruments == {} - config = {"instruments": {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}}} + config = { + "instruments": { + "smu": { + "resource_name": "GPIB::16::INSTR", + "model": "urn:comet:model:keithley:2410", + } + } + } with Station.from_config(config, resource_factory=mock_resource_factory) as station: assert "smu" in station._instruments assert station.smu is station._instruments["smu"] @@ -83,13 +126,19 @@ def test_station_context(mock_resource_factory): def test_instrument_attribute_is_readonly(mock_resource_factory): - config = {"instruments": {"smu": {"resource_name": "GPIB::16::INSTR", "model": "urn:comet:model:keithley:2410"}}} + config = { + "instruments": { + "smu": { + "resource_name": "GPIB::16::INSTR", + "model": "urn:comet:model:keithley:2410", + } + } + } station = Station.from_config(config, resource_factory=mock_resource_factory) with pytest.raises(AttributeError): - station.smu - with station as st: - with pytest.raises(AttributeError): - st.smu = 42 + _ = station.smu + with station as st, pytest.raises(AttributeError): + st.smu = 42 # type: ignore def test_add_and_update_instrument(mock_resource_factory): diff --git a/tests/test_utils.py b/tests/test_utils.py index a8706be..95dc897 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,10 +7,10 @@ def test_to_unit(): - assert utils.to_unit(42, "V") == 42. - assert utils.to_unit(42, "mV") == 42. - assert utils.to_unit("42V", "V") == 42. - assert utils.to_unit("42mV", "V") == .042 + assert utils.to_unit(42, "V") == 42.0 + assert utils.to_unit(42, "mV") == 42.0 + assert utils.to_unit("42V", "V") == 42.0 + assert utils.to_unit("42mV", "V") == 0.042 assert utils.to_unit("42 V", "mV") == 42e3 assert utils.to_unit(utils.ureg("42 V"), "mV") == 42e3 @@ -28,18 +28,36 @@ def test_combine_matrix(): assert utils.combine_matrix("a", "b", "c") == ["abc"] assert utils.combine_matrix("a", "123") == ["a1", "a2", "a3"] assert utils.combine_matrix("ab", "123") == ["a1", "a2", "a3", "b1", "b2", "b3"] - assert utils.combine_matrix("ab", "12", "XY") == ["a1X", "a1Y", "a2X", "a2Y", "b1X", "b1Y", "b2X", "b2Y"] + assert utils.combine_matrix("ab", "12", "XY") == [ + "a1X", + "a1Y", + "a2X", + "a2Y", + "b1X", + "b1Y", + "b2X", + "b2Y", + ] assert utils.combine_matrix(["0x"], ("32", "64")) == ["0x32", "0x64"] assert utils.combine_matrix("ABC", "12") == ["A1", "A2", "B1", "B2", "C1", "C2"] - assert utils.combine_matrix("12", "AB", ["08", "16"]) == ["1A08", "1A16", "1B08", "1B16", "2A08", "2A16", "2B08", "2B16"] + assert utils.combine_matrix("12", "AB", ["08", "16"]) == [ + "1A08", + "1A16", + "1B08", + "1B16", + "2A08", + "2A16", + "2B08", + "2B16", + ] def test_inverse_square(): with pytest.raises(ZeroDivisionError): utils.inverse_square(0) assert utils.inverse_square(1) == 1 - assert utils.inverse_square(2) == .25 - assert utils.inverse_square(8) == .015625 + assert utils.inverse_square(2) == 0.25 + assert utils.inverse_square(8) == 0.015625 def test_t_dew(): @@ -55,7 +73,7 @@ def test_make_iso(): def test_safe_filename(): - assert utils.safe_filename("Monty Python\"s!") == "Monty_Python_s_" + assert utils.safe_filename('Monty Python"s!') == "Monty_Python_s_" assert utils.safe_filename("$2020-02-22 13:14:25") == "_2020-02-22_13_14_25" diff --git a/tox.ini b/tox.ini index 26d8ee5..87d0d8e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,27 @@ [tox] +envlist = py311, py312, py313, py314, lint, type isolated_build = true -envlist = py39,py310,py311,py312,py313,py314 skip_missing_interpreters = true [testenv] deps = - ruff - mypy - types-PyYAML pytest pytest-asyncio +commands = + pytest {posargs:tests} + +[testenv:lint] +description = Run Ruff checks +skip_install = true +deps = + ruff commands = ruff check src tests - mypy src - pytest tests + +[testenv:type] +description = Run Pyright +deps = + pyright + pytest +commands = + pyright src tests From 86f06c0e48cb79775eb9d707a50088fceca3ab9b Mon Sep 17 00:00:00 2001 From: Bernhard Arnold Date: Thu, 30 Jul 2026 12:34:09 +0200 Subject: [PATCH 2/4] chore: updated github workflows, removed unused TypeVar --- .github/workflows/mkdocs.yml | 2 +- .github/workflows/python-package.yml | 4 ++-- src/comet/emulator/resource.py | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 13c036a..ec8415b 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.14' + python-version: "3.14" - name: Install dependencies run: | diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1df20da..904449e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -38,4 +38,4 @@ jobs: pip install tox - name: Test with tox run: | - tox -epy + tox -epy -elint -etype diff --git a/src/comet/emulator/resource.py b/src/comet/emulator/resource.py index 14b9460..bebac81 100644 --- a/src/comet/emulator/resource.py +++ b/src/comet/emulator/resource.py @@ -9,12 +9,10 @@ from __future__ import annotations import time -from typing import Self, TypeVar +from typing import Self from .emulator import Emulator, emulator_factory -T = TypeVar("T") - def open_emulator(module_name: str, options: dict | None = None) -> EmulatorResource: emulator = emulator_factory(module_name)() From 4a0cb95222b1de6ad9505fa1913303f610da407a Mon Sep 17 00:00:00 2001 From: Bernhard Arnold Date: Thu, 30 Jul 2026 12:37:41 +0200 Subject: [PATCH 3/4] chore: added .python-version file --- .python-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 From 9f1cef728d5e9f02314b3134af8229aa0787052b Mon Sep 17 00:00:00 2001 From: Bernhard Arnold Date: Thu, 30 Jul 2026 12:55:06 +0200 Subject: [PATCH 4/4] chore: normalized line endings --- src/comet/driver/driver.py | 2 + src/comet/driver/generic/motion_controller.py | 3 +- src/comet/driver/generic/oscilloscope.py | 3 +- src/comet/driver/generic/power_supply.py | 131 +++++----- src/comet/driver/keithley/__init__.py | 50 ++-- src/comet/driver/rohde_schwarz/nge100.py | 218 ++++++++-------- src/comet/emulator/rohde_schwarz/nge100.py | 200 +++++++-------- tests/test_driver_rohde_schwarz_nge100.py | 238 +++++++++--------- 8 files changed, 425 insertions(+), 420 deletions(-) diff --git a/src/comet/driver/driver.py b/src/comet/driver/driver.py index e8ee9e0..64956c8 100644 --- a/src/comet/driver/driver.py +++ b/src/comet/driver/driver.py @@ -1,5 +1,7 @@ from abc import ABC +__all__ = ["Driver"] + class Driver(ABC): """Base class for instrument drivers.""" diff --git a/src/comet/driver/generic/motion_controller.py b/src/comet/driver/generic/motion_controller.py index 2564b6e..57eb721 100644 --- a/src/comet/driver/generic/motion_controller.py +++ b/src/comet/driver/generic/motion_controller.py @@ -1,7 +1,8 @@ from abc import abstractmethod from collections.abc import Iterable -from .instrument import Driver, Instrument +from ..driver import Driver +from .instrument import Instrument __all__ = ["MotionController", "MotionControllerAxis"] diff --git a/src/comet/driver/generic/oscilloscope.py b/src/comet/driver/generic/oscilloscope.py index 1428c66..0612773 100644 --- a/src/comet/driver/generic/oscilloscope.py +++ b/src/comet/driver/generic/oscilloscope.py @@ -1,7 +1,8 @@ from abc import abstractmethod from collections.abc import Iterator -from .instrument import Driver, Instrument +from ..driver import Driver +from .instrument import Instrument __all__ = ["Oscilloscope", "OscilloscopeChannel"] diff --git a/src/comet/driver/generic/power_supply.py b/src/comet/driver/generic/power_supply.py index 77fd33d..49fd7b3 100644 --- a/src/comet/driver/generic/power_supply.py +++ b/src/comet/driver/generic/power_supply.py @@ -1,65 +1,66 @@ -from abc import abstractmethod -from collections.abc import Iterator - -from .instrument import Driver, Instrument - -__all__ = ["PowerSupply", "PowerSupplyChannel"] - - -class PowerSupplyChannel(Driver): - def __init__(self, resource, channel: int) -> None: - super().__init__(resource) - self.channel: int = channel - - OUTPUT_ON: bool = True - OUTPUT_OFF: bool = False - - @property - @abstractmethod - def enabled(self) -> bool: ... - - @enabled.setter - @abstractmethod - def enabled(self, state: bool) -> None: ... - - # Voltage source - - @property - @abstractmethod - def voltage_level(self) -> float: ... - - @voltage_level.setter - @abstractmethod - def voltage_level(self, level: float) -> None: ... - - # Current source - - @property - @abstractmethod - def current_limit(self) -> float: ... - - @current_limit.setter - @abstractmethod - def current_limit(self, level: float) -> None: ... - - # Measurements - - @abstractmethod - def measure_voltage(self) -> float: ... - - @abstractmethod - def measure_current(self) -> float: ... - - @abstractmethod - def measure_power(self) -> float: ... - - -class PowerSupply(Instrument): - @abstractmethod - def __getitem__(self, channel: int) -> PowerSupplyChannel: ... - - @abstractmethod - def __iter__(self) -> Iterator[PowerSupplyChannel]: ... - - @abstractmethod - def __len__(self) -> int: ... +from abc import abstractmethod +from collections.abc import Iterator + +from ..driver import Driver +from .instrument import Instrument + +__all__ = ["PowerSupply", "PowerSupplyChannel"] + + +class PowerSupplyChannel(Driver): + def __init__(self, resource, channel: int) -> None: + super().__init__(resource) + self.channel: int = channel + + OUTPUT_ON: bool = True + OUTPUT_OFF: bool = False + + @property + @abstractmethod + def enabled(self) -> bool: ... + + @enabled.setter + @abstractmethod + def enabled(self, state: bool) -> None: ... + + # Voltage source + + @property + @abstractmethod + def voltage_level(self) -> float: ... + + @voltage_level.setter + @abstractmethod + def voltage_level(self, level: float) -> None: ... + + # Current source + + @property + @abstractmethod + def current_limit(self) -> float: ... + + @current_limit.setter + @abstractmethod + def current_limit(self, level: float) -> None: ... + + # Measurements + + @abstractmethod + def measure_voltage(self) -> float: ... + + @abstractmethod + def measure_current(self) -> float: ... + + @abstractmethod + def measure_power(self) -> float: ... + + +class PowerSupply(Instrument): + @abstractmethod + def __getitem__(self, channel: int) -> PowerSupplyChannel: ... + + @abstractmethod + def __iter__(self) -> Iterator[PowerSupplyChannel]: ... + + @abstractmethod + def __len__(self) -> int: ... diff --git a/src/comet/driver/keithley/__init__.py b/src/comet/driver/keithley/__init__.py index 5f2f8c8..0b3a083 100644 --- a/src/comet/driver/keithley/__init__.py +++ b/src/comet/driver/keithley/__init__.py @@ -1,25 +1,25 @@ -from .k237 import K237 -from .k707b import K707B -from .k708b import K708B -from .k2400 import K2400 -from .k2410 import K2410 -from .k2470 import K2470 -from .k2657a import K2657A -from .k2700 import K2700 -from .k6510 import K6510 -from .k6514 import K6514 -from .k6517b import K6517B - -__all__ = [ - "K237", - "K707B", - "K708B", - "K2400", - "K2410", - "K2470", - "K2657A", - "K2700", - "K6510", - "K6514", - "K6517B", -] +from .k237 import K237 +from .k707b import K707B +from .k708b import K708B +from .k2400 import K2400 +from .k2410 import K2410 +from .k2470 import K2470 +from .k2657a import K2657A +from .k2700 import K2700 +from .k6510 import K6510 +from .k6514 import K6514 +from .k6517b import K6517B + +__all__ = [ + "K237", + "K707B", + "K708B", + "K2400", + "K2410", + "K2470", + "K2657A", + "K2700", + "K6510", + "K6514", + "K6517B", +] diff --git a/src/comet/driver/rohde_schwarz/nge100.py b/src/comet/driver/rohde_schwarz/nge100.py index da2de36..c8aa317 100644 --- a/src/comet/driver/rohde_schwarz/nge100.py +++ b/src/comet/driver/rohde_schwarz/nge100.py @@ -1,109 +1,109 @@ -from collections.abc import Iterator - -from comet.driver.generic import InstrumentError -from comet.driver.generic.power_supply import PowerSupply, PowerSupplyChannel - -__all__ = ["NGE100", "NGE100Channel"] - - -class NGE100Channel(PowerSupplyChannel): - """Single channel of the NGE100 power supply""" - - @property - def enabled(self) -> bool: - value = int(self.query("OUTPut?")) - return {0: self.OUTPUT_OFF, 1: self.OUTPUT_ON}[value] - - @enabled.setter - def enabled(self, state: bool) -> None: - value = {self.OUTPUT_OFF: 0, self.OUTPUT_ON: 1}[state] - self.write(f"OUTPut {value}") - - @property - def voltage_level(self) -> float: - return float(self.query("SOURce:VOLTage:LEVel:IMMediate:AMPLitude?")) - - @voltage_level.setter - def voltage_level(self, level: float) -> None: - if level < 0: - raise ValueError("Voltage level must be non-negative") - if level > 32: - raise ValueError("Voltage level must be less than 32 V") - self.write(f"SOURce:VOLTage:LEVel:IMMediate:AMPLitude {level}") - - @property - def current_limit(self) -> float: - return float(self.query("SOURce:CURRent:LEVel:IMMediate:AMPLitude?")) - - @current_limit.setter - def current_limit(self, level: float) -> None: - if level < 0: - raise ValueError("Current limit must be non-negative") - if level > 3: - raise ValueError("Current limit must be less than 3 A") - self.write(f"SOURce:CURRent:LEVel:IMMediate:AMPLitude {level}") - - def measure_voltage(self) -> float: - return float(self.query("MEASure:SCALar:VOLTage:DC?")) - - def measure_current(self) -> float: - return float(self.query("MEASure:SCALar:CURRent:DC?")) - - def measure_power(self) -> float: - return float(self.query("MEASure:SCALar:POWer?")) - - # Helper - def query(self, message: str) -> str: - self.resource.write(f"INSTrument {self.channel + 1}") - return self.resource.query(message).strip() - - def write(self, message: str) -> None: - self.resource.write(f"INSTrument {self.channel + 1}") - self.resource.write(message) - self.resource.query("*OPC?") - - -class NGE100(PowerSupply): - """Rohde & Schwarz NGE100 power supply featuring multiple channels""" - - N_CHANNELS: int = 3 - - def identify(self) -> str: - return self.query("*IDN?") - - def reset(self) -> None: - self.write("*RST") - - def clear(self) -> None: - self.write("*CLS") - - def next_error(self) -> InstrumentError | None: - code, message = self.query("SYSTem:ERRor?").split(", ") - if int(code): - return InstrumentError(int(code), message.strip("'")) - return None - - def query(self, message: str) -> str: - return self.resource.query(message).strip() - - def write(self, message: str) -> None: - self.resource.write(message) - self.query("*OPC?") - - def __getitem__(self, channel: int) -> NGE100Channel: - if not isinstance(channel, int): - raise TypeError("Channel index must be an integer") - if channel not in range(type(self).N_CHANNELS): - raise IndexError("Channel index out of range") - return NGE100Channel(self.resource, channel) - - def __iter__(self) -> Iterator[NGE100Channel]: - return iter( - [ - NGE100Channel(self.resource, channel) - for channel in range(type(self).N_CHANNELS) - ] - ) - - def __len__(self) -> int: - return type(self).N_CHANNELS +from collections.abc import Iterator + +from comet.driver.generic import InstrumentError +from comet.driver.generic.power_supply import PowerSupply, PowerSupplyChannel + +__all__ = ["NGE100", "NGE100Channel"] + + +class NGE100Channel(PowerSupplyChannel): + """Single channel of the NGE100 power supply""" + + @property + def enabled(self) -> bool: + value = int(self.query("OUTPut?")) + return {0: self.OUTPUT_OFF, 1: self.OUTPUT_ON}[value] + + @enabled.setter + def enabled(self, state: bool) -> None: + value = {self.OUTPUT_OFF: 0, self.OUTPUT_ON: 1}[state] + self.write(f"OUTPut {value}") + + @property + def voltage_level(self) -> float: + return float(self.query("SOURce:VOLTage:LEVel:IMMediate:AMPLitude?")) + + @voltage_level.setter + def voltage_level(self, level: float) -> None: + if level < 0: + raise ValueError("Voltage level must be non-negative") + if level > 32: + raise ValueError("Voltage level must be less than 32 V") + self.write(f"SOURce:VOLTage:LEVel:IMMediate:AMPLitude {level}") + + @property + def current_limit(self) -> float: + return float(self.query("SOURce:CURRent:LEVel:IMMediate:AMPLitude?")) + + @current_limit.setter + def current_limit(self, level: float) -> None: + if level < 0: + raise ValueError("Current limit must be non-negative") + if level > 3: + raise ValueError("Current limit must be less than 3 A") + self.write(f"SOURce:CURRent:LEVel:IMMediate:AMPLitude {level}") + + def measure_voltage(self) -> float: + return float(self.query("MEASure:SCALar:VOLTage:DC?")) + + def measure_current(self) -> float: + return float(self.query("MEASure:SCALar:CURRent:DC?")) + + def measure_power(self) -> float: + return float(self.query("MEASure:SCALar:POWer?")) + + # Helper + def query(self, message: str) -> str: + self.resource.write(f"INSTrument {self.channel + 1}") + return self.resource.query(message).strip() + + def write(self, message: str) -> None: + self.resource.write(f"INSTrument {self.channel + 1}") + self.resource.write(message) + self.resource.query("*OPC?") + + +class NGE100(PowerSupply): + """Rohde & Schwarz NGE100 power supply featuring multiple channels""" + + N_CHANNELS: int = 3 + + def identify(self) -> str: + return self.query("*IDN?") + + def reset(self) -> None: + self.write("*RST") + + def clear(self) -> None: + self.write("*CLS") + + def next_error(self) -> InstrumentError | None: + code, message = self.query("SYSTem:ERRor?").split(", ") + if int(code): + return InstrumentError(int(code), message.strip("'")) + return None + + def query(self, message: str) -> str: + return self.resource.query(message).strip() + + def write(self, message: str) -> None: + self.resource.write(message) + self.query("*OPC?") + + def __getitem__(self, channel: int) -> NGE100Channel: + if not isinstance(channel, int): + raise TypeError("Channel index must be an integer") + if channel not in range(type(self).N_CHANNELS): + raise IndexError("Channel index out of range") + return NGE100Channel(self.resource, channel) + + def __iter__(self) -> Iterator[NGE100Channel]: + return iter( + [ + NGE100Channel(self.resource, channel) + for channel in range(type(self).N_CHANNELS) + ] + ) + + def __len__(self) -> int: + return type(self).N_CHANNELS diff --git a/src/comet/emulator/rohde_schwarz/nge100.py b/src/comet/emulator/rohde_schwarz/nge100.py index 7e92a6c..240f2d9 100644 --- a/src/comet/emulator/rohde_schwarz/nge100.py +++ b/src/comet/emulator/rohde_schwarz/nge100.py @@ -1,100 +1,100 @@ -"""Rohde&Schwarz NGE100 power supply emulator""" - -import math - -from comet.emulator import Emulator, message, run - -__all__ = ["NGE100Emulator"] - - -class NGE100Emulator(Emulator): - IDENTITY: str = "Rohde&Schwarz,NGE103B,5601.3800k03/101863,1.54" - - def __init__(self) -> None: - super().__init__() - - self.voltage_levels: list[float] = [0.0, 0.0, 0.0] - self.current_limits: list[float] = [0.0, 00.0, 0.0] - self.enabled_channels: list[bool] = [False, False, False] - - self.selected_channel: int = 0 - - self.resistances: list[float] = [1, 1e3, math.inf] # 1 Ohm, 1 kOhm, infinite - - def get_voltage(self) -> float: - voltage_from_current_limit = ( - self.current_limits[self.selected_channel] - * self.resistances[self.selected_channel] - ) - voltage_from_voltage_level = self.voltage_levels[self.selected_channel] - return min(voltage_from_current_limit, voltage_from_voltage_level) - - def get_current(self) -> float: - current_from_voltage_level = ( - self.voltage_levels[self.selected_channel] - / self.resistances[self.selected_channel] - ) - current_from_current_limit = self.current_limits[self.selected_channel] - return min(current_from_voltage_level, current_from_current_limit) - - @message(r"\*IDN\?$") - def identify(self) -> str: - return self.IDENTITY - - @message(r"INST(?:rument)?\s+(\d)$") - def set_channel(self, channel: int) -> None: - self.selected_channel = int(channel) - 1 - - @message(r"INST(?:rument)?\?$") - def get_channel(self) -> str: - return str(self.selected_channel + 1) - - @message(r"^OUTP(?:ut)?\s+(\d)$") - def set_enabled(self, enabled: int) -> None: - self.enabled_channels[self.selected_channel] = bool(int(enabled)) - - @message(r"^OUTP(?:ut)?\?$") - def get_enabled(self) -> str: - return str(int(self.enabled_channels[self.selected_channel])) - - @message( - r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$" - ) - def set_voltage_level(self, voltage_level: float) -> None: - voltage_level = min(max(0, float(voltage_level)), 32) - self.voltage_levels[self.selected_channel] = voltage_level - - @message( - r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$" - ) - def get_voltage_level(self) -> str: - return str(self.voltage_levels[self.selected_channel]) - - @message( - r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$" - ) - def set_current_limit(self, current_limit: float) -> None: - current_limit = min(max(0, float(current_limit)), 3) - self.current_limits[self.selected_channel] = current_limit - - @message( - r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$" - ) - def get_current_limit(self) -> str: - return str(self.current_limits[self.selected_channel]) - - @message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?:VOLT(?:age)?(?::DC)?\?$") - def measure_voltage(self) -> str: - return str(self.get_voltage()) - - @message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?:CURR(?:ent)?(?::DC)?\?$") - def measure_current(self) -> str: - return str(self.get_current()) - - @message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?:POW(?:er)?(?::DC)?\?$") - def measure_power(self) -> str: - return str(self.get_voltage() * self.get_current()) - - -if __name__ == "__main__": - run(NGE100Emulator()) +"""Rohde&Schwarz NGE100 power supply emulator""" + +import math + +from comet.emulator import Emulator, message, run + +__all__ = ["NGE100Emulator"] + + +class NGE100Emulator(Emulator): + IDENTITY: str = "Rohde&Schwarz,NGE103B,5601.3800k03/101863,1.54" + + def __init__(self) -> None: + super().__init__() + + self.voltage_levels: list[float] = [0.0, 0.0, 0.0] + self.current_limits: list[float] = [0.0, 00.0, 0.0] + self.enabled_channels: list[bool] = [False, False, False] + + self.selected_channel: int = 0 + + self.resistances: list[float] = [1, 1e3, math.inf] # 1 Ohm, 1 kOhm, infinite + + def get_voltage(self) -> float: + voltage_from_current_limit = ( + self.current_limits[self.selected_channel] + * self.resistances[self.selected_channel] + ) + voltage_from_voltage_level = self.voltage_levels[self.selected_channel] + return min(voltage_from_current_limit, voltage_from_voltage_level) + + def get_current(self) -> float: + current_from_voltage_level = ( + self.voltage_levels[self.selected_channel] + / self.resistances[self.selected_channel] + ) + current_from_current_limit = self.current_limits[self.selected_channel] + return min(current_from_voltage_level, current_from_current_limit) + + @message(r"\*IDN\?$") + def identify(self) -> str: + return self.IDENTITY + + @message(r"INST(?:rument)?\s+(\d)$") + def set_channel(self, channel: int) -> None: + self.selected_channel = int(channel) - 1 + + @message(r"INST(?:rument)?\?$") + def get_channel(self) -> str: + return str(self.selected_channel + 1) + + @message(r"^OUTP(?:ut)?\s+(\d)$") + def set_enabled(self, enabled: int) -> None: + self.enabled_channels[self.selected_channel] = bool(int(enabled)) + + @message(r"^OUTP(?:ut)?\?$") + def get_enabled(self) -> str: + return str(int(self.enabled_channels[self.selected_channel])) + + @message( + r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$" + ) + def set_voltage_level(self, voltage_level: float) -> None: + voltage_level = min(max(0, float(voltage_level)), 32) + self.voltage_levels[self.selected_channel] = voltage_level + + @message( + r"(?:SOUR(?:ce)?:)?VOLT(?:age)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$" + ) + def get_voltage_level(self) -> str: + return str(self.voltage_levels[self.selected_channel]) + + @message( + r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\s+(.+)$" + ) + def set_current_limit(self, current_limit: float) -> None: + current_limit = min(max(0, float(current_limit)), 3) + self.current_limits[self.selected_channel] = current_limit + + @message( + r"(?:SOUR(?:ce)?:)?CURR(?:ent)?(?::LEV(?:el)?)?(?::IMM(?:ediate)?)?(?::AMPL(?:itude)?)?\?$" + ) + def get_current_limit(self) -> str: + return str(self.current_limits[self.selected_channel]) + + @message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?:VOLT(?:age)?(?::DC)?\?$") + def measure_voltage(self) -> str: + return str(self.get_voltage()) + + @message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?:CURR(?:ent)?(?::DC)?\?$") + def measure_current(self) -> str: + return str(self.get_current()) + + @message(r"MEAS(?:ure)?(?::SCAL(?:ar)?)?:POW(?:er)?(?::DC)?\?$") + def measure_power(self) -> str: + return str(self.get_voltage() * self.get_current()) + + +if __name__ == "__main__": + run(NGE100Emulator()) diff --git a/tests/test_driver_rohde_schwarz_nge100.py b/tests/test_driver_rohde_schwarz_nge100.py index 0319ca8..348f73a 100644 --- a/tests/test_driver_rohde_schwarz_nge100.py +++ b/tests/test_driver_rohde_schwarz_nge100.py @@ -1,119 +1,119 @@ -import pytest - -from comet.driver.rohde_schwarz.nge100 import NGE100 - - -@pytest.fixture -def driver(resource): - return NGE100(resource) - - -def test_identify(driver, resource): - resource.buffer = ["Rohde&Schwarz,NGE103B,5601.3800k03/101863,1.54"] - assert driver.identify() == "Rohde&Schwarz,NGE103B,5601.3800k03/101863,1.54" - assert resource.buffer == ["*IDN?"] - - -def test_reset(driver, resource): - resource.buffer = [""] - driver.reset() - assert resource.buffer == ["*RST", "*OPC?"] - - -def test_clear(driver, resource): - resource.buffer = [""] - driver.clear() - assert resource.buffer == ["*CLS", "*OPC?"] - - -def test_error(driver, resource): - resource.buffer = ["0, 'No error'"] - assert driver.next_error() is None - assert resource.buffer == ["SYSTem:ERRor?"] - - resource.buffer = ["-222, 'Data out of range;INSTrument 5'"] - error = driver.next_error() - assert error.code == -222 - - assert error.message == "Data out of range;INSTrument 5" - assert resource.buffer == ["SYSTem:ERRor?"] - - -def test_get_channel(driver, resource): - assert len(driver) == 3 - for i in range(3): - assert driver[i].channel == i - - with pytest.raises(IndexError): - driver[3] - - with pytest.raises(IndexError): - driver[-1] - - -def test_read_voltage_level(driver, resource): - resource.buffer = ["0"] - assert driver[0].voltage_level == 0.0 - assert resource.buffer == [ - "INSTrument 1", - "SOURce:VOLTage:LEVel:IMMediate:AMPLitude?", - ] - - -def test_set_voltage_level(driver, resource): - resource.buffer = ["1"] - driver[0].voltage_level = 0.0 - assert resource.buffer == [ - "INSTrument 1", - "SOURce:VOLTage:LEVel:IMMediate:AMPLitude 0.0", - "*OPC?", - ] - - with pytest.raises(ValueError): - driver[0].voltage_level = -1 - - with pytest.raises(ValueError): - driver[0].voltage_level = 33 - - -def test_read_current_limit(driver, resource): - resource.buffer = ["0.0"] - assert driver[0].current_limit == 0.0 - assert resource.buffer == [ - "INSTrument 1", - "SOURce:CURRent:LEVel:IMMediate:AMPLitude?", - ] - - -def test_set_current_limit(driver, resource): - resource.buffer = ["1"] - driver[0].current_limit = 0.0 - assert resource.buffer == [ - "INSTrument 1", - "SOURce:CURRent:LEVel:IMMediate:AMPLitude 0.0", - "*OPC?", - ] - - with pytest.raises(ValueError): - driver[0].current_limit = -1 - - with pytest.raises(ValueError): - driver[0].current_limit = 3.1 - - -def test_measure_voltage(driver, resource): - resource.buffer = ["1.0"] - assert driver[0].measure_voltage() == 1.0 - assert resource.buffer == ["INSTrument 1", "MEASure:SCALar:VOLTage:DC?"] - - -def test_measure_current(driver, resource): - resource.buffer = ["1.0"] - assert driver[0].measure_current() == 1.0 - assert resource.buffer == ["INSTrument 1", "MEASure:SCALar:CURRent:DC?"] - - -def test_measure_power(driver, resource): - resource.buffer = ["10.0"] - assert driver[0].measure_power() == 10.0 - assert resource.buffer == ["INSTrument 1", "MEASure:SCALar:POWer?"] +import pytest + +from comet.driver.rohde_schwarz.nge100 import NGE100 + + +@pytest.fixture +def driver(resource): + return NGE100(resource) + + +def test_identify(driver, resource): + resource.buffer = ["Rohde&Schwarz,NGE103B,5601.3800k03/101863,1.54"] + assert driver.identify() == "Rohde&Schwarz,NGE103B,5601.3800k03/101863,1.54" + assert resource.buffer == ["*IDN?"] + + +def test_reset(driver, resource): + resource.buffer = [""] + driver.reset() + assert resource.buffer == ["*RST", "*OPC?"] + + +def test_clear(driver, resource): + resource.buffer = [""] + driver.clear() + assert resource.buffer == ["*CLS", "*OPC?"] + + +def test_error(driver, resource): + resource.buffer = ["0, 'No error'"] + assert driver.next_error() is None + assert resource.buffer == ["SYSTem:ERRor?"] + + resource.buffer = ["-222, 'Data out of range;INSTrument 5'"] + error = driver.next_error() + assert error.code == -222 + + assert error.message == "Data out of range;INSTrument 5" + assert resource.buffer == ["SYSTem:ERRor?"] + + +def test_get_channel(driver, resource): + assert len(driver) == 3 + for i in range(3): + assert driver[i].channel == i + + with pytest.raises(IndexError): + driver[3] + + with pytest.raises(IndexError): + driver[-1] + + +def test_read_voltage_level(driver, resource): + resource.buffer = ["0"] + assert driver[0].voltage_level == 0.0 + assert resource.buffer == [ + "INSTrument 1", + "SOURce:VOLTage:LEVel:IMMediate:AMPLitude?", + ] + + +def test_set_voltage_level(driver, resource): + resource.buffer = ["1"] + driver[0].voltage_level = 0.0 + assert resource.buffer == [ + "INSTrument 1", + "SOURce:VOLTage:LEVel:IMMediate:AMPLitude 0.0", + "*OPC?", + ] + + with pytest.raises(ValueError): + driver[0].voltage_level = -1 + + with pytest.raises(ValueError): + driver[0].voltage_level = 33 + + +def test_read_current_limit(driver, resource): + resource.buffer = ["0.0"] + assert driver[0].current_limit == 0.0 + assert resource.buffer == [ + "INSTrument 1", + "SOURce:CURRent:LEVel:IMMediate:AMPLitude?", + ] + + +def test_set_current_limit(driver, resource): + resource.buffer = ["1"] + driver[0].current_limit = 0.0 + assert resource.buffer == [ + "INSTrument 1", + "SOURce:CURRent:LEVel:IMMediate:AMPLitude 0.0", + "*OPC?", + ] + + with pytest.raises(ValueError): + driver[0].current_limit = -1 + + with pytest.raises(ValueError): + driver[0].current_limit = 3.1 + + +def test_measure_voltage(driver, resource): + resource.buffer = ["1.0"] + assert driver[0].measure_voltage() == 1.0 + assert resource.buffer == ["INSTrument 1", "MEASure:SCALar:VOLTage:DC?"] + + +def test_measure_current(driver, resource): + resource.buffer = ["1.0"] + assert driver[0].measure_current() == 1.0 + assert resource.buffer == ["INSTrument 1", "MEASure:SCALar:CURRent:DC?"] + + +def test_measure_power(driver, resource): + resource.buffer = ["10.0"] + assert driver[0].measure_power() == 10.0 + assert resource.buffer == ["INSTrument 1", "MEASure:SCALar:POWer?"]