Skip to content

Commit

Permalink
tests: Fix mypy errors and run mypy during PR build (#505)
Browse files Browse the repository at this point in the history
* mypy: Update missing imports

* tests: Fix mypy errors in _grpc_utils.py

* tests: Fix mypy errors in conftest.py

tests\conftest.py:56: error: Variable "pytest.ParameterSet" is not valid as a type  [valid-type]
tests\conftest.py:56: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
tests\conftest.py:280: error: Bracketed expression "[...]" is not valid as a type  [valid-type]
tests\conftest.py:280: note: Did you mean "List[...]"?

* tests: Fix mypy errors in _grpc_utils.py

tests\unit\_grpc_utils.py:10: error: Incompatible types in assignment (expression has type "None", variable has type Module)  [assignment]

tests/_grpc_utils.py:24: error: Item "None" of "IO[bytes] | None" has no attribute "readline"  [union-attr]
tests/_grpc_utils.py:57: error: Module has no attribute "ConnectRegistry"  [attr-defined]
tests/_grpc_utils.py:57: error: Module has no attribute "HKEY_LOCAL_MACHINE"  [attr-defined]
tests/_grpc_utils.py:58: error: Module has no attribute "KEY_READ"  [attr-defined]
tests/_grpc_utils.py:58: error: Module has no attribute "KEY_WOW64_64KEY"  [attr-defined]
tests/_grpc_utils.py:59: error: Module has no attribute "OpenKey"  [attr-defined]
tests/_grpc_utils.py:62: error: Module has no attribute "QueryValueEx"  [attr-defined]

* tests: Fix mypy errors in test_grpc_time.py

tests\unit\test_grpc_time.py:18: error: Cannot assign to a type  [misc]
tests\unit\test_grpc_time.py:18: error: Incompatible types in assignment (expression has type "None", variable has type "type[Timestamp]")  [assignment]

* tests: Fix mypy errors for test_utils.py

tests\legacy\test_utils.py:28: error: Need type annotation for "unflattened_channels" (hint: "unflattened_channels: List[<type>] = ...")  [var-annotated]

* tests: Fix mypy errors in test_invalid_writes.py

tests\legacy\test_invalid_writes.py:58: error: Argument 1 to "floating" has incompatible type "list[float]"; expected "Union[None, Union[str, bytes], SupportsFloat, SupportsIndex]"  [arg-type]
tests\legacy\test_invalid_writes.py:109: error: Argument 1 to "floating" has incompatible type "list[list[float]]"; expected "Union[None, Union[str, bytes], SupportsFloat, SupportsIndex]"  [arg-type]
tests\legacy\test_invalid_writes.py:136: error: Argument 1 to "floating" has incompatible type "list[list[float]]"; expected "Union[None, Union[str, bytes], SupportsFloat, SupportsIndex]"  [arg-type]

* tests: Fix mypy errors in test_task_events.py

tests\component\test_task_events.py:526: error: Value of type "Union[tuple[type[BaseException], BaseException, Optional[TracebackType]], tuple[None, None, None], None]" is not indexable  [index]
tests\component\test_task_events.py:526: error: Argument 1 to "_exception_matches" has incompatible type "Union[BaseException, None, Any]"; expected "Exception"  [arg-type]
tests\component\test_task_events.py:530: error: Value of type "Union[tuple[type[BaseException], BaseException, Optional[TracebackType]], tuple[None, None, None], None]" is not indexable  [index]
ption, None, Any]"; expected "Exception"  [arg-type]

* tests: Fix mypy errors in test_examples.py

tests\acceptance\test_examples.py:38: error: Incompatible types in assignment (expression has type "nullcontext[None]", variable has type "catch_warnings[list[WarningMessage]]")  [assignment]

* tests: Fix mypy errors in test_stream_readers_ci.py

