diff --git a/.gitignore b/.gitignore
index d640ca09..9f7fb95e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ tm_devices.yaml
tm_devices.yml
*.log
*.dat
+**/logs/**
# Poetry lock file
poetry.lock
@@ -58,6 +59,7 @@ coverage.xml
.pytest_cache/
.results*/
tests/samples/generated_stubs/*
+tests/generated_**/*
tests/verify_devices.yaml
prof/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1c60602..80dc5443 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,12 @@ Things to be included in the next release go here.
- Added the `get_errors()` method to the `Device` class to enable easy access to the current error code and messages on any device.
- Added more details to the Architectural Overview page of the documentation as well as highlighting to the device driver diagram on the page.
- Added regex matching to the `verify_values()` helper function to allow for more flexible value verification.
+- A main logfile is now created by default (can be disabled if desired) that contains all the logging output of the entire tm_devices package during execution.
+ - Use the `configure_logging()` function to set the logging levels for stdout and file logging.
+ - The default settings will log all messages to the log file and maintain the current printout functionality on stdout.
+- A logfile is now created that contains each command sent to a VISA device.
+ - This file is located next to the main log file and will start with the same name, but have the unique address of the device appended.
+ - This file will only be created if file logging is enabled for the package (which is the default behavior).
### Changed
@@ -46,10 +52,14 @@ However, please read through all changes to be aware of what may potentially imp
- _**BREAKING CHANGE**_: Changed the behavior of the `expect_esr()` method to expect an integer error code input and an optional tuple of error messages to compare against the actual error code and messages returned by the `_get_errors()` private method.
- _**minor breaking change**_: Converted the `device_type` property into an abstract, cached property to force all children of the `Device` class to specify what type of device they are.
- Updated the auto-generated command mixin classes to no longer use an `__init__()` method to enable the driver API documentation to render in a more usable way.
+- Switched from using standard `print()` calls to using the `logging` module for all logging in the `tm_devices` package.
+ - A configuration function provides the ability to set different logging levels for stdout and file logging.
+ - The config file and environment variable can also be used to control the logging functionality.
+ - The debug logging from the `pyvisa` package is also included in the log file by default.
### Removed
-- _**BREAKING CHANGE**_: Removed previously deprecated `TekScopeSW` alias to the `TekScopePC` class
+- _**BREAKING CHANGE**_: Removed previously deprecated `TekScopeSW` alias to the `TekScopePC` class.
- _**BREAKING CHANGE**_: Removed previously deprecated `write_buffers()` from the `TSPControl` class.
- _**BREAKING CHANGE**_: Removed Internal AFG methods from the `TekScopePC` driver, since they wouldn't have worked due to its lack of an IAFG.
- _**BREAKING CHANGE**_: Removed previously deprecated `DEVICE_DRIVER_MODEL_MAPPING` constant.
@@ -57,6 +67,8 @@ However, please read through all changes to be aware of what may potentially imp
- _**BREAKING CHANGE**_: Removed many hacky implementations of `total_channels` and `all_channel_names_list` properties from drivers that don't need them anymore.
- _**BREAKING CHANGE**_: Removed the `verify_values()`, `raise_failure()`, and `raise_error()` methods from all device drivers.
- These methods have been converted to helper functions and can be imported from the `tm_devices.helpers` subpackage now.
+- _**BREAKING CHANGE**_: Removed the `print_with_timestamp()` function since this functionality is now handled by the `logging` module.
+- _**BREAKING CHANGE**_: Removed the `get_timestamp_string()` function since this functionality is now handled by the `logging` module.
---
diff --git a/README.md b/README.md
index 0fb7d296..7b1e995c 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-| | |
-| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Testing** | [![Code testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml) [![Docs testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml) [![Coverage status](https://codecov.io/gh/tektronix/tm_devices/branch/main/graph/badge.svg)](https://codecov.io/gh/tektronix/tm_devices) |
-| **Code Quality** | [![CodeQL status](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml) [![CodeFactor grade](https://www.codefactor.io/repository/github/tektronix/tm_devices/badge)](https://www.codefactor.io/repository/github/tektronix/tm_devices) [![pre-commit status](https://results.pre-commit.ci/badge/github/tektronix/tm_devices/main.svg)](https://results.pre-commit.ci/latest/github/tektronix/tm_devices/main) |
-| **Package** | [![PyPI: Package status](https://img.shields.io/pypi/status/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Latest release version](https://img.shields.io/pypi/v/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Supported Python versions](https://img.shields.io/pypi/pyversions/tm_devices?logo=python)](https://pypi.org/project/tm_devices/) [![PyPI: Downloads](https://pepy.tech/badge/tm-devices)](https://pepy.tech/project/tm_devices) [![License: Apache 2.0](https://img.shields.io/pypi/l/tm_devices)](https://github.com/tektronix/tm_devices/blob/main/LICENSE.md) [![Package build status](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml) [![PyPI upload status](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml) |
-| **Documentation** | [![ReadtheDocs Status](https://img.shields.io/readthedocs/tm-devices/stable?logo=readthedocs)](https://tm-devices.readthedocs.io/stable) |
-| **Code Style** | [![Test style: pytest](https://img.shields.io/badge/test%20style-pytest-blue)](https://github.com/pytest-dev/pytest) [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-black)](https://docs.astral.sh/ruff/formatter/) [![Docstring style: google](https://img.shields.io/badge/docstring%20style-google-tan)](https://google.github.io/styleguide/pyguide.html) |
-| **Linting** | [![pre-commit enabled](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![Docstring formatter: docformatter](https://img.shields.io/badge/docstring%20formatter-docformatter-tan)](https://github.com/PyCQA/docformatter) [![Type Checker: pyright](https://img.shields.io/badge/type%20checker-pyright-yellowgreen)](https://github.com/RobertCraigie/pyright-python) [![Linter: pylint](https://img.shields.io/badge/linter-pylint-purple)](https://github.com/pylint-dev/pylint) [![Linter: Ruff](https://img.shields.io/badge/linter-ruff-purple)](https://github.com/charliermarsh/ruff) |
+| | |
+| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Testing** | [![Code testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml) [![Docs testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml) [![Coverage status](https://codecov.io/gh/tektronix/tm_devices/branch/main/graph/badge.svg)](https://codecov.io/gh/tektronix/tm_devices) |
+| **Code Quality** | [![CodeQL status](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml) [![CodeFactor grade](https://www.codefactor.io/repository/github/tektronix/tm_devices/badge)](https://www.codefactor.io/repository/github/tektronix/tm_devices) [![pre-commit status](https://results.pre-commit.ci/badge/github/tektronix/tm_devices/main.svg)](https://results.pre-commit.ci/latest/github/tektronix/tm_devices/main) |
+| **Package** | [![PyPI: Package status](https://img.shields.io/pypi/status/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Latest release version](https://img.shields.io/pypi/v/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Supported Python versions](https://img.shields.io/pypi/pyversions/tm_devices?logo=python)](https://pypi.org/project/tm_devices/) [![PyPI: Downloads](https://static.pepy.tech/badge/tm-devices)](https://pepy.tech/project/tm_devices) [![License: Apache 2.0](https://img.shields.io/pypi/l/tm_devices)](https://github.com/tektronix/tm_devices/blob/main/LICENSE.md) [![Package build status](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml) [![PyPI upload status](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml) |
+| **Documentation** | [![ReadtheDocs Status](https://img.shields.io/readthedocs/tm-devices/stable?logo=readthedocs)](https://tm-devices.readthedocs.io/stable) |
+| **Code Style** | [![Test style: pytest](https://img.shields.io/badge/test%20style-pytest-blue)](https://github.com/pytest-dev/pytest) [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-black)](https://docs.astral.sh/ruff/formatter/) [![Docstring style: google](https://img.shields.io/badge/docstring%20style-google-tan)](https://google.github.io/styleguide/pyguide.html) |
+| **Linting** | [![pre-commit enabled](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![Docstring formatter: docformatter](https://img.shields.io/badge/docstring%20formatter-docformatter-tan)](https://github.com/PyCQA/docformatter) [![Type Checker: pyright](https://img.shields.io/badge/type%20checker-pyright-yellowgreen)](https://github.com/RobertCraigie/pyright-python) [![Linter: pylint](https://img.shields.io/badge/linter-pylint-purple)](https://github.com/pylint-dev/pylint) [![Linter: Ruff](https://img.shields.io/badge/linter-ruff-purple)](https://github.com/charliermarsh/ruff) |
@@ -50,7 +50,7 @@ pip install tm_devices
```console
$ list-visa-resources
[
- "TCPIP0::192.168.0.100::inst0::INSTR",
+ "TCPIP0::192.168.0.1::inst0::INSTR",
"ASRL4::INSTR"
]
```
@@ -61,7 +61,7 @@ $ list-visa-resources
from tm_devices import DeviceManager
with DeviceManager() as device_manager:
- scope = device_manager.add_scope("192.168.0.100")
+ scope = device_manager.add_scope("192.168.0.1")
scope.query("*IDN?")
print(scope)
```
diff --git a/docs/basic_usage.md b/docs/basic_usage.md
index 47d1c957..59c7c205 100644
--- a/docs/basic_usage.md
+++ b/docs/basic_usage.md
@@ -10,7 +10,7 @@ This will print the available VISA devices to the console when run from a shell
```console
$ list-visa-resources
[
- "TCPIP0::192.168.0.100::inst0::INSTR",
+ "TCPIP0::192.168.0.1::inst0::INSTR",
"ASRL4::INSTR"
]
```
@@ -59,6 +59,23 @@ outside the Python code for ease of automation
--8<-- "examples/miscellaneous/adding_devices_with_env_var.py"
```
+## Customize logging and console output
+
+The amount of console output and logging saved to the log file can be customized as needed. This
+configuration can be done in the Python code itself as demonstrated here, or by using the
+[config file](configuration.md#config-options) or
+[environment variable](configuration.md#environment-variable).
+
+!!! important
+ If any configuration is performed in the Python code prior to instantiating the
+ [`DeviceManager`][tm_devices.DeviceManager], all other logging configuration methods
+ (config file, env var) will be ignored.
+
+```python
+# fmt: off
+--8<-- "examples/miscellaneous/customize_logging.py"
+```
+
## Disable command checking
This removes an extra query that verifies the property was set to the expected
diff --git a/docs/configuration.md b/docs/configuration.md
index b9bce014..ab0bfd32 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -215,8 +215,7 @@ devices:
### Config Options
-These options are flags that enable/disable runtime behaviors of the Device
-Manager.
+These options are used to configure runtime behaviors of `tm_devices`.
#### Yaml Options Syntax
@@ -230,6 +229,12 @@ options:
retry_visa_connection: false
default_visa_timeout: 5000
check_for_updates: false
+ log_console_level: INFO
+ log_file_level: DEBUG
+ log_file_directory: ./logs
+ log_file_name: tm_devices_.log
+ log_colored_output: false
+ log_pyvisa_messages: false
```
These are all `false` by default if not defined, set to `true` to modify the
@@ -259,6 +264,30 @@ runtime behavior configuration.
- `check_for_updates`
- This config option will enable a check for any available updates on pypi.org for the
package when the `DeviceManager` is instantiated.
+- `log_console_level`
+ - This config option is used to set the log level for the console output.
+ The default value of this config option is "INFO". See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_file_level`
+ - This config option is used to set the log level for the file output.
+ The default value of this config option is "DEBUG". See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_file_directory`
+ - This config option is used to set the directory where the log files will be saved.
+ The default value of this config option is "./logs". See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_file_name`
+ - This config option is used to set the name of the log file.
+ The default value of this config option is a timestamped filename with the .log extension. See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_colored_output`
+ - This config option is used to enable or disable colored output in the console.
+ The default value of this config option is false. See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_pyvisa_messages`
+ - This config option is used to enable or disable logging of PyVISA messages within the
+ configured log file. The default value of this config option is false. See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
### Sample Config File
@@ -320,6 +349,12 @@ options:
retry_visa_connection: false
default_visa_timeout: 10000 # 10 second default VISA timeout
check_for_updates: false
+ log_console_level: NONE # completely disable console output
+ log_file_level: DEBUG
+ log_file_directory: ./logs
+ log_file_name: custom_logfile.log # customize the log file name
+ log_colored_output: false
+ log_pyvisa_messages: true # log PyVISA messages in the log file
```
#### TOML
@@ -393,8 +428,14 @@ standalone = false
verbose_mode = false
verbose_visa = false
retry_visa_connection = false
-default_visa_timeout = 10000 # 10 second default VISA timeout
+default_visa_timeout = 10000 # 10 second default VISA timeout
check_for_updates = false
+log_console_level = "NONE" # completely disable console output
+log_file_level = "DEBUG"
+log_file_directory = "./logs"
+log_file_name = "custom_logfile.log" # customize the log file name
+log_colored_output = false
+log_pyvisa_messages = true # log PyVISA messages in the log file
```
---
diff --git a/docs/key_features.md b/docs/key_features.md
index 1fa1f161..555b5dc8 100644
--- a/docs/key_features.md
+++ b/docs/key_features.md
@@ -14,3 +14,4 @@ complex examples.
IntelliSense.
- Organize connections to an entire "Test Bench" of devices with one package!
- Full support for all VISA connection types (some require external drivers).
+- Customizable logging to both the console and a log file.
diff --git a/docs/known_words.txt b/docs/known_words.txt
index 583a3f61..b90d6485 100644
--- a/docs/known_words.txt
+++ b/docs/known_words.txt
@@ -19,11 +19,13 @@ ci
classdiagram
codebase
codecov
+colored
config
conftest
cookiecutter
cov
csv
+customizable
deps
dev
disable_command_verification
@@ -33,6 +35,7 @@ docstring
docstrings
en
enum
+env
executables
filepath
generate_function
diff --git a/docs/macros.py b/docs/macros.py
index 8031460b..99c33017 100644
--- a/docs/macros.py
+++ b/docs/macros.py
@@ -265,8 +265,8 @@ def define_env(env: MacrosPlugin) -> None:
used to perform a transformation
"""
# Read in the current package version number to use in templates and files
- with open(
- pathlib.Path(f"{pathlib.Path(__file__).parents[1]}") / "pyproject.toml", "rb"
+ with (pathlib.Path(f"{pathlib.Path(__file__).parents[1]}") / "pyproject.toml").open(
+ "rb"
) as file_handle:
pyproject_data = tomli.load(file_handle)
package_version = "v" + pyproject_data["tool"]["poetry"]["version"]
diff --git a/examples/miscellaneous/customize_logging.py b/examples/miscellaneous/customize_logging.py
new file mode 100644
index 00000000..5f6cc3b9
--- /dev/null
+++ b/examples/miscellaneous/customize_logging.py
@@ -0,0 +1,21 @@
+"""The console output and level of logging outputs in the log file can be configured as needed."""
+
+from tm_devices import configure_logging, DeviceManager, LoggingLevels
+from tm_devices.drivers import MSO6B
+
+# NOTE: This configuration will prevent any logging config options from a config file or
+# environment variable from being used.
+configure_logging(
+ log_console_level=LoggingLevels.NONE, # completely disable console logging
+ log_file_level=LoggingLevels.DEBUG, # log everything to the file
+ log_file_directory="./log_files", # save the log file in the "./log_files" directory
+ log_file_name="custom_log_filename.log", # customize the filename
+ log_pyvisa_messages=True, # include all the pyvisa debug messages in the same log file
+)
+
+with DeviceManager(verbose=False) as dm:
+ scope: MSO6B = dm.add_scope("192.168.0.1")
+ scope.curve_query(1)
+ scope.check_port_connection(4000)
+ scope.check_network_connection()
+ scope.check_visa_connection()
diff --git a/examples/miscellaneous/register_dm_atexit.py b/examples/miscellaneous/register_dm_atexit.py
index 7207c538..e0ba70aa 100644
--- a/examples/miscellaneous/register_dm_atexit.py
+++ b/examples/miscellaneous/register_dm_atexit.py
@@ -12,7 +12,7 @@
atexit.register(dm.close)
# Add a device
-scope: MSO6B = dm.add_scope("192.168.1.102")
+scope: MSO6B = dm.add_scope("192.168.0.1")
# Use the device
print(scope)
diff --git a/examples/scopes/tekscope/basic_curve_query.py b/examples/scopes/tekscope/basic_curve_query.py
index 219b1a8c..aeb70d6d 100644
--- a/examples/scopes/tekscope/basic_curve_query.py
+++ b/examples/scopes/tekscope/basic_curve_query.py
@@ -1,9 +1,11 @@
"""An example showing a basic curve query."""
+from pathlib import Path
+
from tm_devices import DeviceManager
from tm_devices.drivers import AFG3KC, MSO5
-EXAMPLE_CSV_FILE = "example_curve_query.csv"
+EXAMPLE_CSV_FILE = Path("example_curve_query.csv")
with DeviceManager(verbose=True) as dm:
scope: MSO5 = dm.add_scope("MSO56-100083")
@@ -16,7 +18,7 @@
curve_returned = scope.curve_query(1, output_csv_file=EXAMPLE_CSV_FILE)
# Read in the curve query from file
-with open(EXAMPLE_CSV_FILE, encoding="utf-8") as csv_content:
+with EXAMPLE_CSV_FILE.open(encoding="utf-8") as csv_content:
curve_saved = [int(i) for i in csv_content.read().split(",")]
# Verify query saved to csv is the same as the one returned from curve_query function call
diff --git a/examples/scopes/tekscope/basic_save_recall.py b/examples/scopes/tekscope/basic_save_recall.py
index 33ac6b1a..03c6ca3e 100644
--- a/examples/scopes/tekscope/basic_save_recall.py
+++ b/examples/scopes/tekscope/basic_save_recall.py
@@ -5,7 +5,7 @@
with DeviceManager(verbose=True) as dm:
# Get a scope
- scope: MSO6B = dm.add_scope("192.168.1.177")
+ scope: MSO6B = dm.add_scope("192.168.0.1")
# Send some commands
scope.add_new_math("MATH1", "CH1") # add MATH1 to CH1
diff --git a/examples/scopes/tekscope/generate_internal_afg_signal.py b/examples/scopes/tekscope/generate_internal_afg_signal.py
index e66eca9b..cf0a05f7 100644
--- a/examples/scopes/tekscope/generate_internal_afg_signal.py
+++ b/examples/scopes/tekscope/generate_internal_afg_signal.py
@@ -5,7 +5,7 @@
with DeviceManager(verbose=True) as dm:
# Create a connection to the scope and indicate that it is a MSO5 scope for type hinting
- scope: MSO5 = dm.add_scope("192.168.1.102")
+ scope: MSO5 = dm.add_scope("192.168.0.1")
# Generate the signal using individual PI commands.
scope.commands.afg.frequency.write(10e6) # set frequency
diff --git a/examples/scopes/tekscope/save_screenshot.py b/examples/scopes/tekscope/save_screenshot.py
index 7146ff3e..3503c3ba 100644
--- a/examples/scopes/tekscope/save_screenshot.py
+++ b/examples/scopes/tekscope/save_screenshot.py
@@ -5,7 +5,7 @@
with DeviceManager(verbose=True) as dm:
# Add a scope
- scope: MSO6B = dm.add_scope("192.168.1.5")
+ scope: MSO6B = dm.add_scope("192.168.0.1")
# Send some commands
scope.add_new_math("MATH1", "CH1") # add MATH1 to CH1
diff --git a/examples/source_measure_units/2400/smu_2450_leakage_current.py b/examples/source_measure_units/2400/smu_2450_leakage_current.py
index 33cf1288..243e2918 100644
--- a/examples/source_measure_units/2400/smu_2450_leakage_current.py
+++ b/examples/source_measure_units/2400/smu_2450_leakage_current.py
@@ -13,7 +13,7 @@
from tm_devices.drivers import SMU2450
with DeviceManager(verbose=False) as device_manager:
- inst: SMU2450 = device_manager.add_smu("192.168.1.4", alias="my2450")
+ inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
# Reset the instrument, which also clears the buffer.
inst.commands.reset()
diff --git a/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py b/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py
index aa930091..08c32e3c 100644
--- a/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py
+++ b/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py
@@ -21,7 +21,7 @@
with DeviceManager() as device_manager:
print(device_manager.get_available_devices())
- inst: SMU2450 = device_manager.add_smu("192.168.4.74", alias="my2450")
+ inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
# Configure the Simple Loop trigger model template to make 100 readings.
inst.commands.trigger.model.load_simple_loop(100)
diff --git a/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py b/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py
index 212b9142..7cc0983e 100644
--- a/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py
+++ b/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py
@@ -19,7 +19,7 @@
with DeviceManager() as device_manager:
print(device_manager.get_available_devices())
- inst: SMU2450 = device_manager.add_smu("192.168.4.74", alias="my2450")
+ inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
# Clear the buffer.
inst.commands.buffer_var["defbuffer1"].clear()
diff --git a/examples/source_measure_units/2400/smu_2450_solar_cell.py b/examples/source_measure_units/2400/smu_2450_solar_cell.py
index 56dba26c..05589d3e 100644
--- a/examples/source_measure_units/2400/smu_2450_solar_cell.py
+++ b/examples/source_measure_units/2400/smu_2450_solar_cell.py
@@ -15,7 +15,7 @@
with DeviceManager() as device_manager:
print(device_manager.get_available_devices())
- inst: SMU2450 = device_manager.add_smu("192.168.4.74", alias="my2450")
+ inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
# Define the number of points in the sweep.
POINTS = 56
diff --git a/pyproject.toml b/pyproject.toml
index 0ae8a008..24e3b86c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -76,6 +76,7 @@ repository = "https://github.com/tektronix/tm_devices"
version = "2.5.0"
[tool.poetry.dependencies]
+colorlog = "^6.9.0"
gpib-ctypes = "^0.3.0"
libusb-package = "^1.0.26.0,!=1.0.26.2" # 1.0.26.2 doesn't work with Python 3.12
packaging = "^24.0"
@@ -94,6 +95,7 @@ tomli = "^2.0.1"
tomli-w = "^1.0.0"
traceback-with-variables = "^2.0.4"
typing-extensions = "^4.10.0"
+tzlocal = "^5.2"
urllib3 = "^2.0"
zeroconf = "^0.136.0"
@@ -110,7 +112,7 @@ pre-commit = [
{python = "3.8", version = "^3.5"}
]
pylint = "3.2.7"
-pyright = {extras = ["nodejs"], version = "^1.1.387"}
+pyright = {extras = ["nodejs"], version = "1.1.389"}
pyroma = "^4.2"
tox = "^4.0"
tox-gh-actions = "^3.1.0"
@@ -235,6 +237,7 @@ disable = [
"too-many-lines", # not necessary to check for
"too-many-statements", # caught by ruff
"too-many-statements", # caught by ruff
+ "unexpected-keyword-arg", # caught by pyright
"unused-argument", # caught by ruff
"unused-import", # caught by ruff
"use-implicit-booleaness-not-comparison-to-string", # caught by ruff
@@ -287,6 +290,7 @@ filterwarnings = [
]
junit_family = "xunit2"
junit_logging = "all"
+log_format = "[%(asctime)s] [%(levelname)8s] %(message)s"
markers = [
'docs',
'order',
@@ -315,27 +319,19 @@ fixable = ["ALL"]
flake8-pytest-style = {mark-parentheses = false}
flake8-quotes = {docstring-quotes = "double"}
ignore = [
- "ANN101", # Missing type annotation for self in method
- "ANN102", # Missing type annotation for cls in method
- "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in *args and **kwargs
- "COM812", # Trailing comma missing
- "EM102", # Exception must not use an f-string literal, assign to variable first
- "FA100", # Missing `from __future__ import annotations`, but uses ...
- "FBT", # flake8-boolean-trap
- "FIX002", # Line contains TO DO
+ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in *args and **kwargs (allowed in this package)
+ "COM812", # Trailing comma missing (allowed in this package)
+ "FA100", # Missing `from __future__ import annotations`, but uses ... (allowed in this package)
+ "FBT", # flake8-boolean-trap # TODO: enable this
+ "FIX002", # Line contains TO DO (allowed in this package)
"ISC001", # single-line-implicit-string-concatenation (handled by formatter)
- "PTH109", # `os.getcwd()` should be replaced by `Path.cwd()`
- "PTH123", # `open()` should be replaced by `Path.open()`
- "PTH207", # Replace `iglob` with `Path.glob` or `Path.rglob`
- "PYI021", # Docstrings should not be included in stubs
- "T20", # flake8-print
- "TD002", # Missing author in TO DO
- "TD003", # Missing issue link on the line following this TO DO
- "TRY301", # Abstract raise to an inner function
- "UP006", # Use {to} instead of {from} for type annotation
- "UP007", # Use `X | Y` for type annotations
- "UP024", # Replace aliased errors with `OSError`
- "UP037" # Remove quotes from type annotation
+ "PYI021", # Docstrings should not be included in stubs (allowed in this package)
+ "TD002", # Missing author in TO DO (allowed in this package)
+ "TD003", # Missing issue link on the line following this TO DO (allowed in this package)
+ "UP006", # Use {to} instead of {from} for type annotation (allowed in this package)
+ "UP007", # Use `X | Y` for type annotations (allowed in this package)
+ "UP024", # Replace aliased errors with `OSError` (allowed in this package)
+ "UP037" # Remove quotes from type annotation (allowed in this package)
]
pydocstyle = {convention = "google"}
pylint = {max-args = 7}
@@ -357,7 +353,9 @@ order-by-type = false
[tool.ruff.lint.per-file-ignores]
"examples/**" = [
- "S101" # Use of assert detected
+ "FBT", # flake8-boolean-trap
+ "S101", # Use of assert detected
+ "T201" # `print` found
]
"examples/miscellaneous/custom_device_driver_support.py" = [
"ARG002", # Unused method argument
@@ -445,6 +443,7 @@ setenv =
commands_pre =
python -m poetry install --no-root --without=main
commands =
+ !tests: python -c "import shutil; shutil.rmtree('dist_{envname}', ignore_errors=True)"
!tests: poetry build --output=dist_{envname}
!tests: twine check --strict dist_{envname}/*
!tests: pre-commit run --all-files
diff --git a/scripts/contributor_setup.py b/scripts/contributor_setup.py
index bcb29daf..a5bdca28 100644
--- a/scripts/contributor_setup.py
+++ b/scripts/contributor_setup.py
@@ -24,7 +24,7 @@ def create_virtual_environment(virtual_env_dir: str | os.PathLike[str]) -> None:
Args:
virtual_env_dir: The directory where the virtual environment should be created
"""
- print(f"\nCreating virtualenv located at '{virtual_env_dir}'")
+ print(f"\nCreating virtualenv located at '{virtual_env_dir}'") # noqa: T201
_run_cmd_in_subprocess(f'"{sys.executable}" -m venv "{virtual_env_dir}" --clear')
@@ -35,7 +35,7 @@ def _run_cmd_in_subprocess(command: str) -> None:
command: The command string to send.
"""
command = command.replace("\\", "/")
- print(f"\nExecuting command: {command}")
+ print(f"\nExecuting command: {command}") # noqa: T201
subprocess.check_call(shlex.split(command)) # noqa: S603
@@ -48,7 +48,7 @@ def main() -> None:
starting_dir = Path.cwd()
try:
if RUNNING_IN_VIRTUALENV:
- raise IndexError
+ raise IndexError # noqa: TRY301
# This requires contributors to use newer versions of Python even
# though the package supports older versions.
if sys.version_info < (3, 9):
@@ -76,7 +76,7 @@ def main() -> None:
files = list(
filter(
lambda x: "site-packages" not in x and "pythonw" not in x,
- glob.iglob(
+ glob.iglob( # noqa: PTH207
f"{virtual_env_dir}/{'bin' if RUNNING_ON_LINUX else 'Scripts'}/**/python*",
recursive=True,
),
diff --git a/src/tm_devices/__init__.py b/src/tm_devices/__init__.py
index e4e4cd00..82f8e70d 100644
--- a/src/tm_devices/__init__.py
+++ b/src/tm_devices/__init__.py
@@ -21,12 +21,16 @@
)
from tm_devices.helpers.enums import SupportedModels
from tm_devices.helpers.functions import register_additional_usbtmc_mapping
+from tm_devices.helpers.logging import configure_logging, LoggingLevels
# Read version from installed package.
__version__ = version(PACKAGE_NAME)
+
__all__ = [
+ "configure_logging",
"DeviceManager",
+ "LoggingLevels",
"print_available_visa_devices",
"PYVISA_PY_BACKEND",
"register_additional_usbtmc_mapping",
diff --git a/src/tm_devices/commands/helpers/scpi_commands.py b/src/tm_devices/commands/helpers/scpi_commands.py
index 99c73a68..7980d004 100644
--- a/src/tm_devices/commands/helpers/scpi_commands.py
+++ b/src/tm_devices/commands/helpers/scpi_commands.py
@@ -76,7 +76,7 @@ def verify(self, value: Union[float, str]) -> Tuple[bool, str]:
Returns:
A tuple containing a boolean indicating if the values match and a string with the actual
- return value from the device.
+ return value from the device.
Raises:
tm_devices.commands.NoDeviceProvidedError: Indicates that no device connection exists.
@@ -136,7 +136,7 @@ def verify(self, argument: str, value: Union[float, str]) -> Tuple[bool, str]:
Returns:
A tuple containing a boolean indicating if the values match and a string with the actual
- return value from the device.
+ return value from the device.
Raises:
tm_devices.commands.NoDeviceProvidedError: Indicates that no device connection exists.
diff --git a/src/tm_devices/components/dm_config_parser.py b/src/tm_devices/components/dm_config_parser.py
index 7c515c6e..e35d22c0 100644
--- a/src/tm_devices/components/dm_config_parser.py
+++ b/src/tm_devices/components/dm_config_parser.py
@@ -430,7 +430,7 @@ def __parse_config_file(
if not config_path.is_file():
raise FileNotFoundError(config_path)
# read in data
- with open(config_path, encoding="utf-8") as config_file:
+ with config_path.open(encoding="utf-8") as config_file:
if config_path.suffix == ".toml":
data = tomli.loads(config_file.read())
else: # ["yaml", "yml"]
diff --git a/src/tm_devices/device_manager.py b/src/tm_devices/device_manager.py
index c5fa1b4f..58b4647f 100644
--- a/src/tm_devices/device_manager.py
+++ b/src/tm_devices/device_manager.py
@@ -6,6 +6,7 @@
import contextlib
import inspect
import json
+import logging
import os
import pathlib
import socket
@@ -36,6 +37,7 @@
from tm_devices.helpers import (
AliasDict,
check_for_update,
+ configure_logging,
ConnectionTypes,
create_visa_connection,
detect_visa_resource_expression,
@@ -44,7 +46,6 @@
DMConfigOptions,
get_model_series,
PACKAGE_NAME,
- print_with_timestamp,
PYVISA_PY_BACKEND,
SerialConfig,
Singleton,
@@ -98,6 +99,8 @@
UnsupportedDeviceAlias = TypeVar("UnsupportedDeviceAlias", bound=Device, default=Device)
"""An alias to a custom device driver for an unsupported device type."""
+_logger: logging.Logger = logging.getLogger(__name__)
+
####################################################################################################
# DeviceManager class
@@ -124,7 +127,11 @@ def __init__(
"""Create the instance of the DeviceManager.
Args:
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. This flag cascades
+ down to all connected devices. Setting it to False **will not** remove all printouts
+ to stdout. To remove all console output, use
+ [`configure_logging()`][tm_devices.configure_logging] before instantiating the
+ DeviceManager.
config_options: An optional set of configuration options to use
(updates the current configuration options).
external_device_drivers: An optional dict for passing in additional device drivers.
@@ -151,6 +158,14 @@ def __init__(
# actually populate the options
self.__set_options(verbose)
+ # Pass in the options from the config file or environment variable to the logger
+ logging_options = {
+ key: value
+ for key, value in self.__config.options.to_dict(ignore_none=True).items()
+ if key.startswith("log_")
+ }
+ configure_logging(**logging_options)
+
self.open()
def __del__(self) -> None:
@@ -249,8 +264,11 @@ def verbose_visa(self, value: bool) -> None:
visa.log_to_screen()
else:
for handler in visa.logger.handlers.copy():
- if "DEBUG" in str(handler):
- visa.logger.removeHandler(handler)
+ if "DEBUG" in str(handler) and (
+ isinstance(handler, logging.StreamHandler)
+ and not isinstance(handler, logging.FileHandler)
+ ):
+ visa.logger.removeHandler(handler) # pyright: ignore[reportUnknownArgumentType]
self.__config.options.verbose_visa = value
self.__verbose_visa = value
@@ -683,21 +701,20 @@ def cleanup_all_devices(self) -> None:
try:
device_object.cleanup(verbose=bool(self.__config.options.verbose_mode))
except (visa.errors.Error, socket.error, RPCError, AttributeError): # noqa: PERF203
- print(f"Device cleanup of {device_name} failed. Retrying...")
+ _logger.warning("Device cleanup of %s failed. Retrying...", device_name)
device_object.cleanup()
def close(self) -> None:
"""Close the DeviceManager."""
self.__protect_access()
- print()
if self.__devices:
- print_with_timestamp("Closing Connections to Devices")
+ _logger.info("Closing Connections to Devices")
if self.__teardown_cleanup_enabled:
self.cleanup_all_devices()
for device_object in list(self.__devices.values()):
device_object.close()
self.__is_open = False
- print_with_timestamp(f"{self.__class__.__name__} Closed")
+ _logger.info("%s Closed", self.__class__.__name__)
def disable_device_command_checking(self) -> None:
"""Set the `.enable_verification` attribute of each device to `False`.
@@ -1007,13 +1024,11 @@ def load_config_file(self, config_file_path: Union[str, os.PathLike[str]]) -> No
Args:
config_file_path: The path to the config file to load.
"""
- print_with_timestamp(
- f"Loading Configuration from {pathlib.Path(config_file_path).resolve()}"
- )
+ _logger.info("Loading Configuration from %s", pathlib.Path(config_file_path).resolve())
self.__config.load_config_file(config_file_path)
self.__set_options(self.__verbose)
if len(self.__config.devices) != len(self.__devices):
- print_with_timestamp("Opening Connections to Devices")
+ _logger.info("Opening Connections to Devices")
for device_name, device_config in self.__config.devices.items():
if device_name not in self.__devices:
self.__create_device(device_name, device_config, 3)
@@ -1031,10 +1046,10 @@ def open(self) -> bool:
f"{self.__class__.__name__} has already been closed."
)
raise AssertionError(msg)
- print_with_timestamp(f"Opening {self.__class__.__name__}")
+ _logger.info("Opening %s", self.__class__.__name__)
# Create the devices
if self.__config.devices:
- print_with_timestamp("Opening Connections to Devices")
+ _logger.info("Opening Connections to Devices")
for device_name, device_config in self.__config.devices.items():
self.__create_device(device_name, device_config, 3)
if self.__setup_cleanup_enabled:
@@ -1093,9 +1108,9 @@ def write_current_configuration_to_config_file(
Args:
config_file_path: The path to the config file. If ends in ".toml" will create toml file.
"""
- print_with_timestamp("Writing Configuration to file")
+ _logger.info("Writing Configuration to file")
new_file_path = pathlib.Path(self.__config.write_config_to_file(config_file_path)).resolve()
- print_with_timestamp(f"Wrote Configuration to {new_file_path}")
+ _logger.info("Wrote Configuration to %s", new_file_path)
def get_current_configuration_as_environment_variable_strings(self) -> str:
"""Return the current configuration represented as environment variables."""
@@ -1191,23 +1206,25 @@ def __clear_visa_output_buffer_and_get_idn(visa_resource: MessageBasedResource)
# 16 is the MAV bit (Message Available) from the Status Byte register
# MAV flag is only one bit and turns off after a single response is
# successfully read, even if there is more in the buffer.
- warnings.warn(
+ msg = (
f"\nThe device `{visa_resource.resource_info.resource_name}` had data "
"sitting in the VISA Output Buffer on first connection. "
"\nDetected data in the buffer via the Status Byte register. "
- "\nThe device_clear() will be called so VISA I/O buffers get flushed.",
- stacklevel=1,
+ "\nThe device_clear() will be called so VISA I/O buffers get flushed."
)
+ warnings.warn(msg, stacklevel=1)
+ _logger.warning(msg)
# always flush the VISA I/O Buffers on the device to clean up any stale data.
# (note: the Events are kept in different buffers, so *ESR? is not impacted)
visa_resource.clear()
except visa.VisaIOError as e:
- warnings.warn(
+ msg = (
f"A VISA IO error occurred when attempting to read the status byte or clear the "
f"output buffer of the resource `{visa_resource.resource_info.resource_name}`.\n"
- f"Error: {e}",
- stacklevel=1,
+ f"Error: {e}"
)
+ warnings.warn(msg, stacklevel=1)
+ _logger.warning(msg)
visa_resource.write("*IDN?")
idn_response = ""
error_msg = None
@@ -1271,14 +1288,15 @@ def __create_device(
alias_string = f' "{device_config.alias}"' if device_config.alias else ""
if device_config.device_type == DeviceTypes.UNSUPPORTED:
- warnings.warn(
+ msg = (
f"An unsupported device type is being added to the {self.__class__.__name__}. "
f"Not all functionality will be available in the device driver. "
f"Please consider contributing to {PACKAGE_NAME} to implement official "
- f"support for this device type.",
- stacklevel=warning_stacklevel,
+ f"support for this device type."
)
- print_with_timestamp(f"Creating Connection to {device_config_name}{alias_string}")
+ warnings.warn(msg, stacklevel=warning_stacklevel)
+ _logger.warning(msg)
+ _logger.info("Creating Connection to %s%s", device_config_name, alias_string)
new_device: Device
if device_config.connection_type == ConnectionTypes.REST_API:
device_driver_class = device_drivers[str(device_config.device_driver)]
@@ -1436,4 +1454,4 @@ def print_available_visa_devices() -> None: # pragma: no cover
"""Print all available VISA devices to the console."""
with contextlib.redirect_stdout(None), contextlib.redirect_stderr(None), DeviceManager() as dm:
available_devices = dm.get_available_devices()
- print(json.dumps(available_devices["local"], indent=2))
+ print(json.dumps(available_devices["local"], indent=2)) # noqa: T201
diff --git a/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py b/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py
index 3756411f..2030bc07 100644
--- a/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py
+++ b/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py
@@ -15,7 +15,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
"""
self._is_open = False
self._verbose = verbose
@@ -41,5 +43,5 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
diff --git a/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py b/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py
index bf1a31fb..da654c4d 100644
--- a/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py
+++ b/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py
@@ -1,5 +1,7 @@
"""A class defining methods that VISA devices and control mixins must have."""
+import logging
+
from abc import abstractmethod
from typing import Any, final, Tuple
@@ -8,6 +10,8 @@
)
from tm_devices.helpers import raise_failure, verify_values
+_logger: logging.Logger = logging.getLogger(__name__)
+
class _AbstractDeviceVISAWriteQueryControl(_AbstractDeviceControl): # pyright: ignore[reportUnusedClass]
"""Abstract class defining methods that VISA devices and control mixins must have."""
@@ -39,8 +43,8 @@ def expect_esr(
Returns:
A boolean indicating if the check passed or failed, True means the check passed,
- False means the check failed (however, failing the check will always result in an
- AssertionError being raised, so the result will not really be usable).
+ False means the check failed (however, failing the check will always result in an
+ AssertionError being raised, so the result will not really be usable).
Raises:
AssertionError: Indicating that the device's error code and messages don't match the
@@ -64,7 +68,7 @@ def expect_esr(
)
except AssertionError as exc:
check_passed &= False
- print(exc) # the exception already contains the timestamp
+ _logger.warning(exc)
# Compare the error messages
for expected_message, actual_message in zip(error_messages, actual_error_messages):
@@ -79,7 +83,7 @@ def expect_esr(
)
except AssertionError as exc: # noqa: PERF203
check_passed &= False
- print(exc)
+ _logger.warning(exc)
if not check_passed:
failure_message = (
diff --git a/src/tm_devices/driver_mixins/device_control/pi_control.py b/src/tm_devices/driver_mixins/device_control/pi_control.py
index 2d3249b2..b743ad65 100644
--- a/src/tm_devices/driver_mixins/device_control/pi_control.py
+++ b/src/tm_devices/driver_mixins/device_control/pi_control.py
@@ -1,12 +1,15 @@
"""Base Programmable Interface (PI) control class module."""
+import contextlib
+import logging
+import logging.handlers
import os
import socket
import time
import warnings
from abc import ABC
-from contextlib import contextmanager
+from pathlib import Path
from typing import final, Generator, List, Optional, Sequence, Tuple, Union
import pyvisa as visa
@@ -29,12 +32,14 @@
get_model_series,
get_version,
get_visa_backend,
- print_with_timestamp,
PYVISA_PY_BACKEND,
raise_failure,
verify_values,
)
from tm_devices.helpers import ReadOnlyCachedProperty as cached_property # noqa: N813
+from tm_devices.helpers.constants_and_dataclasses import PACKAGE_NAME
+
+_logger: logging.Logger = logging.getLogger(__name__)
class PIControl(_AbstractDeviceVISAWriteQueryControl, _ExtendableMixin, ABC): # pylint: disable=too-many-public-methods
@@ -64,7 +69,9 @@ def __init__(
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
visa_resource: The VISA resource object.
default_visa_timeout: The default VISA timeout value in milliseconds.
"""
@@ -122,8 +129,7 @@ def visa_timeout(self, timeout_ms: float) -> None:
timeout_ms: The new VISA timeout, in milliseconds.
"""
self._visa_resource.timeout = timeout_ms
- if self._verbose:
- print(f"{self._name_and_alias} VISA timeout set to: {timeout_ms}ms")
+ _logger.debug("%s VISA timeout set to: %.3fms", self._name_and_alias, timeout_ms)
@property
def visa_resource(self) -> visa.resources.MessageBasedResource:
@@ -190,7 +196,7 @@ def visa_backend(self) -> str:
################################################################################################
# Context Manager Methods
################################################################################################
- @contextmanager
+ @contextlib.contextmanager
def temporary_visa_timeout(self, temporary_timeout_ms: float) -> Generator[None, None, None]:
"""Set a temporary VISA timeout value for the duration of the context.
@@ -210,19 +216,15 @@ def temporary_visa_timeout(self, temporary_timeout_ms: float) -> Generator[None,
# Public Methods
################################################################################################
@final
- def check_visa_connection(self, verbose: bool = True) -> bool:
+ def check_visa_connection(self) -> bool:
"""Check if a VISA connection can be made to the device.
Wrapper function for [`check_visa_connection`][tm_devices.helpers.check_visa_connection].
-
- Args:
- verbose: Set this to False in order to disable printouts.
"""
return check_visa_connection(
self._config_entry,
self._visa_library_path,
self._name_and_alias,
- verbose=verbose and self._verbose,
)
def device_clear(self) -> None: # pragma: no cover
@@ -247,7 +249,7 @@ def get_visa_stb(self) -> int: # pragma: no cover
"""Return the VISA status byte."""
return self._visa_resource.read_stb()
- def poll_query( # noqa: PLR0913
+ def poll_query( # noqa: PLR0913 # pylint: disable=too-many-locals
self,
number_of_polls: int,
query: str,
@@ -312,10 +314,11 @@ def poll_query( # noqa: PLR0913
return
time.sleep(sleep_time)
poll_number += 1
- raise AssertionError( # noqa: TRY003
+ msg = (
f"{query} {'never' if not invert_range else 'always'} "
f"returned {wanted_val}, received:\n{query_list}"
)
+ raise AssertionError(msg)
def query( # pylint: disable=arguments-differ
self,
@@ -340,9 +343,13 @@ def query( # pylint: disable=arguments-differ
Error: An error occurred while sending the command.
SystemError: An empty string was returned from the device.
"""
- if self._verbose and verbose:
- print_with_timestamp(f"({self._name_and_alias}) Query >> {query!r}")
-
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "(%s) Query >> %r",
+ self._name_and_alias,
+ query,
+ )
+ self._command_logger.debug(query)
try:
response = self._visa_resource.query(query).strip()
if remove_quotes:
@@ -352,8 +359,12 @@ def query( # pylint: disable=arguments-differ
msg = f"The query{pi_cmd_repr}failed with the following message: {error!r}"
raise visa.Error(msg) from error
- if self._verbose and verbose:
- print_with_timestamp(f"Response from {query!r} >> {response!r}")
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "Response from %r >> %r",
+ query,
+ response,
+ )
if not allow_empty and not response:
pi_cmd_repr = (
@@ -378,9 +389,13 @@ def query_binary(self, query: str, verbose: bool = True) -> Sequence[float]:
Error: An error occurred while sending the command.
SystemError: An empty string was returned from the device.
"""
- if self._verbose and verbose:
- print_with_timestamp(f"({self._name_and_alias}) Query Binary Values >> {query!r}")
-
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "(%s) Query Binary Values >> %r",
+ self._name_and_alias,
+ query,
+ )
+ self._command_logger.debug(query)
try:
response = self._visa_resource.query_binary_values(query) # pyright: ignore[reportUnknownMemberType]
except (visa.VisaIOError, socket.error) as error:
@@ -388,10 +403,12 @@ def query_binary(self, query: str, verbose: bool = True) -> Sequence[float]:
msg = f"The query{pi_cmd_repr}failed with the following message: {error!r}"
raise visa.Error(msg) from error
- if self._verbose and verbose:
- print_with_timestamp(
- f"Response from {query!r} >> {', '.join([str(x) for x in response])!r}"
- )
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "Response from %r >> %r",
+ query,
+ response,
+ )
if not response:
pi_cmd_repr = (
@@ -501,9 +518,13 @@ def query_raw_binary(self, query: str, verbose: bool = True) -> bytes:
Error: An error occurred while sending the command.
SystemError: An empty string was returned from the device.
"""
- if self._verbose and verbose:
- print_with_timestamp(f"({self._name_and_alias}) Query Raw Binary >> {query!r}")
-
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "(%s) Query Raw Binary >> %r",
+ self._name_and_alias,
+ query,
+ )
+ self._command_logger.debug(query)
try:
self._visa_resource.write(query)
response = self.read_raw()
@@ -512,8 +533,12 @@ def query_raw_binary(self, query: str, verbose: bool = True) -> bytes:
msg = f"The query{pi_cmd_repr}failed with the following message: {error!r}"
raise visa.Error(msg) from error
- if self._verbose and verbose:
- print_with_timestamp(f"Response from {query!r} >> {response!r}")
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "Response from %r >> %r",
+ query,
+ response,
+ )
if not response.strip():
pi_cmd_repr = (
@@ -699,7 +724,7 @@ def set_if_needed( # noqa: PLR0913
Returns:
Tuple containing the boolean value indicating if the command needed to be set and
- the value returned from the query.
+ the value returned from the query.
"""
try:
query_passed, actual_value = self.query_response(
@@ -749,7 +774,6 @@ def wait_for_visa_connection(
wait_time: float,
sleep_seconds: int = 5,
accept_immediate_connection: bool = False,
- verbose: bool = True,
) -> bool:
"""Wait for a VISA connection to be made to the device.
@@ -758,7 +782,6 @@ def wait_for_visa_connection(
sleep_seconds: The number of seconds to sleep in between connection attempts.
accept_immediate_connection: A boolean indicating if a connection on the
first attempt is a valid connection.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if a VISA connection was made within the given time limit.
@@ -768,13 +791,14 @@ def wait_for_visa_connection(
"""
attempt_num = 0
visa_connection = False
- if verbose:
- print_with_timestamp(
- f"Attempting to establish a VISA connection with {self._resource_expression}"
- )
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Attempting to establish a VISA connection with %s",
+ self._resource_expression,
+ )
start_time = time.perf_counter()
while (time.perf_counter() - start_time) <= wait_time:
- if visa_connection := self.check_visa_connection(verbose=False):
+ if visa_connection := self.check_visa_connection():
# pylint: disable=compare-to-zero
if not (attempt_num == 0 and not accept_immediate_connection):
break
@@ -790,17 +814,19 @@ def wait_for_visa_connection(
end_time = time.perf_counter()
total_time = end_time - start_time
- if verbose:
- if visa_connection:
- print_with_timestamp(
- f"Successfully established a VISA connection with {self._resource_expression} "
- f"after {total_time:.2f} seconds"
- )
- else:
- print_with_timestamp(
- f"Unable to establish a VISA connection with {self._resource_expression} "
- f"after {total_time:.2f} seconds"
- )
+ if visa_connection:
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Successfully established a VISA connection with %s after %.2f seconds",
+ self._resource_expression,
+ total_time,
+ )
+ else:
+ _logger.warning(
+ "Unable to establish a VISA connection with %s after %.2f seconds",
+ self._resource_expression,
+ total_time,
+ )
return visa_connection
@@ -816,20 +842,21 @@ def write(self, command: str, opc: bool = False, verbose: bool = True) -> None:
Error: An error occurred while sending the command.
SystemError: ``*OPC?`` did not return "1" after sending the command.
"""
- if self._verbose and verbose:
- if "\n" in command:
- # Format any multiline command to print out with a single timestamp
- # followed by as many (whitespace padded) f">> {cmd}" lines as it has
- commands_iter = iter(repr(command.strip()).split("\\n"))
- spaces = " " * len(
- print_with_timestamp(
- f"({self._name_and_alias}) Write >> {next(commands_iter)}"
- ).split(">> ")[0]
- )
- print(*[f"{spaces}>> {cmd}" for cmd in commands_iter], sep="\n")
- else:
- print_with_timestamp(f"({self._name_and_alias}) Write >> {command!r}")
-
+ if "\n" in command:
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "(%s) Write >>\n%s",
+ self._name_and_alias,
+ "\n".join([" " + x for x in command.split("\n")]),
+ )
+ else:
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "(%s) Write >> %r",
+ self._name_and_alias,
+ command,
+ )
+ self._command_logger.debug(command)
try:
self._visa_resource.write(command)
except (visa.VisaIOError, socket.error) as error:
@@ -852,9 +879,13 @@ def write_raw(self, command: bytes, verbose: bool = True) -> None:
Raises:
Error: An error occurred while sending the command.
"""
- if self._verbose and verbose:
- print_with_timestamp(f"({self._name_and_alias}) Write Raw >> {command!r}")
-
+ _logger.log(
+ logging.INFO if self._verbose and verbose else logging.DEBUG,
+ "(%s) Write Raw >> %r",
+ self._name_and_alias,
+ command,
+ )
+ self._command_logger.debug(command)
try:
self._visa_resource.write_raw(command)
except (visa.VisaIOError, socket.error) as error:
@@ -918,12 +949,44 @@ def _close(self) -> None:
try:
self._visa_resource.close()
except VisaIOError as error:
- warnings.warn(
- f"Error encountered while closing the visa resource:\n{error}", stacklevel=2
- )
+ error_msg = f"Error encountered while closing the visa resource:\n{error}"
+ warnings.warn(error_msg, stacklevel=2)
+ _logger.exception(error_msg)
self._visa_resource = None # pyright: ignore[reportAttributeAccessIssue]
self._is_open = False
+ @cached_property
+ def _command_logger(self) -> logging.Logger:
+ """Create a logger that will be used to log commands sent via write/query-like methods."""
+ # Create the logger
+ command_logger = logging.getLogger(f"{self._config_entry.address}-visa_logger")
+ command_logger.setLevel(logging.DEBUG)
+ command_logger.propagate = False
+ command_logger.addHandler(logging.NullHandler())
+ with contextlib.suppress(IndexError, StopIteration):
+ # Get the top-level log filepath
+ main_log_file = Path(
+ next(
+ x
+ for x in logging.getLogger(PACKAGE_NAME).handlers
+ if isinstance(x, logging.FileHandler)
+ ).baseFilename
+ )
+ # Create the handler with the filename based on the main log file
+ command_log_handler = logging.FileHandler(
+ main_log_file.as_posix().replace(
+ main_log_file.suffix, f"_visa_commands_{self._config_entry.address}.log"
+ ),
+ mode="w",
+ encoding="utf-8",
+ )
+ # Create the formatter
+ command_log_formatter = logging.Formatter("%(message)s")
+ command_log_handler.setFormatter(command_log_formatter)
+ command_log_handler.setLevel(logging.DEBUG)
+ command_logger.addHandler(command_log_handler)
+ return command_logger
+
def _open(self) -> bool:
"""Open necessary resources and components and return a boolean indicating success."""
opened = True
diff --git a/src/tm_devices/driver_mixins/device_control/rest_api_control.py b/src/tm_devices/driver_mixins/device_control/rest_api_control.py
index d103a3d7..227639dd 100644
--- a/src/tm_devices/driver_mixins/device_control/rest_api_control.py
+++ b/src/tm_devices/driver_mixins/device_control/rest_api_control.py
@@ -1,6 +1,7 @@
"""Base REST Application Programming Interface (API) control class module."""
import json
+import logging
import time
from abc import ABC, abstractmethod
@@ -14,11 +15,12 @@
)
from tm_devices.helpers import (
DeviceConfigEntry,
- print_with_timestamp,
raise_failure,
SupportedRequestTypes,
)
+_logger: logging.Logger = logging.getLogger(__name__)
+
class RESTAPIControl(_AbstractDeviceControl, ABC):
"""Base REST Application Programming Interface (API) control class.
@@ -41,7 +43,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
"""
super().__init__(config_entry, verbose)
@@ -76,7 +80,6 @@ def delete( # noqa: PLR0913
allow_errors: bool = False,
verify_ssl: bool = True,
allow_redirects: bool = False,
- verbose: bool = True,
) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
"""Perform a DELETE request with the given url and headers.
@@ -90,7 +93,6 @@ def delete( # noqa: PLR0913
verify_ssl: A bool that indicates if the SSL certificate should be verified.
allow_redirects: A bool that indicates if URL redirects should be allowed.
allow_errors: A boolean indicating if errors are allowed.
- verbose: Set this to False in order to disable printouts.
Returns:
A tuple containing a success boolean, the response data, status code, and any errors
@@ -105,7 +107,6 @@ def delete( # noqa: PLR0913
allow_errors=allow_errors,
verify_ssl=verify_ssl,
allow_redirects=allow_redirects,
- verbose=verbose,
)
# pylint: disable=too-many-arguments
@@ -119,7 +120,6 @@ def get( # noqa: PLR0913
allow_errors: bool = False,
verify_ssl: bool = True,
allow_redirects: bool = False,
- verbose: bool = True,
) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
"""Perform a GET request with the given url and headers.
@@ -133,7 +133,6 @@ def get( # noqa: PLR0913
verify_ssl: A bool that indicates if the SSL certificate should be verified.
allow_redirects: A bool that indicates if URL redirects should be allowed.
allow_errors: A boolean indicating if errors are allowed.
- verbose: Set this to False in order to disable printouts.
Returns:
A tuple containing a success boolean, the response data, status code, and any errors
@@ -148,7 +147,6 @@ def get( # noqa: PLR0913
allow_errors=allow_errors,
verify_ssl=verify_ssl,
allow_redirects=allow_redirects,
- verbose=verbose,
)
# pylint: disable=too-many-arguments
@@ -164,7 +162,6 @@ def patch( # noqa: PLR0913
allow_errors: bool = False,
verify_ssl: bool = True,
allow_redirects: bool = False,
- verbose: bool = True,
) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
"""Perform a PATCH request with the given url and headers.
@@ -183,7 +180,6 @@ def patch( # noqa: PLR0913
verify_ssl: A bool that indicates if the SSL certificate should be verified.
allow_redirects: A bool that indicates if URL redirects should be allowed.
allow_errors: A boolean indicating if errors are allowed.
- verbose: Set this to False in order to disable printouts.
Returns:
A tuple containing a success boolean, the response data, status code, and any errors
@@ -200,7 +196,6 @@ def patch( # noqa: PLR0913
allow_errors=allow_errors,
verify_ssl=verify_ssl,
allow_redirects=allow_redirects,
- verbose=verbose,
)
# pylint: disable=too-many-arguments
@@ -216,7 +211,6 @@ def post( # noqa: PLR0913
allow_errors: bool = False,
verify_ssl: bool = True,
allow_redirects: bool = False,
- verbose: bool = True,
) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
"""Perform a POST request with the given url and headers.
@@ -235,7 +229,6 @@ def post( # noqa: PLR0913
verify_ssl: A bool that indicates if the SSL certificate should be verified.
allow_redirects: A bool that indicates if URL redirects should be allowed.
allow_errors: A boolean indicating if errors are allowed.
- verbose: Set this to False in order to disable printouts.
Returns:
A tuple containing a success boolean, the response data, status code, and any errors
@@ -252,7 +245,6 @@ def post( # noqa: PLR0913
allow_errors=allow_errors,
verify_ssl=verify_ssl,
allow_redirects=allow_redirects,
- verbose=verbose,
)
# pylint: disable=too-many-arguments
@@ -268,7 +260,6 @@ def put( # noqa: PLR0913
allow_errors: bool = False,
verify_ssl: bool = True,
allow_redirects: bool = False,
- verbose: bool = True,
) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
"""Perform a PUT request with the given url and headers.
@@ -287,7 +278,6 @@ def put( # noqa: PLR0913
verify_ssl: A bool that indicates if the SSL certificate should be verified.
allow_redirects: A bool that indicates if URL redirects should be allowed.
allow_errors: A boolean indicating if errors are allowed.
- verbose: Set this to False in order to disable printouts.
Returns:
A tuple containing a success boolean, the response data, status code, and any errors
@@ -304,26 +294,27 @@ def put( # noqa: PLR0913
allow_errors=allow_errors,
verify_ssl=verify_ssl,
allow_redirects=allow_redirects,
- verbose=verbose,
)
- def set_api_version(self, api_version: int, verbose: bool = True) -> None:
+ def set_api_version(self, api_version: int) -> None:
"""Set the API version used by the device.
Args:
api_version: The API version to use for this device
- verbose: Set this to False in order to disable printouts.
"""
self._api_url = self._base_url + self.API_VERSIONS[api_version]
- if self._verbose and verbose:
- print(f"{self._name_and_alias} API version set to: {api_version} ({self._api_url})")
+ _logger.debug(
+ "%s API version set to: %d (%s)",
+ self._name_and_alias,
+ api_version,
+ self._api_url,
+ )
def wait_for_api_connection(
self,
wait_time: float,
sleep_seconds: int = 5,
accept_immediate_connection: bool = False,
- verbose: bool = True,
) -> bool:
"""Wait for an API call to go through to the device.
@@ -334,7 +325,6 @@ def wait_for_api_connection(
sleep_seconds: The number of seconds to sleep in between connection attempts.
accept_immediate_connection: A boolean indicating if a connection on the
first attempt is a valid connection.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if an API connection was made within the given time limit.
@@ -344,8 +334,11 @@ def wait_for_api_connection(
"""
attempt_num = 0
api_connection = False
- if verbose:
- print_with_timestamp(f"Attempting to establish an API connection with {self._api_url}")
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Attempting to establish an API connection with %s",
+ self._api_url,
+ )
start_time = time.perf_counter()
while (time.perf_counter() - start_time) <= wait_time:
if api_connection := self._check_api_connection():
@@ -364,17 +357,19 @@ def wait_for_api_connection(
end_time = time.perf_counter()
total_time = end_time - start_time
- if verbose:
- if api_connection:
- print_with_timestamp(
- f"Successfully established an API connection with {self._api_url} "
- f"after {total_time:.2f} seconds"
- )
- else:
- print_with_timestamp(
- f"Unable to establish an API connection with {self._api_url} "
- f"after {total_time:.2f} seconds"
- )
+ if api_connection:
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Successfully established an API connection with %s after %.2f seconds",
+ self._api_url,
+ total_time,
+ )
+ else:
+ _logger.warning(
+ "Unable to establish an API connection with %s after %.2f seconds",
+ self._api_url,
+ total_time,
+ )
return api_connection
################################################################################################
@@ -403,7 +398,6 @@ def _send_request( # noqa: PLR0913,PLR0912,C901
allow_errors: bool = False,
verify_ssl: bool = True,
allow_redirects: bool = False,
- verbose: bool = True,
) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
"""Perform a request with the given url and headers.
@@ -423,7 +417,6 @@ def _send_request( # noqa: PLR0913,PLR0912,C901
verify_ssl: A bool that indicates if the SSL certificate should be verified.
allow_redirects: A bool that indicates if URL redirects should be allowed.
allow_errors: A boolean indicating if errors are allowed.
- verbose: Set this to False in order to disable printouts.
Returns:
A tuple containing a success boolean, the response data, status code, and any errors
@@ -441,13 +434,15 @@ def _send_request( # noqa: PLR0913,PLR0912,C901
url = self._api_url + url
response = cast(requests.Response, None)
retval: Union[Dict[str, Any], bytes] = {}
- if self._verbose and verbose:
- print_with_timestamp(f"({self._name_and_alias}) {request_type.value} >> {url}", end="")
- if headers:
- print(f", {headers=}", end="")
- if json_body:
- print(f", {json_body=}", end="")
- print()
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "(%s) %s >> %s%s%s",
+ self._name_and_alias,
+ getattr(request_type, "value", request_type),
+ url,
+ f", {headers=}" if headers else "",
+ f", {json_body=}" if json_body else "",
+ )
try:
if request_type == SupportedRequestTypes.DELETE:
response = requests.delete(
@@ -503,13 +498,13 @@ def _send_request( # noqa: PLR0913,PLR0912,C901
else:
msg = f"{request_type} is an unsupported request type."
raise ValueError(msg)
- if self._verbose and verbose:
- print_with_timestamp(f"Response from {request_type.value} >>")
- if not return_bytes:
- print(f"{json.dumps(response.json(), indent=2)}")
- else:
- # Print .zip files as byte strings, response.content.decode() throws errors
- print(f"\n{response.text}")
+
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Response from %s >>\n%s",
+ request_type.value,
+ json.dumps(response.json(), indent=2) if not return_bytes else response.text,
+ )
retval = response.content if return_bytes else response.json()
# If the response was successful, no Exception will be raised
response.raise_for_status()
diff --git a/src/tm_devices/driver_mixins/device_control/tsp_control.py b/src/tm_devices/driver_mixins/device_control/tsp_control.py
index 7cca6753..26ff0caa 100644
--- a/src/tm_devices/driver_mixins/device_control/tsp_control.py
+++ b/src/tm_devices/driver_mixins/device_control/tsp_control.py
@@ -2,7 +2,10 @@
from __future__ import annotations
+import logging
+
from abc import ABC
+from pathlib import Path
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
from tm_devices.driver_mixins.device_control.pi_control import PIControl
@@ -13,6 +16,9 @@
import os
+_logger: logging.Logger = logging.getLogger(__name__)
+
+
class TSPControl(PIControl, ABC):
"""Base Test Script Processing (TSP) control class.
@@ -47,7 +53,9 @@ def ieee_cmds(self) -> TSPIEEE4882Commands:
################################################################################################
# Public Methods
################################################################################################
- def export_buffers(self, filepath: str, *args: str, sep: str = ",") -> None:
+ def export_buffers(
+ self, filepath: Union[str, os.PathLike[str]], *args: str, sep: str = ","
+ ) -> None:
"""Export one or more of the device's buffers to the given filepath.
Args:
@@ -55,7 +63,7 @@ def export_buffers(self, filepath: str, *args: str, sep: str = ",") -> None:
args: The buffer name(s) to export.
sep: The delimiter used to separate data. Defaults to ",".
"""
- with open(filepath, mode="w", encoding="utf-8") as file:
+ with Path(filepath).open(mode="w", encoding="utf-8") as file:
buffer_data = self.get_buffers(*args)
column_length = max(len(x) for x in buffer_data.values())
file.write(sep.join(buffer_data) + "\n")
@@ -132,8 +140,7 @@ def load_script(
"""
if file_path is not None:
# script_body argument is overwritten by file contents
- with open(file_path, encoding="utf-8") as script_tsp:
- script_body = script_tsp.read().strip()
+ script_body = Path(file_path).read_text(encoding="utf-8").strip()
# Check if the script exists, delete it if it does
self.write(f"if {script_name} ~= nil then script.delete('{script_name}') end")
@@ -164,11 +171,18 @@ def print_buffers(self, *args: str) -> None:
def fix_width(key: str, value: Any) -> str: # Function to add spaces if needed
return str(value) + " " * (column_widths[key] - len(str(value)))
- print(*[fix_width(x, x) for x in buffer_data])
- for index in range(column_length):
- print(
- *[fix_width(k, v[index] if index < len(v) else "") for k, v in buffer_data.items()]
- )
+ buffer_headers = [fix_width(x, x) for x in buffer_data]
+ buffer_rows: List[List[Any]] = [
+ [fix_width(k, v[index] if index < len(v) else "") for k, v in buffer_data.items()]
+ for index in range(column_length)
+ ]
+ _logger.info(
+ "(%s) Printing Buffers %s >>\n%s\n%s",
+ self._name_and_alias,
+ buffer_headers,
+ " ".join(buffer_headers),
+ "\n".join(" ".join(row) for row in buffer_rows),
+ )
def run_script(self, script_name: str) -> None:
"""Run a TSP script on the instrument.
diff --git a/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py b/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py
index 4b20b20f..17e24547 100644
--- a/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py
+++ b/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py
@@ -30,7 +30,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
result = int(self.query("*ESR?").strip())
allev_list = [
diff --git a/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py b/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py
index cd153154..40874c95 100644
--- a/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py
+++ b/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py
@@ -30,7 +30,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
result = int(self.query("*ESR?").strip())
diff --git a/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py b/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py
index 6ff784d7..184fdea0 100644
--- a/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py
+++ b/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py
@@ -25,7 +25,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
# instrument returns exponential numbers so converting to float before int
error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/data_acquisition_systems/daq6510.py b/src/tm_devices/drivers/data_acquisition_systems/daq6510.py
index 827b7cfb..ca4d0c64 100644
--- a/src/tm_devices/drivers/data_acquisition_systems/daq6510.py
+++ b/src/tm_devices/drivers/data_acquisition_systems/daq6510.py
@@ -55,7 +55,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
# instrument returns exponential numbers so converting to float before int
error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/device.py b/src/tm_devices/drivers/device.py
index 20f9f326..71658a81 100644
--- a/src/tm_devices/drivers/device.py
+++ b/src/tm_devices/drivers/device.py
@@ -1,6 +1,7 @@
"""Base device driver module."""
import concurrent.futures
+import logging
import socket
import time
@@ -29,13 +30,14 @@
check_port_connection,
ConnectionTypes,
DeviceConfigEntry,
- print_with_timestamp,
)
from tm_devices.helpers import ReadOnlyCachedProperty as cached_property # noqa: N813
_T = TypeVar("_T")
_FAMILY_BASE_CLASS_PROPERTY_NAME = "_product_family_base_class"
+_logger: logging.Logger = logging.getLogger(__name__)
+
def family_base_class(cls: _T) -> _T:
"""A decorator indicating a class is the top level of a unique product family tree.
@@ -71,7 +73,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
"""
super().__init__(config_entry, verbose)
self._is_open = True
@@ -146,7 +150,7 @@ def _open(self) -> bool:
def _reboot(self) -> None:
"""Perform the actual rebooting code."""
raise NotImplementedError(
- f"``._reboot()`` is not yet implemented for the {self.__class__.__name__} driver"
+ f"``._reboot()`` is not yet implemented for the {self.__class__.__name__} driver", # noqa: EM102
)
################################################################################################
@@ -177,7 +181,7 @@ def alias(self) -> str:
def command_argument_constants(self) -> Any:
"""Return the device command argument constants."""
raise NotImplementedError(
- f"The {self.__class__.__name__} driver does not have a Python API for its commands yet."
+ f"The {self.__class__.__name__} driver does not have a Python API for its commands yet." # noqa: EM102
)
@property
@@ -212,7 +216,7 @@ def command_verification_enabled(self) -> bool:
def commands(self) -> Any:
"""Return the device commands."""
raise NotImplementedError(
- f"The {self.__class__.__name__} driver does not have a Python API for its commands yet."
+ f"The {self.__class__.__name__} driver does not have a Python API for its commands yet." # noqa: EM102
)
@cached_property
@@ -362,27 +366,20 @@ def temporary_verbose(self, temporary_verbose: bool) -> Generator[None, None, No
# Public Methods
################################################################################################
@final
- def check_network_connection(self, verbose: bool = True) -> Tuple[bool, str]:
+ def check_network_connection(self) -> Tuple[bool, str]:
"""Check the network connection to the device using an external ping command.
Wrapper function for
[`check_network_connection`][tm_devices.helpers.check_network_connection].
- Args:
- verbose: Set this to False in order to disable printouts.
-
Returns:
A tuple containing a boolean indicating if there is a network connection and
- a string with the result of the ping command.
+ a string with the result of the ping command.
"""
- return check_network_connection(
- self._name_and_alias, self.ip_address, verbose=verbose and self._verbose
- )
+ return check_network_connection(self._name_and_alias, self.ip_address)
@final
- def check_port_connection(
- self, port: int, timeout_seconds: int = 5, verbose: bool = True
- ) -> bool:
+ def check_port_connection(self, port: int, timeout_seconds: int = 5) -> bool:
"""Check if the given port is open on the device.
Wrapper function for [`check_port_connection`][tm_devices.helpers.check_port_connection].
@@ -390,7 +387,6 @@ def check_port_connection(
Args:
port: The port to check.
timeout_seconds: The number of seconds to use as the socket timeout.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if the port is open.
@@ -400,7 +396,6 @@ def check_port_connection(
self.ip_address,
port,
timeout_seconds=timeout_seconds,
- verbose=verbose and self._verbose,
)
@final
@@ -411,14 +406,14 @@ def cleanup(self, verbose: bool = True) -> None:
verbose: Set this to False in order to disable printouts.
"""
with self.temporary_verbose(verbose):
- print(f"Beginning Device Cleanup on {self._name_and_alias}")
+ _logger.info("Beginning Device Cleanup on %s", self._name_and_alias)
self._cleanup()
- print(f"Finished Device Cleanup on {self._name_and_alias}")
+ _logger.info("Finished Device Cleanup on %s", self._name_and_alias)
@final
def close(self) -> None:
"""Close this device and all its used resources and components."""
- print_with_timestamp(f"Closing Connection to {self._name_and_alias}")
+ _logger.info("Closing Connection to %s", self._name_and_alias)
self._close()
@final
@@ -430,7 +425,7 @@ def get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
return self._get_errors()
@@ -443,7 +438,7 @@ def has_errors(self) -> bool:
Returns:
A boolean indicating if any errors were found in the device. True means there were
- errors, False means no errors were found.
+ errors, False means no errors were found.
"""
return bool(self.get_errors()[0])
@@ -467,21 +462,19 @@ def reboot(self, quiet_period: int = 0) -> bool:
self.__delattr__(prop) # pylint: disable=unnecessary-dunder-call
# Reboot the device
- print_with_timestamp(f"Rebooting {self._name_and_alias}")
+ _logger.info("Rebooting %s", self._name_and_alias)
self._reboot()
self.close()
# Depending on the instrument model, shutdown time or reboot time can take longer
if quiet_period or self._reboot_quiet_period:
sleep_time = max(self._reboot_quiet_period, quiet_period)
- print_with_timestamp(f"Waiting for {sleep_time} seconds")
+ _logger.info("Waiting for %d seconds", sleep_time)
time.sleep(sleep_time)
- print_with_timestamp(f"Reopening Connection to {self._name_and_alias}")
+ _logger.info("Reopening Connection to %s", self._name_and_alias)
if rebooted := self._open():
- print_with_timestamp(f"Connection Reestablished with {self._name_and_alias}")
+ _logger.info("Connection Reestablished with %s", self._name_and_alias)
else:
- print_with_timestamp(
- f"Failed to reestablish the connection with {self._name_and_alias}"
- )
+ _logger.error("Failed to reestablish the connection with %s", self._name_and_alias)
return rebooted
@final
@@ -490,7 +483,6 @@ def wait_for_network_connection(
wait_time: float,
sleep_seconds: int = 2,
accept_immediate_connection: bool = False,
- verbose: bool = True,
) -> bool:
"""Wait for a network connection to the device.
@@ -499,7 +491,6 @@ def wait_for_network_connection(
sleep_seconds: The number of seconds to sleep in between connection attempts.
accept_immediate_connection: A boolean indicating if a connection on the
first attempt is a valid connection.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if a network connection was made within the given time limit.
@@ -509,13 +500,14 @@ def wait_for_network_connection(
"""
attempt_num = 0
network_connection = False
- if verbose:
- print_with_timestamp(
- f"Attempting to establish a network connection with {self.ip_address}"
- )
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Attempting to establish a network connection with %s",
+ self.ip_address,
+ )
start_time = time.perf_counter()
while (time.perf_counter() - start_time) <= wait_time:
- if network_connection := self.check_network_connection(verbose=False)[0]:
+ if network_connection := self.check_network_connection()[0]:
# pylint: disable=compare-to-zero
if not (attempt_num == 0 and not accept_immediate_connection):
break
@@ -531,17 +523,19 @@ def wait_for_network_connection(
end_time = time.perf_counter()
total_time = end_time - start_time
- if verbose:
- if network_connection:
- print_with_timestamp(
- f"Successfully established a network connection with {self.ip_address} "
- f"after {total_time:.2f} seconds"
- )
- else:
- print_with_timestamp(
- f"Unable to establish a network connection with {self.ip_address} "
- f"after {total_time:.2f} seconds"
- )
+ if network_connection:
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Successfully established a network connection with %s after %.2f seconds",
+ self.ip_address,
+ total_time,
+ )
+ else:
+ _logger.warning(
+ "Unable to establish a network connection with %s after %.2f seconds",
+ self.ip_address,
+ total_time,
+ )
return network_connection
def wait_for_port_connection(
@@ -550,7 +544,6 @@ def wait_for_port_connection(
wait_time: float,
sleep_seconds: int = 5,
accept_immediate_connection: bool = False,
- verbose: bool = True,
) -> bool:
"""Wait for a connection to be made to the given port on the device.
@@ -560,7 +553,6 @@ def wait_for_port_connection(
sleep_seconds: The number of seconds to sleep in between connection attempts.
accept_immediate_connection: A boolean indicating if a connection on the
first attempt is a valid connection.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if a connection was made to the port within the given time limit.
@@ -570,16 +562,15 @@ def wait_for_port_connection(
"""
attempt_num = 0
port_connection = False
- if verbose:
- print_with_timestamp(
- f"Attempting to establish a connection to port {port} on {self.ip_address}"
- )
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Attempting to establish a connection to port %d on %s",
+ port,
+ self.ip_address,
+ )
start_time = time.perf_counter()
while (time.perf_counter() - start_time) <= wait_time:
- port_connection = self.check_port_connection(
- port, timeout_seconds=sleep_seconds, verbose=False
- )
- if port_connection:
+ if port_connection := self.check_port_connection(port, timeout_seconds=sleep_seconds):
# pylint: disable=compare-to-zero
if not (attempt_num == 0 and not accept_immediate_connection):
break
@@ -595,17 +586,21 @@ def wait_for_port_connection(
end_time = time.perf_counter()
total_time = end_time - start_time
- if verbose:
- if port_connection:
- print_with_timestamp(
- f"Successfully established a connection to port {port} on {self.ip_address} "
- f"after {total_time:.2f} seconds"
- )
- else:
- print_with_timestamp(
- f"Unable to establish a connection to port {port} on {self.ip_address} "
- f"after {total_time:.2f} seconds"
- )
+ if port_connection:
+ _logger.log(
+ logging.INFO if self._verbose else logging.DEBUG,
+ "Successfully established a connection to port %d on %s after %.2f seconds",
+ port,
+ self.ip_address,
+ total_time,
+ )
+ else:
+ _logger.warning(
+ "Unable to establish a connection to port %d on %s after %.2f seconds",
+ port,
+ self.ip_address,
+ total_time,
+ )
return port_connection
################################################################################################
diff --git a/src/tm_devices/drivers/digital_multimeters/dmm6500.py b/src/tm_devices/drivers/digital_multimeters/dmm6500.py
index 16f1acef..4338ad2e 100644
--- a/src/tm_devices/drivers/digital_multimeters/dmm6500.py
+++ b/src/tm_devices/drivers/digital_multimeters/dmm6500.py
@@ -48,7 +48,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
# instrument returns exponential numbers so converting to float before int
error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py b/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py
index 9d250751..f1cdb0ca 100644
--- a/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py
+++ b/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py
@@ -48,7 +48,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
# instrument returns exponential numbers so converting to float before int
error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/margin_testers/margin_tester.py b/src/tm_devices/drivers/margin_testers/margin_tester.py
index 9cd84f6c..11fba7ad 100644
--- a/src/tm_devices/drivers/margin_testers/margin_tester.py
+++ b/src/tm_devices/drivers/margin_testers/margin_tester.py
@@ -1,12 +1,14 @@
"""Base Margin Tester device driver module."""
+from __future__ import annotations
+
import os
import time
from abc import ABC, abstractmethod
-from typing import Any, Dict, Mapping, MutableMapping, Tuple
+from pathlib import Path
+from typing import Any, Dict, Mapping, MutableMapping, Tuple, TYPE_CHECKING, Union
-from packaging.version import Version
from requests.structures import CaseInsensitiveDict
from tm_devices.driver_mixins.device_control import RESTAPIControl
@@ -14,6 +16,9 @@
from tm_devices.helpers import DeviceConfigEntry, DeviceTypes
from tm_devices.helpers import ReadOnlyCachedProperty as cached_property # noqa: N813
+if TYPE_CHECKING:
+ from packaging.version import Version
+
@family_base_class
class MarginTester(Device, RESTAPIControl, ABC):
@@ -27,11 +32,13 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
"""
super().__init__(config_entry, verbose)
- self._auth_token_file_path = ""
+ self._auth_token_file_path: Path = Path()
################################################################################################
# Abstract Cached Properties
@@ -89,12 +96,12 @@ def device_type(self) -> str:
@property
def auth_token_file_path(self) -> str:
"""Return the path to the file containing the auth token."""
- return self._auth_token_file_path
+ return self._auth_token_file_path.as_posix()
@auth_token_file_path.setter
- def auth_token_file_path(self, value: str) -> None:
+ def auth_token_file_path(self, value: Union[str, os.PathLike[str]]) -> None:
"""Set the path to the file containing the auth token."""
- self._auth_token_file_path = value
+ self._auth_token_file_path = Path(value)
################################################################################################
# Public Methods
@@ -117,7 +124,11 @@ def _generate_headers(self) -> Mapping[str, str]:
Raises:
AssertionError: Indicates that no auth token file is available.
"""
- if not self._auth_token_file_path:
+ if not (
+ self._auth_token_file_path
+ and self._auth_token_file_path.exists()
+ and self._auth_token_file_path.is_file()
+ ):
msg = (
"No auth token file set! Please set the ``.auth_token_file_path`` attribute "
"to point to a file with an authorization token."
@@ -125,8 +136,7 @@ def _generate_headers(self) -> Mapping[str, str]:
raise AssertionError(msg)
headers: MutableMapping[str, str] = CaseInsensitiveDict()
headers["Accept"] = "application/json"
- with open(self._auth_token_file_path, encoding="utf-8") as auth_file:
- token = auth_file.read().replace("\n", "")
+ token = self._auth_token_file_path.read_text(encoding="utf-8").replace("\n", "")
headers["Authorization"] = f"Bearer {token}"
return headers
@@ -138,7 +148,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
return 0, ()
@@ -148,12 +158,10 @@ def _open(self) -> bool:
time.sleep(15)
self._is_open = self.wait_for_network_connection(
120,
- verbose=False,
accept_immediate_connection=bool(os.environ.get("TM_DEVICES_UNIT_TESTS_RUNNING")),
)
self._is_open &= self.wait_for_api_connection(
120,
- verbose=False,
accept_immediate_connection=True,
)
return self._is_open
diff --git a/src/tm_devices/drivers/margin_testers/tmt4.py b/src/tm_devices/drivers/margin_testers/tmt4.py
index 719e4e46..bfb687ca 100644
--- a/src/tm_devices/drivers/margin_testers/tmt4.py
+++ b/src/tm_devices/drivers/margin_testers/tmt4.py
@@ -26,7 +26,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
Notes:
The default port is 5000, but device could be configured for any specific port.
@@ -110,11 +112,12 @@ def request_about_info(self, verbose: bool = True) -> Dict[str, Any]:
A Dictionary with information about the Margin device.
"""
# Make get request to about and version endpoints
- _, res_json, _, _ = self.get("/device/about", verbose=verbose)
- res_json = cast(Dict[str, Any], res_json)
- _, res_json2, _, _ = self.get("/device/version", verbose=verbose)
- res_json2 = cast(Dict[str, Any], res_json2)
- res_json.update(res_json2)
+ with self.temporary_verbose(verbose):
+ _, res_json, _, _ = self.get("/device/about")
+ res_json = cast(Dict[str, Any], res_json)
+ _, res_json2, _, _ = self.get("/device/version")
+ res_json2 = cast(Dict[str, Any], res_json2)
+ res_json.update(res_json2)
return res_json
def wait_till_unlocked(self, timeout: int = 120) -> None:
@@ -144,8 +147,9 @@ def _check_api_connection(self) -> bool:
Returns:
A boolean indicating if the call was successful.
"""
- # allow_errors is set to True since this is just checking if the API is reachable
- return self.get("/device/about", allow_errors=True, verbose=False)[0]
+ with self.temporary_verbose(False):
+ # allow_errors is set to True since this is just checking if the API is reachable
+ return self.get("/device/about", allow_errors=True)[0]
def _cleanup(self) -> None:
"""Perform the cleanup defined for the device."""
diff --git a/src/tm_devices/drivers/scopes/tekscope/mso2.py b/src/tm_devices/drivers/scopes/tekscope/mso2.py
index adfe019d..cc108dc6 100644
--- a/src/tm_devices/drivers/scopes/tekscope/mso2.py
+++ b/src/tm_devices/drivers/scopes/tekscope/mso2.py
@@ -27,7 +27,9 @@ def __init__(
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
visa_resource: The VISA resource object.
default_visa_timeout: The default VISA timeout value in milliseconds.
"""
diff --git a/src/tm_devices/drivers/scopes/tekscope/tekscope.py b/src/tm_devices/drivers/scopes/tekscope/tekscope.py
index b3aeddaf..ecad6895 100644
--- a/src/tm_devices/drivers/scopes/tekscope/tekscope.py
+++ b/src/tm_devices/drivers/scopes/tekscope/tekscope.py
@@ -1,5 +1,9 @@
"""Base TekScope scope device driver module."""
+from __future__ import annotations
+
+import logging
+
# pylint: disable=too-many-lines
import math
import os
@@ -10,20 +14,10 @@
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType
-from typing import Any, cast, Dict, List, Literal, Optional, Tuple, Type, Union
+from typing import Any, cast, Dict, List, Literal, Optional, Tuple, Type, TYPE_CHECKING, Union
import pyvisa as visa
-from tm_devices.commands import (
- LPD6Commands,
- MSO2Commands,
- MSO4Commands,
- MSO5BCommands,
- MSO5Commands,
- MSO5LPCommands,
- MSO6BCommands,
- MSO6Commands,
-)
from tm_devices.driver_mixins.abstract_device_functionality import (
BaseAFGSourceChannel,
BusMixin,
@@ -58,6 +52,20 @@
SignalGeneratorOutputPathsBase,
)
+if TYPE_CHECKING:
+ from tm_devices.commands import (
+ LPD6Commands,
+ MSO2Commands,
+ MSO4Commands,
+ MSO5BCommands,
+ MSO5Commands,
+ MSO5LPCommands,
+ MSO6BCommands,
+ MSO6Commands,
+ )
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
@dataclass(frozen=True)
class TekScopeSourceDeviceConstants(SourceDeviceConstants):
@@ -121,7 +129,9 @@ def __init__(
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
visa_resource: The VISA resource object.
default_visa_timeout: The default VISA timeout value in milliseconds.
"""
@@ -321,7 +331,7 @@ def curve_query( # noqa: PLR0912,C901
self,
channel_num: int,
wfm_type: str = "TimeDomain",
- output_csv_file: Optional[str] = None,
+ output_csv_file: Optional[Union[str, os.PathLike[str]]] = None,
) -> List[Any]:
"""Perform a curve query on a specific channel.
@@ -376,6 +386,7 @@ def curve_query( # noqa: PLR0912,C901
break # break out of loop
if not found:
warnings.warn(f"source not available for curve query: CH{channel_num}", stacklevel=2)
+ _logger.warning("source not available for curve query: CH%d", channel_num)
return []
self.set_and_check(":DATA:ENC", "ASCII")
@@ -390,7 +401,7 @@ def curve_query( # noqa: PLR0912,C901
wfm_data.append([float(b) for b in frame.split(",")]) # noqa: PERF401
if output_csv_file:
- with open(output_csv_file, "w", encoding="UTF-8") as csv_file:
+ with Path(output_csv_file).open("w", encoding="UTF-8") as csv_file:
for frame in frames:
csv_file.write(frame)
csv_file.write(",")
diff --git a/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py b/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py
index 4d1554c7..d4b49811 100644
--- a/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py
+++ b/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py
@@ -1,5 +1,6 @@
"""TekScopePC device driver module."""
+import logging
import warnings
from tm_devices.commands import TekScopePCMixin
@@ -7,6 +8,8 @@
from tm_devices.drivers.scopes.tekscope.tekscope import AbstractTekScope
from tm_devices.helpers import ReadOnlyCachedProperty as cached_property # noqa: N813
+_logger: logging.Logger = logging.getLogger(__name__)
+
@family_base_class
class TekScopePC(TekScopePCMixin, AbstractTekScope): # pyright: ignore[reportIncompatibleVariableOverride]
@@ -31,8 +34,9 @@ def total_channels(self) -> int:
################################################################################################
def _reboot(self) -> None:
"""Reboot the device."""
- warnings.warn(
+ msg = (
f"Rebooting is not supported for the {self.__class__.__name__} driver, "
- f"{self._name_and_alias} will not be rebooted.",
- stacklevel=3,
+ f"{self._name_and_alias} will not be rebooted."
)
+ _logger.warning(msg)
+ warnings.warn(msg, stacklevel=3)
diff --git a/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py b/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py
index 82863616..8c2e53a2 100644
--- a/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py
+++ b/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py
@@ -1,9 +1,13 @@
"""Base TekScope2k scope device driver module."""
+from __future__ import annotations
+
+import logging
import warnings
from abc import ABC
-from typing import Any, List, Optional
+from pathlib import Path
+from typing import Any, List, Optional, TYPE_CHECKING, Union
from tm_devices.driver_mixins.abstract_device_functionality.channel_control_mixin import (
ChannelControlMixin,
@@ -16,6 +20,11 @@
from tm_devices.drivers.scopes.scope import Scope
from tm_devices.helpers import ReadOnlyCachedProperty as cached_property # noqa: N813
+if TYPE_CHECKING:
+ import os
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
@family_base_class
class TekScope2k(_TektronixPIScopeMixin, PIControl, Scope, ChannelControlMixin, ABC):
@@ -62,7 +71,7 @@ def curve_query( # pylint: disable=too-many-locals
self,
channel_num: int,
wfm_type: str = "TimeDomain",
- output_csv_file: Optional[str] = None,
+ output_csv_file: Optional[Union[str, os.PathLike[str]]] = None,
) -> List[Any]:
"""Perform a curve query on a specific channel.
@@ -97,6 +106,7 @@ def curve_query( # pylint: disable=too-many-locals
break # break out of loop
if not found:
warnings.warn(f"source not available for curve query: CH{channel_num}", stacklevel=2)
+ _logger.warning("source not available for curve query: CH%d", channel_num)
return []
self.set_and_check(":DATA:ENC", "ASCI")
@@ -112,7 +122,7 @@ def curve_query( # pylint: disable=too-many-locals
wfm_data = [round(i, 3) for i in data]
if output_csv_file:
- with open(output_csv_file, "w", encoding="UTF-8") as csv_file:
+ with Path(output_csv_file).open("w", encoding="UTF-8") as csv_file:
for frame in wfm_data:
csv_file.write(str(frame))
csv_file.write(",")
diff --git a/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py b/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py
index c9c47264..3d0a0289 100644
--- a/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py
+++ b/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py
@@ -32,7 +32,9 @@ def __init__(
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
visa_resource: The VISA resource object.
default_visa_timeout: The default VISA timeout value in milliseconds.
"""
diff --git a/src/tm_devices/drivers/scopes/tso/tsovu.py b/src/tm_devices/drivers/scopes/tso/tsovu.py
index 01653ffe..0559c6ec 100644
--- a/src/tm_devices/drivers/scopes/tso/tsovu.py
+++ b/src/tm_devices/drivers/scopes/tso/tsovu.py
@@ -30,7 +30,9 @@ def __init__(
Args:
config_entry: A config entry object parsed by the DMConfigParser.
- verbose: A boolean indicating if verbose output should be printed.
+ verbose: A boolean indicating if verbose output should be printed. If True,
+ communication printouts will be logged with a level of INFO. If False,
+ communication printouts will be logged with a level of DEBUG.
visa_resource: The VISA resource object.
default_visa_timeout: The default VISA timeout value in milliseconds.
"""
diff --git a/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py b/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py
index a4e58c1b..dd5eb015 100644
--- a/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py
+++ b/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py
@@ -54,7 +54,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
Returns:
A tuple containing the current error code alongside a tuple of the current error
- messages.
+ messages.
"""
# instrument returns exponential numbers so converting to float before int
error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/helpers/__init__.py b/src/tm_devices/helpers/__init__.py
index de918cb5..e733ae69 100644
--- a/src/tm_devices/helpers/__init__.py
+++ b/src/tm_devices/helpers/__init__.py
@@ -30,14 +30,13 @@
create_visa_connection,
detect_visa_resource_expression,
get_model_series,
- get_timestamp_string,
get_version,
get_visa_backend,
ping_address,
- print_with_timestamp,
register_additional_usbtmc_mapping,
sanitize_enum,
)
+from tm_devices.helpers.logging import configure_logging, LoggingLevels
from tm_devices.helpers.read_only_cached_property import ReadOnlyCachedProperty
from tm_devices.helpers.singleton_metaclass import Singleton
from tm_devices.helpers.standalone_functions import validate_address
@@ -51,18 +50,18 @@
"check_network_connection",
"check_port_connection",
"check_visa_connection",
+ "configure_logging",
"create_visa_connection",
"detect_visa_resource_expression",
"DeviceConfigEntry",
"DeviceTypes",
"DMConfigOptions",
"get_model_series",
- "get_timestamp_string",
"get_version",
"get_visa_backend",
+ "LoggingLevels",
"PACKAGE_NAME",
"ping_address",
- "print_with_timestamp",
"PYVISA_PY_BACKEND",
"raise_error",
"raise_failure",
diff --git a/src/tm_devices/helpers/constants_and_dataclasses.py b/src/tm_devices/helpers/constants_and_dataclasses.py
index edda8fe7..52feed37 100644
--- a/src/tm_devices/helpers/constants_and_dataclasses.py
+++ b/src/tm_devices/helpers/constants_and_dataclasses.py
@@ -18,6 +18,7 @@
LoadImpedanceAFG,
SupportedModels,
)
+from tm_devices.helpers.logging import LoggingLevels
from tm_devices.helpers.standalone_functions import validate_address
@@ -306,7 +307,7 @@ def __post_init__(self) -> None: # noqa: PLR0912, C901
)
)
):
- raise ValueError
+ raise ValueError # noqa: TRY301
except ValueError as exc:
msg = f"""Found invalid value of `address={self.address}`
Must provide an valid address format for the `connection_type={self.connection_type.value}':
@@ -449,6 +450,67 @@ class DMConfigOptions(AsDictionaryMixin):
"""
check_for_updates: Optional[bool] = None
"""A flag indicating if a check for updates for the package should be performed on creation of the DeviceManager.""" # noqa: E501
+ log_console_level: Optional[str] = None
+ """The logging level to set for the console.
+
+ One of `["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"]`, see the
+ [`LoggingLevels`][tm_devices.helpers.logging.LoggingLevels] enum for details.
+ Defaults to `"INFO"`. See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+ information.
+ """
+ log_file_level: Optional[str] = None
+ """The logging level to set for the log file.
+
+ One of `["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"]`, see the
+ [`LoggingLevels`][tm_devices.helpers.logging.LoggingLevels] enum for details.
+ Defaults to `"DEBUG"`. See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+ information.
+ """
+ log_file_directory: Optional[str] = None
+ """The directory to save log files to.
+
+ Defaults to "./logs". See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+ information and default values.
+ """
+ log_file_name: Optional[str] = None
+ """The name of the log file to save the logs to.
+
+ Defaults to a timestamped filename with the .log extension. See the
+ [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+ information and default values.
+ """
+ log_colored_output: Optional[bool] = None
+ """Whether to use colored output from the `colorlog` package for the console.
+
+ Defaults to False. See the [`configure_logging()`][tm_devices.helpers.logging.configure_logging]
+ function for more information and default values.
+ """
+ log_pyvisa_messages: Optional[bool] = None
+ """Whether to include logs from the `pyvisa` package in the log file.
+
+ Defaults to False. See the [`configure_logging()`][tm_devices.helpers.logging.configure_logging]
+ function for more information and default values.
+ """
+
+ def __post_init__(self) -> None:
+ """Validate data after creation.
+
+ Raises:
+ ValueError: Indicates a bad input value.
+ """
+ for attribute in ("log_console_level", "log_file_level"):
+ try:
+ if (value := getattr(self, attribute)) is not None:
+ _ = LoggingLevels(value)
+ except ValueError as error: # noqa: PERF203
+ msg = (
+ f'Invalid value for {attribute}: "{getattr(self, attribute)}". '
+ f"Valid values are {LoggingLevels.list_values()}"
+ )
+ raise ValueError(msg) from error
def __str__(self) -> str:
"""Complete config entry line for an environment variable."""
diff --git a/src/tm_devices/helpers/functions.py b/src/tm_devices/helpers/functions.py
index f3f9859e..36cfc3ed 100644
--- a/src/tm_devices/helpers/functions.py
+++ b/src/tm_devices/helpers/functions.py
@@ -1,9 +1,9 @@
"""Module containing helpers for the `tm_devices` package."""
import contextlib
-import datetime
import importlib.metadata
import json
+import logging
import platform
import re
import shlex
@@ -18,7 +18,6 @@
import requests
-from dateutil.tz import tzlocal
from packaging.version import InvalidVersion, Version
from tm_devices.helpers.constants_and_dataclasses import (
@@ -34,6 +33,8 @@
from tm_devices.helpers.enums import CustomStrEnum, SupportedModels
from tm_devices.helpers.standalone_functions import validate_address
+_logger: logging.Logger = logging.getLogger(__name__)
+
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
import pyvisa as visa
@@ -186,54 +187,56 @@ def check_for_update(package_name: str = PACKAGE_NAME, index_name: str = "pypi")
latest_version = version_list[0]
if installed_version != latest_version:
- print(
- f"\n\n\033[91mVersion {latest_version} of "
- f"{package_name} is available on {index_name}.org.\n"
- f"Version {installed_version} of "
- f"{package_name} is currently installed.\n\n"
- f"To upgrade {package_name} run the following command: "
- f"python -m pip install -U {package_name}\n\n\033[0m"
+ _logger.debug(
+ "\n\n\033[91mVersion %(latest_version)s of "
+ "%(package_name)s is available on %(index_name)s.org.\n"
+ "Version %(installed_version)s of "
+ "%(package_name)s is currently installed.\n\n"
+ "To upgrade %(package_name)s run the following command: "
+ "python -m pip install -U %(package_name)s\n\n\033[0m",
+ {
+ "latest_version": latest_version,
+ "package_name": package_name,
+ "index_name": index_name,
+ "installed_version": installed_version,
+ },
)
except ModuleNotFoundError:
- print(
- f"\n\n\033[91m{package_name} is not installed, "
- f"unable to check for updates.\n\n\033[0m"
+ _logger.warning(
+ "\n\n\033[91m%s is not installed, unable to check for updates.\n\n\033[0m",
+ package_name,
)
except (IndexError, ValueError):
- print(
- f"\n\n\033[91m{package_name} is not available on {index_name}.org, "
- f"unable to check for updates.\n\n\033[0m"
+ _logger.warning(
+ "\n\n\033[91m%s is not available on %s.org, unable to check for updates.\n\n\033[0m",
+ package_name,
+ index_name,
)
-def check_network_connection(
- device_name: str, ip_address: str, verbose: bool = True
-) -> Tuple[bool, str]:
+def check_network_connection(device_name: str, ip_address: str) -> Tuple[bool, str]:
"""Check the network connection to the device using the external ping command.
Args:
device_name: The name of the device.
ip_address: The ip address of the device.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if there is a network connection and
- a string with the result of the ping command.
+ a string with the result of the ping command.
"""
- if verbose:
- print_with_timestamp(f"({device_name}) ping >> {ip_address}")
- print(f"{get_timestamp_string()} - ")
+ _logger.debug("(%s) ping >> %s", device_name, ip_address)
ping_result = ping_address(ip_address)
- if verbose:
- print_with_timestamp("Response from ping >>")
- for line in ping_result.strip().split("\n"):
- print(f" {line}")
+ _logger.debug(
+ "Response from ping >>\n%s",
+ "\n".join([" " + x for x in ping_result.strip().split("\n")]),
+ )
return "ttl=" in ping_result.lower(), ping_result
def check_port_connection(
- device_name: str, ip_address: str, port: int, timeout_seconds: int = 5, verbose: bool = True
+ device_name: str, ip_address: str, port: int, timeout_seconds: int = 5
) -> bool:
"""Check if the given port is open on the device.
@@ -242,13 +245,11 @@ def check_port_connection(
ip_address: The ip address.
port: The port to check.
timeout_seconds: The number of seconds to use as the socket timeout.
- verbose: Set this to False in order to disable printouts.
Returns:
A boolean indicating if the port is open.
"""
- if verbose:
- print_with_timestamp(f"({device_name}) >> checking if port {port} is open on {ip_address}")
+ _logger.debug("(%s) >> checking if port %d is open on %s", device_name, port, ip_address)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as temp_socket:
try:
@@ -259,13 +260,12 @@ def check_port_connection(
except (socket.error, socket.gaierror, socket.herror):
port_open = False
- if verbose:
- print_with_timestamp(f"Result >> {port_open}")
+ _logger.debug("(%s) port %d open = %s", device_name, port, port_open)
return port_open
def check_visa_connection(
- config_entry: DeviceConfigEntry, visa_library: str, device_name: str, verbose: bool = True
+ config_entry: DeviceConfigEntry, visa_library: str, device_name: str
) -> bool:
"""Check if a VISA connection can be made to the device.
@@ -273,16 +273,15 @@ def check_visa_connection(
config_entry: The device listed in the config of the framework to check.
visa_library: Indicates which visa library to use for the connection.
device_name: The name of the device.
- verbose: Set this to False in order to disable printouts.
Returns:
a boolean indicating if the visa connection was successful
"""
- if verbose:
- print_with_timestamp(
- f"({device_name}) >> checking if a VISA connection can be made to "
- f"{config_entry.get_visa_resource_expression()}"
- )
+ _logger.debug(
+ "(%s) >> checking if a VISA connection can be made to %s",
+ device_name,
+ config_entry.get_visa_resource_expression(),
+ )
try:
# Create new VISA connection
temp_resource = create_visa_connection(
@@ -294,8 +293,7 @@ def check_visa_connection(
except ConnectionError: # raised by the create_visa_connection() function
visa_connected = False
- if verbose:
- print_with_timestamp(f"Result >> {visa_connected}")
+ _logger.debug("(%s) VISA connected = %s", device_name, visa_connected)
return visa_connected
@@ -331,12 +329,15 @@ def create_visa_connection(
if (
visa_object.visalib.library_path.path == "py" and visa_library != PYVISA_PY_BACKEND
): # pragma: no cover
- warnings.warn(
+ warning_msg = (
"No VISA installation was detected, defaulting to using "
"PyVISA-py as the VISA backend.\n\n"
- 'Add the "STANDALONE" option to the configuration to silence this warning.',
- stacklevel=4,
+ 'Add the "STANDALONE" option to the configuration or set the '
+ "`DeviceManager.visa_library` attribute to a valid VISA library to "
+ "silence this warning."
)
+ warnings.warn(warning_msg, stacklevel=4)
+ _logger.warning(warning_msg)
# The broad except is because pyvisa_py can throw a base exception in the tcpip.py file
except Exception as error_1:
if not retry_connection:
@@ -466,15 +467,11 @@ def get_model_series(model: str) -> str:
if not model_series:
if model not in SupportedModels.list_values():
warnings.warn(f'The "{model}" model is not supported by {PACKAGE_NAME}', stacklevel=2)
+ _logger.warning('The "%s" model is not supported by %s', model, PACKAGE_NAME)
model_series = model
return model_series
-def get_timestamp_string() -> str:
- """Return a string containing the current timestamp."""
- return str(datetime.datetime.now(tz=tzlocal()))[:-3]
-
-
def get_version(version_string: str) -> Version:
"""Get a Version object from a string.
@@ -527,7 +524,7 @@ def get_visa_backend(visa_lib_path: str) -> str:
if visa_lib_path == "py":
visa_name = "PyVISA-py"
else:
- raise KeyError
+ raise KeyError # noqa: TRY301
except KeyError:
found_visa = False
for visa_type in visa_backends:
@@ -564,18 +561,6 @@ def ping_address(address: str, timeout: int = 2) -> str:
return output.replace("\r\n", "\n")
-def print_with_timestamp(message: str, end: str = "\n") -> str:
- """Print and return a string prepended with a timestamp.
-
- Args:
- message: The message to print.
- end: The end of the line to print.
- """
- message = f"{get_timestamp_string()} - {message}"
- print(message, end=end)
- return message
-
-
def register_additional_usbtmc_mapping(model_series: str, *, model_id: str, vendor_id: str) -> None:
"""Register USBTMC connection information for a device that doesn't have native `tm_devices` USBTMC support.
diff --git a/src/tm_devices/helpers/logging.py b/src/tm_devices/helpers/logging.py
new file mode 100644
index 00000000..c109a7b2
--- /dev/null
+++ b/src/tm_devices/helpers/logging.py
@@ -0,0 +1,158 @@
+# pyright: reportUnnecessaryTypeIgnoreComment=none
+"""Helpers for logging."""
+
+from __future__ import annotations
+
+import importlib.metadata
+import logging
+import sys
+import time
+
+from pathlib import Path
+from typing import Optional, TYPE_CHECKING, Union
+
+import colorlog
+import pyvisa
+
+from tzlocal import (
+ get_localzone, # pyright: ignore[reportUnknownVariableType]
+)
+
+from tm_devices.helpers.enums import CustomStrEnum
+
+if TYPE_CHECKING:
+ import os
+
+_logger_initialized = False
+
+
+class _CustomFormatter(logging.Formatter): # pragma: no cover
+ def format(self, record: logging.LogRecord) -> str:
+ # Add the package name to the log record
+ record.package_name = record.name.split(".", maxsplit=1)[0]
+ # Call the original format method
+ return super().format(record)
+
+
+class LoggingLevels(CustomStrEnum):
+ """A class holding the valid logging levels supported."""
+
+ DEBUG = "DEBUG"
+ """An enum member representing the DEBUG logging level."""
+ INFO = "INFO"
+ """An enum member representing the INFO logging level."""
+ WARNING = "WARNING"
+ """An enum member representing the WARNING logging level."""
+ ERROR = "ERROR"
+ """An enum member representing the ERROR logging level."""
+ CRITICAL = "CRITICAL"
+ """An enum member representing the CRITICAL logging level."""
+ NONE = "NONE"
+ """An enum member indicating no logging messages should be captured."""
+
+
+def configure_logging(
+ *,
+ log_console_level: Union[str, LoggingLevels] = LoggingLevels.INFO,
+ log_file_level: Union[str, LoggingLevels] = LoggingLevels.DEBUG,
+ log_file_directory: Optional[Union[str, os.PathLike[str], Path]] = None,
+ log_file_name: Optional[str] = None,
+ log_colored_output: bool = False,
+ log_pyvisa_messages: bool = False,
+) -> logging.Logger:
+ """Configure the logging for this package.
+
+ !!! note
+ After this function is called once, if it is called again, it will not perform any
+ additional configuration. It will simply return the base logger for the package. This means
+ that if logging is configured explicitly in Python code, then any configuration options set
+ in the config file or environment variables will be ignored.
+
+ Args:
+ log_console_level: The logging level to set for the console. Defaults to INFO. Set to
+ [`LoggingLevels.NONE`][tm_devices.helpers.logging.LoggingLevels.NONE] to disable all
+ console logging/printouts except for certain warnings and exceptions.
+ log_file_level: The logging level to set for the file. Defaults to DEBUG. Set to
+ [`LoggingLevels.NONE`][tm_devices.helpers.logging.LoggingLevels.NONE] to disable logging
+ to a file entirely.
+ log_file_directory: The directory to save log files to. Defaults to "./logs" in the
+ current working directory.
+ log_file_name: The name of the log file to save the logs to. Defaults to a timestamped name
+ with the .log extension.
+ log_colored_output: Whether to use colored output from the `colorlog` package for the
+ console. Defaults to False.
+ log_pyvisa_messages: Whether to include logs from the `pyvisa` package in the log file. The
+ logging level will match the `file_logging_level`. Defaults to False.
+
+ Returns:
+ The base logger for the package, this base logger can also be accessed using
+ `logging.getLogger(tm_devices.PACKAGE_NAME)`.
+ """
+ from tm_devices.helpers.constants_and_dataclasses import ( # pylint: disable=import-outside-toplevel
+ PACKAGE_NAME,
+ )
+
+ global _logger_initialized # noqa: PLW0603
+
+ _logger: logging.Logger = logging.getLogger(PACKAGE_NAME)
+ if _logger_initialized:
+ # If the logger was previously initialized, just return it
+ return _logger
+ # Convert object types into enum values
+ log_console_level = LoggingLevels(log_console_level)
+ log_file_level = LoggingLevels(log_file_level)
+ # Set the logger level to the lowest level, the handlers will filter out specific levels
+ # based on user configuration
+ _logger.setLevel(logging.DEBUG)
+ _logger.addHandler(logging.NullHandler())
+ # The logger/module name is not included in the message, since formatting the messages to
+ # be aligned would cause the width of the message prefix to be almost 100 characters before
+ # the message is even added to the line.
+ logging_file_format_string = "[%(asctime)s] [%(package_name)10s] [%(levelname)8s] %(message)s"
+ logging_console_format_string = "%(asctime)s - %(message)s"
+ if not log_file_directory: # pragma: no cover
+ log_file_directory = Path("./logs")
+ if not log_file_name: # pragma: no cover
+ log_file_name = f"{PACKAGE_NAME}_{time.strftime('%m-%d-%Y_%H-%M-%S', time.localtime())}.log"
+ log_filepath = Path(log_file_directory) / log_file_name
+
+ if log_file_level != LoggingLevels.NONE:
+ # Set up logger for tm_devices
+ log_filepath.parent.mkdir(parents=True, exist_ok=True)
+ file_handler = logging.FileHandler(log_filepath, mode="w", encoding="utf-8")
+ file_formatter = _CustomFormatter(logging_file_format_string)
+ file_formatter.default_msec_format = "%s.%06d" # Use 6 digits of precision for milliseconds
+
+ file_handler.setLevel(getattr(logging, log_file_level.value))
+ file_handler.setFormatter(file_formatter)
+ _logger.addHandler(file_handler)
+
+ if log_pyvisa_messages:
+ # Hook into pyvisa's logging to save it into the same file
+ pyvisa.logger.setLevel(getattr(logging, log_file_level.value))
+ pyvisa.logger.addHandler(file_handler)
+
+ # Log a few things to just the file
+ _logger.debug("timezone==%s", get_localzone()) # pyright: ignore[reportUnknownArgumentType,reportUnnecessaryTypeIgnoreComment]
+ _logger.debug("%s==%s", PACKAGE_NAME, importlib.metadata.version(PACKAGE_NAME))
+
+ if log_console_level != LoggingLevels.NONE:
+ if log_colored_output:
+ console_handler = colorlog.StreamHandler(stream=sys.stdout)
+ console_formatter = colorlog.ColoredFormatter(
+ "%(log_color)s" + logging_console_format_string,
+ log_colors=colorlog.default_log_colors,
+ )
+ else:
+ console_handler = logging.StreamHandler(stream=sys.stdout)
+ console_formatter = logging.Formatter(logging_console_format_string)
+ console_formatter.default_msec_format = (
+ "%s.%06d" # Use 6 digits of precision for milliseconds
+ )
+
+ console_handler.setLevel(getattr(logging, log_console_level.value))
+ console_handler.setFormatter(console_formatter)
+ _logger.addHandler(console_handler)
+
+ _logger_initialized = True
+ return _logger
diff --git a/src/tm_devices/helpers/singleton_metaclass.py b/src/tm_devices/helpers/singleton_metaclass.py
index aa2c224b..c5dad32e 100644
--- a/src/tm_devices/helpers/singleton_metaclass.py
+++ b/src/tm_devices/helpers/singleton_metaclass.py
@@ -1,10 +1,13 @@
"""A Module containing a metaclass that converts any class into a Singleton."""
+import logging
import warnings
from typing import Any, MutableMapping
from weakref import WeakValueDictionary
+_logger: logging.Logger = logging.getLogger(__name__)
+
class Singleton(type):
"""A metaclass that converts a standard class into a Singleton (one instance).
@@ -23,10 +26,11 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
new_instance = super(Singleton, cls).__call__(*args, **kwargs) # noqa: UP008
cls._class_instances[cls] = new_instance
else:
- warnings.warn(
+ msg = (
f"The {cls.__name__} has already been created and is not "
f"allowed to be instantiated twice. Previously created instance "
- f"will be used instead.\n",
- stacklevel=3,
+ f"will be used instead."
)
+ warnings.warn(msg, stacklevel=3)
+ _logger.warning(msg)
return cls._class_instances[cls]
diff --git a/src/tm_devices/helpers/stubgen.py b/src/tm_devices/helpers/stubgen.py
index 3c7591f6..ecd7f1ef 100644
--- a/src/tm_devices/helpers/stubgen.py
+++ b/src/tm_devices/helpers/stubgen.py
@@ -18,7 +18,7 @@ def _get_data_type(data_object: Any) -> str:
"""
try:
if "." in str(data_object):
- raise AttributeError
+ raise AttributeError # noqa: TRY301
data_type = str(data_object.__name__) if data_object else str(data_object)
except AttributeError:
data_type = str(
@@ -46,15 +46,14 @@ def add_info_to_stub(cls: Any, method: Any, is_property: bool = False) -> None:
"""
if stub_dir := os.getenv("TM_DEVICES_STUB_DIR"):
method_filepath = inspect.getfile(cls)
- stub_dir = str(
- Path(stub_dir) / "tm_devices" if not stub_dir.endswith("tm_devices") else stub_dir
+ stub_dir_path = Path(stub_dir) / (
+ "tm_devices" if not stub_dir.endswith("tm_devices") else stub_dir
)
- method_filepath = str(
- Path(stub_dir)
- / method_filepath.rsplit("tm_devices", maxsplit=1)[-1].lstrip(os.path.sep)
+ # stub files have the .pyi extension
+ method_path_obj = stub_dir_path / (
+ method_filepath.rsplit("tm_devices", maxsplit=1)[-1].lstrip(os.path.sep) + "i"
)
- method_filepath += "i" # stub files have the .pyi extension
- if not os.path.exists(method_filepath): # noqa: PTH110
+ if not method_path_obj.exists():
msg = (
f'The stub file "{method_filepath}" must already exist in order to use this '
f"functionality to add method stubs."
@@ -86,8 +85,7 @@ def add_info_to_stub(cls: Any, method: Any, is_property: bool = False) -> None:
method_signature = " @property\n" + method_signature
method_stub_content = method_signature + " " + '"""' + method.__doc__ + '"""' + "\n"
# Read in the content of the stub file to avoid adding duplicate methods
- with open(method_filepath, encoding="utf-8") as file_pointer:
- contents = file_pointer.read()
+ contents = method_path_obj.read_text(encoding="utf-8")
if f" def {method.__name__}(" not in contents:
if typing_imports:
contents = f"from typing import {', '.join(typing_imports)}\n" + contents
@@ -105,5 +103,4 @@ def add_info_to_stub(cls: Any, method: Any, is_property: bool = False) -> None:
msg = f"Could not find the end of the {cls.__name__} class."
raise ValueError(msg)
- with open(method_filepath, "w", encoding="utf-8") as file_pointer:
- file_pointer.write(contents)
+ method_path_obj.write_text(contents, encoding="utf-8")
diff --git a/src/tm_devices/helpers/verification_functions.py b/src/tm_devices/helpers/verification_functions.py
index eaea9938..bcbe9f6f 100644
--- a/src/tm_devices/helpers/verification_functions.py
+++ b/src/tm_devices/helpers/verification_functions.py
@@ -4,8 +4,6 @@
from typing import Tuple, Union
-from tm_devices.helpers.functions import get_timestamp_string
-
def raise_error(unique_identifier: str, message: str, *, condense_printout: bool = True) -> None:
"""Raise an AssertionError with the provided message indicating there was an error.
@@ -18,12 +16,10 @@ def raise_error(unique_identifier: str, message: str, *, condense_printout: bool
Raises:
AssertionError: Prints out the error message with a traceback.
"""
- # TODO: integrate this with logging
- # https://github.com/tektronix/tm_devices/issues/316
if condense_printout:
# Make the message smaller
message = ", ".join([x.strip() for x in message.split("\n")])
- message = f"{get_timestamp_string()} - ERROR: ({unique_identifier}) : {message}"
+ message = f"ERROR: ({unique_identifier}) : {message}"
raise AssertionError(message)
@@ -38,12 +34,10 @@ def raise_failure(unique_identifier: str, message: str, *, condense_printout: bo
Raises:
AssertionError: Prints out the failure message with a traceback.
"""
- # TODO: integrate this with logging
- # https://github.com/tektronix/tm_devices/issues/316
if condense_printout:
# Make the message smaller
message = ", ".join([x.strip() for x in message.split("\n")])
- message = f"{get_timestamp_string()} - FAILURE: ({unique_identifier}) : {message}"
+ message = f"FAILURE: ({unique_identifier}) : {message}"
raise AssertionError(message)
diff --git a/tests/conftest.py b/tests/conftest.py
index e6e04dd2..23d2eeb6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,9 @@
"""Pytest configuration file."""
+import logging
import os
import socket
+import sys
from pathlib import Path
from typing import Generator, List, Tuple
@@ -11,7 +13,7 @@
import pyvisa.constants
from mock_server import mocker_server, PORT
-from tm_devices import DeviceManager
+from tm_devices import configure_logging, DeviceManager, LoggingLevels
from tm_devices.components import DMConfigParser
from tm_devices.helpers import DMConfigOptions, validate_address
@@ -24,13 +26,38 @@
UNIT_TEST_TIMEOUT = 50
+####################################################################################################
+# Configure the logging for the package that will run during unit tests
+class _DynamicStreamHandler(logging.StreamHandler): # pyright: ignore[reportMissingTypeArgument]
+ def emit(self, record: logging.LogRecord) -> None:
+ self.stream = sys.stdout
+ super().emit(record)
+
+
+_logger = configure_logging(
+ log_console_level=LoggingLevels.NONE,
+ log_file_level=LoggingLevels.DEBUG,
+ log_file_directory=Path(__file__).parent / "logs",
+ log_file_name=f"unit_test_py{sys.version_info.major}{sys.version_info.minor}.log",
+)
+_unit_test_console_handler = _DynamicStreamHandler(stream=sys.stdout)
+_unit_test_console_handler.setLevel(logging.DEBUG)
+_unit_test_console_formatter = logging.Formatter("%(asctime)s - %(message)s")
+_unit_test_console_formatter.default_msec_format = (
+ "%s.%06d" # Use 6 digits of precision for milliseconds
+)
+_unit_test_console_handler.setFormatter(_unit_test_console_formatter)
+_logger.addHandler(_unit_test_console_handler)
+####################################################################################################
+
+
def mock_gethostbyname(address: str) -> str:
"""Mock the socket.gethostbyname function."""
try:
is_hostname = validate_address(address)
if not is_hostname: # pylint: disable=consider-using-assignment-expr
return address
- raise ValueError
+ raise ValueError # noqa: TRY301
except ValueError as error:
raise socket.herror from error
@@ -41,7 +68,7 @@ def mock_gethostbyaddr(address: str) -> Tuple[str, List[str], List[str]]:
is_hostname = validate_address(address)
if is_hostname: # pylint: disable=consider-using-assignment-expr
return address, [], []
- raise ValueError
+ raise ValueError # noqa: TRY301
except ValueError as error:
raise socket.herror from error
@@ -58,9 +85,9 @@ def _auto_add_newline_to_test_start() -> ( # pyright: ignore [reportUnusedFunct
Generator[None, None, None]
):
"""Automatically add a newline at the start of each test."""
- print(f"\n{'#' * 90}\nExecuting {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n")
+ print(f"\n{'#' * 90}\nExecuting {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n") # noqa: T201
yield
- print(f"\n\nFinished {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n{'#' * 90}")
+ print(f"\n\nFinished {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n{'#' * 90}") # noqa: T201
@pytest.fixture(name="device_manager", scope="session")
@@ -70,7 +97,7 @@ def fixture_device_manager() -> Generator[DeviceManager, None, None]:
Yields:
The DeviceManager instance.
"""
- print()
+ print() # noqa: T201
with mock.patch(
"socket.gethostbyname", mock.MagicMock(side_effect=mock_gethostbyname)
), mock.patch(
diff --git a/tests/samples/sample_devices.toml b/tests/samples/sample_devices.toml
index 7d931743..400a2784 100644
--- a/tests/samples/sample_devices.toml
+++ b/tests/samples/sample_devices.toml
@@ -29,6 +29,7 @@ end_input = "none"
[options]
default_visa_timeout = 1000
+log_file_level = "WARNING"
setup_cleanup = false
standalone = true
teardown_cleanup = false
diff --git a/tests/samples/sample_devices.yaml b/tests/samples/sample_devices.yaml
index 9082a5fd..400ec99e 100644
--- a/tests/samples/sample_devices.yaml
+++ b/tests/samples/sample_devices.yaml
@@ -22,6 +22,7 @@ devices:
end_input: none
options:
default_visa_timeout: 1000
+ log_file_level: WARNING
setup_cleanup: false
standalone: true
teardown_cleanup: false
diff --git a/tests/samples/tsp_script.tsp b/tests/samples/tsp_script.tsp
index 60da9791..4a51ccc6 100644
--- a/tests/samples/tsp_script.tsp
+++ b/tests/samples/tsp_script.tsp
@@ -1,2 +1,2 @@
-"""Sample script used in test_smu.py."""
+-- Sample script used in test_smu.py.
print("TEK")
diff --git a/tests/sim_devices/smu/smu2601b.yaml b/tests/sim_devices/smu/smu2601b.yaml
index e08146ff..241c9a55 100644
--- a/tests/sim_devices/smu/smu2601b.yaml
+++ b/tests/sim_devices/smu/smu2601b.yaml
@@ -7,8 +7,8 @@ devices:
r: Keithley Instruments Inc., Model 2601B, 4498311, 3.3.5
- q: print(available(gpib))
r: 'true'
- - q: loadscript loadfuncs\n"""Sample script used in test_smu.py."""\nprint("TEK")\nendscript
- - q: loadscript tsp_function\n"""Sample script used in test_smu.py."""\nprint("TEK")\nendscript
+ - q: loadscript loadfuncs\n-- Sample script used in test_smu.py.\nprint("TEK")\nendscript
+ - q: loadscript tsp_function\n-- Sample script used in test_smu.py.\nprint("TEK")\nendscript
- q: loadfuncs.save()
- q: loadfuncs()
- q: errorqueue.clear()
diff --git a/tests/sim_devices/smu/smu2601b_pulse.yaml b/tests/sim_devices/smu/smu2601b_pulse.yaml
index 8cfd52d2..9627689c 100644
--- a/tests/sim_devices/smu/smu2601b_pulse.yaml
+++ b/tests/sim_devices/smu/smu2601b_pulse.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2602b.yaml b/tests/sim_devices/smu/smu2602b.yaml
index 6a85a12d..3f29fc5e 100644
--- a/tests/sim_devices/smu/smu2602b.yaml
+++ b/tests/sim_devices/smu/smu2602b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2604b.yaml b/tests/sim_devices/smu/smu2604b.yaml
index df0a1a25..6c167073 100644
--- a/tests/sim_devices/smu/smu2604b.yaml
+++ b/tests/sim_devices/smu/smu2604b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2606b.yaml b/tests/sim_devices/smu/smu2606b.yaml
index b7958699..8624bc7c 100644
--- a/tests/sim_devices/smu/smu2606b.yaml
+++ b/tests/sim_devices/smu/smu2606b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2611b.yaml b/tests/sim_devices/smu/smu2611b.yaml
index f7e01632..c0e17f00 100644
--- a/tests/sim_devices/smu/smu2611b.yaml
+++ b/tests/sim_devices/smu/smu2611b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2612b.yaml b/tests/sim_devices/smu/smu2612b.yaml
index 3355842c..e686bb88 100644
--- a/tests/sim_devices/smu/smu2612b.yaml
+++ b/tests/sim_devices/smu/smu2612b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2614b.yaml b/tests/sim_devices/smu/smu2614b.yaml
index a6eda98f..87d56c88 100644
--- a/tests/sim_devices/smu/smu2614b.yaml
+++ b/tests/sim_devices/smu/smu2614b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2634b.yaml b/tests/sim_devices/smu/smu2634b.yaml
index b35e8f9c..e66283f6 100644
--- a/tests/sim_devices/smu/smu2634b.yaml
+++ b/tests/sim_devices/smu/smu2634b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2635b.yaml b/tests/sim_devices/smu/smu2635b.yaml
index 271b6e3e..7d2db1c5 100644
--- a/tests/sim_devices/smu/smu2635b.yaml
+++ b/tests/sim_devices/smu/smu2635b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2636b.yaml b/tests/sim_devices/smu/smu2636b.yaml
index 6fef3be0..c46ac58f 100644
--- a/tests/sim_devices/smu/smu2636b.yaml
+++ b/tests/sim_devices/smu/smu2636b.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2651a.yaml b/tests/sim_devices/smu/smu2651a.yaml
index f99a2492..58abba85 100644
--- a/tests/sim_devices/smu/smu2651a.yaml
+++ b/tests/sim_devices/smu/smu2651a.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2657a.yaml b/tests/sim_devices/smu/smu2657a.yaml
index 32550b72..ca306550 100644
--- a/tests/sim_devices/smu/smu2657a.yaml
+++ b/tests/sim_devices/smu/smu2657a.yaml
@@ -10,7 +10,7 @@ devices:
- q: loadscript tsp_script
- q: loadscript tsp_function
- q: print("TEK")
- - q: '"""Sample script used in test_smu.py."""'
+ - q: -- Sample script used in test_smu.py.
- q: endscript
- q: tsp_script.save()
- q: tsp_script()
diff --git a/tests/test_all_device_drivers.py b/tests/test_all_device_drivers.py
index e9819d9d..9b3c9c98 100644
--- a/tests/test_all_device_drivers.py
+++ b/tests/test_all_device_drivers.py
@@ -2,8 +2,10 @@
"""Verify that all device drivers and connection types can be used."""
import contextlib
+import sys
from collections import Counter
+from pathlib import Path
from typing import Generator, List, Optional
import pytest
@@ -205,7 +207,29 @@ def test_all_device_drivers() -> None:
sorted_created_connections_list == supported_connections_list
), f"Some connections are not tested: {connections_without_testing=}"
- print(f"\nVerified all {len(SIMULATED_DEVICE_LIST)} device drivers")
- print(
+ print(f"\nVerified all {len(SIMULATED_DEVICE_LIST)} device drivers") # noqa: T201
+ print( # noqa: T201
f"{len(drivers_with_auto_generated_commands)} device drivers have auto-generated commands"
)
+
+
+@pytest.mark.order(3)
+@pytest.mark.depends(on=["test_device_driver"])
+def test_visa_device_command_logging() -> None:
+ """Test the VISA command log file contents."""
+ generated_file = (
+ Path(__file__).parent / f"logs/unit_test_py{sys.version_info.major}{sys.version_info.minor}"
+ f"_visa_commands_SMU2410-HOSTNAME.log"
+ )
+ assert generated_file.exists(), f"File not found: {generated_file}"
+ assert (
+ generated_file.read_text()
+ == """*CLS
+*RST
+*OPC?
+*ESR?
+SYSTEM:ERROR?
+*ESR?
+SYSTEM:ERROR?
+"""
+ )
diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py
index 15f70357..4b72db5b 100644
--- a/tests/test_config_parser.py
+++ b/tests/test_config_parser.py
@@ -32,7 +32,7 @@ def test_nested_config_prefix_mapping() -> None:
def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None:
"""Test the environment variable config method."""
- options = ["STANDALONE", "DEFAULT_VISA_TIMEOUT=10000"]
+ options = ["STANDALONE", "DEFAULT_VISA_TIMEOUT=10000", "LOG_FILE_LEVEL=NONE"]
expected_device_string = (
"address=MSO54-123456,connection_type=TCPIP,device_type=SCOPE,lan_device_name=hislip0"
)
@@ -54,6 +54,7 @@ def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None
assert str(expected_device) == expected_device_string
assert not config.options.teardown_cleanup
assert config.options.standalone
+ assert config.options.log_file_level == "NONE"
assert config.options.default_visa_timeout == 10000
assert (
config.devices == expected_entry
@@ -61,10 +62,10 @@ def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None
# test that the config string representation looks like env declaration
expected_entry_string = (
- f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,STANDALONE\n"
+ f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,LOG_FILE_LEVEL=NONE,STANDALONE\n"
f"TM_DEVICES=~~~{expected_device_string}~~~"
)
- print(config)
+ print(config) # noqa: T201
assert capsys.readouterr().out.strip() == expected_entry_string
# test smu with serial properties
@@ -101,10 +102,10 @@ def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None
config.devices == expected_entry
), f"\nDevice dictionaries don't match:\n{expected_entry}\n{config.devices}"
expected_entry_string = (
- f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,STANDALONE\n"
+ f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,LOG_FILE_LEVEL=NONE,STANDALONE\n"
f"TM_DEVICES=~~~{expected_device_string}~~~"
)
- print(config)
+ print(config) # noqa: T201
assert capsys.readouterr().out.strip() == expected_entry_string
expected_device = DeviceConfigEntry(
@@ -149,6 +150,7 @@ def test_file_config_default_path() -> None:
verbose_mode: false
verbose_visa: false
default_visa_timeout: 10000
+ log_console_level: DEBUG
"""
expected_options = DMConfigOptions(
standalone=False,
@@ -157,6 +159,7 @@ def test_file_config_default_path() -> None:
verbose_mode=False,
verbose_visa=False,
default_visa_timeout=10000,
+ log_console_level="DEBUG",
)
expected_devices = {
"SCOPE 1": DeviceConfigEntry(
@@ -179,7 +182,7 @@ def test_file_config_default_path() -> None:
}
with mock.patch.dict("os.environ", {}, clear=True), mock.patch(
"pathlib.Path.is_file", mock.MagicMock(return_value=True)
- ), mock.patch("builtins.open", mock.mock_open(read_data=file_contents)):
+ ), mock.patch("pathlib.Path.open", mock.mock_open(read_data=file_contents)):
config = DMConfigParser()
assert expected_options == config.options
@@ -214,6 +217,7 @@ def test_file_config_non_default_path(
verbose_mode=False,
verbose_visa=False,
default_visa_timeout=1000,
+ log_file_level="WARNING",
)
expected_devices = {
"SCOPE 1": DeviceConfigEntry(
@@ -251,9 +255,7 @@ def test_file_config_non_default_path(
config_2.load_config_file(os_environ["TM_DEVICES_CONFIG"])
# Read in the golden files
- with open(os_environ["TM_DEVICES_CONFIG"], encoding="utf-8") as config_file:
- text = config_file.read()
-
+ text = Path(os_environ["TM_DEVICES_CONFIG"]).read_text(encoding="utf-8")
assert config.to_config_file_text(file_type) == text, "issue generating config file text"
assert config_2.to_config_file_text(file_type) == text, "issue generating config file text"
@@ -372,6 +374,11 @@ def test_file_config_non_default_path(
{"TM_DEVICES": "device_type=MT,connection_type=REST_API,address=localhost"},
ValueError,
),
+ # Test invalid configuration options
+ (
+ {"TM_OPTIONS": "STANDALONE,LOG_FILE_LEVEL=INVALID"},
+ ValueError,
+ ),
],
)
def test_invalid_config_creation(
diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py
index 1eb1a480..6a88d5d8 100644
--- a/tests/test_device_manager.py
+++ b/tests/test_device_manager.py
@@ -69,8 +69,7 @@ def test_get_config_methods_with_devices(self, device_manager: DeviceManager) ->
device_manager.write_current_configuration_to_config_file()
# respect the env var if no path is given
device_manager.write_current_configuration_to_config_file("./temp_config.yaml")
- with open("./temp_config.toml", encoding="utf-8") as temp_config:
- text = temp_config.read()
+ text = Path("./temp_config.toml").read_text(encoding="utf-8")
assert (
text
== f"""[[devices]]
@@ -101,9 +100,8 @@ def test_get_config_methods_with_devices(self, device_manager: DeviceManager) ->
verbose_visa = false
"""
)
- with open("./temp_config.yaml", encoding="utf-8") as temp_config:
- text = temp_config.read()
- print(text)
+ text = Path("./temp_config.yaml").read_text(encoding="utf-8")
+ print(text) # noqa: T201
assert (
text
== f"""---
diff --git a/tests/test_docs.py b/tests/test_docs.py
index 66e2a133..f9713c9e 100644
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -51,7 +51,7 @@ def fixture_site_dir(pytestconfig: pytest.Config) -> str:
@pytest.fixture(scope="module", autouse=True)
def _docs_tests_setup() -> Generator[None, None, None]: # pyright: ignore [reportUnusedFunction]
"""Setup for docs tests.."""
- starting_directory = os.getcwd()
+ starting_directory = Path.cwd()
try:
os.chdir(PROJECT_ROOT_DIR)
yield
diff --git a/tests/test_extension_mixin.py b/tests/test_extension_mixin.py
index 96f2b7ec..b261ca67 100644
--- a/tests/test_extension_mixin.py
+++ b/tests/test_extension_mixin.py
@@ -243,7 +243,7 @@ def custom_model_getter_afg3kc(device: AFG3KC, value: str) -> str:
return f"AFG3KC {device.model} {value}"
############################################################################################
- start_dir = os.getcwd()
+ start_dir = Path.cwd()
try:
os.chdir(generated_stub_dir)
subprocess.check_call( # noqa: S603
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index c9862c23..d2285a80 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,7 +1,6 @@
# pyright: reportPrivateUsage=none
"""Tests for the helpers subpackage."""
-import datetime
import random
import socket
@@ -12,11 +11,9 @@
from typing import Any, ClassVar, Dict, List, Optional, Tuple
from unittest import mock
-import dateutil.parser
import pytest
import pyvisa as visa
-from dateutil.tz import tzlocal
from packaging.version import InvalidVersion, Version
from requests import Response
@@ -35,7 +32,6 @@
get_version,
get_visa_backend,
ping_address,
- print_with_timestamp,
sanitize_enum,
SupportedModels,
VALID_DEVICE_CONNECTION_TYPES,
@@ -136,11 +132,6 @@ def test_check_network_connection() -> None:
assert "ping >> 127.0.0.1" in message
assert "Response from ping >>" in message
- stdout = StringIO()
- with redirect_stdout(stdout):
- assert check_network_connection("name", "127.0.0.1", verbose=False)[0]
- assert stdout.getvalue() == ""
-
def test_check_port_connection() -> None:
"""Test checking a port connection."""
@@ -151,39 +142,10 @@ def test_check_port_connection() -> None:
assert check_port_connection("name", "127.0.0.1", 80, timeout_seconds=1)
message = stdout.getvalue()
assert "(name) >> checking if port 80 is open on 127.0.0.1" in message
- assert message.endswith("Result >> True\n")
+ assert message.endswith("(name) port 80 open = True\n")
- stdout = StringIO()
- with redirect_stdout(stdout), mock.patch(
- "socket.socket.connect", mock.MagicMock(side_effect=socket.error(""))
- ):
- assert not check_port_connection(
- "name", "127.0.0.1", 55555, timeout_seconds=1, verbose=False
- )
- assert stdout.getvalue() == ""
-
-
-def test_print_with_timestamp() -> None:
- """Test the print_with_timestamp helper function."""
- stdout = StringIO()
- with redirect_stdout(stdout):
- now = datetime.datetime.now(tz=tzlocal())
- print_with_timestamp("message")
-
- message = stdout.getvalue()
- message_parts = message.split(" - ")
- assert len(message_parts) == 2
- assert message_parts[1] == "message\n"
- parsed_datetime = dateutil.parser.parse(message_parts[0].strip())
- allowed_difference = datetime.timedelta(
- days=0,
- hours=0,
- minutes=0,
- seconds=1,
- microseconds=0,
- )
- calculated_difference = abs(parsed_datetime - now)
- assert calculated_difference <= allowed_difference
+ with mock.patch("socket.socket.connect", mock.MagicMock(side_effect=socket.error(""))):
+ assert not check_port_connection("name", "127.0.0.1", 55555, timeout_seconds=1)
def test_sanitizing_enums() -> None:
@@ -257,18 +219,11 @@ def test_create_and_check_visa_connection(capsys: pytest.CaptureFixture[str]) ->
assert check_visa_connection(dev_config_3, SIMULATED_VISA_LIB, "dev_config_3")
stdout = capsys.readouterr().out
- assert "checking if a VISA connection can be made to " in stdout
- assert "Result >> True" in stdout
-
- assert check_visa_connection(dev_config_3, SIMULATED_VISA_LIB, "dev_config_3", verbose=False)
- stdout = capsys.readouterr().out
- assert "checking if a VISA connection can be made to " not in stdout
- assert "Result >> True" not in stdout
+ assert "(dev_config_3) >> checking if a VISA connection can be made to " in stdout
+ assert "(dev_config_3) VISA connected = True" in stdout
with mock.patch("pyvisa.ResourceManager", mock.MagicMock(side_effect=visa.Error())):
- assert not check_visa_connection(
- dev_config_3, SIMULATED_VISA_LIB, "dev_config_3", verbose=False
- )
+ assert not check_visa_connection(dev_config_3, SIMULATED_VISA_LIB, "dev_config_3")
def test_check_for_update(capsys: pytest.CaptureFixture[str]) -> None:
diff --git a/tests/test_logging.py b/tests/test_logging.py
new file mode 100644
index 00000000..bb9712e0
--- /dev/null
+++ b/tests/test_logging.py
@@ -0,0 +1,120 @@
+"""Tests for the logging functionality."""
+
+import contextlib
+import logging
+import shutil
+import sys
+
+from pathlib import Path
+from typing import Generator, TYPE_CHECKING
+
+import colorlog
+import pytest
+import pyvisa
+
+import tm_devices
+
+from tm_devices import configure_logging, DeviceManager, LoggingLevels, PACKAGE_NAME
+from tm_devices.helpers import logging as tm_devices_logging
+
+if TYPE_CHECKING:
+ from tm_devices.drivers import MSO2
+
+
+@pytest.fixture(name="remove_log_file_handler")
+def _remove_log_file_handler(device_manager: DeviceManager) -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction]
+ """Remove the file handler from the logger."""
+ device_manager.remove_all_devices()
+ logger = logging.getLogger(PACKAGE_NAME)
+ file_handler = None
+ with contextlib.suppress(StopIteration):
+ file_handler = next(
+ handler for handler in logger.handlers if isinstance(handler, logging.FileHandler)
+ )
+ logger.removeHandler(file_handler)
+ yield
+ if file_handler is not None:
+ logger.addHandler(file_handler)
+ device_manager.remove_all_devices()
+
+
+def test_visa_command_logging_edge_cases(
+ device_manager: DeviceManager,
+ remove_log_file_handler: None, # noqa: ARG001
+) -> None:
+ """Test VISA command logging edge cases."""
+ scope: MSO2 = device_manager.add_scope("MSO22-HOSTNAME")
+ assert scope.model == "MSO22"
+
+
+def test_logging_singleton() -> None:
+ """Verify the singleton behavior of the logging configuration function."""
+ package_logger = logging.getLogger(PACKAGE_NAME)
+ logger_handlers_copy = package_logger.handlers.copy()
+ assert len(logger_handlers_copy) == 3
+ logger = configure_logging()
+ assert len(logger.handlers) == 3
+ assert logger.handlers == logger_handlers_copy
+
+
+@pytest.fixture(name="reset_package_logger")
+def _reset_package_logger() -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction]
+ """Reset the package logger."""
+ logger = logging.getLogger(PACKAGE_NAME)
+ handlers_copy = logger.handlers.copy()
+ for handler in handlers_copy:
+ logger.removeHandler(handler)
+ tm_devices_logging._logger_initialized = False # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
+ yield
+ # Reset the handlers back to what they were
+ for handler in logger.handlers.copy():
+ logger.removeHandler(handler)
+ for handler in handlers_copy:
+ logger.addHandler(handler)
+
+
+def test_configure_logger_full(reset_package_logger: None) -> None: # noqa: ARG001
+ """Test the configuration function with all types of logs."""
+ log_dir = (
+ Path(__file__).parent / f"generated_logs_py{sys.version_info.major}{sys.version_info.minor}"
+ )
+ log_name = "custom_log.log"
+ shutil.rmtree(log_dir, ignore_errors=True)
+
+ assert not any(isinstance(handler, logging.FileHandler) for handler in pyvisa.logger.handlers)
+ assert len(logging.getLogger(PACKAGE_NAME).handlers) == 0 # pylint: disable=use-implicit-booleaness-not-comparison-to-zero
+ logger = configure_logging(
+ log_console_level="DEBUG",
+ log_file_level="DEBUG",
+ log_file_directory=log_dir,
+ log_file_name=log_name,
+ log_colored_output=False,
+ log_pyvisa_messages=True,
+ )
+ assert len(logger.handlers) == 3
+ assert any(isinstance(handler, logging.FileHandler) for handler in pyvisa.logger.handlers)
+ log_contents = (log_dir / log_name).read_text().split("\n")
+ assert len(log_contents) == 3
+ assert f"] [{PACKAGE_NAME}] [ DEBUG] timezone==" in log_contents[0]
+ assert log_contents[1].endswith(
+ f"] [{PACKAGE_NAME}] [ DEBUG] {PACKAGE_NAME}=={tm_devices.__version__}"
+ )
+ assert [type(x) for x in logger.handlers] == [
+ logging.NullHandler,
+ logging.FileHandler,
+ logging.StreamHandler,
+ ]
+
+
+def test_configure_logger_no_file(reset_package_logger: None) -> None: # noqa: ARG001
+ """Test the configuration function with no file logging."""
+ assert len(logging.getLogger(PACKAGE_NAME).handlers) == 0 # pylint: disable=use-implicit-booleaness-not-comparison-to-zero
+ logger = configure_logging(
+ log_console_level="DEBUG",
+ log_file_level=LoggingLevels.NONE,
+ log_colored_output=True,
+ log_pyvisa_messages=False,
+ )
+ assert len(logger.handlers) == 2
+ assert [type(x) for x in logger.handlers] == [logging.NullHandler, colorlog.StreamHandler]
+ assert isinstance(logger.handlers[1].formatter, colorlog.ColoredFormatter)
diff --git a/tests/test_margin_testers.py b/tests/test_margin_testers.py
index c588894c..f38d2f27 100644
--- a/tests/test_margin_testers.py
+++ b/tests/test_margin_testers.py
@@ -12,7 +12,7 @@
from tm_devices import DeviceManager
from tm_devices.drivers.margin_testers.margin_tester import MarginTester
-AUTH_TOKEN_FILE_PATH = f"{Path(__file__).parent}/samples/token.auth_token_file_path" # nosec
+AUTH_TOKEN_FILE_PATH = Path(__file__).parent / "samples/token.auth_token_file_path" # nosec
################################################################################################
@@ -121,6 +121,6 @@ def test_generate_headers(tmt4: MarginTester) -> None:
tmt4: Margin Tester device.
"""
tmt4.auth_token_file_path = AUTH_TOKEN_FILE_PATH
- assert tmt4.auth_token_file_path == AUTH_TOKEN_FILE_PATH
+ assert tmt4.auth_token_file_path == AUTH_TOKEN_FILE_PATH.as_posix()
headers = tmt4._generate_headers() # noqa: SLF001
assert headers["Authorization"] == "Bearer UNIT-TEST"
diff --git a/tests/test_pi_device.py b/tests/test_pi_device.py
index 5e639415..2923200f 100644
--- a/tests/test_pi_device.py
+++ b/tests/test_pi_device.py
@@ -14,24 +14,23 @@
def test_pi_control( # noqa: PLR0915
- device_manager: DeviceManager, capsys: pytest.CaptureFixture[str]
+ device_manager: DeviceManager,
+ capsys: pytest.CaptureFixture[str],
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test generic PIControl functionality.
Args:
device_manager: The DeviceManager object.
capsys: The captured stdout and stderr.
+ caplog: The captured log messages.
"""
scope: MSO2 = device_manager.add_scope("MSO22-HOSTNAME")
assert scope._open() # noqa: SLF001
assert scope.query_binary("CURVE?") == [0.0]
assert "Query Binary Values >> " in capsys.readouterr().out
- assert scope.query_binary("CURVE?", verbose=False) == [0.0]
- assert "Query Binary Values >> " not in capsys.readouterr().out
assert scope.query_raw_binary("CURVE?") == b"#14\x00\x00\x00\x00\n"
assert "Query Raw Binary >> " in capsys.readouterr().out
- assert scope.query_raw_binary("CURVE?", verbose=False) == b"#14\x00\x00\x00\x00\n"
- assert "Query Raw Binary >> " not in capsys.readouterr().out
scope.factory_reset()
assert "Write >> 'FACTORY'" in capsys.readouterr().out
with pytest.raises(AssertionError) as error:
@@ -48,8 +47,6 @@ def test_pi_control( # noqa: PLR0915
with pytest.raises(AssertionError):
scope.query_less_than("*OPC?", 1)
- scope.write_raw(b"FACTORY", verbose=False)
- assert "Write Raw >> " not in capsys.readouterr().out
with mock.patch(
"pyvisa.resources.messagebased.MessageBasedResource.write_raw",
mock.MagicMock(side_effect=visa.VisaIOError(123)),
@@ -85,12 +82,7 @@ def test_pi_control( # noqa: PLR0915
stdout = capsys.readouterr().out
assert f"Attempting to establish a VISA connection with {scope.resource_expression}" in stdout
assert f"Successfully established a VISA connection with {scope.resource_expression} " in stdout
- assert scope.wait_for_visa_connection(
- 0.1, sleep_seconds=1, accept_immediate_connection=True, verbose=False
- )
- stdout = capsys.readouterr().out
- assert "Attempting to establish a VISA connection with " not in stdout
- assert "Successfully established a VISA connection with " not in stdout
+
with pytest.raises(AssertionError):
scope.wait_for_visa_connection(0.1, sleep_seconds=0, accept_immediate_connection=False)
with mock.patch("pyvisa.ResourceManager", mock.MagicMock(side_effect=visa.Error())):
@@ -106,17 +98,17 @@ def test_pi_control( # noqa: PLR0915
stdout = capsys.readouterr().out
assert scope.visa_timeout != old_timeout
assert scope.visa_timeout == 6000
- assert "VISA timeout set to: 6000ms" in stdout
+ assert "VISA timeout set to: 6000.000ms" in stdout
# test a temporary verbose OFF
with scope.temporary_verbose(False):
assert scope.verbose != old_verbose
# do something that would normally cause a printout
scope.visa_timeout = 5000
- stdout = capsys.readouterr().out
- assert stdout == ""
assert scope.visa_timeout == 5000
+ assert caplog.records[-1].levelname == "DEBUG"
+ assert caplog.records[-1].message.endswith("VISA timeout set to: 5000.000ms")
stdout = capsys.readouterr().out
- assert f"VISA timeout set to: {old_timeout}ms" in stdout
+ assert f"VISA timeout set to: {old_timeout}.000ms" in stdout
assert scope.visa_timeout == old_timeout
# Test closing a device that is powered off
diff --git a/tests/test_rest_api_device.py b/tests/test_rest_api_device.py
index c609f130..1bd0a4a5 100644
--- a/tests/test_rest_api_device.py
+++ b/tests/test_rest_api_device.py
@@ -28,7 +28,7 @@ class CustomRestApiDevice(RESTAPIControl, Device):
def _check_api_connection(self) -> bool:
"""Define abstract method _check_api_connection."""
- return self.get("/api", verbose=False, allow_errors=True)[0]
+ return self.get("/api", allow_errors=True)[0]
def _close(self) -> None:
"""Define abstract method _close."""
@@ -182,7 +182,7 @@ def test_set_api_version_non_verbose(rest_api_control: CustomRestApiDevice) -> N
rest_api_control: Rest API Device.
"""
rest_api_control.API_VERSIONS = MappingProxyType({1: "/api", 2: "/api2"})
- rest_api_control.set_api_version(api_version=2, verbose=False)
+ rest_api_control.set_api_version(api_version=2)
assert rest_api_control.api_url == rest_api_control.base_url + rest_api_control.API_VERSIONS[2]
diff --git a/tests/test_scopes.py b/tests/test_scopes.py
index 0755ee54..c4201efa 100644
--- a/tests/test_scopes.py
+++ b/tests/test_scopes.py
@@ -155,10 +155,10 @@ def test_tekscope(device_manager: DeviceManager) -> None: # noqa: PLR0915
scope.expect_esr(0, ('0,"No events to report - queue empty"',))
# Test curve query write to csv functionality with multi-frame curve
- filepath = f"temp_{sys.version_info.major}{sys.version_info.minor}.csv"
+ filepath = pathlib.Path(f"temp_{sys.version_info.major}{sys.version_info.minor}.csv")
try:
- curve = scope.curve_query(1, wfm_type="TimeDomain", output_csv_file=filepath)
- with open(filepath, encoding="utf8") as curve_csv:
+ curve = scope.curve_query(1, wfm_type="TimeDomain", output_csv_file=filepath.as_posix())
+ with filepath.open(encoding="utf8") as curve_csv:
# Remove trailing command a format string as list of ints based on commas
curve_reformatted_from_file = list(map(int, curve_csv.read()[:-1].split(",")))
# Flatten list of lists returned from multi-frame curve query
@@ -364,44 +364,51 @@ def test_exceptions(device_manager: DeviceManager) -> None:
device_manager.remove_all_devices()
-def test_tekscope70k(device_manager: DeviceManager, capsys: pytest.CaptureFixture[str]) -> None:
+def test_tekscope70k(
+ device_manager: DeviceManager,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
"""Test the tekscope_70k implementation.
Args:
device_manager: The DeviceManager object.
- capsys: The captured stdout and stderr.
+ caplog: The captured log messages.
"""
scope: TekScope5k7k70k = device_manager.add_scope("127.0.0.1")
assert scope.ip_address == "127.0.0.1"
assert scope.hostname == ""
# Test some generic device functionality
assert scope.wait_for_network_connection(
- wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True, verbose=True
- )
- assert (
- f"Successfully established a network connection with {scope.ip_address}"
- in capsys.readouterr().out
- )
- assert scope.wait_for_network_connection(
- wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True, verbose=False
+ wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True
)
- assert (
+ assert caplog.records[-1].message.startswith(
f"Successfully established a network connection with {scope.ip_address}"
- not in capsys.readouterr().out
)
+ assert caplog.records[-1].levelname == "INFO"
+
+ with scope.temporary_verbose(False):
+ assert scope.wait_for_network_connection(
+ wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True
+ )
+ assert caplog.records[-1].message.startswith(
+ f"Successfully established a network connection with {scope.ip_address}"
+ )
+ assert caplog.records[-1].levelname == "DEBUG"
+
with mock.patch(
"subprocess.check_output", mock.MagicMock(side_effect=subprocess.CalledProcessError(1, ""))
):
assert not scope.wait_for_network_connection(
- wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True, verbose=True
+ wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True
)
- assert (
- f"Unable to establish a network connection with {scope.ip_address}"
- in capsys.readouterr().out
+ assert caplog.records[-1].message.startswith(
+ f"Unable to establish a network connection with {scope.ip_address} after"
)
+ assert caplog.records[-1].levelname == "WARNING"
+
with pytest.raises(AssertionError):
scope.wait_for_network_connection(
- wait_time=0.05, sleep_seconds=1, accept_immediate_connection=False, verbose=False
+ wait_time=0.05, sleep_seconds=1, accept_immediate_connection=False
)
scope.expect_esr(0)
with pytest.raises(SystemError):
@@ -581,7 +588,7 @@ def test_tekscope2k(device_manager: DeviceManager, tmp_path: pathlib.Path) -> No
filename = tmp_path / "temp.txt"
curve = scope.curve_query(1, wfm_type="TimeDomain", output_csv_file=str(filename))
- with open(filename, encoding="utf8") as curve_csv:
+ with filename.open(encoding="utf8") as curve_csv:
curve_reformatted_from_file = list(map(float, curve_csv.read()[:-1].split(",")))
assert curve == curve_reformatted_from_file
diff --git a/tests/test_singleton.py b/tests/test_singleton.py
index 701c2ff5..eeaf8719 100644
--- a/tests/test_singleton.py
+++ b/tests/test_singleton.py
@@ -13,7 +13,7 @@ class DummyClass(metaclass=Singleton):
def __init__(self, value: bool = False) -> None:
"""Create an instance of the dummy class."""
- print("running init")
+ print("running init") # noqa: T201
self.init_count += 1
self.value = value
diff --git a/tests/test_smu.py b/tests/test_smu.py
index 2d02fd6f..f875c7ab 100644
--- a/tests/test_smu.py
+++ b/tests/test_smu.py
@@ -23,13 +23,16 @@
# pylint: disable=too-many-locals
def test_smu( # noqa: PLR0915
- device_manager: DeviceManager, capsys: pytest.CaptureFixture[str]
+ device_manager: DeviceManager,
+ capsys: pytest.CaptureFixture[str],
+ caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the SMU driver and TSP's IEEE commands.
Args:
device_manager: The DeviceManager object.
capsys: The captured stdout and stderr.
+ caplog: The captured log messages.
"""
smu: SMU2601B = device_manager.add_smu("smu2601b-hostname", alias="smu-device")
assert id(device_manager.get_smu(number_or_alias="smu-device")) == id(smu)
@@ -138,7 +141,7 @@ def test_smu( # noqa: PLR0915
f" object at {id(smu)}
address='SMU2601B-HOSTNAME'
@@ -183,35 +186,37 @@ def test_smu( # noqa: PLR0915
"socket.socket.shutdown", mock.MagicMock(return_value=None)
):
assert smu.wait_for_port_connection(
- 4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True, verbose=True
+ 4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True
)
assert (
f"Successfully established a connection to port 4000 on {smu.ip_address}"
- in capsys.readouterr().out
- )
- assert smu.wait_for_port_connection(
- 4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True, verbose=False
+ in caplog.records[-1].message
)
+ assert caplog.records[-1].levelname == "INFO"
+ with smu.temporary_verbose(False):
+ assert smu.wait_for_port_connection(
+ 4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True
+ )
assert (
f"Successfully established a connection to port 4000 on {smu.ip_address}"
- not in capsys.readouterr().out
+ in caplog.records[-1].message
)
+ assert caplog.records[-1].levelname == "DEBUG"
with pytest.raises(AssertionError):
smu.wait_for_port_connection(
4000,
wait_time=0.05,
sleep_seconds=0,
accept_immediate_connection=False,
- verbose=False,
)
with mock.patch("socket.socket.connect", mock.MagicMock(side_effect=socket.error(""))):
assert not smu.wait_for_port_connection(
- 4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True, verbose=True
+ 4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True
)
- assert (
- f"Successfully established a connection to port 4000 on {smu.ip_address}"
- not in capsys.readouterr().out
+ assert caplog.records[-1].message.startswith(
+ f"Unable to establish a connection to port 4000 on {smu.ip_address} after"
)
+ assert caplog.records[-1].levelname == "WARNING"
buffer = smu.get_buffers("smua.nvbuffer1")
expected_buffer = {"smua.nvbuffer1": [1.0, 2.0, 3.0, 4.0, 5.0]}
@@ -227,12 +232,12 @@ def test_smu( # noqa: PLR0915
for value in (1.0, 2.0, 3.0, 4.0, 5.0):
assert str(value) in stdout
- filepath = f"./temp_test_{sys.version_info.major}{sys.version_info.minor}.csv"
+ filepath = Path(f"./temp_test_{sys.version_info.major}{sys.version_info.minor}.csv")
try:
- smu.export_buffers(filepath, "smua.nvbuffer1")
- assert os.path.exists(filepath) # noqa: PTH110
- with open(filepath, encoding="utf-8") as file:
+ smu.export_buffers(filepath.as_posix(), "smua.nvbuffer1")
+ assert filepath.exists()
+ with filepath.open(encoding="utf-8") as file:
lines = file.readlines()
for index, value in enumerate(["smua.nvbuffer1", "1.0", "2.0", "3.0", "4.0", "5.0"]):
assert value in lines[index]
diff --git a/tests/test_verification_functions.py b/tests/test_verification_functions.py
index 66a3506d..487a89b5 100644
--- a/tests/test_verification_functions.py
+++ b/tests/test_verification_functions.py
@@ -23,7 +23,7 @@ def test_verify_values_fail() -> None:
log_error=True,
)
assert (
- " - ERROR: (failing-check) : Actual result does not match the expected "
+ "ERROR: (failing-check) : Actual result does not match the expected "
"result within a tolerance of 0.0, max: 0.1, act: 0.2, min: 0.1"
) in str(assertion_info.value)
@@ -48,7 +48,7 @@ def test_verify_values_regex_match_fail() -> None:
use_regex_match=True,
)
assert (
- " - FAILURE: (regex-fail-check) : Actual result does not match the expected result, "
+ "FAILURE: (regex-fail-check) : Actual result does not match the expected result, "
"exp: ^test.*value$, act: fail123value"
) in str(assertion_info.value)
@@ -71,7 +71,7 @@ def test_verify_values_condense_printout(log_error: bool, message_level: str) ->
log_error=log_error,
)
assert (
- f" - {message_level}: (condense-printout-check) : "
+ f"{message_level}: (condense-printout-check) : "
f"Actual result does not match the expected result"
"\n exp: expected"
"\n act: actual"
diff --git a/tests/validate_device_manager_delete.py b/tests/validate_device_manager_delete.py
index af27b597..ff3260eb 100644
--- a/tests/validate_device_manager_delete.py
+++ b/tests/validate_device_manager_delete.py
@@ -11,9 +11,11 @@
import pyvisa.constants
from conftest import mock_gethostbyaddr, mock_gethostbyname, SIMULATED_VISA_LIB
-from tm_devices import DeviceManager, PYVISA_PY_BACKEND
+from tm_devices import configure_logging, DeviceManager, LoggingLevels, PYVISA_PY_BACKEND
from tm_devices.helpers import DMConfigOptions
+configure_logging(log_console_level=LoggingLevels.DEBUG)
+
@mock.patch("socket.gethostbyname", mock.MagicMock(side_effect=mock_gethostbyname))
@mock.patch("socket.gethostbyaddr", mock.MagicMock(side_effect=mock_gethostbyaddr))
@@ -48,7 +50,7 @@ def verify_deleting_device_manager() -> None:
del dev_manager
stdout = stdout_buffer.getvalue()
- print(stdout)
+ print(stdout) # noqa: T201
assert "Closing Connections to Devices" in stdout
assert "Closing Connection to AFG 1" in stdout
assert "DeviceManager Closed" in stdout
@@ -67,7 +69,7 @@ def verify_deleting_device_manager() -> None:
del dev_manager
stdout = stdout_buffer.getvalue()
- print(stdout)
+ print(stdout) # noqa: T201
assert stdout == ""
# Test that the closing happens when the python interpreter exits
diff --git a/tests/verify_physical_device_support.py b/tests/verify_physical_device_support.py
index 57b1c9eb..85209727 100644
--- a/tests/verify_physical_device_support.py
+++ b/tests/verify_physical_device_support.py
@@ -18,6 +18,6 @@
device_manager.setup_cleanup_enabled = False
device_manager.teardown_cleanup_enabled = False
for device in device_manager.devices.values():
- print(device)
+ print(device) # noqa: T201
device.cleanup()
assert not device.has_errors()