From ce7cd54a70a5ce315a7e22247b633eee657c44ee Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Fri, 25 Oct 2024 12:28:55 +0200 Subject: [PATCH] Fix some mypy issues --- pyproject.toml | 6 ++ src/instamatic/TEMController/TEMController.py | 31 ++++-- .../TEMController/jeol_microscope.py | 14 +-- src/instamatic/TEMController/lenses.py | 8 +- .../TEMController/microscope_base.py | 6 +- .../TEMController/simu_microscope.py | 30 +++--- src/instamatic/TEMController/stage.py | 98 +++++++++---------- src/instamatic/config/__init__.py | 43 ++++---- src/instamatic/config/config_updater.py | 12 ++- 9 files changed, 139 insertions(+), 109 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4f91611f..1d270210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ changelog = "https://github.com/instamatic-dev/instamatic/releases" develop = [ "bump2version", "check-manifest", + "mypy", "pre-commit", "pytest >= 5.4.1", "coverage", @@ -151,6 +152,11 @@ required-imports = ["from __future__ import annotations"] quote-style = "single" indent-style = "space" +[tool.mypy] +files = ["src", "tests"] +allow_redefinition = true +ignore_missing_imports = true + [tool.bumpversion] current_version = "2.0.5" diff --git a/src/instamatic/TEMController/TEMController.py b/src/instamatic/TEMController/TEMController.py index 8c7b294c..3c9f76f3 100644 --- a/src/instamatic/TEMController/TEMController.py +++ b/src/instamatic/TEMController/TEMController.py @@ -524,7 +524,9 @@ def from_dict(self, dct: dict): except TypeError: func(v) - def get_raw_image(self, exposure: float = None, binsize: int = None) -> np.ndarray: + def get_raw_image( + self, exposure: Optional[float] = None, binsize: Optional[int] = None + ) -> np.ndarray: """Simplified function equivalent to `get_image` that only returns the raw data array. @@ -542,7 +544,9 @@ def get_raw_image(self, exposure: float = None, binsize: int = None) -> np.ndarr """ return self.cam.get_image(exposure=exposure, binsize=binsize) - def get_future_image(self, exposure: float = None, binsize: int = None) -> 'future': + def get_future_image( + self, exposure: Optional[float] = None, binsize: Optional[int] = None + ) -> 'future': """Simplified function equivalent to `get_image` that returns the raw image as a future. This makes the data acquisition call non-blocking. @@ -566,7 +570,9 @@ def get_future_image(self, exposure: float = None, binsize: int = None) -> 'futu future = self._executor.submit(self.get_raw_image, exposure=exposure, binsize=binsize) return future - def get_rotated_image(self, exposure: float = None, binsize: int = None) -> np.ndarray: + def get_rotated_image( + self, exposure: Optional[float] = None, binsize: Optional[int] = None + ) -> np.ndarray: """Simplified function equivalent to `get_image` that returns the rotated image array. @@ -598,13 +604,13 @@ def get_rotated_image(self, exposure: float = None, binsize: int = None) -> np.n def get_image( self, - exposure: float = None, - binsize: int = None, + exposure: Optional[float] = None, + binsize: Optional[int] = None, comment: str = '', - out: str = None, + out: Optional[str] = None, plot: bool = False, verbose: bool = False, - header_keys: Tuple[str] = 'all', + header_keys: Optional[Tuple[str]] = None, ) -> Tuple[np.ndarray, dict]: """Retrieve image as numpy array from camera. If the exposure and binsize are not given, the default values are read from the config @@ -685,7 +691,12 @@ def get_image( return arr, h def get_movie( - self, n_frames: int, *, exposure: float = None, binsize: int = None, out: str = None + self, + n_frames: int, + *, + exposure: Optional[float] = None, + binsize: Optional[int] = None, + out: Optional[str] = None, ) -> Tuple[np.ndarray]: """Collect a stack of images using the camera's movie mode, if available. @@ -740,7 +751,9 @@ def store_diff_beam(self, name: str = 'beam', save_to_file: bool = False): keys = 'FunctionMode', 'Brightness', 'GunTilt', 'DiffFocus', 'SpotSize' self.store(name=name, keys=keys, save_to_file=save_to_file) - def store(self, name: str = 'stash', keys: tuple = None, save_to_file: bool = False): + def store( + self, name: str = 'stash', keys: Optional[tuple] = None, save_to_file: bool = False + ): """Stores current settings to dictionary. Multiple settings can be stored under different names. Specify diff --git a/src/instamatic/TEMController/jeol_microscope.py b/src/instamatic/TEMController/jeol_microscope.py index f9d6ada5..9549664d 100644 --- a/src/instamatic/TEMController/jeol_microscope.py +++ b/src/instamatic/TEMController/jeol_microscope.py @@ -3,7 +3,7 @@ import atexit import logging import time -from typing import Tuple +from typing import Optional, Tuple import comtypes.client @@ -279,7 +279,7 @@ def getImageShift2(self) -> Tuple[int, int]: def setImageShift2(self, x: int, y: int): self.def3.SetIS2(x, y) - def getStagePosition(self) -> Tuple[int, int, int, int, int]: + def getStagePosition(self) -> Tuple[float, float, float, float, float]: """X, y, z in nanometer a and b in degrees.""" x, y, z, a, b, result = self.stage3.GetPos() return x, y, z, a, b @@ -334,11 +334,11 @@ def stopStage(self): def setStagePosition( self, - x: int = None, - y: int = None, - z: int = None, - a: int = None, - b: int = None, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + a: Optional[float] = None, + b: Optional[float] = None, wait: bool = True, ): if z is not None: diff --git a/src/instamatic/TEMController/lenses.py b/src/instamatic/TEMController/lenses.py index 35023725..ceb431d0 100644 --- a/src/instamatic/TEMController/lenses.py +++ b/src/instamatic/TEMController/lenses.py @@ -121,14 +121,14 @@ def __repr__(self): def index(self) -> int: return self._indexgetter() - @property - def absolute_index(self) -> int: - return self._tem.getMagnificationAbsoluteIndex() - @index.setter def index(self, index: int): self._indexsetter(index) + @property + def absolute_index(self) -> int: + return self._tem.getMagnificationAbsoluteIndex() + def increase(self) -> None: try: self.index += 1 diff --git a/src/instamatic/TEMController/microscope_base.py b/src/instamatic/TEMController/microscope_base.py index 4cfa2ee3..c917973d 100644 --- a/src/instamatic/TEMController/microscope_base.py +++ b/src/instamatic/TEMController/microscope_base.py @@ -86,7 +86,7 @@ def getSpotSize(self) -> int: pass @abstractmethod - def getStagePosition(self) -> Tuple[int, int, int, int, int]: + def getStagePosition(self) -> Tuple[float, float, float, float, float]: pass @abstractmethod @@ -170,7 +170,9 @@ def setSpotSize(self, value: int) -> None: pass @abstractmethod - def setStagePosition(self, x: int, y: int, z: int, a: int, b: int, wait: bool) -> None: + def setStagePosition( + self, x: float, y: float, z: float, a: float, b: float, wait: bool + ) -> None: pass @abstractmethod diff --git a/src/instamatic/TEMController/simu_microscope.py b/src/instamatic/TEMController/simu_microscope.py index 8f224d2b..88b932a5 100644 --- a/src/instamatic/TEMController/simu_microscope.py +++ b/src/instamatic/TEMController/simu_microscope.py @@ -2,7 +2,7 @@ import random import time -from typing import Tuple +from typing import Optional, Tuple from instamatic import config from instamatic.exceptions import TEMValueError @@ -87,8 +87,8 @@ def __init__(self, name: str = 'simulate'): self._HT = 200_000 # V # self.Magnification_value = random.choice(self.MAGNIFICATIONS) - self.Magnification_value = config.microscope.ranges['mag1'][10] - self.Magnification_value_diff = config.microscope.ranges['diff'][3] + self.Magnification_value: int = config.microscope.ranges['mag1'][10] # type: ignore + self.Magnification_value_diff: int = config.microscope.ranges['diff'][3] # type: ignore self.beamblank = False @@ -372,7 +372,7 @@ def setImageShift2(self, x: int, y: int): self.ImageShift2_x = x self.ImageShift2_y = y - def getStagePosition(self) -> Tuple[int, int, int, int, int]: + def getStagePosition(self) -> Tuple[float, float, float, float, float]: return ( self.StagePosition_x, self.StagePosition_y, @@ -390,32 +390,32 @@ def waitForStage(self, delay: float = 0.1): while self.isStageMoving(): time.sleep(delay) - def setStageX(self, value: int, wait: bool = True): + def setStageX(self, value: float, wait: bool = True): self.StagePosition_x = value if wait: self.waitForStage() - def setStageY(self, value: int, wait: bool = True): + def setStageY(self, value: float, wait: bool = True): self.StagePosition_y = value if wait: self.waitForStage() - def setStageZ(self, value: int, wait: bool = True): + def setStageZ(self, value: float, wait: bool = True): self.StagePosition_z = value if wait: self.waitForStage() - def setStageA(self, value: int, wait: bool = True): + def setStageA(self, value: float, wait: bool = True): self.StagePosition_a = value if wait: self.waitForStage() - def setStageB(self, value: int, wait: bool = True): + def setStageB(self, value: float, wait: bool = True): self.StagePosition_b = value if wait: self.waitForStage() - def setStageXY(self, x: int, y: int, wait: bool = True): + def setStageXY(self, x: float, y: float, wait: bool = True): self.StagePosition_x = x self.StagePosition_y = y if wait: @@ -426,11 +426,11 @@ def stopStage(self): def setStagePosition( self, - x: int = None, - y: int = None, - z: int = None, - a: int = None, - b: int = None, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + a: Optional[float] = None, + b: Optional[float] = None, speed: float = -1, wait: bool = True, ): diff --git a/src/instamatic/TEMController/stage.py b/src/instamatic/TEMController/stage.py index 4eed5039..a805971b 100644 --- a/src/instamatic/TEMController/stage.py +++ b/src/instamatic/TEMController/stage.py @@ -3,7 +3,7 @@ import time from collections import namedtuple from contextlib import contextmanager -from typing import Tuple +from typing import Optional, Tuple import numpy as np @@ -32,11 +32,11 @@ def name(self) -> str: def set( self, - x: int = None, - y: int = None, - z: int = None, - a: int = None, - b: int = None, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + a: Optional[float] = None, + b: Optional[float] = None, wait: bool = True, ) -> None: """Wait: bool, block until stage movement is complete (JEOL only)""" @@ -44,11 +44,11 @@ def set( def set_with_speed( self, - x: int = None, - y: int = None, - z: int = None, - a: int = None, - b: int = None, + x: Optional[float] = None, + y: Optional[float] = None, + z: Optional[float] = None, + a: Optional[float] = None, + b: Optional[float] = None, wait: bool = True, speed: float = 1.0, ) -> None: @@ -95,43 +95,43 @@ def rotating_speed(self, speed: int): else: yield # if requested speed is the same as current - def get(self) -> Tuple[int, int, int, int, int]: + def get(self) -> Tuple[float, float, float, float, float]: """Get stage positions; x, y, z, and status of the rotation axes; a, b.""" return StagePositionTuple(*self._getter()) @property - def x(self) -> int: + def x(self) -> float: """X position.""" x, y, z, a, b = self.get() return x @x.setter - def x(self, value: int): + def x(self, value: float): self.set(x=value, wait=self._wait) @property - def y(self) -> int: + def y(self) -> float: """Y position.""" x, y, z, a, b = self.get() return y @y.setter - def y(self, value: int): + def y(self, value: float): self.set(y=value, wait=self._wait) @property - def xy(self) -> Tuple[int, int]: + def xy(self) -> Tuple[float, float]: """XY position as a tuple.""" x, y, z, a, b = self.get() return x, y @xy.setter - def xy(self, values: Tuple[int, int]): + def xy(self, values: Tuple[float, float]): x, y = values self.set(x=x, y=y, wait=self._wait) - def move_in_projection(self, delta_x: int, delta_y: int) -> None: + def move_in_projection(self, delta_x: float, delta_y: float) -> None: r"""Y and z are always perpendicular to the sample stage. To achieve the movement in the projection, x and yshould be broken down into the components z' and y'. @@ -153,7 +153,7 @@ def move_in_projection(self, delta_x: int, delta_y: int) -> None: z = z - delta_y * np.sin(a) self.set(x=x, y=y, z=z) - def move_along_optical_axis(self, delta_z: int): + def move_along_optical_axis(self, delta_z: float): """See `Stage.move_in_projection`""" x, y, z, a, b = self.get() a = np.radians(a) @@ -162,33 +162,33 @@ def move_along_optical_axis(self, delta_z: int): self.set(y=y, z=z) @property - def z(self) -> int: + def z(self) -> float: """Stage height Z.""" x, y, z, a, b = self.get() return z @z.setter - def z(self, value: int): + def z(self, value: float): self.set(z=value, wait=self._wait) @property - def a(self) -> int: + def a(self) -> float: """Rotation angle.""" x, y, z, a, b = self.get() return a @a.setter - def a(self, value: int): + def a(self, value: float): self.set(a=value, wait=self._wait) @property - def b(self) -> int: + def b(self) -> float: """Secondary rotation angle.""" x, y, z, a, b = self.get() return b @b.setter - def b(self, value: int): + def b(self, value: float): self.set(b=value, wait=self._wait) def neutral(self) -> None: @@ -252,7 +252,11 @@ def relax_xy(self, step: int = 100) -> None: pass def set_xy_with_backlash_correction( - self, x: int = None, y: int = None, step: float = 10000, settle_delay: float = 0.200 + self, + x: Optional[float] = None, + y: Optional[float] = None, + step: float = 10000, + settle_delay: float = 0.200, ) -> None: """Move to new x/y position with backlash correction. This is done by approaching the target x/y position always from the same direction. @@ -265,7 +269,7 @@ def set_xy_with_backlash_correction( delay between movements in seconds to allow the stage to settle """ wait = True - self.set(x=x - step, y=y - step) + self.set(x=self.x - step, y=self.y - step) if settle_delay: time.sleep(settle_delay) @@ -275,8 +279,8 @@ def set_xy_with_backlash_correction( def move_xy_with_backlash_correction( self, - shift_x: int = None, - shift_y: int = None, + shift_x: Optional[float] = None, + shift_y: Optional[float] = None, step: float = 5000, settle_delay: float = 0.200, wait=True, @@ -297,27 +301,24 @@ def move_xy_with_backlash_correction( wait: bool, block until stage movement is complete (JEOL only) """ - stage = self.get() + pre_x = None + target_x = None + pre_y = None + target_y = None if shift_x: - target_x = stage.x + shift_x - if target_x > stage.x: - pre_x = stage.x - step - elif target_x < stage.x: - pre_x = stage.x + step - else: - pre_x = None - target_x = None + target_x = self.x + shift_x + if target_x > self.x: + pre_x = self.x - step + elif target_x < self.x: + pre_x = self.x + step if shift_y: - target_y = stage.y + shift_y - if target_y > stage.y: - pre_y = stage.y - step - elif target_y < stage.y: - pre_y = stage.y + step - else: - pre_y = None - target_y = None + target_y = self.y + shift_y + if target_y > self.y: + pre_y = self.y - step + elif target_y < self.y: + pre_y = self.y + step self.set(x=pre_x, y=pre_y) if settle_delay: @@ -337,9 +338,8 @@ def eliminate_backlash_xy(self, step: float = 10000, settle_delay: float = 0.200 settle_delay: float, delay between movements in seconds to allow the stage to settle """ - stage = self.get() self.set_xy_with_backlash_correction( - x=stage.x, y=stage.y, step=step, settle_delay=settle_delay + x=self.x, y=self.y, step=step, settle_delay=settle_delay ) def eliminate_backlash_a( diff --git a/src/instamatic/config/__init__.py b/src/instamatic/config/__init__.py index 92f9c4aa..ebb8c035 100644 --- a/src/instamatic/config/__init__.py +++ b/src/instamatic/config/__init__.py @@ -7,6 +7,7 @@ import sys from collections.abc import Mapping from pathlib import Path +from typing import Optional, Union import yaml @@ -43,9 +44,9 @@ def nested_update(d: dict, u: dict) -> dict: def initialize_in_appData(): - """Initialize the configuration directory on first run Default to. + """Initialize the configuration directory on first run. - %appdata%/instamatic. + Default to %appdata%/instamatic. """ src = Path(__file__).parent dst = Path(os.environ['AppData']) / _instamatic @@ -69,10 +70,9 @@ def initialize_in_appData(): print(f'Directory: {dst}') print('Please review and restart the program.') os.startfile(dst) - sys.exit() -def get_base_drc(): +def get_base_drc() -> Path: """Figure out where configuration files for instamatic are stored.""" try: search = Path(os.environ[_instamatic]) # if installed in portable way @@ -85,6 +85,7 @@ def get_base_drc(): return search else: initialize_in_appData() + sys.exit() def get_alignments() -> dict: @@ -101,7 +102,7 @@ def get_alignments() -> dict: class ConfigObject: """Namespace for configuration (maps dict items to attributes).""" - def __init__(self, mapping: dict, name: str = 'config', location: str = None): + def __init__(self, mapping: dict, name: str = 'config', location: Optional[str] = None): super().__init__() self.name = name self.location = location @@ -115,12 +116,12 @@ def __getitem__(self, item): return self.mapping[item] @classmethod - def from_file(cls, path: str): + def from_file(cls, path: Union[Path, str]): """Read configuration from yaml file, returns namespace.""" name = Path(path).stem - return cls(yaml.load(open(path), Loader=yaml.Loader), name=name, location=path) + return cls(yaml.load(open(path), Loader=yaml.Loader), name=name, location=str(path)) - def update_from_file(self, path: str) -> None: + def update_from_file(self, path: Union[str, Path]) -> None: """Update configuration from yaml file.""" self.update(yaml.load(open(path), Loader=yaml.Loader)) self.location = path @@ -137,7 +138,7 @@ def update(self, mapping: dict): nested_update(self.mapping, mapping) -def load_calibration(calibration_name: str = None): +def load_calibration(calibration_name: Optional[str] = None): global calibration if not calibration_name: @@ -160,9 +161,11 @@ def load_calibration(calibration_name: str = None): settings.calibration = calibration.name -def load_microscope_config(microscope_name: str = None): +def load_microscope_config(microscope_name: Optional[str] = None): global microscope + assert settings + if not microscope_name: microscope_name = settings.microscope @@ -180,9 +183,11 @@ def load_microscope_config(microscope_name: str = None): settings.microscope = microscope.name -def load_camera_config(camera_name: str = None): +def load_camera_config(camera_name: Optional[str] = None): global camera + assert camera + if not camera_name: camera_name = settings.camera @@ -229,9 +234,9 @@ def load_settings(): def load_all( - microscope_name: str = None, - calibration_name: str = None, - camera_name: str = None, + microscope_name: Optional[str] = None, + calibration_name: Optional[str] = None, + camera_name: Optional[str] = None, ): """Load the settings.yaml file and microscope/calib/camera configs The config files to load can be overridden by specifying @@ -261,11 +266,11 @@ def load_all( print(f'Config directory: {config_drc}') -settings = None -defaults = None -microscope = None -calibration = None -camera = None +settings: Optional[ConfigObject] = None +defaults: Optional[ConfigObject] = None +microscope: Optional[ConfigObject] = None +calibration: Optional[ConfigObject] = None +camera: Optional[ConfigObject] = None load_all() diff --git a/src/instamatic/config/config_updater.py b/src/instamatic/config/config_updater.py index 99728548..a8716945 100644 --- a/src/instamatic/config/config_updater.py +++ b/src/instamatic/config/config_updater.py @@ -3,11 +3,15 @@ import shutil from collections import defaultdict from pathlib import Path +from typing import TYPE_CHECKING, Optional, Union from instamatic.config.utils import yaml +if TYPE_CHECKING: + from . import ConfigObject -def is_oldstyle(dct: dict, kind: str): + +def is_oldstyle(dct: ConfigObject, kind: str): """Check if the config format has been deprecated.""" oldstyle = False if kind in ('microscope', 'calibration'): @@ -17,7 +21,7 @@ def is_oldstyle(dct: dict, kind: str): return oldstyle -def check_settings_yaml(src: str, dst: str): +def check_settings_yaml(src: Path, dst: Path): """Check if `dst` exists, else rename `src` to `dst`.""" if dst.exists(): return True @@ -27,7 +31,7 @@ def check_settings_yaml(src: str, dst: str): print(f'Moved {src}->{dst}') -def check_defaults_yaml(drc: str, fn: str, src_fn: str = None): +def check_defaults_yaml(drc: Path, fn: str, src_fn: Optional[str] = None): """Check if `drc/fn` exists, else copy `fn` from local drc (copy `src_fn` if specified).""" dst = drc / fn @@ -40,7 +44,7 @@ def check_defaults_yaml(drc: str, fn: str, src_fn: str = None): print(f'Copying {src}->{dst}') -def convert_config(fn: str, kind: str) -> dict: +def convert_config(fn: Union[str, Path], kind: str) -> dict: """`kind` must be one of `microscope`/`camera`/`calibration`""" fn = Path(fn)