tests\component\test_stream_readers_ci.py:180: error: Argument 1 to "_validate_frequency_data" has incompatible type "ndarray[Any, dtype[floating[_64Bit]]]"; expected "ndarray[Any, dtype[unsignedinteger[_32Bit]]]"  [arg-type]

* tests: Fix mypy errors in test_stream_readers_di.py

tests\component\test_stream_readers_di.py:186: error: Type argument "_DType" of "NDArray" must be a subtype of "generic"  [type-var]

* GitHub: Update build.yml to run mypy on tests and install extras

* mypy: Copy more options from the NI Python template

* tests: Fix unreachable code in conftest.py

tests\conftest.py:119: error: Statement is unreachable  [unreachable]
tests\conftest.py:140: error: Statement is unreachable  [unreachable]
tests\conftest.py:178: error: Statement is unreachable  [unreachable]
tests\conftest.py:197: error: Statement is unreachable  [unreachable]
tests\conftest.py:216: error: Statement is unreachable  [unreachable]
tests\conftest.py:238: error: Statement is unreachable  [unreachable]
tests\conftest.py:302: error: Statement is unreachable  [unreachable]
tests\conftest.py:318: error: Statement is unreachable  [unreachable]
tests\conftest.py:433: error: Statement is unreachable  [unreachable]
tests\conftest.py:447: error: Statement is unreachable  [unreachable]
tests\conftest.py:463: error: Statement is unreachable  [unreachable]
  • Loading branch information
bkeryan authored Feb 22, 2024
1 parent 0905e65 commit 14f953c
Show file tree
Hide file tree
Showing 13 changed files with 74 additions and 59 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
poetry install
poetry install --all-extras
- name: Run linters
run: |
poetry run ni-python-styleguide lint
- name: Run mypy on tests
run: |
poetry run mypy tests
- name: Generate ni-daqmx files
run: |
rm -fr generated/nidaqmx
Expand Down
12 changes: 10 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,21 @@ build-backend = "poetry.masonry.api"

[tool.mypy]
check_untyped_defs = true
namespace_packages = true
plugins = "numpy.typing.mypy_plugin"
warn_redundant_casts = true
warn_unreachable = true
warn_unused_configs = true

