Skip to content

Commit ff57f61

Browse files
authored
357 rich and textual (#367)
* Setup Qodana, install Textual * Reset to just rich * Prep for using rich table * First pass re-implement using live table * Table is too laggy * Use Logging handler * Bracket time stamp
1 parent a30c4fa commit ff57f61

File tree

10 files changed

+157
-68
lines changed

10 files changed

+157
-68
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Qodana
2+
on:
3+
workflow_dispatch:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
qodana:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v3
14+
with:
15+
fetch-depth: 0
16+
- name: 'Qodana Scan'
17+
uses: JetBrains/qodana-action@v2024.1
18+
env:
19+
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies = [
3838
"pythonnet==3.0.3",
3939
"requests==2.32.3",
4040
"sensapex==1.400.1",
41+
"rich==13.7.1",
4142
"vbl-aquarium==0.0.19"
4243
]
4344

@@ -114,4 +115,7 @@ exclude_lines = [
114115
"no cov",
115116
"if __name__ == .__main__.:",
116117
"if TYPE_CHECKING:",
117-
]
118+
]
119+
120+
[tool.ruff.lint]
121+
extend-ignore = ["DTZ005"]

qodana.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#-------------------------------------------------------------------------------#
2+
# Qodana analysis is configured by qodana.yaml file #
3+
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
4+
#-------------------------------------------------------------------------------#
5+
version: "1.0"
6+
7+
#Specify inspection profile for code analysis
8+
profile:
9+
name: qodana.starter
10+
11+
#Enable inspections
12+
#include:
13+
# - name: <SomeEnabledInspectionId>
14+
15+
#Disable inspections
16+
#exclude:
17+
# - name: <SomeDisabledInspectionId>
18+
# paths:
19+
# - <path/where/not/run/inspection>
20+
21+
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
22+
#bootstrap: sh ./prepare-qodana.sh
23+
24+
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
25+
#plugins:
26+
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
27+
28+
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
29+
linter: jetbrains/qodana-python:latest

scripts/logger_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import logging
2+
3+
from rich.logging import RichHandler
4+
5+
logging.basicConfig(level="NOTSET", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True)])
6+
7+
log = logging.getLogger("rich")
8+
log.debug("This message should go to the log file")
9+
log.info("So should this")
10+
log.warning("And this, too")
11+
log.error("And non-ASCII stuff, too, like Øresund and Malmö")
12+
log.error("[bold red blink]Server is shutting down!", extra={"markup": True})
13+
log.critical("Critical error! [b red]Server is shutting down!", extra={"markup": True})
14+
try:
15+
print(1 / 0)
16+
except Exception:
17+
log.exception("[b magenta]unable print![/] [i magenta]asdf", extra={"markup": True})

src/ephys_link/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.0b0"
1+
__version__ = "2.0.0b2"

src/ephys_link/back_end/platform_handler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def _match_platform_type(self, platform_type: str) -> BaseBindings:
7171
return FakeBindings()
7272
case _:
7373
error_message = f'Platform type "{platform_type}" not recognized.'
74-
self._console.labeled_error_print("PLATFORM", error_message)
74+
self._console.critical_print(error_message)
7575
raise ValueError(error_message)
7676

7777
# Ephys Link metadata.
@@ -185,7 +185,7 @@ async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
185185
# Disallow setting manipulator position while inside the brain.
186186
if request.manipulator_id in self._inside_brain:
187187
error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
188-
self._console.error_print(error_message)
188+
self._console.error_print("Set Position", error_message)
189189
return PositionalResponse(error=error_message)
190190

191191
# Move to the new position.
@@ -209,7 +209,7 @@ async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
209209
f" position on axis {list(Vector4.model_fields.keys())[index]}."
210210
f"Requested: {request.position}, got: {final_unified_position}."
211211
)
212-
self._console.error_print(error_message)
212+
self._console.error_print("Set Position", error_message)
213213
return PositionalResponse(error=error_message)
214214
except Exception as e:
215215
self._console.exception_error_print("Set Position", e)

src/ephys_link/back_end/server.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async def connect_proxy() -> None:
7575
# Helper functions.
7676
def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str:
7777
"""Return a response for a malformed request."""
78-
self._console.labeled_error_print("MALFORMED REQUEST", f"{request}: {data}")
78+
self._console.error_print("MALFORMED REQUEST", f"{request}: {data}")
7979
return dumps({"error": "Malformed request."})
8080

