Place runtime code in src/erlab/ (analysis routines, interactive Qt tools, IO plugins, visualization helpers). Tests mirror that tree under tests/, with plugin fixtures in tests/io/plugins/. Build outputs (build/, dist/, manager.spec), shared assets (resources/, PythonInterface.ipf), and docs (docs/) stay isolated so releases remain lean.
uv sync --all-extras --dev --group pyqt6— editable env with every optional feature and Qt bindings installed for GUI tests.uv run pytest— whole suite with coverage configuration frompyproject.toml.uv run python -m scripts.ci_test_groups --check-partition— verify that the fast CI coverage shards still cover every test file exactly once.uv run python -m scripts.ci_test_groups compat— print the compatibility smoke targets used in non-primary CI lanes.uv run ruff format .anduv run ruff check --fix .after every change to keep style consistent. Prefer automatic fixes instead of manual lint cleanups.uv run mypy src— static typing pass.uv run pyinstaller manager.spec— bundle the ImageTool manager app.uv build— produce wheels and sdists for release.
Use ASD-STE100 Simplified Technical English for agent responses, commit messages, documentation, and pull request titles and descriptions. Use short and direct sentences. Put one idea or instruction in each sentence. Use precise words and consistent terms. Do not use idioms, unnecessary jargon, or ambiguous wording. Keep a necessary technical term when an approved alternative is not accurate. Define the term when the reader might not know it.
Sources live in docs/source/ (MyST + Sphinx). Install extras using uv sync --all-extras --dev --group docs, then render locally via uv run --directory docs make html. Put tutorials in docs/source/user-guide/, guides in docs/source/contributing.md, and images in docs/source/images/. Run make linkcheck before pushing large doc edits to guard cross-references. The sphinxext-rediraffe extension is used for maintaining redirects. Add to the rediraffe_redirects dict in docs/source/conf.py when moving or renaming pages.
When changing docs content or URLs, verify that skills/arpes-analysis/SKILL.md still matches current docs/links and update it if needed. Write documentation in concrete user-facing terms. Prefer naming the visible object or action over abstract implementation phrasing. Avoid vague category labels and compressed prose that only sounds precise and does not reflect what the user will see.
Use 4-space indentation, Ruff’s 88-character limit, and double quotes. Modules/functions stay snake_case, classes use CapWords. Some Qt widgets keep co-located .ui files, import bindings through qtpy, and rely on explicit enums such as QtCore.Qt.CheckState.Checked. In case of Qt imports, prefer from qtpy import QtWidgets, QtCore, QtGui. Install prek so Ruff, mypy, and commitizen hooks run automatically. Docstrings use NumPy style. It is recommended to follow PEP 484 type hinting for all public APIs.
Prefer importing top-level erlab in modules that already use lazy_loader, even if a narrower import is possible.
Follow package import conventions by preferring absolute imports over relative imports.
Keep small implementation paths direct. Do not introduce dataclasses, wrapper helpers, or renamed import aliases for simple values or one-off calls unless they remove real complexity, define a meaningful boundary, or match an established local pattern.
Use modern typing syntax as a default rule: use built-in generics (list[str], dict[str, int]) and collections.abc types (for example Callable), and avoid deprecated typing aliases (deprecated since Python 3.9).
Typing rule: do not write typing.Iterable, typing.Iterator, typing.Mapping, typing.Sequence, typing.Callable, typing.Dict, typing.List, or typing.Tuple. Import abstract collection types from collections.abc (inside if typing.TYPE_CHECKING: when used only for annotations) and use built-in containers such as list[str] and dict[str, int].
Do not use assert in runtime code under src/; use explicit if/raise checks for runtime invariants and typing.cast for type narrowing that should not affect runtime behavior.
Avoid mutating globals() in library modules to inject symbols into module scope (for example via globals().update); prefer explicit imports and direct references instead.
Pytest enforces strict markers and xfail_strict; name files test_<feature>.py beside the code they cover. Loader plugins need regression data in tests/io/plugins/test_<plugin>.py; set ERLAB_TEST_DATA_DIR to a local clone of erlabpy-data so fixtures resolve. Coverage already skips legacy updater code, so aim for branch coverage elsewhere and parametrize datasets to catch multidimensional regressions.
- If newly added or expanded tests fail after an initial implementation, re-examine the runtime code before assuming the tests are wrong. Do not modify tests only to make them pass unless you can clearly justify that the asserted behavior is incorrect; otherwise you may mask a real defect in the implementation.
- The fast PR workflow runs one fully covered, sharded
3.13 + pyqt6lane plus smaller compatibility smoke jobs. The weekly compatibility workflow keeps the full upgraded3.11-3.14 x pyqt6/pyside6matrix. - Test grouping is centralized in
scripts/_ci_test_groups.py. When adding a new top-level test module undertests/analysis/,tests/interactive/,tests/io/, ortests/, update that file so the new test lands in exactly one coverage shard and, if appropriate, in the compatibility smoke set. tests/conftest.pyassigns thecompat,gui, andserialmarkers during collection based on the centralized grouping rules. Keep those markers semantically meaningful; do not scatter ad hoc CI-only marker assignments across unrelated test files.- Run
uv run python -m scripts.ci_test_groups --check-partitionafter changing the test tree or CI grouping rules. - Tests and monkeypatched stubs must implement the current runtime contract. Do not add production fallbacks or compatibility branches only to accommodate outdated fake objects in tests.
- Do not assert user-facing text label content in tests; prefer stable metadata, object names, roles, or behavioral state. When testing generated or copied code, execute the code with
exec()in an explicit namespace and assert the produced object/value exactly matches the expected result. Do not compare copied code strings or formatting unless the exact text is the behavior under test.
User-facing generated/copied/replay code must be as clean and direct as possible. Treat readability of generated code as product behavior, not a cosmetic afterthought. Copied code is also teaching material for users moving between scripts and the GUI, so prefer public APIs, semantic variable names, complete expressions, and best-practice examples. Avoid meaningless relay assignments, repeated scratch variables, reused generic names such as derived across unrelated subexpressions, leaked internal manager helpers, and temporaries that do not carry semantic value. Prefer semantic names from watched variables, console assignments, provenance inputs, or visible actions, and inline one-use aliases when it is safe to do so.
When changing provenance or code-generation paths, scan the emitted code for redundant reassignments and internal implementation details. Add property-style tests for cleanliness when it is the behavior under test, while still executing the generated code and asserting the resulting object/value exactly.
- All Qt-facing runtime code and tests must behave the same under both PyQt6 and PySide6. Prefer
qtpyAPIs that are binding-neutral, avoid binding-specific signal/metaobject assumptions, and when lower-level Qt behavior is unavoidable, add a compatibility helper and validate the touched path under both bindings. - Write Qt code with explicit lifetime ownership across bindings from the start. Guard queued or deferred Qt access with
qt_is_valid, keep neededQMenu/QActionand other Qt wrappers strongly referenced for as long as they are queried, makedestroyedhandlers verify object identity instead of acting only by reusable IDs, and disconnect exactly the signals that were connected. Validate touched lifetime-sensitive paths under both PySide6 and PyQt6. - For Qt action and menu labels, use a trailing Unicode ellipsis (
…) only when the command requires additional user input, a required selection, or required confirmation before it can complete. Do not use an ellipsis for immediate commands, toggles, submenus, informational windows/panels, or commands that only sometimes prompt. Use…, not ASCII..., for user-facing Qt action and menu labels. Tests must not assert action label text; use object names, action data, roles, shortcuts, enabled state, or behavior instead. - For tools launched from ImageTool, new actions that open/show data in ImageTool should be manager-aware (use manager flow when the parent tool is managed).
- For new context-menu or file-dialog features, add tests for both accept and cancel dialog paths.
- Prefer
accept_dialogfor real dialog interactions; use monkeypatch stubs to target hard-to-hit branches. - New or modified lines in touched interactive modules should be directly covered by tests, including warning/early-return branches, unless there is a clear reason a branch is untestable.
ImageToolManager.main()inspectssys.argv[1:]as potential file paths. For manager tests, prefer explicit test node IDs over long-kexpressions, or patchsys.argvin tests, to avoid accidental file-path parsing side effects.- ImageTool manager tests use a fixed ZMQ port (
45555). If tests fail withAddress already in useor timeout, check and terminate stale manager processes before rerunning. - Avoid running multiple manager test jobs in parallel on the same machine unless ports are isolated.
- If a test only needs to cover manager integration branches, prefer patching
erlab.interactive.imagetool.manager.is_runningandshow_in_managerover launching the real manager. Reservemanager_contextfor behavior that genuinely depends on a live manager instance. - For coverage runs,
--covis generally stable; if--cov=<module path>triggers local Qt import issues, use broader--cov=erlaband filter coverage output to target files. - Do not make runtime code behave differently under pytest (e.g.,
PYTEST_VERSION,sys.modules["pytest"], or test-only env checks insrc/). Keep production behavior explicit via function arguments/state, and implement test-specific behavior in tests/fixtures/monkeypatching instead. - If a code path already shows an explicit UI dialog (
MessageDialog/QMessageBox), avoid duplicate manager alert popups by logging withextra={"suppress_ui_alert": True}. # pragma: no cover/# pragma: no branchis allowed for edge cases that are hard or impractical to exercise in CI; prefer tests when feasible and add a brief comment explaining why the pragma is needed.- Do not export private compatibility shims only for monkeypatched tests; update tests to patch the real module/function location instead.
- When manager internals change, update test doubles and helper methods to the new interface instead of preserving obsolete paths in runtime code with
hasattrguards or wrapper-scanning fallbacks. - If a subclass override only delegates to
super(), remove the override instead of leaving a redundant forwarding method behind. Audit nearby overrides when consolidating logic into a base class. - Keep watcher semantics stable for IPython users when adding non-IPython support; validate both post-run-cell (IPython) and polling fallback (e.g., marimo/plain namespace) paths in tests.
Follow Conventional Commits with scopes (e.g., feat(analysis.gold): support multi-angle Fermi fits) and reference issues via (#123) when relevant. PRs should summarize behavior changes, list the commands you ran, and attach screenshots or GIFs for GUI tweaks. Run uv run ruff check, uv run ruff format --check, uv run mypy src, uv run pytest, and uv run python -m scripts.ci_test_groups --check-partition when you change CI grouping or add new top-level test modules, and mention dependent data/doc PRs in the description.
When a user asks for a commit message, provide a Conventional Commit subject and include a longer, user-facing description paragraph if there are user-visible changes.