Skip to content
Merged
13 changes: 9 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,19 @@ sigmon = [
"pygraphviz>=1.14",
"typer>=0.15.1",
"ezmsg-qt>=0.2.1",
"phosphor>=0.5.0",
"phosphor>=0.8.0",
"pandas",
]
viewer = [
"PySide6>=6.7",
"typer>=0.15.1",
"ezmsg-qt>=0.2.1",
"phosphor>=0.5.0",
"phosphor>=0.8.0",
]

[project.scripts]
ezmsg-performance-monitor = "ezmsg.tools.perfmon.cli:main"
ezmsg-signal-monitor = "ezmsg.tools.sigmon.cli:main"
ezmsg-performance-monitor = "ezmsg.tools.perfmon:main"
ezmsg-signal-monitor = "ezmsg.tools.sigmon:main"

[build-system]
requires = ["hatchling", "hatch-vcs"]
Expand Down Expand Up @@ -99,5 +99,10 @@ known-first-party = ["ezmsg.tools"]
known-third-party = ["ezmsg"]

[tool.uv.sources]
# Local path sources are a developer convenience and must not be committed: CI
# has no sibling checkouts, so a path here fails resolution for every job,
# including ones that never touch the package (`uv sync --only-group docs`).
# Add them locally with `uv add ../phosphor --editable --frozen` and drop the
# change before committing.
# Uncomment to use development version of ezmsg from git
#ezmsg = { git = "https://github.com/ezmsg-org/ezmsg.git", branch = "dev" }
42 changes: 42 additions & 0 deletions src/ezmsg/tools/_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Launching a console script whose dependencies live in an optional extra.

Console scripts are installed unconditionally -- ``[project.scripts]`` has no
notion of extras -- so ``pip install ezmsg-tools`` puts commands on the PATH
whose imports are not satisfied. Run one and you get a bare
``ModuleNotFoundError: dash`` with no hint that an extra exists or what it is
called.

The alternative would be promoting those dependencies to the core install, so
that a Dash web app drags in Qt and a GPU stack for everyone. A clear message
is the cheaper fix.
"""

import importlib
import os
import sys
import typing

__all__ = ["run_cli"]


def run_cli(module: str, extra: str) -> typing.NoReturn:
"""Import ``module`` and call its ``main()``, or explain what is missing.

:param module: Dotted path of the CLI module to run.
:param extra: The extra that declares this command's dependencies.
"""
try:
cli = importlib.import_module(module)
except ImportError as exc:
command = os.path.basename(sys.argv[0]) or module
missing = getattr(exc, "name", None)
# Name the module that was actually missing rather than assuming the
# extra is the whole story: an ImportError from inside the CLI is a
# different problem, and saying which one it was keeps this honest.
detail = f" (could not import {missing!r})" if missing else ""
raise SystemExit(
f"{command} needs the optional '{extra}' dependencies{detail}.\n"
f"Install them with:\n\n"
f" pip install 'ezmsg-tools[{extra}]'\n"
) from exc
sys.exit(cli.main())
12 changes: 12 additions & 0 deletions src/ezmsg/tools/perfmon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Performance monitor: a Dash app over ezmsg's profiler output.

The console script points here rather than at :mod:`.cli` so that a missing
``perfmon`` extra produces an explanation instead of a traceback -- see
:mod:`ezmsg.tools._entry`.
"""

from .._entry import run_cli


def main() -> None:
run_cli("ezmsg.tools.perfmon.cli", "perfmon")
56 changes: 56 additions & 0 deletions src/ezmsg/tools/plot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Putting ezmsg streams onto phosphor plots.

:mod:`.describe` is the pure half -- given dims, axes and attrs, work out what
is being plotted -- and imports neither Qt nor phosphor, so it is usable from a
topic subscriber, a shared-memory mirror, or a test with neither.
:mod:`.shmem_sweep` is the Qt widget built on it, and needs the ``viewer`` or
``sigmon`` extra.

``ShmemSweepWidget`` is resolved lazily so that importing this package, or
anything under it, does not pull in Qt. Eagerly importing it here would make
``from ezmsg.tools.plot.describe import ...`` fail without phosphor installed,
since importing a submodule runs its parent's ``__init__`` first -- which would
put a GPU stack behind a module that deliberately has no rendering dependency
at all.
"""

import typing

from .describe import (
METRIC_KINDS,
SWEEP_RENDERABLE_METRICS,
MetricSpec,
StreamShape,
UnsupportedMetricError,
describe_axisarray,
describe_mirror,
flatten_for_plot,
metric_axis,
require_sweep_renderable,
)

if typing.TYPE_CHECKING: # pragma: no cover - import for type checkers only
from .shmem_sweep import ShmemSweepWidget

__all__ = [
"METRIC_KINDS",
"SWEEP_RENDERABLE_METRICS",
"MetricSpec",
"ShmemSweepWidget",
"StreamShape",
"UnsupportedMetricError",
"describe_axisarray",
"describe_mirror",
"flatten_for_plot",
"metric_axis",
"require_sweep_renderable",
]


def __getattr__(name: str) -> typing.Any:
"""Resolve the Qt widget on first use (PEP 562)."""
if name == "ShmemSweepWidget":
from .shmem_sweep import ShmemSweepWidget

return ShmemSweepWidget
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Loading
Loading