diff --git a/.github/workflows/check_nipanel.yml b/.github/workflows/check_nipanel.yml index 7e5829d..0d8f4ae 100644 --- a/.github/workflows/check_nipanel.yml +++ b/.github/workflows/check_nipanel.yml @@ -33,3 +33,15 @@ jobs: run: poetry run mypy --platform win32 - name: Bandit security checks run: poetry run bandit -c pyproject.toml -r src/nipanel + - 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@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 + with: + python-platform: Linux + version: PATH + - name: Pyright static analysis (Windows) + uses: jakebailey/pyright-action@b5d50e5cde6547546a5c4ac92e416a8c2c1a1dfe # v2.3.2 + with: + python-platform: Windows + version: PATH diff --git a/poetry.lock b/poetry.lock index d2358dd..7c7866b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1355,6 +1355,35 @@ numpy = [ ] typing-extensions = ">=4.13.2" +[[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.16.0" +description = "unoffical Node.js package" +optional = false +python-versions = ">=3.7" +files = [ + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:986b715a96ed703f8ce0c15712f76fc42895cf09067d72b6ef29e8b334eccf64"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:4ae3cf22138891cb44c3ee952862a257ce082b098b29024d7175684a9a77b0c0"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f2de4dc0b64ae43e146897ce811f80ac4f9acfbae6ccf814226282bf4ef174"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbfccbcd558d2f142ccf66d8c3a098022bf4436db9525b5b8d32169ce185d99e"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:447ad796850eb52ca20356ad39b2d296ed8fef3f214921f84a1ccdad49f2eba1"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7f526ca6a132b0caf633566a2a78c6985fe92857e7bfdb37380f76205a10b808"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:2fffb4bf1066fb5f660da20819d754f1b424bca1b234ba0f4fa901c52e3975fb"}, + {file = "nodejs_wheel_binaries-22.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:2728972d336d436d39ee45988978d8b5d963509e06f063e80fe41b203ee80b28"}, + {file = "nodejs_wheel_binaries-22.16.0.tar.gz", hash = "sha256:d695832f026df3a0cf9a089d222225939de9d1b67f8f0a353b79f015aabbe7e2"}, +] + [[package]] name = "numpy" version = "2.0.2" @@ -1936,6 +1965,27 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyright" +version = "1.1.402" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982"}, + {file = "pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683"}, +] + +[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.4.0" @@ -2822,4 +2872,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "b5a867eac14c2241ae4e15f741ddbc388fb60c5bf11483bdb6c869f7d59816de" +content-hash = "4d6ea1ed746a31a8780b057475409256690f3db3e5c27d8345d19b11703aa03c" diff --git a/pyproject.toml b/pyproject.toml index 5bc8033..573dc8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ types-protobuf = ">=4.21" 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" @@ -81,3 +82,7 @@ skips = [ [tool.pytest.ini_options] addopts = "--doctest-modules --strict-markers" testpaths = ["src/nipanel", "tests"] + +[tool.pyright] +include = ["examples/", "src/", "tests/"] +exclude = ["src/ni/protobuf/types/", "src/ni/pythonpanel/v1/"] \ No newline at end of file diff --git a/src/nipanel/converters/builtin.py b/src/nipanel/converters/builtin.py index e41be8d..bcd1e44 100644 --- a/src/nipanel/converters/builtin.py +++ b/src/nipanel/converters/builtin.py @@ -26,9 +26,9 @@ def to_protobuf_message(self, python_value: bool) -> wrappers_pb2.BoolValue: """Convert the Python bool to a protobuf wrappers_pb2.BoolValue.""" return self.protobuf_message(value=python_value) - def to_python_value(self, protobuf_value: wrappers_pb2.BoolValue) -> bool: + def to_python_value(self, protobuf_message: wrappers_pb2.BoolValue) -> bool: """Convert the protobuf message to a Python bool.""" - return protobuf_value.value + return protobuf_message.value class BytesConverter(Converter[bytes, wrappers_pb2.BytesValue]): @@ -48,9 +48,9 @@ def to_protobuf_message(self, python_value: bytes) -> wrappers_pb2.BytesValue: """Convert the Python bytes string to a protobuf wrappers_pb2.BytesValue.""" return self.protobuf_message(value=python_value) - def to_python_value(self, protobuf_value: wrappers_pb2.BytesValue) -> bytes: + def to_python_value(self, protobuf_message: wrappers_pb2.BytesValue) -> bytes: """Convert the protobuf message to a Python bytes string.""" - return protobuf_value.value + return protobuf_message.value class FloatConverter(Converter[float, wrappers_pb2.DoubleValue]): @@ -70,9 +70,9 @@ def to_protobuf_message(self, python_value: float) -> wrappers_pb2.DoubleValue: """Convert the Python float to a protobuf wrappers_pb2.DoubleValue.""" return self.protobuf_message(value=python_value) - def to_python_value(self, protobuf_value: wrappers_pb2.DoubleValue) -> float: + def to_python_value(self, protobuf_message: wrappers_pb2.DoubleValue) -> float: """Convert the protobuf message to a Python float.""" - return protobuf_value.value + return protobuf_message.value class IntConverter(Converter[int, wrappers_pb2.Int64Value]): @@ -92,9 +92,9 @@ def to_protobuf_message(self, python_value: int) -> wrappers_pb2.Int64Value: """Convert the Python int to a protobuf wrappers_pb2.Int64Value.""" return self.protobuf_message(value=python_value) - def to_python_value(self, protobuf_value: wrappers_pb2.Int64Value) -> int: + def to_python_value(self, protobuf_message: wrappers_pb2.Int64Value) -> int: """Convert the protobuf message to a Python int.""" - return protobuf_value.value + return protobuf_message.value class StrConverter(Converter[str, wrappers_pb2.StringValue]): @@ -114,9 +114,9 @@ def to_protobuf_message(self, python_value: str) -> wrappers_pb2.StringValue: """Convert the Python str to a protobuf wrappers_pb2.StringValue.""" return self.protobuf_message(value=python_value) - def to_python_value(self, protobuf_value: wrappers_pb2.StringValue) -> str: + def to_python_value(self, protobuf_message: wrappers_pb2.StringValue) -> str: """Convert the protobuf message to a Python string.""" - return protobuf_value.value + return protobuf_message.value class BoolCollectionConverter(Converter[Collection[bool], python_panel_types_pb2.BoolCollection]): @@ -139,10 +139,10 @@ def to_protobuf_message( return self.protobuf_message(values=python_value) def to_python_value( - self, protobuf_value: python_panel_types_pb2.BoolCollection + self, protobuf_message: python_panel_types_pb2.BoolCollection ) -> Collection[bool]: """Convert the protobuf message to a Python collection of bools.""" - return list(protobuf_value.values) + return list(protobuf_message.values) class BytesCollectionConverter( @@ -167,10 +167,10 @@ def to_protobuf_message( return self.protobuf_message(values=python_value) def to_python_value( - self, protobuf_value: python_panel_types_pb2.ByteStringCollection + self, protobuf_message: python_panel_types_pb2.ByteStringCollection ) -> Collection[bytes]: """Convert the protobuf message to a Python collection of byte strings.""" - return list(protobuf_value.values) + return list(protobuf_message.values) class FloatCollectionConverter( @@ -195,10 +195,10 @@ def to_protobuf_message( return self.protobuf_message(values=python_value) def to_python_value( - self, protobuf_value: python_panel_types_pb2.FloatCollection + self, protobuf_message: python_panel_types_pb2.FloatCollection ) -> Collection[float]: """Convert the protobuf message to a Python collection of floats.""" - return list(protobuf_value.values) + return list(protobuf_message.values) class IntCollectionConverter(Converter[Collection[int], python_panel_types_pb2.IntCollection]): @@ -221,10 +221,10 @@ def to_protobuf_message( return self.protobuf_message(values=python_value) def to_python_value( - self, protobuf_value: python_panel_types_pb2.IntCollection + self, protobuf_message: python_panel_types_pb2.IntCollection ) -> Collection[int]: """Convert the protobuf message to a Python collection of integers.""" - return list(protobuf_value.values) + return list(protobuf_message.values) class StrCollectionConverter(Converter[Collection[str], python_panel_types_pb2.StringCollection]): @@ -247,7 +247,7 @@ def to_protobuf_message( return self.protobuf_message(values=python_value) def to_python_value( - self, protobuf_value: python_panel_types_pb2.StringCollection + self, protobuf_message: python_panel_types_pb2.StringCollection ) -> Collection[str]: """Convert the protobuf message to a Python collection of strings.""" - return list(protobuf_value.values) + return list(protobuf_message.values) diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 21eb99c..0b5aa70 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -42,14 +42,14 @@ def to_protobuf_message(self, python_value: Scalar[_AnyScalarType]) -> scalar_pb return message - def to_python_value(self, protobuf_value: scalar_pb2.ScalarData) -> Scalar[_AnyScalarType]: + def to_python_value(self, protobuf_message: scalar_pb2.ScalarData) -> Scalar[_AnyScalarType]: """Convert the protobuf message to a Python Scalar.""" - if protobuf_value.units is None: + if protobuf_message.units is None: raise ValueError("protobuf.units cannot be None.") - pb_type = str(protobuf_value.WhichOneof("value")) + pb_type = str(protobuf_message.WhichOneof("value")) if pb_type not in _SCALAR_TYPE_TO_PB_ATTR_MAP.values(): raise ValueError(f"Unexpected value for protobuf_value.WhichOneOf: {pb_type}") - value = getattr(protobuf_value, pb_type) - return Scalar(value, protobuf_value.units) + value = getattr(protobuf_message, pb_type) + return Scalar(value, protobuf_message.units) diff --git a/tests/conftest.py b/tests/conftest.py index 1cb4284..963fc3a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ """Fixtures for testing.""" from collections.abc import Generator +from concurrent.futures import ThreadPoolExecutor +from typing import cast import grpc import pytest from grpc.framework.foundation import logging_pool -from ni.pythonpanel.v1.python_panel_service_pb2_grpc import ( - PythonPanelServiceStub, -) +from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub from tests.utils._fake_python_panel_service import FakePythonPanelService @@ -16,6 +16,8 @@ def fake_python_panel_service() -> Generator[FakePythonPanelService]: """Fixture to create a FakePythonPanelServicer for testing.""" with logging_pool.pool(max_workers=10) as thread_pool: + # _LoggingPool is not a ThreadPoolExecutor, but it's duck-typing compatible with one. + thread_pool = cast(ThreadPoolExecutor, thread_pool) service = FakePythonPanelService() service.start(thread_pool) yield service