From 5ae76da9c9d87dbde9ff39a37b9d8f0a5dc6924f Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 10 Jul 2025 17:06:58 -0400 Subject: [PATCH 1/2] Replace mypy with pyright and fix errors --- .pre-commit-config.yaml | 12 ++++++------ pixi.lock | 18 ++++++++++++++++-- pyproject.toml | 28 ++++++++++++---------------- src/cditools/eiger.py | 25 +++++++++++++------------ src/cditools/simulated/black_hole.py | 2 +- tests/test_motors.py | 5 +++-- 6 files changed, 51 insertions(+), 39 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3b4bd28..0d156235 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,14 +48,14 @@ repos: args: ["--fix", "--show-fixes"] - id: ruff-format - - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.15.0" + - repo: local hooks: - - id: mypy + - id: pyright + name: pyright + entry: pyright + language: system + types: [python] files: src|tests - args: [] - additional_dependencies: - - pytest - repo: https://github.com/codespell-project/codespell rev: "v2.4.1" diff --git a/pixi.lock b/pixi.lock index dc42997d..ece738fd 100644 --- a/pixi.lock +++ b/pixi.lock @@ -112,6 +112,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl @@ -379,8 +380,8 @@ packages: requires_python: '>=3.8' - pypi: . name: cditools - version: 0.1.dev18+g22d45e0.d20250515 - sha256: 2a690b8e82f56c90db4126ff52415e1fca013a04ccaa3f4e60594e428a8b7b74 + version: 0.1.dev34+ga046dfc.d20250710 + sha256: dad64b187309de0f2f9b697f8c3db9aed41ac45331ac2cfc86122a070bb83645 requires_dist: - ophyd - h5py @@ -394,6 +395,7 @@ packages: - pre-commit ; extra == 'dev' - ipython ; extra == 'dev' - ruff ; extra == 'dev' + - pyright ; extra == 'dev' - sphinx>=7.0 ; extra == 'docs' - myst-parser>=0.13 ; extra == 'docs' - sphinx-copybutton ; extra == 'docs' @@ -1227,6 +1229,18 @@ packages: requires_dist: - colorama>=0.4.6 ; extra == 'windows-terminal' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl + name: pyright + version: 1.1.403 + sha256: c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3 + requires_dist: + - nodeenv>=1.6.0 + - typing-extensions>=4.1 + - twine>=3.4.1 ; extra == 'all' + - nodejs-wheel-binaries ; extra == 'all' + - twine>=3.4.1 ; extra == 'dev' + - nodejs-wheel-binaries ; extra == 'nodejs' + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl name: pytest version: 8.3.5 diff --git a/pyproject.toml b/pyproject.toml index d9a1fc14..0cfb33ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dev = [ "pre-commit", "ipython", "ruff", + "pyright", ] docs = [ "sphinx>=7.0", @@ -72,7 +73,7 @@ write_to = "src/cditools/_version.py" [tool.uv] dev-dependencies = [ - "cditools[test]", + "cditools[dev]", ] @@ -96,21 +97,16 @@ report.exclude_also = [ 'if typing.TYPE_CHECKING:', ] -[tool.mypy] -files = ["src", "tests"] -python_version = "3.9" -warn_unused_configs = true -strict = true -enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] -warn_unreachable = true -disallow_untyped_defs = false -disallow_incomplete_defs = false -disallow_subclassing_any = false - -[[tool.mypy.overrides]] -module = "cditools.*" -disallow_untyped_defs = true -disallow_incomplete_defs = true +[tool.pyright] +include = ["src", "tests"] +pythonVersion = "3.9" +typeCheckingMode = "strict" +reportMissingImports = true +# Required for untyped packages +reportMissingTypeStubs = false +reportUnknownMemberType = false +reportUnknownArgumentType = false +reportUnknownVariableType = false [tool.ruff] diff --git a/src/cditools/eiger.py b/src/cditools/eiger.py index 5614d875..33ec826e 100644 --- a/src/cditools/eiger.py +++ b/src/cditools/eiger.py @@ -2,7 +2,7 @@ from datetime import datetime from pathlib import Path, PurePath -from typing import Any +from typing import Any, cast from ophyd import Component as Cpt # type: ignore[import-not-found] from ophyd import ( @@ -12,7 +12,6 @@ ProcessPlugin, ROIPlugin, StatsPlugin, - StatusBase, ) from ophyd.areadetector.base import ( # type: ignore[import-not-found] ADComponent, @@ -23,6 +22,7 @@ new_short_uid, ) from ophyd.areadetector.trigger_mixins import ( # type: ignore[import-not-found] + ADTriggerStatus, SingleTrigger, ) @@ -69,7 +69,7 @@ def master_file_paths(self) -> list[PurePath]: def sequence_number(self) -> int: return self.sequence_id_offset + int(self.sequence_id.get()) - def stage(self) -> list[object]: + def stage(self) -> list[object]: # type: ignore[reportIncompatibleMethodOverride] res_uid = new_short_uid() write_path = Path(f"{datetime.now().strftime(self.write_path_template)}/") self.file_path.set(write_path.as_posix()).wait(1.0) @@ -82,14 +82,14 @@ def stage(self) -> list[object]: # * ... self.file_write_name_pattern.set(f"{res_uid}_$id").wait(1.0) - ret: list[object] = super().stage() + ret: list[object] = super().stage() # type: ignore[reportIncompatibleMethodOverride] # Set the filename for the resource document. file_prefix = PurePath(self.file_path.get()) / res_uid self._fn = file_prefix - images_per_file = self.file_write_images_per_file.get() - resource_kwargs = {"images_per_file": images_per_file} + images_per_file: str = self.file_write_images_per_file.get() + resource_kwargs: dict[str, str] = {"images_per_file": images_per_file} self._generate_resource(resource_kwargs) @@ -145,22 +145,23 @@ class EigerBase(EigerDetector): def stage(self, *args: Any, **kwargs: dict[str, Any]) -> list[object]: staged_devices: list[object] = super().stage(*args, **kwargs) self.cam.manual_trigger.set(True).wait(5.0) - file_write_path = self.file_handler.file_path.get() + file_write_path: Path = Path(cast(str, self.file_handler.file_path.get())) if not Path.exists(file_write_path): msg = f"Path {file_write_path} does not exist." raise FileNotFoundError(msg) return staged_devices - def unstage(self) -> None: + def unstage(self) -> list[object]: self.cam.manual_trigger.set(False).wait(5.0) - super().unstage() + ret = super().unstage() - if not all(Path.exists(path) for path in self.file_handler.master_file_paths): + if not all(Path(path).exists() for path in self.file_handler.master_file_paths): msg = f"Paths {self.file_handler.master_file_paths} were not written." raise FileNotFoundError(msg) + return ret -class EigerSingleTrigger(SingleTrigger, EigerBase): +class EigerSingleTrigger(SingleTrigger, EigerBase): # type: ignore[reportIncompatibleMethodOverride] """Eiger detector that uses the single trigger acquisition mode.""" def __init__(self, *args: Any, **kwargs: dict[str, Any]) -> None: @@ -170,7 +171,7 @@ def __init__(self, *args: Any, **kwargs: dict[str, Any]) -> None: self.stage_sigs["file_handler.enable"] = True self.stage_sigs["file_handler.save_files"] = True - def trigger(self, *args: Any, **kwargs: dict[str, Any]) -> StatusBase: + def trigger(self, *args: Any, **kwargs: dict[str, Any]) -> ADTriggerStatus: status = super().trigger(*args, **kwargs) # If the manual trigger is enabled, we need to press the special trigger button # to actually trigger the detector. diff --git a/src/cditools/simulated/black_hole.py b/src/cditools/simulated/black_hole.py index 7c7ce705..8d3b7a03 100644 --- a/src/cditools/simulated/black_hole.py +++ b/src/cditools/simulated/black_hole.py @@ -64,7 +64,7 @@ def __init__(self, *args: Any, **kwargs: dict[str, Any]) -> None: # Overwrite the pvdb with the blackhole, while keeping the explicit pv properties self.pvdb: dict[str, ChannelData] = self.pvdb self.old_pvdb = self.pvdb.copy() - self.pvdb = ReallyDefaultDict(self.fabricate_channel) + self.pvdb = ReallyDefaultDict(self.fabricate_channel) # type: ignore[reportIncompatibleMethodOverride] def fabricate_channel(self, key: str) -> ChannelData: # If the channel already exists from initialization, return it diff --git a/tests/test_motors.py b/tests/test_motors.py index 05ea1f49..dff6a44b 100644 --- a/tests/test_motors.py +++ b/tests/test_motors.py @@ -2,6 +2,7 @@ import os import time +from collections.abc import Generator from subprocess import PIPE, Popen import pytest @@ -34,7 +35,7 @@ @pytest.fixture(scope="session") -def black_hole_ioc(): +def black_hole_ioc() -> Generator[None, None, None]: os.environ["EPICS_CA_ADDR_LIST"] = "127.0.0.1" os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO" p = Popen(["black-hole-ioc", "--interfaces", "127.0.0.1"], stdout=PIPE) @@ -60,7 +61,7 @@ def black_hole_ioc(): p.wait() -def test_motors_can_connect(black_hole_ioc): +def test_motors_can_connect(black_hole_ioc: None) -> None: slt_wb1 = SltWB1(prefix="XF:09IDA-OP:1{Slt:WB1", name="slt_wb1") slt_wb1.wait_for_connection(timeout=10.0) From d1a49cda3013c59741896ac9ef6fa52acfa22c76 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Thu, 10 Jul 2025 17:16:18 -0400 Subject: [PATCH 2/2] Use local pre-commit in workflow --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feec6188..a7ae8150 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.x" - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - with: - extra_args: --hook-stage manual --all-files + + - name: Install package + run: python -m pip install .[dev] + + - name: Run pre-commit + run: pre-commit run --all-files checks: name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}