8181
async def _run_if_data_available(
@@ -127,7 +127,9 @@ async def connect(self, sid: str, _: str) -> bool:
127127
self._console.info_print("CONNECTION GRANTED", sid)
128128
return True
129129

130-
self._console.error_print(f"CONNECTION REFUSED to {sid}. Client {self._client_sid} already connected.")
130+
self._console.error_print(
131+
"CONNECTION REFUSED", f"Cannot connect {sid} as {self._client_sid} is already connected."
132+
)
131133
return False
132134

133135
async def disconnect(self, sid: str) -> None:
@@ -142,7 +144,7 @@ async def disconnect(self, sid: str) -> None:
142144
if self._client_sid == sid:
143145
self._client_sid = ""
144146
else:
145-
self._console.error_print(f"Client {sid} disconnected without being connected.")
147+
self._console.error_print("DISCONNECTION", f"Client {sid} disconnected without being connected.")
146148

147149
# noinspection PyTypeChecker
148150
async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
@@ -196,5 +198,5 @@ async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
196198
case "stop_all":
197199
return await self._platform_handler.stop_all()
198200
case _:
199-
self._console.error_print(f"Unknown event: {event}.")
201+
self._console.error_print("EVENT", f"Unknown event: {event}.")
200202
return dumps({"error": "Unknown event."})

src/ephys_link/bindings/ump_4_bindings.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from ephys_link.util.base_bindings import BaseBindings
1212
from ephys_link.util.common import RESOURCES_PATH, array_to_vector4, mm_to_um, mmps_to_umps, um_to_mm, vector4_to_array
13-
from ephys_link.util.console import Console
1413

1514

1615
class Ump4Bindings(BaseBindings):
@@ -24,7 +23,6 @@ def __init__(self) -> None:
2423
self._ump = UMP.get_ump()
2524
if self._ump is None:
2625
error_message = "Unable to connect to uMp"
27-
Console.error_print(error_message)
2826
raise ValueError(error_message)
2927

3028
async def get_manipulators(self) -> list[str]:

src/ephys_link/util/common.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from vbl_aquarium.models.unity import Vector4
1010

1111
from ephys_link.__about__ import __version__
12-
from ephys_link.util.console import Console
1312

1413
# Ephys Link ASCII.
1514
ASCII = r"""
@@ -47,8 +46,8 @@ def check_for_updates() -> None:
4746
response = get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags", timeout=10)
4847
latest_version = response.json()[0]["name"]
4948
if parse(latest_version) > parse(__version__):
50-
Console.info_print("Update available", latest_version)
51-
Console.info_print("", "Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
49+
print(f"Update available: {latest_version} !")
50+
print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
5251

5352

5453
# Unit conversions

src/ephys_link/util/console.py

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
Usage: Create a Console object and call the appropriate method to print messages.
77
"""
88

9-
from traceback import print_exc
9+
from logging import DEBUG, ERROR, INFO, basicConfig, getLogger
1010

11-
from colorama import Back, Fore, Style, init
12-
13-
# Constants.
14-
TAB_BLOCK = "\t\t"
11+
from rich.logging import RichHandler
12+
from rich.traceback import install
1513

1614

1715
class Console:
@@ -21,34 +19,59 @@ def __init__(self, *, enable_debug: bool) -> None:
2119
:param enable_debug: Enable debug mode.
2220
:type enable_debug: bool
2321
"""
24-
self._enable_debug = enable_debug
25-
2622
# Repeat message fields.
27-
self._last_message = ""
28-
self._repeat_counter = 1
23+
self._last_message = (0, "", "")
24+
self._repeat_counter = 0
2925

30-
# Initialize colorama.
31-
init(autoreset=True)
26+
# Config logger.
27+
basicConfig(
28+
level=DEBUG if enable_debug else INFO,
29+
format="%(message)s",
30+
datefmt="[%I:%M:%S %p]",
31+
handlers=[RichHandler(rich_tracebacks=True)],
32+
)
33+
self._log = getLogger("rich")
3234

33-
@staticmethod
34-
def error_print(msg: str) -> None:
35-
"""Print an error message to the console.
35+
# Install Rich traceback.
36+
install()
3637

37-
:param msg: Error message to print.
38+
def debug_print(self, label: str, msg: str) -> None:
39+
"""Print a debug message to the console.
40+
41+
:param label: Label for the debug message.
42+
:type label: str
43+
:param msg: Debug message to print.
3844
:type msg: str
3945
"""
40-
print(f"\n{Back.RED}{Style.BRIGHT} ERROR {Style.RESET_ALL}{TAB_BLOCK}{Fore.RED}{msg}")
46+
self._repeatable_log(DEBUG, f"[b green]{label}", f"[green]{msg}")
4147

42-
@staticmethod
43-
def labeled_error_print(label: str, msg: str) -> None:
44-
"""Print an error message with a label to the console.
48+
def info_print(self, label: str, msg: str) -> None:
49+
"""Print info to console.
50+
51+
:param label: Label for the message.
52+
:type label: str
53+
:param msg: Message to print.
54+
:type msg: str
55+
"""
56+
self._repeatable_log(INFO, f"[b blue]{label}", msg)
57+
58+
def error_print(self, label: str, msg: str) -> None:
59+
"""Print an error message to the console.
4560
4661
:param label: Label for the error message.
4762
:type label: str
4863
:param msg: Error message to print.
4964
:type msg: str
5065
"""
51-
print(f"\n{Back.RED}{Style.BRIGHT} ERROR {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.RED}{msg}")
66+
self._repeatable_log(ERROR, f"[b red]{label}", f"[red]{msg}")
67+
68+
def critical_print(self, msg: str) -> None:
69+
"""Print a critical message to the console.
70+
71+
:param msg: Critical message to print.
72+
:type msg: str
73+
"""
74+
self._log.critical(f"[b i red]{msg}", extra={"markup": True})
5275

5376
@staticmethod
5477
def pretty_exception(exception: Exception) -> str:
@@ -61,52 +84,50 @@ def pretty_exception(exception: Exception) -> str:
6184
"""
6285
return f"{type(exception).__name__}: {exception}"
6386

64-
@staticmethod
65-
def exception_error_print(label: str, exception: Exception) -> None:
87+
def exception_error_print(self, label: str, exception: Exception) -> None:
6688
"""Print an error message with exception details to the console.
6789
6890
:param label: Label for the error message.
6991
:type label: str
7092
:param exception: Exception to print.
7193
:type exception: Exception
7294
"""
73-
Console.labeled_error_print(label, Console.pretty_exception(exception))
74-
print_exc()
75-
76-
def debug_print(self, label: str, msg: str) -> None:
77-
"""Print a debug message to the console.
78-
79-
:param label: Label for the debug message.
80-
:type label: str
81-
:param msg: Debug message to print.
82-
:type msg: str
83-
"""
84-
if self._enable_debug:
85-
self._repeat_print(f"{Back.BLUE}{Style.BRIGHT} DEBUG {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.BLUE}{msg}")
95+
self._log.exception(
96+
f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}", extra={"markup": True}
97+
)
8698

