diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..65cebe3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +# Description + +Please include a summary of the change and (optionally) which issue is fixed. Please also include +relevant motivation and context. + +Fixes # (issue) + +## Type of change + +Choose which options apply, and delete the ones which do not apply. + +- Bug fix (non-breaking change that fixes an issue) +- New feature (non-breaking change that adds functionality) +- Breaking change (fix or feature that would cause existing functionality to not work as expected) +- Documentation update +- Code maintenance/cleanup diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index dc699af..10fa469 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -132,6 +132,12 @@ jobs: COVERALLS_FLAG_NAME: ${{ matrix.python-version }} # -${{ matrix.redis-version }} COVERALLS_PARALLEL: true + - name: IPython Startup Test + shell: bash -l {0} + run: | + set -vxeuo pipefail + ipython -c "from instrument.startup import *; RE(make_devices())" + # https://coveralls-python.readthedocs.io/en/latest/usage/configuration.html#github-actions-support coveralls: name: Report unit test coverage to coveralls diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fe3cd1..1ba8aee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: # exclude_types: [jupyter] - id: check-yaml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..016a849 --- /dev/null +++ b/LICENSE @@ -0,0 +1,48 @@ +Copyright (c) 2023-2025, UChicago Argonne, LLC + +All Rights Reserved + +BITS + +BCDA, Advanced Photon Source, Argonne National Laboratory + + +OPEN SOURCE LICENSE + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. Software changes, + modifications, or derivative works, should be noted with comments and + the author and organization's name. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the names of UChicago Argonne, LLC or the Department of Energy + nor the names of its contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +4. The software and the end-user documentation included with the + redistribution, if any, must include the following acknowledgment: + + "This product includes software produced by UChicago Argonne, LLC + under Contract No. DE-AC02-06CH11357 with the Department of Energy." + +**************************************************************************** + +DISCLAIMER + +THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND. + +Neither the United States GOVERNMENT, nor the United States Department +of Energy, NOR uchicago argonne, LLC, nor any of their employees, makes +any warranty, express or implied, or assumes any legal liability or +responsibility for the accuracy, completeness, or usefulness of any +information, data, apparatus, product, or process disclosed, or +represents that its use would not infringe privately owned rights. + +**************************************************************************** diff --git a/README.md b/README.md index 3be04fc..71d5a3e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ cd bs_model_instrument Set up the development environment. ```bash -export ENV_NAME=bs_model_env +export ENV_NAME=bits conda create -y -n $ENV_NAME python=3.11 pyepics conda activate $ENV_NAME pip install -e ."[all]" diff --git a/docs/source/api/devices.rst b/docs/source/api/devices.rst index 5405557..981e1f7 100644 --- a/docs/source/api/devices.rst +++ b/docs/source/api/devices.rst @@ -8,6 +8,11 @@ See the advice below to declare your instrument's ophyd-style in YAML files: * :ref:`api.configs.devices` * :ref:`api.configs.devices_aps_only` +.. autosummary:: + :nosignatures: + + ~instrument.devices.factories + The `instrument.startup` module calls ``RE(make_devices())`` to make the devices as described. @@ -17,8 +22,11 @@ Declare all devices All ophyd-style devices are declared in one of the two YAML files listed above: * when code (classes, factory functions) is provided by a package: + * refer to the package class (or function) directly in the YAML file + * when local customization is needed (new support or modify a packaged class): + * create local custom code that defines the class or factory * refer to local custom code in the YAML file @@ -28,19 +36,19 @@ this API signature: .. code-block:: py :linenos: - callable(*, prefix="", name="", labels=[], **kwargs) + creator(*, prefix="", name="", labels=[], **kwargs) .. rubric:: Quick example An ophyd object for an EPICS motor PV ``gp:m1`` is created in Python code where -``ophyd.EpicsMotor`` is the *callable*, ``"gp:m1"`` is the `prefix`, and the +``ophyd.EpicsMotor`` is the *creator*, ``"gp:m1"`` is the `prefix`, and the other kwargs are ``name`` and ``labels``. .. code-block:: py :linenos: import ophyd - m1 = ophyd.EpicsMotor("ioc:m1", name="m1", labels=["motor"]) + m1 = ophyd.EpicsMotor("ioc:m1", name="m1", labels=["motors"]) This YAML replaces all the Python code above to create the ``m1`` object: @@ -50,7 +58,7 @@ This YAML replaces all the Python code above to create the ``m1`` object: ophyd.EpicsMotor: - name: "m1" prefix: "ioc:m1" - labels: ["motor"] + labels: ["motors"] .. tip:: The devices are (re)created each time ``RE(make_devices())`` is run. @@ -60,10 +68,11 @@ This YAML replaces all the Python code above to create the ``m1`` object: the Python code. The :func:`~instrument.utils.make_devices_yaml.make_devices()` plan stub - imports the callable and creates any devices listed below it. In YAML: + imports the 'creator' (Python code) and creates any devices listed + below it. In YAML: - * Each callable can only be listed once. - * All devices that are created with a callable are listed below it. + * Each 'creator' can only be listed once. + * All devices that are created with a 'creator' are listed below it. * Each device starts with a `-` and then the kwargs, as shown. Indentation is important. Follow the examples. @@ -82,9 +91,12 @@ This YAML replaces all the Python code above to create the ``m1`` object: - motor ophyd.EpicsMotor: - - {name: m1, prefix: ioc:m1, labels: ["motor"]} + - {name: m1, prefix: ioc:m1, labels: ["motors"]} + + ophyd.EpicsMotor: [{name: m1, prefix: ioc:m1, labels: ["motors"]}] - ophyd.EpicsMotor: [{name: m1, prefix: ioc:m1, labels: ["motor"]}] + instrument.devices.factories.motors: + - {prefix: ioc:m, names: m, first: 1, last: 1, labels: ["motors"]} Examples -------- @@ -108,11 +120,23 @@ describes five motors, using a one-line format for each dictionary. :linenos: ophyd.EpicsMotor: - - {name: m1, prefix: ioc:m1, labels: ["motor"]} - - {name: m2, prefix: ioc:m2, labels: ["motor"]} - - {name: m3, prefix: ioc:m3, labels: ["motor"]} - - {name: dx, prefix: vme:m58:c0:m1, labels: ["motor"]} - - {name: dy, prefix: vme:m58:c0:m2, labels: ["motor"]} + - {name: m1, prefix: ioc:m1, labels: ["motors"]} + - {name: m2, prefix: ioc:m2, labels: ["motors"]} + - {name: m3, prefix: ioc:m3, labels: ["motors"]} + - {name: dx, prefix: vme:m58:c0:m1, labels: ["motors"]} + - {name: dy, prefix: vme:m58:c0:m2, labels: ["motors"]} + +Using a factory to define some of these motors that fit a numerical pattern: + +.. code-block:: yaml + :linenos: + + instrument.devices.factories.motors: + - {prefix: ioc:m, names: m, first: 1, last: 3, labels: ["motors"]} + + ophyd.EpicsMotor: + - {name: dx, prefix: vme:m58:c0:m1, labels: ["motors"]} + - {name: dy, prefix: vme:m58:c0:m2, labels: ["motors"]} Scalers ~~~~~~~ @@ -192,12 +216,12 @@ Here's the local support code (in new file # the reciprocal axes are defined by SimMixin - mu = FCpt(EpicsMotor, "{prefix}{m_mu}", kind="hinted", labels=["motor"]) - omega = FCpt(EpicsMotor, "{prefix}{m_omega}", kind="hinted", labels=["motor"]) - chi = FCpt(EpicsMotor, "{prefix}{m_chi}", kind="hinted", labels=["motor"]) - phi = FCpt(EpicsMotor, "{prefix}{m_phi}", kind="hinted", labels=["motor"]) - gamma = FCpt(EpicsMotor, "{prefix}{m_gamma}", kind="hinted", labels=["motor"]) - delta = FCpt(EpicsMotor, "{prefix}{m_delta}", kind="hinted", labels=["motor"]) + mu = FCpt(EpicsMotor, "{prefix}{m_mu}", kind="hinted", labels=["motors"]) + omega = FCpt(EpicsMotor, "{prefix}{m_omega}", kind="hinted", labels=["motors"]) + chi = FCpt(EpicsMotor, "{prefix}{m_chi}", kind="hinted", labels=["motors"]) + phi = FCpt(EpicsMotor, "{prefix}{m_phi}", kind="hinted", labels=["motors"]) + gamma = FCpt(EpicsMotor, "{prefix}{m_gamma}", kind="hinted", labels=["motors"]) + delta = FCpt(EpicsMotor, "{prefix}{m_delta}", kind="hinted", labels=["motors"]) energy = Component(EpicsSignalRO, "BraggERdbkAO", kind="hinted", labels=["energy"]) energy_units = Component(EpicsSignalRO, "BraggERdbkAO.EGU", kind="config") @@ -279,3 +303,7 @@ can provide them to your plan stub: dither_x = oregistry["user_calcs.calc9"] dither_y = oregistry["user_calcs.calc10"] + +------------------ + +.. automodule:: instrument.devices.factories diff --git a/docs/source/install.rst b/docs/source/install.rst index 9708099..6c90301 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -35,7 +35,7 @@ by this ``instrument`` package for routine data acquisition. .. code-block:: bash :linenos: - export INSTALL_ENVIRONMENT_NAME=model_instrument_env + export INSTALL_ENVIRONMENT_NAME=bits conda create -y -n "${INSTALL_ENVIRONMENT_NAME}" python pyqt=5 pyepics conda activate "${INSTALL_ENVIRONMENT_NAME}" pip install -e . diff --git a/pyproject.toml b/pyproject.toml index f93d029..91c0623 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ maintainers = [ readme = "README.md" requires-python = ">=3.11" keywords = ['bluesky', 'queueserver'] -license = { file = "src/instrument/LICENSE" } +license = { file = "LICENSE" } classifiers = [ "Development Status :: 6 - Mature", "Environment :: Console", @@ -74,8 +74,8 @@ doc = [ all = ["instrument[dev,doc]"] [project.urls] -"Homepage" = "https://BCDA-APS.github.io/bs_model_instrument/" -"Bug Tracker" = "https://github.com/BCDA-APS/bs_model_instrument/issues" +"Homepage" = "https://BCDA-APS.github.io/BITS/" +"Bug Tracker" = "https://github.com/BCDA-APS/BITS/issues" # [project.scripts] # instrument = "instrument.app:main" diff --git a/src/instrument/configs/devices.yml b/src/instrument/configs/devices.yml index 3e8f01c..b9c12be 100644 --- a/src/instrument/configs/devices.yml +++ b/src/instrument/configs/devices.yml @@ -1,5 +1,9 @@ # Guarneri-style device YAML configuration +instrument.devices.factories.predefined_device: +- {creator: ophyd.sim.motor, name: sim_motor} +- {creator: ophyd.sim.noisy_det, name: sim_det} + apstools.devices.SimulatedApsPssShutterWithStatus: - name: shutter labels: ["shutters"] diff --git a/src/instrument/devices/__init__.py b/src/instrument/devices/__init__.py index 68ad866..892bfd6 100644 --- a/src/instrument/devices/__init__.py +++ b/src/instrument/devices/__init__.py @@ -1,4 +1 @@ """Ophyd-style devices.""" - -from ophyd.sim import motor as sim_motor # noqa: F401 -from ophyd.sim import noisy_det as sim_det # noqa: F401 diff --git a/src/instrument/devices/factories.py b/src/instrument/devices/factories.py new file mode 100644 index 0000000..a772f0a --- /dev/null +++ b/src/instrument/devices/factories.py @@ -0,0 +1,199 @@ +""" +Device factories +================ + +Device factories are used to: + +* *Create* several similar ophyd-style devices (such as ``ophyd.Device`` + or ``ophyd.Signal``) that fit a pattern. + +* *Import* a device which is pre-defined in a module, such as the + ophyd simulators in ``ophyd.sim``. + +.. autosummary:: + + ~factory_base + ~motors + ~predefined_device +""" + +import logging + +from apstools.utils import dynamic_import + +logger = logging.getLogger(__name__) +logger.bsdev(__file__) + + +def predefined_device(*, name="", creator=""): + """ + Provide a predefined device such as from the 'ophyd.sim' module. + + PARAMETERS + + creator : str + Name of the predefined device to be used + name : str + Simulator will be assigned this name. (default: use existing name) + + Example entry in `devices.yml` file: + + .. code-block:: yaml + :linenos: + + instrument.devices.factories.predefined_device: + - {creator: ophyd.sim.motor, name: sim_motor} + - {creator: ophyd.sim.noisy_det, name: sim_det} + """ + if creator == "": + raise ValueError("Must provide a value for 'creator'.") + device = dynamic_import(creator) + if name != "": + device.name = name + logger.debug(device) + yield device + + +def factory_base( + *, + prefix=None, + names="object{}", + first=0, + last=0, + creator="ophyd.Signal", + **kwargs, +): + """ + Make one or more objects using 'creator'. + + PARAMETERS + + prefix : str + Prefix *pattern* for the EPICS PVs (default: ``None``). + + names : str + Name *pattern* for the objects. The default pattern is ``"object{}"`` which + produces devices named ``object1, object2, ..., ```. If a formatting + specification (``{}``) is not provided, it is appended. Each object + will be named using this code: ``names.format(number)``, such as:: + + In [23]: "object{}".format(22) + Out[23]: 'object22' + + first : int + The first object number in the continuous series from 'first' through + 'last' (inclusive). + + last : int + The first object number in the continuous series from 'first' through + 'last' (inclusive). + + creator : str + Name of the *creator* code that will be used to construct each device. + (default: ``"ophyd.Signal"``) + + kwargs : dict + Dictionary of additional keyword arguments. This is included + when creating each object. + """ + if "{" not in names: + names += "{}" + if prefix is not None and "{" not in prefix: + prefix += "{}" + + klass = dynamic_import(creator) + + first, last = sorted([first, last]) + for i in range(first, 1 + last): + keywords = {"name": names.format(i)} + if prefix is not None: + keywords["prefix"] = prefix.format(i) + keywords.update(kwargs) + device = klass(**keywords) + logger.debug(device) + yield device + + +def motors( + *, + prefix=None, + names="m{}", + first=0, + last=0, + **kwargs, +): + """ + Make one or more '``ophyd.EpicsMotor``' objects. + + Example entry in `devices.yml` file: + + .. code-block:: yaml + :linenos: + + instrument.devices.factories.motors: + - {prefix: "ioc:m", first: 1, last: 4, labels: ["motor"]} + # skip m5 & m6 + - {prefix: "ioc:m", first: 7, last: 22, labels: ["motor"]} + + Uses this pattern: + + .. code-block:: py + :linenos: + + ophyd.EpicsMotor( + prefix=prefix.format(i), + name=names.format(i), + **kwargs, + ) + + where ``i`` iterates from 'first' through 'last' (inclusive). + + PARAMETERS + + prefix : str + Name *pattern* for the EPICS PVs. There is no default pattern. If a + formatting specification (``{}``) is not provided, it is appended (as + with other ophyd devices). Each motor will be configured with this + prefix: ``prefix.format(number)``, such as:: + + In [23]: "ioc:m{}".format(22) + Out[23]: 'ioc:m22' + + names : str + Name *pattern* for the motors. The default pattern is ``"m{}"`` which + produces motors named ``m1, m2, ..., m22, m23, ...```. If a formatting + specification (``{}``) is not provided, it is appended. Each motor + will be named using this code: ``names.format(number)``, such as:: + + In [23]: "m{}".format(22) + Out[23]: 'm22' + + first : int + The first motor number in the continuous series from 'first' through + 'last' (inclusive). + + last : int + The first motor number in the continuous series from 'first' through + 'last' (inclusive). + + kwargs : dict + Dictionary of additional keyword arguments. This is included + with each EpicsMotor object. + """ + if prefix is None: + raise ValueError("Must define a string value for 'prefix'.") + + kwargs["names"] = names or "m{}" + kwargs["prefix"] = prefix + kwargs.update( + { + "prefix": prefix, + "names": names or "m{}", + "first": first, + "last": last, + "creator": "ophyd.EpicsMotor", + } + ) + + for motor in factory_base(**kwargs): + yield motor diff --git a/src/instrument/plans/sim_plans.py b/src/instrument/plans/sim_plans.py index 7a6a35f..424269d 100644 --- a/src/instrument/plans/sim_plans.py +++ b/src/instrument/plans/sim_plans.py @@ -15,8 +15,7 @@ from bluesky import plan_stubs as bps from bluesky import plans as bp -from ..devices import sim_det -from ..devices import sim_motor +from ..utils.controls_setup import oregistry logger = logging.getLogger(__name__) logger.bsdev(__file__) @@ -27,6 +26,7 @@ def sim_count_plan(num: int = 1, imax: float = 10_000, md: dict = DEFAULT_MD): """Demonstrate the ``count()`` plan.""" logger.debug("sim_count_plan()") + sim_det = oregistry["sim_det"] yield from bps.mv(sim_det.Imax, imax) yield from bp.count([sim_det], num=num, md=md) @@ -35,6 +35,8 @@ def sim_print_plan(): """Demonstrate a ``print()`` plan stub (no data streams).""" logger.debug("sim_print_plan()") yield from bps.null() + sim_det = oregistry["sim_det"] + sim_motor = oregistry["sim_motor"] print("sim_print_plan(): This is a test.") print(f"sim_print_plan(): {sim_motor.position=} {sim_det.read()=}.") @@ -50,6 +52,8 @@ def sim_rel_scan_plan( ): """Demonstrate the ``rel_scan()`` plan.""" logger.debug("sim_rel_scan_plan()") + sim_det = oregistry["sim_det"] + sim_motor = oregistry["sim_motor"] # fmt: off yield from bps.mv( sim_det.Imax, imax, diff --git a/src/instrument/startup.py b/src/instrument/startup.py index e780604..5c59b62 100644 --- a/src/instrument/startup.py +++ b/src/instrument/startup.py @@ -23,7 +23,7 @@ from .utils.config_loaders import iconfig from .utils.helper_functions import register_bluesky_magics from .utils.helper_functions import running_in_queueserver -from .utils.make_devices_yaml import make_devices +from .utils.make_devices_yaml import make_devices # noqa: F401 logger = logging.getLogger(__name__) logger.bsdev(__file__) @@ -56,5 +56,3 @@ from bluesky import plans as bp # noqa: F401 from .utils.controls_setup import oregistry # noqa: F401 - -RE(make_devices()) # create all the ophyd-style control devices diff --git a/src/instrument/tests/conftest.py b/src/instrument/tests/conftest.py new file mode 100644 index 0000000..db19437 --- /dev/null +++ b/src/instrument/tests/conftest.py @@ -0,0 +1,32 @@ +""" +Pytest fixtures for instrument tests. + +This module provides fixtures for initializing the RunEngine with devices, +allowing tests to operate with device-dependent configurations without relying +on the production startup logic. + +Fixtures: + runengine_with_devices: A RunEngine object in a session with devices configured. +""" + +from typing import Any + +import pytest + +from ..startup import RE +from ..startup import make_devices + + +@pytest.fixture(scope="session") +def runengine_with_devices() -> Any: + """ + Initialize the RunEngine with devices for testing. + + This fixture calls RE with the `make_devices()` plan stub to mimic + the behavior previously performed in the startup module. + + Returns: + Any: An instance of the RunEngine with devices configured. + """ + RE(make_devices()) + return RE diff --git a/src/instrument/tests/test_device_factories.py b/src/instrument/tests/test_device_factories.py new file mode 100644 index 0000000..39588a3 --- /dev/null +++ b/src/instrument/tests/test_device_factories.py @@ -0,0 +1,44 @@ +"""Test the device factories.""" + +import pytest + +from ..devices.factories import motors +from ..devices.factories import predefined_device + + +@pytest.mark.parametrize( + "creator, name, klass", + [ + ["ophyd.sim.motor", None, "SynAxis"], + ["ophyd.sim.motor", "sim_motor", "SynAxis"], + ["ophyd.sim.noisy_det", None, "SynGauss"], + ["ophyd.sim.noisy_det", "sim_det", "SynGauss"], + ], +) +def test_predefined(creator, name, klass): + """import predefined devices""" + for device in predefined_device(creator=creator, name=name): + assert device is not None + assert device.__class__.__name__ == klass + if name is not None: + assert device.name == name + + +@pytest.mark.parametrize( + "kwargs", + [ + {"prefix": "ioc:m", "first": 1, "last": 4, "labels": ["motor"]}, + {"prefix": "ioc:m", "names": "m", "first": 7, "last": 22, "labels": ["motor"]}, + ], +) +def test_motors(kwargs): + """create a block of motors""" + count = 0 + for device in motors(**kwargs): + count += 1 + assert device is not None + assert device.__class__.__name__ == "EpicsMotor" + if kwargs.get("names") is None: + assert device.name.startswith("m") + assert isinstance(int(device.name[1:]), int) + assert count == (1 + kwargs["last"] - kwargs["first"]) diff --git a/src/instrument/tests/test_general.py b/src/instrument/tests/test_general.py index caa4527..c808c3c 100644 --- a/src/instrument/tests/test_general.py +++ b/src/instrument/tests/test_general.py @@ -9,7 +9,6 @@ from ..plans.sim_plans import sim_count_plan from ..plans.sim_plans import sim_print_plan from ..plans.sim_plans import sim_rel_scan_plan -from ..startup import RE from ..startup import bec from ..startup import cat from ..startup import iconfig @@ -19,15 +18,19 @@ from ..startup import specwriter -def test_startup(): - """Test that standard startup works.""" +def test_startup(runengine_with_devices: object) -> None: + """ + Test that standard startup works and the RunEngine has initialized the devices. + """ + # The fixture ensures that runengine_with_devices is initialized. + assert runengine_with_devices is not None assert cat is not None assert bec is not None assert peaks is not None assert sd is not None assert iconfig is not None - assert RE is not None assert specwriter is not None + if iconfig.get("DATABROKER_CATALOG", "temp") == "temp": assert len(cat) == 0 assert not running_in_queueserver() @@ -41,21 +44,26 @@ def test_startup(): [sim_rel_scan_plan, 1], ], ) -def test_sim_plans(plan, n_uids): - """Test supplied simulator plans.""" +def test_sim_plans(runengine_with_devices: object, plan: object, n_uids: int) -> None: + """ + Test supplied simulator plans using the RunEngine with devices. + """ bec.disable_plots() n_runs = len(cat) - uids = RE(plan()) + # Use the fixture-provided run engine to run the plan. + uids = runengine_with_devices(plan()) assert len(uids) == n_uids assert len(cat) == n_runs + len(uids) -def test_iconfig(): - """Test the instrument configuration.""" - version = iconfig.get("ICONFIG_VERSION", "0.0.0") +def test_iconfig() -> None: + """ + Test the instrument configuration. + """ + version: str = iconfig.get("ICONFIG_VERSION", "0.0.0") assert version >= "2.0.0" - cat_name = iconfig.get("DATABROKER_CATALOG") + cat_name: str = iconfig.get("DATABROKER_CATALOG") assert cat_name is not None assert cat_name == cat.name diff --git a/src/instrument/utils/helper_functions.py b/src/instrument/utils/helper_functions.py index 426c3fd..2e765f1 100644 --- a/src/instrument/utils/helper_functions.py +++ b/src/instrument/utils/helper_functions.py @@ -24,59 +24,90 @@ logger.bsdev(__file__) -def register_bluesky_magics(): - """The Bluesky Magick functions are useful with command-lines.""" - _ipython = get_ipython() - if _ipython is not None: - _ipython.register_magics(BlueskyMagics) +def register_bluesky_magics() -> None: + """ + Register Bluesky magics if an IPython environment is detected. + + This function registers the BlueskyMagics if get_ipython() returns a valid IPython + instance. + """ + ipython = get_ipython() + if ipython is not None: + ipython.register_magics(BlueskyMagics) -def running_in_queueserver(): - """Detect if running in the bluesky queueserver.""" +def running_in_queueserver() -> bool: + """ + Detect if running in the bluesky queueserver. + + Returns: + bool: True if running in the queueserver, False otherwise. + """ try: - active = is_re_worker_active() - # print(f"{active=!r}") + active: bool = is_re_worker_active() return active except Exception as cause: print(f"{cause=}") return False -def debug_python(): - """""" - # terse error dumps (Exception tracebacks) - _ip = get_ipython() - if _ip is not None: - _xmode_level = iconfig.get("XMODE_DEBUG_LEVEL", "Minimal") - _ip.run_line_magic("xmode", _xmode_level) +def debug_python() -> None: + """ + Enable detailed debugging for Python exceptions in the IPython environment. + + This function adjusts the xmode settings for exception tracebacks based on the + configuration. + """ + ipython = get_ipython() + if ipython is not None: + xmode_level: str = iconfig.get("XMODE_DEBUG_LEVEL", "Minimal") + ipython.run_line_magic("xmode", xmode_level) print("\nEnd of IPython settings\n") - logger.bsdev("xmode exception level: '%s'", _xmode_level) + logger.bsdev("xmode exception level: '%s'", xmode_level) -def is_notebook(): +def is_notebook() -> bool: """ - Detect if running in a notebook. + Detect if the current environment is a Jupyter Notebook. - see: https://stackoverflow.com/a/39662359/1046449 + Returns: + bool: True if running in a notebook (Jupyter notebook or qtconsole), + False otherwise. """ try: - shell = get_ipython().__class__.__name__ + shell: str = get_ipython().__class__.__name__ if shell == "ZMQInteractiveShell": return True # Jupyter notebook or qtconsole elif shell == "TerminalInteractiveShell": return False # Terminal running IPython else: - return False # Other type (?) - + return False # Other type except NameError: - return False # Probably standard Python interpreter + return False # Standard Python interpreter -def mpl_setup(): +def mpl_setup() -> None: """ - Matplotlib setup based on environment (Notebook or non-Notebook). + Configure the Matplotlib backend based on the current environment. + + For non-queueserver and non-notebook environments, attempts to use the 'qtAgg' + backend. + If 'qtAgg' is not available due to missing dependencies, falls back to the 'Agg' + backend. + + Returns: + None """ if not running_in_queueserver(): if not is_notebook(): - mpl.use("qtAgg") # Set the backend early - plt.ion() + try: + mpl.use("qtAgg") + plt.ion() + logger.info("Using qtAgg backend for matplotlib.") + except Exception as exc: + logger.error( + "qtAgg backend is not available, falling back to Agg backend. \ + Error: %s", + exc, + ) + mpl.use("Agg") diff --git a/src/instrument/utils/logging_setup.py b/src/instrument/utils/logging_setup.py index 3873be0..22f7f4b 100644 --- a/src/instrument/utils/logging_setup.py +++ b/src/instrument/utils/logging_setup.py @@ -193,7 +193,7 @@ def _setup_ipython_logger(logger, cfg): "\nBelow are the IPython logging settings for your session." "\nThese settings have no impact on your experiment.\n" ) - _ipython.magic(f"logstart {options} {log_file} {log_mode}") + _ipython.run_line_magic("logstart", f"{options} {log_file} {log_mode}") if logger is not None: logger.bsdev("Console logging: %s", log_file) except Exception as exc: diff --git a/src/instrument/utils/make_devices_yaml.py b/src/instrument/utils/make_devices_yaml.py index 2248cca..ab2f649 100644 --- a/src/instrument/utils/make_devices_yaml.py +++ b/src/instrument/utils/make_devices_yaml.py @@ -103,12 +103,12 @@ def parse_yaml_file(self, config_file: pathlib.Path | str) -> list[dict]: if isinstance(config_file, str): config_file = pathlib.Path(config_file) - def parser(class_name, specs): - if class_name not in self.device_classes: - self.device_classes[class_name] = dynamic_import(class_name) + def parser(creator, specs): + if creator not in self.device_classes: + self.device_classes[creator] = dynamic_import(creator) entries = [ { - "device_class": class_name, + "device_class": creator, "args": (), # ALL specs are kwargs! "kwargs": table, }