[[tool.mypy.overrides]]
module = [
"deprecation",
# https://github.com/briancurtin/deprecation/issues/56 - Add type information (PEP 561)
"deprecation.*",
"grpc.experimental.*",
# https://github.com/ni/hightime/issues/4 - Add type annotations
"hightime.*",
"importlib_metadata",
"mako.*"
"mako.*",
"nidaqmx.*",
]
ignore_missing_imports = true
41 changes: 22 additions & 19 deletions tests/_grpc_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Helper functions to be used in nidaqmx tests."""
import os

import pathlib
import re
import subprocess
import sys
import threading

import pytest
Expand All @@ -15,6 +16,7 @@ def __init__(self):
"""Creates a GrpcServerProcess instance."""
server_exe = self._get_grpc_server_exe()
self._proc = subprocess.Popen([str(server_exe)], stdout=subprocess.PIPE)
assert self._proc.stdout is not None

# Read/parse output until we find the port number or the process exits; discard the rest.
try:
Expand Down Expand Up @@ -48,22 +50,23 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self._stdout_thread.join()

def _get_grpc_server_exe(self):
if os.name != "nt":
pytest.skip("Only supported on Windows")
import winreg
if sys.platform == "win32":
import winreg

try:
reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
read64key = winreg.KEY_READ | winreg.KEY_WOW64_64KEY
with winreg.OpenKey(
reg, r"SOFTWARE\National Instruments\Common\Installer", access=read64key
) as key:
shared_dir, _ = winreg.QueryValueEx(key, "NISHAREDDIR64")
except OSError:
pytest.skip("NI gRPC Device Server not installed")
server_exe = (
pathlib.Path(shared_dir) / "NI gRPC Device Server" / "ni_grpc_device_server.exe"
)
if not server_exe.exists():
pytest.skip("NI gRPC Device Server not installed")
return server_exe
try:
reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
read64key = winreg.KEY_READ | winreg.KEY_WOW64_64KEY
with winreg.OpenKey(
reg, r"SOFTWARE\National Instruments\Common\Installer", access=read64key
) as key:
shared_dir, _ = winreg.QueryValueEx(key, "NISHAREDDIR64")
except OSError:
pytest.skip("NI gRPC Device Server not installed")
server_exe = (
pathlib.Path(shared_dir) / "NI gRPC Device Server" / "ni_grpc_device_server.exe"
)
if not server_exe.exists():
pytest.skip("NI gRPC Device Server not installed")
return server_exe
else:
pytest.skip("Only supported on Windows")
2 changes: 1 addition & 1 deletion tests/acceptance/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test___shipping_example___run___no_errors(example_path: Path, system):
pytest.skip("Example waits for keyboard input.")
if example_path.name == "nidaqmx_warnings.py":
# Ignore warnings from this example.
context_manager = warnings.catch_warnings(record=True)
context_manager: contextlib.AbstractContextManager = warnings.catch_warnings(record=True)
else:
context_manager = contextlib.nullcontext()

Expand Down
9 changes: 5 additions & 4 deletions tests/component/_task_modules/channels/test_ai_channel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pathlib
from typing import List

import pytest

Expand Down Expand Up @@ -379,8 +380,8 @@ def test___task___add_ai_force_bridge_polynomial_chan___sets_channel_attributes(
sim_bridge_device: Device,
bridge_config: BridgeConfiguration,
nominal_bridge_resistance: float,
forward_coeffs: [float],
reverse_coeffs: [float],
forward_coeffs: List[float],
reverse_coeffs: List[float],
) -> None:
chan: AIChannel = task.ai_channels.add_ai_force_bridge_polynomial_chan(
sim_bridge_device.ai_physical_chans[0].name,
Expand Down Expand Up @@ -409,8 +410,8 @@ def test___task___add_ai_force_bridge_table_chan___sets_channel_attributes(
sim_bridge_device: Device,
bridge_config: BridgeConfiguration,
nominal_bridge_resistance: float,
electrical_vals: [float],
physical_vals: [float],
electrical_vals: List[float],
physical_vals: List[float],
) -> None:
chan: AIChannel = task.ai_channels.add_ai_force_bridge_table_chan(
sim_bridge_device.ai_physical_chans[0].name,
Expand Down
2 changes: 1 addition & 1 deletion tests/component/test_stream_readers_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def _validate_count_edges_data(data: numpy.typing.NDArray[numpy.uint32]) -> None
assert datum >= last


def _validate_frequency_data(data: numpy.typing.NDArray[numpy.uint32]) -> None:
def _validate_frequency_data(data: numpy.typing.NDArray[numpy.float64]) -> None:
assert data == pytest.approx(EXPECTED_FREQUENCY, abs=EXPECTED_FREQUENCY_TOLERANCE)


Expand Down
6 changes: 3 additions & 3 deletions tests/component/test_stream_readers_di.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,12 @@ def _bool_array_to_int(bool_array: numpy.typing.NDArray[numpy.bool_]) -> int:
return result


_DType = TypeVar("_DType", bound=numpy.typing.DTypeLike)
_D = TypeVar("_D", bound=numpy.generic, covariant=True)


def _read_and_copy(
read_func: Callable[[numpy.typing.NDArray[_DType]], None], array: numpy.typing.NDArray[_DType]
) -> numpy.typing.NDArray[_DType]:
read_func: Callable[[numpy.typing.NDArray[_D]], None], array: numpy.typing.NDArray[_D]
) -> numpy.typing.NDArray[_D]:
read_func(array)
return array.copy()

Expand Down
11 changes: 8 additions & 3 deletions tests/component/test_task_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,19 +523,24 @@ def test___event_callback_that_raises_exceptions___run_finite_acquisition___exce
caplog, "handle_every_n_samples_event", every_n_samples_event_count
)
assert all(
_exception_matches(record.exc_info[1], done_event_exception)
_exception_matches(_get_exception(record), done_event_exception)
for record in done_event_records
)
assert all(
_exception_matches(record.exc_info[1], every_n_samples_event_exception)
_exception_matches(_get_exception(record), every_n_samples_event_exception)
for record in every_n_samples_event_records
)


def _exception_matches(e1: Exception, e2: Exception) -> bool:
def _exception_matches(e1: BaseException, e2: BaseException) -> bool:
return type(e1) == type(e2) and e1.args == e2.args


def _get_exception(record: LogRecord) -> BaseException:
assert record.exc_info and record.exc_info[1]
return record.exc_info[1]


def _wait_for_log_records(
caplog: pytest.LogCaptureFixture, message_substring: str, expected_count: int, timeout=10.0
) -> List[LogRecord]:
Expand Down
22 changes: 8 additions & 14 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pathlib
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
from typing import Generator, List
from typing import Generator, List, TYPE_CHECKING

import pytest

Expand All @@ -18,7 +18,11 @@

from tests._grpc_utils import GrpcServerProcess
except ImportError:
grpc = None
grpc = None # type: ignore

if TYPE_CHECKING:
# Not public yet: https://github.com/pytest-dev/pytest/issues/7469
import _pytest.mark.structures


class Error(Exception):
Expand Down Expand Up @@ -53,7 +57,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:

grpc_only = metafunc.definition.get_closest_marker("grpc_only")
library_only = metafunc.definition.get_closest_marker("library_only")
params: List[pytest.ParameterSet] = []
params: List[_pytest.mark.structures.ParameterSet] = []

if not grpc_only:
library_marks: List[pytest.MarkDecorator] = []
Expand Down Expand Up @@ -112,7 +116,6 @@ def _x_series_device(
f"{device_type}. Cannot proceed to run tests. Import the NI MAX configuration file located "
"at nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return None


def _device_by_product_type(
Expand Down Expand Up @@ -189,7 +192,6 @@ def sim_ts_chassis(system: nidaqmx.system.System) -> nidaqmx.system.Device:
"Cannot proceed to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return None


@pytest.fixture(scope="function")
Expand All @@ -208,7 +210,6 @@ def sim_ts_power_device(sim_ts_chassis: nidaqmx.system.Device) -> nidaqmx.system
"Cannot proceed to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return None


@pytest.fixture(scope="function")
Expand All @@ -227,7 +228,6 @@ def sim_ts_voltage_device(sim_ts_chassis: nidaqmx.system.Device) -> nidaqmx.syst
"Cannot proceed to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return None


@pytest.fixture(scope="function")
Expand All @@ -249,7 +249,6 @@ def sim_ts_power_devices(sim_ts_chassis: nidaqmx.system.Device) -> List[nidaqmx.
"device. Cannot proceed to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return []


@pytest.fixture(scope="function")
Expand Down Expand Up @@ -295,7 +294,7 @@ def sim_velocity_device(system: nidaqmx.system.System) -> nidaqmx.system.Device:


@pytest.fixture(scope="function")
def multi_threading_test_devices(system: nidaqmx.system.System) -> [nidaqmx.system.Device]:
def multi_threading_test_devices(system: nidaqmx.system.System) -> List[nidaqmx.system.Device]:
"""Gets multi threading test devices information."""
devices = []
for device in system.devices:
Expand All @@ -313,7 +312,6 @@ def multi_threading_test_devices(system: nidaqmx.system.System) -> [nidaqmx.syst
"to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return None


@pytest.fixture(scope="function")
Expand All @@ -329,7 +327,6 @@ def device(request, system: nidaqmx.system.System) -> nidaqmx.system.Device:
"Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create these devices."
)
return None


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -444,7 +441,6 @@ def persisted_task(request, system: nidaqmx.system.System):
"Cannot proceed to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create the required tasks."
)
return None


@pytest.fixture(scope="function")
Expand All @@ -458,7 +454,6 @@ def persisted_scale(request, system: nidaqmx.system.System):
"to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create the required scales."
)
return None


@pytest.fixture(scope="function")
Expand All @@ -474,7 +469,6 @@ def persisted_channel(request, system: nidaqmx.system.System):
"Cannot proceed to run tests. Import the NI MAX configuration file located at "
"nidaqmx\\tests\\max_config\\nidaqmxMaxConfig.ini to create the required channels."
)
return None


@pytest.fixture(scope="function")
Expand Down
11 changes: 7 additions & 4 deletions tests/legacy/test_invalid_writes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def test_insufficient_numpy_write_data(self, task, sim_6363_device, seed):
)

number_of_samples = random.randint(1, number_of_channels - 1)
values_to_test = numpy.float64([random.uniform(-10, 10) for _ in range(number_of_samples)])
values_to_test = numpy.array(
[random.uniform(-10, 10) for _ in range(number_of_samples)], dtype=numpy.float64
)

with pytest.raises(DaqError) as e:
task.write(values_to_test, auto_start=True)
Expand Down Expand Up @@ -106,7 +108,7 @@ def test_extraneous_numpy_write_data(self, task, sim_6363_device, seed):
[random.uniform(-10, 10) for _ in range(10)] for _ in range(number_of_data_rows)
]

numpy_data = numpy.float64(values_to_test)
numpy_data = numpy.array(values_to_test, dtype=numpy.float64)

with pytest.raises(DaqError) as e:
task.write(numpy_data, auto_start=True)
Expand All @@ -132,11 +134,12 @@ def test_numpy_write_incorrectly_shaped_data(self, task, sim_6363_device, seed):
# Generate write data but swap the rows and columns so the numpy
# array is shaped incorrectly, but the amount of samples is still
# the same.
values_to_test = numpy.float64(
values_to_test = numpy.array(
[
[random.uniform(-10, 10) for _ in range(number_of_channels)]
for _ in range(number_of_samples)
]
],
dtype=numpy.float64,
)

with pytest.raises(DaqError) as e:
Expand Down
4 changes: 3 additions & 1 deletion tests/legacy/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for validating utilities functionality."""

