diff --git a/.github/workflows/check_nitypes.yml b/.github/workflows/check_nitypes.yml index 38373d35..32fa33ec 100644 --- a/.github/workflows/check_nitypes.yml +++ b/.github/workflows/check_nitypes.yml @@ -41,3 +41,15 @@ jobs: run: poetry run mypy --platform win32 - name: Bandit security checks run: poetry run bandit -c pyproject.toml -r src/nitypes + - name: Add virtualenv to the path for pyright-action + run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH + - name: Pyright static analysis (Linux) + uses: jakebailey/pyright-action@v2 + with: + python-platform: Linux + version: PATH + - name: Pyright static analysis (Windows) + uses: jakebailey/pyright-action@v2 + with: + python-platform: Windows + version: PATH diff --git a/poetry.lock b/poetry.lock index 5756cc40..512dfcac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -750,6 +750,35 @@ pycodestyle = [ ] toml = ">=0.10.1" +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "22.15.0" +description = "unoffical Node.js package" +optional = false +python-versions = ">=3.7" +files = [ + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa16366d48487fff89446fb237693e777aa2ecd987208db7d4e35acc40c3e1b1"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:a54bb3fee9170003fa8abc69572d819b2b1540344eff78505fcc2129a9175596"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:867121ccf99d10523f6878a26db86e162c4939690e24cfb5bea56d01ea696c93"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab0fbcda2ddc8aab7db1505d72cb958f99324b3834c4543541a305e02bfe860"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2bde1d8e00cd955b9ce9ee9ac08309923e2778a790ee791b715e93e487e74bfd"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:acdd4ef73b6701aab9fbe02ac5e104f208a5e3c300402fa41ad7bc7f49499fbf"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:51deaf13ee474e39684ce8c066dfe86240edb94e7241950ca789befbbbcbd23d"}, + {file = "nodejs_wheel_binaries-22.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:01a3fe4d60477f93bf21a44219db33548c75d7fed6dc6e6f4c05cf0adf015609"}, + {file = "nodejs_wheel_binaries-22.15.0.tar.gz", hash = "sha256:ff81aa2a79db279c2266686ebcb829b6634d049a5a49fc7dc6921e4f18af9703"}, +] + [[package]] name = "numpy" version = "2.0.2" @@ -1024,6 +1053,27 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyright" +version = "1.1.400" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e"}, + {file = "pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +nodejs-wheel-binaries = {version = "*", optional = true, markers = "extra == \"nodejs\""} +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + [[package]] name = "pytest" version = "8.3.5" @@ -1538,4 +1588,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "b79abfa57894303381b413b8b6a12154b7fcb6bc80948e667306b460ceb92d0a" +content-hash = "c5fc9f4649cba7d01b16fb247bc2fe0c5be960aeb051d4592db036063732c91f" diff --git a/pyproject.toml b/pyproject.toml index d12741a4..95db7fb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ typing-extensions = ">=4.13.2" bandit = { version = ">=1.7", extras = ["toml"] } ni-python-styleguide = ">=0.4.1" mypy = ">=1.0" +pyright = { version = ">=1.1.400", extras = ["nodejs"] } [tool.poetry.group.test.dependencies] pytest = ">=7.2" @@ -62,3 +63,6 @@ strict = true skips = [ "B101", # assert_used ] + +[tool.pyright] +include = ["src/", "tests/"] \ No newline at end of file diff --git a/src/nitypes/waveform/_analog.py b/src/nitypes/waveform/_analog.py index 4813104f..3e0c8d10 100644 --- a/src/nitypes/waveform/_analog.py +++ b/src/nitypes/waveform/_analog.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any, SupportsIndex, Union, overload +from typing import Any, SupportsIndex, Union, cast, overload import numpy as np import numpy.typing as npt @@ -92,7 +92,7 @@ def from_array_1d( def from_array_1d( cls, array: npt.NDArray[Any] | Sequence[Any], - dtype: type[_TRaw] | np.dtype[_TRaw] = ..., + dtype: type[_TRaw] | np.dtype[_TRaw], *, copy: bool = ..., start_index: SupportsIndex | None = ..., @@ -119,7 +119,7 @@ def from_array_1d( @override @classmethod - def from_array_1d( + def from_array_1d( # pyright: ignore[reportIncompatibleMethodOverride] cls, array: npt.NDArray[Any] | Sequence[Any], dtype: npt.DTypeLike = None, @@ -178,7 +178,7 @@ def from_array_2d( def from_array_2d( cls, array: npt.NDArray[Any] | Sequence[Sequence[Any]], - dtype: type[_TRaw] | np.dtype[_TRaw] = ..., + dtype: type[_TRaw] | np.dtype[_TRaw], *, copy: bool = ..., start_index: SupportsIndex | None = ..., @@ -205,7 +205,7 @@ def from_array_2d( @override @classmethod - def from_array_2d( + def from_array_2d( # pyright: ignore[reportIncompatibleMethodOverride] cls, array: npt.NDArray[Any] | Sequence[Sequence[Any]], dtype: npt.DTypeLike = None, @@ -237,15 +237,19 @@ def from_array_2d( information, and scale mode are applied to all waveforms. Consider assigning these properties after construction. """ - return super(AnalogWaveform, cls).from_array_2d( - array, - dtype, - copy=copy, - start_index=start_index, - sample_count=sample_count, - extended_properties=extended_properties, - timing=timing, - scale_mode=scale_mode, + # list[T] is invariant but we are using it in a covariant way here. + return cast( + list[AnalogWaveform[Any]], + super(AnalogWaveform, cls).from_array_2d( + array, + dtype, + copy=copy, + start_index=start_index, + sample_count=sample_count, + extended_properties=extended_properties, + timing=timing, + scale_mode=scale_mode, + ), ) __slots__ = () diff --git a/src/nitypes/waveform/_complex.py b/src/nitypes/waveform/_complex.py index ae52d5ce..b82525c4 100644 --- a/src/nitypes/waveform/_complex.py +++ b/src/nitypes/waveform/_complex.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import Any, SupportsIndex, Union, overload +from typing import Any, SupportsIndex, Union, cast, overload import numpy as np import numpy.typing as npt @@ -80,7 +80,7 @@ def from_array_1d( def from_array_1d( cls, array: npt.NDArray[Any] | Sequence[Any], - dtype: type[_TRaw] | np.dtype[_TRaw] = ..., + dtype: type[_TRaw] | np.dtype[_TRaw], *, copy: bool = ..., start_index: SupportsIndex | None = ..., @@ -107,7 +107,7 @@ def from_array_1d( @override @classmethod - def from_array_1d( + def from_array_1d( # pyright: ignore[reportIncompatibleMethodOverride] cls, array: npt.NDArray[Any] | Sequence[Any], dtype: npt.DTypeLike = None, @@ -166,7 +166,7 @@ def from_array_2d( def from_array_2d( cls, array: npt.NDArray[Any] | Sequence[Sequence[Any]], - dtype: type[_TRaw] | np.dtype[_TRaw] = ..., + dtype: type[_TRaw] | np.dtype[_TRaw], *, copy: bool = ..., start_index: SupportsIndex | None = ..., @@ -193,7 +193,7 @@ def from_array_2d( @override @classmethod - def from_array_2d( + def from_array_2d( # pyright: ignore[reportIncompatibleMethodOverride] cls, array: npt.NDArray[Any] | Sequence[Sequence[Any]], dtype: npt.DTypeLike = None, @@ -225,15 +225,19 @@ def from_array_2d( information, and scale mode are applied to all waveforms. Consider assigning these properties after construction. """ - return super(ComplexWaveform, cls).from_array_2d( - array, - dtype, - copy=copy, - start_index=start_index, - sample_count=sample_count, - extended_properties=extended_properties, - timing=timing, - scale_mode=scale_mode, + # list[T] is invariant but we are using it in a covariant way here. + return cast( + list[ComplexWaveform[Any]], + super(ComplexWaveform, cls).from_array_2d( + array, + dtype, + copy=copy, + start_index=start_index, + sample_count=sample_count, + extended_properties=extended_properties, + timing=timing, + scale_mode=scale_mode, + ), ) __slots__ = () diff --git a/src/nitypes/waveform/_numeric.py b/src/nitypes/waveform/_numeric.py index cef7a01c..5227ca47 100644 --- a/src/nitypes/waveform/_numeric.py +++ b/src/nitypes/waveform/_numeric.py @@ -424,7 +424,7 @@ def get_scaled_data( # noqa: D107 - Missing docstring in __init__ (auto-generat @overload def get_scaled_data( # noqa: D107 - Missing docstring in __init__ (auto-generated noqa) self, - dtype: type[_TOtherScaled] | np.dtype[_TOtherScaled] = ..., + dtype: type[_TOtherScaled] | np.dtype[_TOtherScaled], *, start_index: SupportsIndex | None = ..., sample_count: SupportsIndex | None = ..., @@ -546,12 +546,12 @@ def _get_timing(self, requested_type: type[_TTiming]) -> _TTiming: self._converted_timing_cache[requested_type] = value return value - def _set_timing(self, value: _TTiming) -> None: + def _set_timing(self, value: _AnyTiming) -> None: if self._timing is not value: self._timing = value self._converted_timing_cache.clear() - def _validate_timing(self, value: _TTiming) -> None: + def _validate_timing(self, value: _AnyTiming) -> None: if value._timestamps is not None and len(value._timestamps) != self._sample_count: raise ValueError( "The number of irregular timestamps is not equal to the number of samples in the waveform.\n\n" diff --git a/src/nitypes/waveform/_timing/_precision.py b/src/nitypes/waveform/_timing/_precision.py index c3e8a256..623dc766 100644 --- a/src/nitypes/waveform/_timing/_precision.py +++ b/src/nitypes/waveform/_timing/_precision.py @@ -1,15 +1,16 @@ from __future__ import annotations from collections.abc import Sequence -from typing import ClassVar +from typing import ClassVar, final import hightime as ht -from typing_extensions import override +from typing_extensions import Self, override from nitypes.waveform._timing._base import BaseTiming from nitypes.waveform._timing._sample_interval import SampleIntervalMode +@final class PrecisionTiming(BaseTiming[ht.datetime, ht.timedelta]): """High-precision waveform timing using the hightime package. @@ -24,27 +25,29 @@ class PrecisionTiming(BaseTiming[ht.datetime, ht.timedelta]): """A waveform timing object with no timestamp, time offset, or sample interval.""" @override - @staticmethod + @classmethod def create_with_no_interval( # noqa: D102 - Missing docstring in public method - override - timestamp: ht.datetime | None = None, time_offset: ht.timedelta | None = None - ) -> PrecisionTiming: - return PrecisionTiming(SampleIntervalMode.NONE, timestamp, time_offset) + cls, timestamp: ht.datetime | None = None, time_offset: ht.timedelta | None = None + ) -> Self: + return cls(SampleIntervalMode.NONE, timestamp, time_offset) @override - @staticmethod + @classmethod def create_with_regular_interval( # noqa: D102 - Missing docstring in public method - override + cls, sample_interval: ht.timedelta, timestamp: ht.datetime | None = None, time_offset: ht.timedelta | None = None, - ) -> PrecisionTiming: - return PrecisionTiming(SampleIntervalMode.REGULAR, timestamp, time_offset, sample_interval) + ) -> Self: + return cls(SampleIntervalMode.REGULAR, timestamp, time_offset, sample_interval) @override - @staticmethod + @classmethod def create_with_irregular_interval( # noqa: D102 - Missing docstring in public method - override + cls, timestamps: Sequence[ht.datetime], - ) -> PrecisionTiming: - return PrecisionTiming(SampleIntervalMode.IRREGULAR, timestamps=timestamps) + ) -> Self: + return cls(SampleIntervalMode.IRREGULAR, timestamps=timestamps) @override @staticmethod diff --git a/src/nitypes/waveform/_timing/_standard.py b/src/nitypes/waveform/_timing/_standard.py index 51bbee99..61363c59 100644 --- a/src/nitypes/waveform/_timing/_standard.py +++ b/src/nitypes/waveform/_timing/_standard.py @@ -2,14 +2,15 @@ import datetime as dt from collections.abc import Sequence -from typing import ClassVar +from typing import ClassVar, final -from typing_extensions import override +from typing_extensions import Self, override from nitypes.waveform._timing._base import BaseTiming from nitypes.waveform._timing._sample_interval import SampleIntervalMode +@final class Timing(BaseTiming[dt.datetime, dt.timedelta]): """Waveform timing using the standard datetime module. @@ -25,27 +26,29 @@ class Timing(BaseTiming[dt.datetime, dt.timedelta]): """A waveform timing object with no timestamp, time offset, or sample interval.""" @override - @staticmethod + @classmethod def create_with_no_interval( # noqa: D102 - Missing docstring in public method - override - timestamp: dt.datetime | None = None, time_offset: dt.timedelta | None = None - ) -> Timing: - return Timing(SampleIntervalMode.NONE, timestamp, time_offset) + cls, timestamp: dt.datetime | None = None, time_offset: dt.timedelta | None = None + ) -> Self: + return cls(SampleIntervalMode.NONE, timestamp, time_offset) @override - @staticmethod + @classmethod def create_with_regular_interval( # noqa: D102 - Missing docstring in public method - override + cls, sample_interval: dt.timedelta, timestamp: dt.datetime | None = None, time_offset: dt.timedelta | None = None, - ) -> Timing: - return Timing(SampleIntervalMode.REGULAR, timestamp, time_offset, sample_interval) + ) -> Self: + return cls(SampleIntervalMode.REGULAR, timestamp, time_offset, sample_interval) @override - @staticmethod + @classmethod def create_with_irregular_interval( # noqa: D102 - Missing docstring in public method - override + cls, timestamps: Sequence[dt.datetime], - ) -> Timing: - return Timing(SampleIntervalMode.IRREGULAR, timestamps=timestamps) + ) -> Self: + return cls(SampleIntervalMode.IRREGULAR, timestamps=timestamps) @override @staticmethod diff --git a/tests/unit/complex/test_conversion.py b/tests/unit/complex/test_conversion.py index 1e783631..90265342 100644 --- a/tests/unit/complex/test_conversion.py +++ b/tests/unit/complex/test_conversion.py @@ -166,7 +166,9 @@ def test___complexint32_scalar_to_complex128_scalar___convert_complex___converts value_out = convert_complex(np.complex128, value_in) - assert_type(value_out, np.ndarray[Any, Any]) # This is less than ideal. + # Mypy infers np.ndarray[Any, Any], which seems wrong. + # Pyright infers np.ndarray[Any, np.dtype[np.complex128]], which seems right. + assert_type(value_out, np.ndarray[Any, np.dtype[np.complex128]]) # type: ignore[assert-type] assert isinstance(value_out, np.complex128) assert value_out == (1 + 2j) @@ -189,7 +191,9 @@ def test___complexint32_scalar_to_complexint32_scalar___convert_complex___return value_out = convert_complex(ComplexInt32DType, value_in) - assert_type(value_out, np.ndarray[Any, Any]) # This is less than ideal. + # Mypy infers np.ndarray[Any, Any], which seems wrong. + # Pyright infers np.ndarray[Any, np.dtype[np.void]], which seems right. + assert_type(value_out, np.ndarray[Any, np.dtype[np.void]]) # type: ignore[assert-type] assert value_out is value_in diff --git a/tests/unit/time/test_conversion.py b/tests/unit/time/test_conversion.py index 809fcb05..80ae4aa7 100644 --- a/tests/unit/time/test_conversion.py +++ b/tests/unit/time/test_conversion.py @@ -76,7 +76,9 @@ def test___variable_requested_type___convert_datetime___static_return_type_unkno value_out = convert_datetime(requested_type, value_in) - assert_type(value_out, Any) + # Mypy infers Any, which seems right. + # Pyright infers dt.datetime, which seems wrong. + assert_type(value_out, Any) # pyright: ignore[reportAssertTypeFailure] assert isinstance(value_out, requested_type) @@ -161,7 +163,9 @@ def test___variable_requested_type___convert_timedelta___static_return_type_unkn value_out = convert_timedelta(requested_type, value_in) - assert_type(value_out, Any) + # Mypy infers Any, which seems right. + # Pyright infers dt.datetime, which seems wrong. + assert_type(value_out, Any) # pyright: ignore[reportAssertTypeFailure] assert isinstance(value_out, requested_type)