Skip to content

Commit b21eae9

Browse files
authored
371 switch from mypy to basedpyright (#421)
* Add strict basedpyright * Start fixing * Remove base binding constructor * WIP, working on typing sensapex * Don't do stubs, ignore unknown * Allow Any for MPM * Fixed fake * Server * Platform handler * Add static analysis to docs * Remove most file ignores * Remove all file ignores, split common * Add argument documentation * Reformat
1 parent 7dc63e8 commit b21eae9

File tree

17 files changed

+388
-288
lines changed

17 files changed

+388
-288
lines changed

docs/development/adding_a_manipulator.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ read [how the system works first](../home/how_it_works.md) before proceeding.
99
1. Fork the [Ephys Link repository](https://github.com/VirtualBrainLab/ephys-link).
1010
2. Follow the instructions for [installing Ephys Link for development](index.md#installing-for-development) to get all
1111
the necessary dependencies and tools set up. In this case, you'll want to clone your fork.
12+
3. (Optional) Familiarize yourself with the [repo's organization](code_organization.md).
1213

1314
## Create a Manipulator Binding
1415

@@ -62,6 +63,12 @@ Use [New Scale Pathfinder MPM's binding][ephys_link.bindings.mpm_binding] as an
6263
Once you've implemented your binding, you can test it by running Ephys Link using your binding
6364
`ephys_link -b -t <cli_name>`. You can interact with it using the [Socket.IO API](socketio_api.md) or Pinpoint.
6465

66+
## Code standards
67+
68+
We use automatic static analyzers to check code quality. See
69+
the [corresponding section in the code organization documentation](code_organization.md#static-analysis) for more
70+
information.
71+
6572
## Submit Your Changes
6673

6774
When you're satisfied with your changes, submit a pull request to the main repository. We will review your changes and

docs/development/code_organization.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,17 @@ to return responses to the clients when ready.
2525
[`PlatformHandler`][ephys_link.back_end.platform_handler] is responsible for converting between the server API and the
2626
manipulator binding API. Because of this module, you don't have to worry about the details of the server API when
2727
writing a manipulator binding.
28+
29+
## Static Analysis
30+
31+
The project is strictly type checked using [`hatch fmt` (ruff)](https://hatch.pypa.io/1.9/config/static-analysis/)
32+
and [basedpyright](https://docs.basedpyright.com/latest/). All PRs are checked against these tools.
33+
34+
While they are very helpful in enforcing good code, they can be annoying when working with libraries that inherently
35+
return `Any` (like HTTP requests) or are not strictly statically typed. In those situations we have added inline
36+
comments to ignore specific checks. We try to only use this in scenarios where missing typing information came from
37+
external sources, and it is not possible to make local type hints. Do not use file-wide ignores under any circumstances.
38+
We also do not make stubs since they would be challenging to maintain.
39+
40+
We encourage using the type checker as a tool to help strengthen your code and only apply inline comments to ignore
41+
specific instances where external libraries cause errors.

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ exclude = ["/.github", "/.idea"]
6161
python = "3.13.1"
6262
dependencies = [
6363
"pyinstaller==6.11.1",
64+
"basedpyright==1.23.1"
6465
]
6566
[tool.hatch.envs.default.scripts]
6667
exe = "pyinstaller.exe ephys_link.spec -y -- -d && pyinstaller.exe ephys_link.spec -y"
6768
exe-clean = "pyinstaller.exe ephys_link.spec -y --clean"
69+
check = "basedpyright"
70+
check-watched = "basedpyright --watch"
6871

6972
[tool.hatch.envs.docs]
7073
python = "3.13.1"
@@ -81,7 +84,9 @@ serve = "mkdocs serve"
8184
build = "mkdocs build"
8285

8386
[tool.ruff]
87+
exclude = ["typings"]
8488
unsafe-fixes = true
8589

86-
[tool.ruff.lint]
87-
extend-ignore = ["DTZ005"]
90+
[tool.basedpyright]
91+
include = ["src/ephys_link"]
92+
strict = ["src/ephys_link"]

src/ephys_link/__main__.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,31 @@
1515
from ephys_link.front_end.cli import CLI
1616
from ephys_link.front_end.gui import GUI
1717
from ephys_link.utils.console import Console
18+
from ephys_link.utils.startup import check_for_updates, preamble
1819

1920

2021
def main() -> None:
21-
"""Ephys Link entry point.
22+
"""Ephys Link entry point."""
2223

23-
1. Get options via CLI or GUI.
24-
2. Instantiate the Console and make it globally accessible.
25-
3. Instantiate the Platform Handler with the appropriate platform bindings.
26-
4. Instantiate the Emergency Stop service.
27-
5. Start the server.
28-
"""
24+
# 0. Print the startup preamble.
25+
preamble()
2926

3027
# 1. Get options via CLI or GUI (if no CLI options are provided).
3128
options = CLI().parse_args() if len(argv) > 1 else GUI().get_options()
3229

3330
# 2. Instantiate the Console and make it globally accessible.
3431
console = Console(enable_debug=options.debug)
3532

36-
# 3. Instantiate the Platform Handler with the appropriate platform bindings.
33+
# 3. Check for updates if not disabled.
34+
if not options.ignore_updates:
35+
check_for_updates(console)
36+
37+
# 4. Instantiate the Platform Handler with the appropriate platform bindings.
3738
platform_handler = PlatformHandler(options, console)
3839

39-
# 4. Instantiate the Emergency Stop service.
40+
# 5. Instantiate the Emergency Stop service.
4041

41-
# 5. Start the server.
42+
# 6. Start the server.
4243
Server(options, platform_handler, console).launch()
4344

4445

src/ephys_link/back_end/platform_handler.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# ruff: noqa: BLE001
21
"""Manipulator platform handler.
32
43
Responsible for performing the various manipulator commands.
@@ -8,6 +7,7 @@
87
Instantiate PlatformHandler with the platform type and call the desired command.
98
"""
109

10+
from typing import final
1111
from uuid import uuid4
1212

1313
from vbl_aquarium.models.ephys_link import (
@@ -25,11 +25,14 @@
2525
)
2626
from vbl_aquarium.models.unity import Vector4
2727

28+
from ephys_link.bindings.mpm_binding import MPMBinding
2829
from ephys_link.utils.base_binding import BaseBinding
29-
from ephys_link.utils.common import get_bindings, vector4_to_array
3030
from ephys_link.utils.console import Console
31+
from ephys_link.utils.converters import vector4_to_array
32+
from ephys_link.utils.startup import get_bindings
3133

3234

35+
@final
3336
class PlatformHandler:
3437
"""Handler for platform commands."""
3538

@@ -73,7 +76,7 @@ def _get_binding_instance(self, options: EphysLinkOptions) -> BaseBinding:
7376
if binding_cli_name == options.type:
7477
# Pass in HTTP port for Pathfinder MPM.
7578
if binding_cli_name == "pathfinder-mpm":
76-
return binding_type(options.mpm_port)
79+
return MPMBinding(options.mpm_port)
7780

7881
# Otherwise just return the binding.
7982
return binding_type()
@@ -103,7 +106,7 @@ async def get_platform_info(self) -> PlatformInfo:
103106
name=self._bindings.get_display_name(),
104107
cli_name=self._bindings.get_cli_name(),
105108
axes_count=await self._bindings.get_axes_count(),
106-
dimensions=await self._bindings.get_dimensions(),
109+
dimensions=self._bindings.get_dimensions(),
107110
)
108111

109112
# Manipulator commands.
@@ -116,7 +119,7 @@ async def get_manipulators(self) -> GetManipulatorsResponse:
116119
"""
117120
try:
118121
manipulators = await self._bindings.get_manipulators()
119-
except Exception as e:
122+
except Exception as e: # noqa: BLE001
120123
self._console.exception_error_print("Get Manipulators", e)
121124
return GetManipulatorsResponse(error=self._console.pretty_exception(e))
122125
else:
@@ -135,7 +138,7 @@ async def get_position(self, manipulator_id: str) -> PositionalResponse:
135138
unified_position = self._bindings.platform_space_to_unified_space(
136139
await self._bindings.get_position(manipulator_id)
137140
)
138-
except Exception as e:
141+
except Exception as e: # noqa: BLE001
139142
self._console.exception_error_print("Get Position", e)
140143
return PositionalResponse(error=str(e))
141144
else:
@@ -152,7 +155,7 @@ async def get_angles(self, manipulator_id: str) -> AngularResponse:
152155
"""
153156
try:
154157
angles = await self._bindings.get_angles(manipulator_id)
155-
except Exception as e:
158+
except Exception as e: # noqa: BLE001
156159
self._console.exception_error_print("Get Angles", e)
157160
return AngularResponse(error=self._console.pretty_exception(e))
158161
else:
@@ -169,7 +172,7 @@ async def get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
169172
"""
170173
try:
171174
shank_count = await self._bindings.get_shank_count(manipulator_id)
172-
except Exception as e:
175+
except Exception as e: # noqa: BLE001
173176
self._console.exception_error_print("Get Shank Count", e)
174177
return ShankCountResponse(error=self._console.pretty_exception(e))
175178
else:
@@ -214,7 +217,7 @@ async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
214217
)
215218
self._console.error_print("Set Position", error_message)
216219
return PositionalResponse(error=error_message)
217-
except Exception as e:
220+
except Exception as e: # noqa: BLE001
218221
self._console.exception_error_print("Set Position", e)
219222
return PositionalResponse(error=self._console.pretty_exception(e))
220223
else:
@@ -246,7 +249,7 @@ async def set_depth(self, request: SetDepthRequest) -> SetDepthResponse:
246249
)
247250
self._console.error_print("Set Depth", error_message)
248251
return SetDepthResponse(error=error_message)
249-
except Exception as e:
252+
except Exception as e: # noqa: BLE001
250253
self._console.exception_error_print("Set Depth", e)
251254
return SetDepthResponse(error=self._console.pretty_exception(e))
252255
else:
@@ -268,7 +271,7 @@ async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanState
268271
self._inside_brain.add(request.manipulator_id)
269272
else:
270273
self._inside_brain.discard(request.manipulator_id)
271-
except Exception as e:
274+
except Exception as e: # noqa: BLE001
272275
self._console.exception_error_print("Set Inside Brain", e)
273276
return BooleanStateResponse(error=self._console.pretty_exception(e))
274277
else:
@@ -285,7 +288,7 @@ async def stop(self, manipulator_id: str) -> str:
285288
"""
286289
try:
287290
await self._bindings.stop(manipulator_id)
288-
except Exception as e:
291+
except Exception as e: # noqa: BLE001
289292
self._console.exception_error_print("Stop", e)
290293
return self._console.pretty_exception(e)
291294
else:
@@ -300,7 +303,7 @@ async def stop_all(self) -> str:
300303
try:
301304
for manipulator_id in await self._bindings.get_manipulators():
302305
await self._bindings.stop(manipulator_id)
303-
except Exception as e:
306+
except Exception as e: # noqa: BLE001
304307
self._console.exception_error_print("Stop", e)
305308
return self._console.pretty_exception(e)
306309
else:

src/ephys_link/back_end/server.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from asyncio import get_event_loop, run
1616
from collections.abc import Callable, Coroutine
1717
from json import JSONDecodeError, dumps, loads
18-
from typing import Any
18+
from typing import Any, TypeVar, final
1919
from uuid import uuid4
2020

2121
from aiohttp.web import Application, run_app
2222
from pydantic import ValidationError
23-
from socketio import AsyncClient, AsyncServer
23+
from socketio import AsyncClient, AsyncServer # pyright: ignore [reportMissingTypeStubs]
2424
from vbl_aquarium.models.ephys_link import (
2525
EphysLinkOptions,
2626
SetDepthRequest,
@@ -32,10 +32,15 @@
3232

3333
from ephys_link.__about__ import __version__
3434
from ephys_link.back_end.platform_handler import PlatformHandler
35-
from ephys_link.utils.common import PORT, check_for_updates, server_preamble
3635
from ephys_link.utils.console import Console
36+
from ephys_link.utils.constants import PORT
3737

38+
# Server message generic types.
39+
INPUT_TYPE = TypeVar("INPUT_TYPE", bound=VBLBaseModel)
40+
OUTPUT_TYPE = TypeVar("OUTPUT_TYPE", bound=VBLBaseModel)
3841

42+
43+
@final
3944
class Server:
4045
def __init__(self, options: EphysLinkOptions, platform_handler: PlatformHandler, console: Console) -> None:
4146
"""Initialize server fields based on options and platform handler.
@@ -54,12 +59,18 @@ def __init__(self, options: EphysLinkOptions, platform_handler: PlatformHandler,
5459
# Initialize based on proxy usage.
5560
self._sio: AsyncServer | AsyncClient = AsyncClient() if self._options.use_proxy else AsyncServer()
5661
if not self._options.use_proxy:
62+
# Exit if _sio is not a Server.
63+
if not isinstance(self._sio, AsyncServer):
64+
error = "Server not initialized."
65+
self._console.critical_print(error)
66+
raise TypeError(error)
67+
5768
self._app = Application()
58-
self._sio.attach(self._app)
69+
self._sio.attach(self._app) # pyright: ignore [reportUnknownMemberType]
5970

6071
# Bind connection events.
61-
self._sio.on("connect", self.connect)
62-
self._sio.on("disconnect", self.disconnect)
72+
_ = self._sio.on("connect", self.connect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
73+
_ = self._sio.on("disconnect", self.disconnect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
6374

6475
# Store connected client.
6576
self._client_sid: str = ""
@@ -68,19 +79,13 @@ def __init__(self, options: EphysLinkOptions, platform_handler: PlatformHandler,
6879
self._pinpoint_id = str(uuid4())[:8]
6980

7081
# Bind events.
71-
self._sio.on("*", self.platform_event_handler)
82+
_ = self._sio.on("*", self.platform_event_handler) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
7283

7384
def launch(self) -> None:
7485
"""Launch the server.
7586
7687
Based on the options, either connect to a proxy or launch the server locally.
7788
"""
78-
# Preamble.
79-
server_preamble()
80-
81-
# Check for updates.
82-
if not self._options.ignore_updates:
83-
check_for_updates()
8489

8590
# List platform and available manipulators.
8691
self._console.info_print("PLATFORM", self._platform_handler.get_display_name())
@@ -94,16 +99,22 @@ def launch(self) -> None:
9499
self._console.info_print("PINPOINT ID", self._pinpoint_id)
95100

96101
async def connect_proxy() -> None:
102+
# Exit if _sio is not a proxy client.
103+
if not isinstance(self._sio, AsyncClient):
104+
error = "Proxy client not initialized."
105+
self._console.critical_print(error)
106+
raise TypeError(error)
107+
97108
# noinspection HttpUrlsUsage
98-
await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}")
109+
await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}") # pyright: ignore [reportUnknownMemberType]
99110
await self._sio.wait()
100111

101112
run(connect_proxy())
102113
else:
103114
run_app(self._app, port=PORT)
104115

105116
# Helper functions.
106-
def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str:
117+
def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str: # pyright: ignore [reportExplicitAny]
107118
"""Return a response for a malformed request.
108119
109120
Args:
@@ -117,7 +128,10 @@ def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]
117128
return dumps({"error": "Malformed request."})
118129

119130
async def _run_if_data_available(
120-
self, function: Callable[[str], Coroutine[Any, Any, VBLBaseModel]], event: str, data: tuple[tuple[Any], ...]
131+
self,
132+
function: Callable[[str], Coroutine[Any, Any, VBLBaseModel]], # pyright: ignore [reportExplicitAny]
133+
event: str,
134+
data: tuple[tuple[Any], ...], # pyright: ignore [reportExplicitAny]
121135
) -> str:
122136
"""Run a function if data is available.
123137
@@ -136,10 +150,10 @@ async def _run_if_data_available(
136150

137151
async def _run_if_data_parses(
138152
self,
139-
function: Callable[[VBLBaseModel], Coroutine[Any, Any, VBLBaseModel]],
140-
data_type: type[VBLBaseModel],
153+
function: Callable[[INPUT_TYPE], Coroutine[Any, Any, OUTPUT_TYPE]], # pyright: ignore [reportExplicitAny]
154+
data_type: type[INPUT_TYPE],
141155
event: str,
142-
data: tuple[tuple[Any], ...],
156+
data: tuple[tuple[Any], ...], # pyright: ignore [reportExplicitAny]
143157
) -> str:
144158
"""Run a function if data parses.
145159
@@ -203,8 +217,7 @@ async def disconnect(self, sid: str) -> None:
203217
else:
204218
self._console.error_print("DISCONNECTION", f"Client {sid} disconnected without being connected.")
205219

206-
# noinspection PyTypeChecker
207-
async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
220+
async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str: # pyright: ignore [reportExplicitAny]
208221
"""Handle events from the server.
209222
210223
Matches incoming events based on the Socket.IO API.

0 commit comments

Comments
 (0)