from typing import List

from nidaqmx.utils import flatten_channel_string, unflatten_channel_string


Expand All @@ -25,7 +27,7 @@ def test_backwards_flatten_flatten_and_unflatten(self):

def test_empty_flatten_flatten_and_unflatten(self):
"""Test to validate flatten and unflatten empty channel."""
unflattened_channels = []
unflattened_channels: List[str] = []
flattened_channels = ""
assert flatten_channel_string(unflattened_channels) == flattened_channels
assert unflatten_channel_string(flattened_channels) == unflattened_channels
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/_grpc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
try:
import grpc
except ImportError:
grpc = None
grpc = None # type: ignore


def create_grpc_options(mocker: MockerFixture, session_name="") -> nidaqmx.GrpcSessionOptions:
Expand Down
6 changes: 1 addition & 5 deletions tests/unit/test_grpc_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,10 @@
)

try:
from google.protobuf.timestamp_pb2 import Timestamp as GrpcTimestamp

import nidaqmx._grpc_time as grpc_time
import nidaqmx._stubs.nidaqmx_pb2 as nidaqmx_pb2
except ImportError:
GrpcTimestamp = None
grpc_time = None
nidaqmx_pb2 = None
pass


@pytest.mark.parametrize("from_dt", [(JAN_01_2002_DATETIME), (JAN_01_2002_HIGHTIME)])
Expand Down

0 comments on commit 14f953c

Please sign in to comment.