87-
@staticmethod
88-
def info_print(label: str, msg: str) -> None:
89-
"""Print info to console.
99+
# Helper methods.
100+
def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
101+
"""Add a row to the output table.
90102
103+
:param log_type: Type of log.
104+
:type log_type: int
91105
:param label: Label for the message.
92106
:type label: str
93-
:param msg: Message to print.
94-
:type msg: str
107+
:param message: Message.
108+
:type message: str
95109
"""
96-
print(f"\n{Back.GREEN}{Style.BRIGHT} {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.GREEN}{msg}")
97-
98-
# Helper methods.
99-
def _repeat_print(self, msg: str) -> None:
100-
"""Print a message to the console with repeat counter.
101110

102-
:param msg: Message to print.
103-
:type msg: str
104-
"""
105-
if msg == self._last_message:
111+
# Compute if this is a repeated message.
112+
message_set = (log_type, label, message)
113+
if message_set == self._last_message:
114+
# Handle repeat.
106115
self._repeat_counter += 1
107-
else:
108-
self._repeat_counter = 1
109-
self._last_message = msg
110-
print()
111116

112-
print(f"\r{msg}{f" (x{self._repeat_counter})" if self._repeat_counter > 1 else ""}", end="")
117+
# Add an ellipsis row for first repeat.
118+
if self._repeat_counter == 1:
119+
self._log.log(log_type, "...")
120+
else:
121+
# Handle novel message.
122+
if self._repeat_counter > 0:
123+
# Complete previous repeat.
124+
self._log.log(
125+
self._last_message[0],
126+
f"{self._last_message[1]}:[/] {self._last_message[2]}[/] x {self._repeat_counter}",
127+
extra={"markup": True},
128+
)
129+
self._repeat_counter = 0
130+
131+
# Log new message.
132+
self._log.log(log_type, f"{label}:[/] {message}", extra={"markup": True})
133+
self._last_message = message_set

0 commit comments

Comments
 (0)