From f17e51ee124af4036c74c2d225f08e7ba784a5a3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 24 Apr 2021 15:40:45 +0100 Subject: [PATCH 01/33] tui experiment --- rich/tui/__init__.py | 0 rich/tui/app.py | 104 +++++++++++++++++++++++++++++++++++++++++ rich/tui/case.py | 22 +++++++++ rich/tui/driver.py | 67 ++++++++++++++++++++++++++ rich/tui/event_pump.py | 78 +++++++++++++++++++++++++++++++ rich/tui/events.py | 100 +++++++++++++++++++++++++++++++++++++++ rich/tui/types.py | 4 ++ 7 files changed, 375 insertions(+) create mode 100644 rich/tui/__init__.py create mode 100644 rich/tui/app.py create mode 100644 rich/tui/case.py create mode 100644 rich/tui/driver.py create mode 100644 rich/tui/event_pump.py create mode 100644 rich/tui/events.py create mode 100644 rich/tui/types.py diff --git a/rich/tui/__init__.py b/rich/tui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rich/tui/app.py b/rich/tui/app.py new file mode 100644 index 000000000..4a55636a2 --- /dev/null +++ b/rich/tui/app.py @@ -0,0 +1,104 @@ +import asyncio +from contextlib import contextmanager +from dis import dis +import logging +import signal +from typing import AsyncGenerator, ClassVar, Iterable, Optional + +from .events import Event, KeyEvent, ShutdownRequestEvent +from .. import get_console +from ..console import Console +from .driver import Driver, CursesDriver +from .event_pump import EventPump +from .types import Callback + + +log = logging.getLogger("rich") + + +# def signal_handler(sig, frame): +# print("You pressed Ctrl+C!") +# App.on_keyboard_interupt() + + +# signal.signal(signal.SIGINT, signal_handler) + + +class App: + + _active_app: ClassVar[Optional["App"]] = None + + def __init__(self, console: Console = None, screen: bool = True): + self.console = console or get_console() + self._screen = screen + self._events: Optional[EventPump] = None + + @property + def events(self) -> EventPump: + assert self._events is not None + return self._events + + @classmethod + def on_keyboard_interupt(cls) -> None: + if App._active_app is not None: + App._active_app.events.post(ShutdownRequestEvent()) + + async def __aiter__(self) -> AsyncGenerator[Event, None]: + loop = asyncio.get_event_loop() + self._events = EventPump() + driver = CursesDriver(self.console, self.events) + driver.start_application_mode() + loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt) + App._active_app = self + try: + while True: + event = await self.events.get() + if event is None: + break + yield event + finally: + App._active_app = None + loop.remove_signal_handler(signal.SIGINT) + driver.stop_application_mode() + + def run(self) -> None: + asyncio.run(self._run()) + + async def _run(self) -> None: + async for event in self: + log.debug(event) + dispatch_function = getattr(self, f"on_{event.name}", None) + if dispatch_function is not None: + log.debug(await dispatch_function(event)) + else: + log.debug("No handler for %r", event) + + async def on_shutdown_request(self, event: ShutdownRequestEvent) -> None: + log.debug("%r shutting down", self) + self.events.close() + + # def add_interval(self, delay: float, callback: Callback = None) -> IntervalID: + # pass + + # def add_timer(self, period: float, callback: Callback = None) -> TimerID: + # pass + + +if __name__ == "__main__": + import asyncio + from logging import FileHandler + + logging.basicConfig( + level="NOTSET", + format="%(message)s", + datefmt="[%X]", + handlers=[FileHandler("richtui.log")], + ) + + class MyApp(App): + async def on_key(self, event: KeyEvent) -> None: + if event.key == ord("q"): + raise ValueError() + + app = MyApp() + app.run() \ No newline at end of file diff --git a/rich/tui/case.py b/rich/tui/case.py new file mode 100644 index 000000000..c3db63854 --- /dev/null +++ b/rich/tui/case.py @@ -0,0 +1,22 @@ +import re + + +def camel_to_snake(name: str, _re_snake=re.compile("[a-z][A-Z]")) -> str: + """Convert name from CamelCase to snake_case. + + Args: + name (str): A symbol name, such as a class name. + + Returns: + str: Name in camel case. + """ + + def repl(match) -> str: + lower, upper = match.group() + return f"{lower}_{upper.lower()}" + + return _re_snake.sub(repl, name).lower() + + +if __name__ == "__main__": + print(camel_to_snake("HelloWorldEvent")) \ No newline at end of file diff --git a/rich/tui/driver.py b/rich/tui/driver.py new file mode 100644 index 000000000..8d16891ce --- /dev/null +++ b/rich/tui/driver.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +import asyncio +import curses +from functools import partial +from threading import Event, Thread +from typing import Optional, TYPE_CHECKING + +from .events import KeyEvent + +if TYPE_CHECKING: + from .event_pump import EventPump + from ..console import Console + + +class Driver(ABC): + def __init__(self, console: "Console", events: "EventPump") -> None: + self.console = console + self.events = events + + @abstractmethod + def start_application_mode(self): + ... + + @abstractmethod + def stop_application_mode(self): + ... + + +class CursesDriver(Driver): + def __init__(self, console: "Console", events: "EventPump") -> None: + super().__init__(console, events) + self._stdscr = None + self._exit_event = Event() + + self._key_thread: Optional[Thread] = None + + def start_application_mode(self): + self._stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + curses.halfdelay(1) + self._stdscr.keypad(True) + self.console.show_cursor(False) + self._key_thread = Thread( + target=self.run_key_thread, args=(asyncio.get_event_loop(),) + ) + self._key_thread.start() + + def stop_application_mode(self): + self._exit_event.set() + self._key_thread.join() + curses.nocbreak() + self._stdscr.keypad(False) + curses.echo() + curses.endwin() + self.console.show_cursor(True) + + def run_key_thread(self, loop) -> None: + stdscr = self._stdscr + assert stdscr is not None + exit_event = self._exit_event + while not exit_event.is_set(): + code = stdscr.getch() + if code != -1: + loop.call_soon_threadsafe( + partial(self.events.post, KeyEvent(code=code)) + ) diff --git a/rich/tui/event_pump.py b/rich/tui/event_pump.py new file mode 100644 index 000000000..3284a8672 --- /dev/null +++ b/rich/tui/event_pump.py @@ -0,0 +1,78 @@ +from asyncio import PriorityQueue +from functools import total_ordering +from typing import AsyncGenerator, NamedTuple, Optional + +from .events import Event + + +@total_ordering +class QueueItem(NamedTuple): + """An item and meta data on the event queue.""" + + event: Event + priority: int + + def __eq__(self, other: "QueueItem") -> bool: + return self.priority == other.priority + + def __lt__(self, other: "QueueItem") -> bool: + return self.priority < other.priority + + +class EventPump: + def __init__(self) -> None: + self.queue: "PriorityQueue[Optional[QueueItem]]" = PriorityQueue() + self._closing = False + self._closed = False + + @property + def is_closing(self) -> bool: + return self._closing + + @property + def is_closed(self) -> bool: + return self._closed + + def post(self, event: Event, priority: int = 0) -> bool: + """Post an event on the queue. + + If the event pump is closing or closed, the event will not be posted, and the method + will return ``False``. + + Args: + event (Event): An Event object + priority (int, optional): Priority of event (greater priority processed first). Defaults to 0. + + Returns: + bool: Return True if the event was posted, otherwise False. + """ + if self._closing or self._closed: + return False + self.queue.put_nowait(QueueItem(event, priority=-priority)) + return True + + def close(self) -> None: + """Close the event pump after processing remaining events.""" + self._closing = True + self.queue.put_nowait(None) + + async def get(self) -> Optional[Event]: + """Get the next event on the queue, or None if queue is closed. + + Returns: + Optional[Event]: Event object or None. + """ + if self._closed: + return None + queue_item = await self.queue.get() + if queue_item is None: + self._closed = True + return None + return queue_item.event + + async def __aiter__(self) -> AsyncGenerator[Event, None]: + while not self._closed: + event = await self.get() + if event is None: + break + yield event \ No newline at end of file diff --git a/rich/tui/events.py b/rich/tui/events.py new file mode 100644 index 000000000..5b5d2ddfe --- /dev/null +++ b/rich/tui/events.py @@ -0,0 +1,100 @@ +from dataclasses import dataclass, field +import re +from enum import auto, Enum +from time import time +from typing import ClassVar, Optional + +from rich.repr import rich_repr, RichReprResult +from .case import camel_to_snake +from .types import Callback + + +class EventType(Enum): + """Event type enumeration.""" + + LOAD = auto() + STARTUP = auto() + SHUTDOWN_REQUEST = auto + SHUTDOWN = auto() + EXIT = auto() + REFRESH = auto() + TIMER = auto() + INTERVAL = auto() + KEY = auto() + + +class Event: + type: ClassVar[EventType] + + def __init__(self) -> None: + self.time = time() + + def __rich_repr__(self) -> RichReprResult: + return + yield + + @property + def name(self) -> str: + if not hasattr(self, "_name"): + _name = camel_to_snake(self.__class__.__name__) + if _name.endswith("_event"): + _name = _name[:-6] + self._name = _name + return self._name + + def __enter__(self) -> "Event": + return self + + def __exit__(self, exc_type, exc_value, exc_tb) -> Optional[bool]: + if exc_type is not None: + # Log and suppress exception + return True + + def suppress(self, suppress: bool = True) -> None: + self._suppressed = suppress + + +class ShutdownRequestEvent(Event): + type: EventType = EventType.SHUTDOWN_REQUEST + + +class LoadEvent(Event): + type: EventType = EventType.SHUTDOWN_REQUEST + + +class StartupEvent(Event): + type: EventType = EventType.SHUTDOWN_REQUEST + + +class ShutdownEvent(Event): + pass + + +class RefreshEvent(Event): + pass + + +@rich_repr +class KeyEvent(Event): + type: EventType = EventType.KEY + code: int = 0 + + def __init__(self, code: int) -> None: + super().__init__() + self.code = code + + def __rich_repr__(self) -> RichReprResult: + yield "code", self.code + yield "key", self.key + + @property + def key(self) -> str: + return chr(self.code) + + +class TimerEvent(Event): + type: EventType = EventType.TIMER + + +class IntervalEvent(Event): + type: EventType = EventType.INTERVAL diff --git a/rich/tui/types.py b/rich/tui/types.py new file mode 100644 index 000000000..419f1217c --- /dev/null +++ b/rich/tui/types.py @@ -0,0 +1,4 @@ +from typing import Callable + +Callback = Callable[[], None] +# IntervalID = int From ccacd5c1d7f2786f5012620c3247a4ff4047b080 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 2 May 2021 19:34:22 +0100 Subject: [PATCH 02/33] events --- rich/tui/app.py | 8 ++++++- rich/tui/dispatch.py | 0 rich/tui/event_pump.py | 9 ++++++-- rich/tui/events.py | 50 +++++++++++++++++++++++++++++------------- rich/tui/widget.py | 23 +++++++++++++++++++ 5 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 rich/tui/dispatch.py create mode 100644 rich/tui/widget.py diff --git a/rich/tui/app.py b/rich/tui/app.py index 4a55636a2..35c315428 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -3,7 +3,7 @@ from dis import dis import logging import signal -from typing import AsyncGenerator, ClassVar, Iterable, Optional +from typing import AsyncGenerator, ClassVar, Dict, Iterable, Optional from .events import Event, KeyEvent, ShutdownRequestEvent from .. import get_console @@ -11,6 +11,7 @@ from .driver import Driver, CursesDriver from .event_pump import EventPump from .types import Callback +from .widget import Widget log = logging.getLogger("rich") @@ -33,6 +34,8 @@ def __init__(self, console: Console = None, screen: bool = True): self._screen = screen self._events: Optional[EventPump] = None + self._mounts: Dict[Widget, List[Widget]] + @property def events(self) -> EventPump: assert self._events is not None @@ -43,6 +46,9 @@ def on_keyboard_interupt(cls) -> None: if App._active_app is not None: App._active_app.events.post(ShutdownRequestEvent()) + def mount(self, widget: Widget, parent: Optional[Widget] = None) -> None: + pass + async def __aiter__(self) -> AsyncGenerator[Event, None]: loop = asyncio.get_event_loop() self._events = EventPump() diff --git a/rich/tui/dispatch.py b/rich/tui/dispatch.py new file mode 100644 index 000000000..e69de29bb diff --git a/rich/tui/event_pump.py b/rich/tui/event_pump.py index 3284a8672..d30ac5076 100644 --- a/rich/tui/event_pump.py +++ b/rich/tui/event_pump.py @@ -20,7 +20,8 @@ def __lt__(self, other: "QueueItem") -> bool: class EventPump: - def __init__(self) -> None: + def __init__(self, parent: Optional["EventPump"] = None) -> None: + self.parent = parent self.queue: "PriorityQueue[Optional[QueueItem]]" = PriorityQueue() self._closing = False self._closed = False @@ -75,4 +76,8 @@ async def __aiter__(self) -> AsyncGenerator[Event, None]: event = await self.get() if event is None: break - yield event \ No newline at end of file + yield event + if event.is_suppressed: + continue + if event.bubble and self.parent: + self.parent.post(event, priority=10) \ No newline at end of file diff --git a/rich/tui/events.py b/rich/tui/events.py index 5b5d2ddfe..7c68c5991 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -12,9 +12,12 @@ class EventType(Enum): """Event type enumeration.""" + CUSTOM = auto() LOAD = auto() STARTUP = auto() - SHUTDOWN_REQUEST = auto + MOUNT = auto() + UNMOUNT = auto() + SHUTDOWN_REQUEST = auto() SHUTDOWN = auto() EXIT = auto() REFRESH = auto() @@ -25,14 +28,24 @@ class EventType(Enum): class Event: type: ClassVar[EventType] + bubble: bool = False def __init__(self) -> None: self.time = time() + self._suppressed = False def __rich_repr__(self) -> RichReprResult: return yield + def __init_subclass__(cls, type: EventType) -> None: + super().__init_subclass__() + cls.type = type + + @property + def is_suppressed(self) -> bool: + return self._suppressed + @property def name(self) -> str: if not hasattr(self, "_name"): @@ -54,29 +67,36 @@ def suppress(self, suppress: bool = True) -> None: self._suppressed = suppress -class ShutdownRequestEvent(Event): - type: EventType = EventType.SHUTDOWN_REQUEST +class ShutdownRequestEvent(Event, type=EventType.SHUTDOWN_REQUEST): + pass + + +class LoadEvent(Event, type=EventType.SHUTDOWN_REQUEST): + pass + +class StartupEvent(Event, type=EventType.SHUTDOWN_REQUEST): + pass -class LoadEvent(Event): - type: EventType = EventType.SHUTDOWN_REQUEST +class MountEvent(Event, type=EventType.MOUNT): + pass -class StartupEvent(Event): - type: EventType = EventType.SHUTDOWN_REQUEST +class UnmountEvent(Event, type=EventType.UNMOUNT): + pass -class ShutdownEvent(Event): + +class ShutdownEvent(Event, type=EventType.SHUTDOWN): pass -class RefreshEvent(Event): +class RefreshEvent(Event, type=EventType.REFRESH): pass @rich_repr -class KeyEvent(Event): - type: EventType = EventType.KEY +class KeyEvent(Event, type=EventType.KEY): code: int = 0 def __init__(self, code: int) -> None: @@ -92,9 +112,9 @@ def key(self) -> str: return chr(self.code) -class TimerEvent(Event): - type: EventType = EventType.TIMER +class TimerEvent(Event, type=EventType.TIMER): + pass -class IntervalEvent(Event): - type: EventType = EventType.INTERVAL +class IntervalEvent(Event, type=EventType.INTERVAL): + pass diff --git a/rich/tui/widget.py b/rich/tui/widget.py new file mode 100644 index 000000000..b12984ba6 --- /dev/null +++ b/rich/tui/widget.py @@ -0,0 +1,23 @@ +from .app import App +from . import events +from .event_pump import EventPump + + +class Widget: + def __init__(self, app: App) -> None: + self.app = app + self.events = EventPump() + + async def mount(self, widget: "Widget") -> None: + self.events.parent = self.events + widget.events.post(events.MountEvent()) + + async def unmount(self) -> None: + self.events.post(events.UnmountEvent()) + self.events.parent = None + + async def run(self) -> None: + async for event in self.events: + dispatch_function = getattr(self, f"on_{event.name}", None) + if dispatch_function is not None: + await dispatch_function(event) \ No newline at end of file From 7a9e4b7d0de41c9ecb227415a9f3cc9ee5f8d570 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 4 May 2021 12:59:21 +0100 Subject: [PATCH 03/33] event experiements --- examples/e.py | 2 ++ rich/tui/app.py | 15 ++++++----- rich/tui/{event_pump.py => bus.py} | 40 ++++++++++++------------------ rich/tui/driver.py | 6 ++--- rich/tui/events.py | 4 +++ rich/tui/examples/simple.py | 28 +++++++++++++++++++++ rich/tui/widget.py | 31 +++++++++++------------ rich/widgets/__init__.py | 0 rich/widgets/color_changer.py | 18 ++++++++++++++ 9 files changed, 94 insertions(+), 50 deletions(-) create mode 100644 examples/e.py rename rich/tui/{event_pump.py => bus.py} (61%) create mode 100644 rich/tui/examples/simple.py create mode 100644 rich/widgets/__init__.py create mode 100644 rich/widgets/color_changer.py diff --git a/examples/e.py b/examples/e.py new file mode 100644 index 000000000..8f6ba3824 --- /dev/null +++ b/examples/e.py @@ -0,0 +1,2 @@ +a, *_, b = range(100000) +print(len(_)) diff --git a/rich/tui/app.py b/rich/tui/app.py index 35c315428..2e316c29b 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -3,13 +3,13 @@ from dis import dis import logging import signal -from typing import AsyncGenerator, ClassVar, Dict, Iterable, Optional +from typing import AsyncGenerator, ClassVar, Dict, Iterable, List, Optional from .events import Event, KeyEvent, ShutdownRequestEvent from .. import get_console from ..console import Console from .driver import Driver, CursesDriver -from .event_pump import EventPump +from .events import EventBus from .types import Callback from .widget import Widget @@ -32,12 +32,12 @@ class App: def __init__(self, console: Console = None, screen: bool = True): self.console = console or get_console() self._screen = screen - self._events: Optional[EventPump] = None - + self._events: Optional[EventBus] = None + self._widget: Optional[Widget] = None self._mounts: Dict[Widget, List[Widget]] @property - def events(self) -> EventPump: + def events(self) -> EventBus: assert self._events is not None return self._events @@ -46,12 +46,15 @@ def on_keyboard_interupt(cls) -> None: if App._active_app is not None: App._active_app.events.post(ShutdownRequestEvent()) + def mount(self, widget: Widget) -> None: + self._widget = widget + def mount(self, widget: Widget, parent: Optional[Widget] = None) -> None: pass async def __aiter__(self) -> AsyncGenerator[Event, None]: loop = asyncio.get_event_loop() - self._events = EventPump() + self._events = EventBus() driver = CursesDriver(self.console, self.events) driver.start_application_mode() loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt) diff --git a/rich/tui/event_pump.py b/rich/tui/bus.py similarity index 61% rename from rich/tui/event_pump.py rename to rich/tui/bus.py index d30ac5076..4b2183f1b 100644 --- a/rich/tui/event_pump.py +++ b/rich/tui/bus.py @@ -1,28 +1,23 @@ from asyncio import PriorityQueue +from dataclasses import dataclass from functools import total_ordering -from typing import AsyncGenerator, NamedTuple, Optional +from typing import AsyncGenerator, Generic, NamedTuple, Optional, TypeVar -from .events import Event +BusType = TypeVar("BusType") -@total_ordering -class QueueItem(NamedTuple): + +@dataclass(order=True, frozen=True) +class QueueItem(Generic[BusType]): """An item and meta data on the event queue.""" - event: Event + event: BusType priority: int - def __eq__(self, other: "QueueItem") -> bool: - return self.priority == other.priority - - def __lt__(self, other: "QueueItem") -> bool: - return self.priority < other.priority - -class EventPump: - def __init__(self, parent: Optional["EventPump"] = None) -> None: - self.parent = parent - self.queue: "PriorityQueue[Optional[QueueItem]]" = PriorityQueue() +class Bus(Generic[BusType]): + def __init__(self) -> None: + self.queue: "PriorityQueue[Optional[QueueItem[BusType]]]" = PriorityQueue() self._closing = False self._closed = False @@ -34,8 +29,8 @@ def is_closing(self) -> bool: def is_closed(self) -> bool: return self._closed - def post(self, event: Event, priority: int = 0) -> bool: - """Post an event on the queue. + def post(self, event: BusType, priority: int = 0) -> bool: + """Post an item on the bus. If the event pump is closing or closed, the event will not be posted, and the method will return ``False``. @@ -49,7 +44,8 @@ def post(self, event: Event, priority: int = 0) -> bool: """ if self._closing or self._closed: return False - self.queue.put_nowait(QueueItem(event, priority=-priority)) + item: QueueItem[BusType] = QueueItem(event, priority=-priority) + self.queue.put_nowait(item) return True def close(self) -> None: @@ -57,7 +53,7 @@ def close(self) -> None: self._closing = True self.queue.put_nowait(None) - async def get(self) -> Optional[Event]: + async def get(self) -> Optional[BusType]: """Get the next event on the queue, or None if queue is closed. Returns: @@ -71,13 +67,9 @@ async def get(self) -> Optional[Event]: return None return queue_item.event - async def __aiter__(self) -> AsyncGenerator[Event, None]: + async def __aiter__(self) -> AsyncGenerator[BusType, None]: while not self._closed: event = await self.get() if event is None: break yield event - if event.is_suppressed: - continue - if event.bubble and self.parent: - self.parent.post(event, priority=10) \ No newline at end of file diff --git a/rich/tui/driver.py b/rich/tui/driver.py index 8d16891ce..813bb4c40 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -8,12 +8,12 @@ from .events import KeyEvent if TYPE_CHECKING: - from .event_pump import EventPump + from .events import EventBus from ..console import Console class Driver(ABC): - def __init__(self, console: "Console", events: "EventPump") -> None: + def __init__(self, console: "Console", events: "EventBus") -> None: self.console = console self.events = events @@ -27,7 +27,7 @@ def stop_application_mode(self): class CursesDriver(Driver): - def __init__(self, console: "Console", events: "EventPump") -> None: + def __init__(self, console: "Console", events: "EventBus") -> None: super().__init__(console, events) self._stdscr = None self._exit_event = Event() diff --git a/rich/tui/events.py b/rich/tui/events.py index 7c68c5991..dfeb85b5b 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -5,6 +5,7 @@ from typing import ClassVar, Optional from rich.repr import rich_repr, RichReprResult +from .bus import Bus from .case import camel_to_snake from .types import Callback @@ -26,6 +27,9 @@ class EventType(Enum): KEY = auto() +EventBus = Bus["Event"] + + class Event: type: ClassVar[EventType] bubble: bool = False diff --git a/rich/tui/examples/simple.py b/rich/tui/examples/simple.py new file mode 100644 index 000000000..f2c003b9e --- /dev/null +++ b/rich/tui/examples/simple.py @@ -0,0 +1,28 @@ +from rich.layout import Layout +from rich.table import Table +from rich.tui.app import App + +from rich.widgets.color_changer import ColorChanger + + +class SimpleApp(App): + table: Table + + def __init__(self): + super().__init__() + + self.table = table = Table("foo", "bar", "baz") + table.add_row("1", "2", "3") + + def visualize(self): + layout = Layout() + layout.split_column( + Layout(self.table, name="top"), Layout(ColorChanger(), name="bottom") + ) + layout["bottom"].split_row(Layout(name="left"), Layout(name="right")) + return layout + + +if __name__ == "__main__": + app = SimpleApp() + app.run() diff --git a/rich/tui/widget.py b/rich/tui/widget.py index b12984ba6..1ce87eeb2 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -1,23 +1,20 @@ -from .app import App -from . import events -from .event_pump import EventPump - +from typing import TYPE_CHECKING -class Widget: - def __init__(self, app: App) -> None: - self.app = app - self.events = EventPump() +from . import events - async def mount(self, widget: "Widget") -> None: - self.events.parent = self.events - widget.events.post(events.MountEvent()) +if TYPE_CHECKING: + from .app import App - async def unmount(self) -> None: - self.events.post(events.UnmountEvent()) - self.events.parent = None - async def run(self) -> None: - async for event in self.events: +class Widget: + async def run(self, events: events.EventBus) -> None: + async for event in events: dispatch_function = getattr(self, f"on_{event.name}", None) if dispatch_function is not None: - await dispatch_function(event) \ No newline at end of file + await dispatch_function(event) + + def focus(self): + pass + + def blur(self): + pass \ No newline at end of file diff --git a/rich/widgets/__init__.py b/rich/widgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rich/widgets/color_changer.py b/rich/widgets/color_changer.py new file mode 100644 index 000000000..ccf1236b0 --- /dev/null +++ b/rich/widgets/color_changer.py @@ -0,0 +1,18 @@ +from rich.align import Align +from rich.padding import Padding + +from rich.tui.widget import Widget +from rich.tui.events import KeyEvent + + +class ColorChanger(Widget): + def __init__(self) -> None: + self.color = 0 + + async def render(self): + return Align.center( + "Press any key", vertical="middle", style=f"color({self.color})" + ) + + async def on_key(self, event: KeyEvent) -> None: + self.color = ord(event.key) % 255 \ No newline at end of file From a0e58d9d19bb348a4b8c97f92ce10c7a2fcf6a17 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 6 May 2021 12:10:12 +0100 Subject: [PATCH 04/33] actions --- rich/tui/app.py | 4 ++-- rich/tui/bus.py | 47 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/rich/tui/app.py b/rich/tui/app.py index 2e316c29b..b0e9121ae 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -34,7 +34,6 @@ def __init__(self, console: Console = None, screen: bool = True): self._screen = screen self._events: Optional[EventBus] = None self._widget: Optional[Widget] = None - self._mounts: Dict[Widget, List[Widget]] @property def events(self) -> EventBus: @@ -44,9 +43,10 @@ def events(self) -> EventBus: @classmethod def on_keyboard_interupt(cls) -> None: if App._active_app is not None: - App._active_app.events.post(ShutdownRequestEvent()) + App._active_app.events.post_event(ShutdownRequestEvent()) def mount(self, widget: Widget) -> None: + Bus() self._widget = widget def mount(self, widget: Widget, parent: Optional[Widget] = None) -> None: diff --git a/rich/tui/bus.py b/rich/tui/bus.py index 4b2183f1b..b0fbef8fc 100644 --- a/rich/tui/bus.py +++ b/rich/tui/bus.py @@ -1,23 +1,40 @@ -from asyncio import PriorityQueue +from asyncio import Queue, PriorityQueue from dataclasses import dataclass -from functools import total_ordering -from typing import AsyncGenerator, Generic, NamedTuple, Optional, TypeVar +from typing import ( + AsyncGenerator, + Generic, + NamedTuple, + Optional, + Protocol, + TypeVar, + TYPE_CHECKING, +) -BusType = TypeVar("BusType") +from .events import Event +from .actions import Action + +if TYPE_CHECKING: + from .widget import Widget + + +class Actionable: + def post_action(self, sender: Widget, action: Action): + ... @dataclass(order=True, frozen=True) -class QueueItem(Generic[BusType]): +class EventQueueItem: """An item and meta data on the event queue.""" - event: BusType + event: Event priority: int -class Bus(Generic[BusType]): - def __init__(self) -> None: - self.queue: "PriorityQueue[Optional[QueueItem[BusType]]]" = PriorityQueue() +class Bus: + def __init__(self, actions_queue: Queue[Action]) -> None: + self.action_queue: "Queue[Action]" = actions_queue + self.event_queue: "PriorityQueue[Optional[EventQueueItem]]" = PriorityQueue() self._closing = False self._closed = False @@ -29,7 +46,7 @@ def is_closing(self) -> bool: def is_closed(self) -> bool: return self._closed - def post(self, event: BusType, priority: int = 0) -> bool: + def post_event(self, event: Event, priority: int = 0) -> bool: """Post an item on the bus. If the event pump is closing or closed, the event will not be posted, and the method @@ -44,16 +61,16 @@ def post(self, event: BusType, priority: int = 0) -> bool: """ if self._closing or self._closed: return False - item: QueueItem[BusType] = QueueItem(event, priority=-priority) + item = EventQueueItem(event, priority=-priority) self.queue.put_nowait(item) return True def close(self) -> None: """Close the event pump after processing remaining events.""" self._closing = True - self.queue.put_nowait(None) + self.event_queue.put_nowait(None) - async def get(self) -> Optional[BusType]: + async def get(self) -> Optional[Event]: """Get the next event on the queue, or None if queue is closed. Returns: @@ -61,13 +78,13 @@ async def get(self) -> Optional[BusType]: """ if self._closed: return None - queue_item = await self.queue.get() + queue_item = await self.event_queue.get() if queue_item is None: self._closed = True return None return queue_item.event - async def __aiter__(self) -> AsyncGenerator[BusType, None]: + async def __aiter__(self) -> AsyncGenerator[Event, None]: while not self._closed: event = await self.get() if event is None: From f22f8e603165a85d1df86b982629142c934fc706 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 8 May 2021 15:38:24 +0100 Subject: [PATCH 05/33] event loop --- rich/tui/_event_loop.py | 108 ++++++++++++++++++++++++++++++++++++++++ rich/tui/_timer.py | 68 +++++++++++++++++++++++++ rich/tui/actions.py | 34 +++++++++++++ rich/tui/events.py | 26 +++++----- rich/tui/types.py | 10 +++- 5 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 rich/tui/_event_loop.py create mode 100644 rich/tui/_timer.py create mode 100644 rich/tui/actions.py diff --git a/rich/tui/_event_loop.py b/rich/tui/_event_loop.py new file mode 100644 index 000000000..1d3390a72 --- /dev/null +++ b/rich/tui/_event_loop.py @@ -0,0 +1,108 @@ +from typing import AsyncIterable, Optional, TYPE_CHECKING +from asyncio import ensure_future, Task, PriorityQueue +from asyncio import Event as AIOEvent +from dataclasses import dataclass + +from . import events +from ._timer import Timer, TimerCallback + + +@dataclass(order=True, frozen=True) +class EventQueueItem: + event: events.Event + priority: int + + +class EventLoop: + def __init__(self) -> None: + self._event_queue: "PriorityQueue[Optional[EventQueueItem]]" = PriorityQueue() + self._closing: bool = False + self._closed: bool = False + self._done_event = AIOEvent() + + async def get_event(self) -> Optional[events.Event]: + """Get the next event on the queue, or None if queue is closed. + + Returns: + Optional[Event]: Event object or None. + """ + if self._closed: + return None + queue_item = await self._event_queue.get() + if queue_item is None: + self._closed = True + return None + return queue_item.event + + def set_timer( + self, + delay: float, + *, + name: Optional[str] = None, + callback: TimerCallback = None, + ) -> Timer: + timer = Timer(self, delay, name=name, callback=callback, repeat=0) + asyncio.get_event_loop().create_task(timer.run()) + return timer + + def set_interval( + self, + interval: float, + *, + name: Optional[str] = None, + callback: TimerCallback = None, + repeat: int = 0, + ): + timer = Timer( + self, interval, name=name, callback=callback, repeat=repeat or None + ) + asyncio.get_event_loop().create_task(timer.run()) + return timer + + def close_events(self) -> None: + self._closing = True + self._event_queue.put_nowait(None) + + async def wait_for_events(self) -> None: + await self._done_event.wait() + + async def run(self) -> None: + async def stuff(timer: Timer): + print("TIMER", timer) + + self.set_interval(1, callback=stuff, repeat=1) + try: + while not self._closed: + event = await self.get_event() + if event is None: + break + dispatch_function = getattr(self, f"on_{event.name}", None) + if dispatch_function is not None: + await dispatch_function(event) + finally: + self._done_event.set() + + def post_event(self, event: events.Event, priority: Optional[int] = None) -> bool: + if self._closing or self._closed: + return False + event_priority = priority if priority is not None else event.default_priority + item = EventQueueItem(event, priority=event_priority or 0) + self._event_queue.put_nowait(item) + return True + + async def on_timer(self, event: events.TimerEvent) -> None: + if event.callback is not None: + await event.callback(event.timer) + + +if __name__ == "__main__": + + class Widget(EventLoop): + pass + + widget1 = Widget() + widget2 = Widget() + + import asyncio + + asyncio.get_event_loop().run_until_complete(widget1.run()) \ No newline at end of file diff --git a/rich/tui/_timer.py b/rich/tui/_timer.py new file mode 100644 index 000000000..565004ad6 --- /dev/null +++ b/rich/tui/_timer.py @@ -0,0 +1,68 @@ +from time import time +from typing import Optional, Callable + +from asyncio import Event, wait_for, TimeoutError +import weakref + +from .events import TimerEvent +from .types import Callback, EventTarget + + +TimerCallback = Callable[["Timer"], None] + + +class EventTargetGone(Exception): + pass + + +class Timer: + _timer_count: int = 1 + + def __init__( + self, + event_target: EventTarget, + interval: float, + *, + name: Optional[str] = None, + callback: Optional[TimerCallback] = None, + repeat: int = None, + ) -> None: + self._target_repr = repr(event_target) + self._target = weakref.ref(event_target) + self._interval = interval + self.name = f"Timer#{self._timer_count}" if name is None else name + self._callback = callback + self._repeat = repeat + self._stop_event = Event() + self.count = 0 + + def __repr__(self) -> str: + return f"Timer({self._target_repr}, {self._interval}, name={self.name!r}, repeat={self._repeat})" + + @property + def target(self) -> EventTarget: + target = self._target() + if target is None: + raise EventTargetGone() + return target + + def stop(self) -> None: + self._stop_event.set() + + async def run(self) -> None: + self.count = 0 + start = time() + while self._repeat is None or self.count <= self._repeat: + next_timer = start + (self.count * self._interval) + sleep_time = max(0, next_timer - time()) + try: + if await wait_for(self._stop_event.wait(), sleep_time): + break + except TimeoutError: + pass + event = TimerEvent(callback=self._callback, timer=self) + try: + self.target.post_event(event) + except EventTargetGone: + break + self.count += 1 diff --git a/rich/tui/actions.py b/rich/tui/actions.py new file mode 100644 index 000000000..3a567dc24 --- /dev/null +++ b/rich/tui/actions.py @@ -0,0 +1,34 @@ +from time import time +from typing import ClassVar +from enum import auto, Enum + +from .case import camel_to_snake + + +class ActionType(Enum): + CUSTOM = auto() + QUIT = auto() + + +class Action: + type: ClassVar[ActionType] + + def __init__(self) -> None: + self.time = time() + + def __init_subclass__(cls, type: ActionType) -> None: + super().__init_subclass__() + cls.type = type + + @property + def name(self) -> str: + if not hasattr(self, "_name"): + _name = camel_to_snake(self.__class__.__name__) + if _name.endswith("_event"): + _name = _name[:-6] + self._name = _name + return self._name + + +class QuitAction(Action, type=ActionType.QUIT): + pass diff --git a/rich/tui/events.py b/rich/tui/events.py index dfeb85b5b..bd59dd5d0 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -2,14 +2,17 @@ import re from enum import auto, Enum from time import time -from typing import ClassVar, Optional +from typing import ClassVar, Optional, TYPE_CHECKING -from rich.repr import rich_repr, RichReprResult -from .bus import Bus +from ..repr import rich_repr, RichReprResult from .case import camel_to_snake from .types import Callback +if TYPE_CHECKING: + from .timer import Timer, TimerCallback + + class EventType(Enum): """Event type enumeration.""" @@ -27,12 +30,10 @@ class EventType(Enum): KEY = auto() -EventBus = Bus["Event"] - - class Event: type: ClassVar[EventType] bubble: bool = False + default_priority: Optional[int] = None def __init__(self) -> None: self.time = time() @@ -42,9 +43,10 @@ def __rich_repr__(self) -> RichReprResult: return yield - def __init_subclass__(cls, type: EventType) -> None: + def __init_subclass__(cls, type: EventType, priority: Optional[int] = None) -> None: super().__init_subclass__() cls.type = type + cls.default_priority = priority @property def is_suppressed(self) -> bool: @@ -116,9 +118,7 @@ def key(self) -> str: return chr(self.code) -class TimerEvent(Event, type=EventType.TIMER): - pass - - -class IntervalEvent(Event, type=EventType.INTERVAL): - pass +@dataclass +class TimerEvent(Event, type=EventType.TIMER, priority=10): + timer: "Timer" + callback: Optional["TimerCallback"] = None diff --git a/rich/tui/types.py b/rich/tui/types.py index 419f1217c..8e7227351 100644 --- a/rich/tui/types.py +++ b/rich/tui/types.py @@ -1,4 +1,12 @@ -from typing import Callable +from typing import Callable, Protocol, TYPE_CHECKING + +if TYPE_CHECKING: + from .events import Event Callback = Callable[[], None] # IntervalID = int + + +class EventTarget(Protocol): + def post_event(self, event: "Event", priority: int = 0) -> bool: + ... \ No newline at end of file From 117a83433adba17fa1c89dee8fceb97d09bfac70 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 9 May 2021 18:27:37 +0100 Subject: [PATCH 06/33] revamp message system --- rich/tui/_timer.py | 34 ++++---- rich/tui/events.py | 84 ++++++++++++-------- rich/tui/message.py | 15 ++++ rich/tui/{_event_loop.py => message_pump.py} | 74 ++++++++++------- rich/tui/types.py | 8 +- rich/tui/widget.py | 8 +- 6 files changed, 140 insertions(+), 83 deletions(-) create mode 100644 rich/tui/message.py rename rich/tui/{_event_loop.py => message_pump.py} (51%) diff --git a/rich/tui/_timer.py b/rich/tui/_timer.py index 565004ad6..593bdb95d 100644 --- a/rich/tui/_timer.py +++ b/rich/tui/_timer.py @@ -1,14 +1,14 @@ -from time import time +from time import monotonic from typing import Optional, Callable from asyncio import Event, wait_for, TimeoutError import weakref -from .events import TimerEvent -from .types import Callback, EventTarget +from . import events +from .types import Callback, EventTarget, MessageTarget -TimerCallback = Callable[["Timer"], None] +TimerCallback = Callable[[events.Timer], None] class EventTargetGone(Exception): @@ -22,6 +22,7 @@ def __init__( self, event_target: EventTarget, interval: float, + sender: MessageTarget, *, name: Optional[str] = None, callback: Optional[TimerCallback] = None, @@ -30,11 +31,12 @@ def __init__( self._target_repr = repr(event_target) self._target = weakref.ref(event_target) self._interval = interval + self.sender = sender self.name = f"Timer#{self._timer_count}" if name is None else name + self._timer_count += 1 self._callback = callback self._repeat = repeat self._stop_event = Event() - self.count = 0 def __repr__(self) -> str: return f"Timer({self._target_repr}, {self._interval}, name={self.name!r}, repeat={self._repeat})" @@ -50,19 +52,23 @@ def stop(self) -> None: self._stop_event.set() async def run(self) -> None: - self.count = 0 - start = time() - while self._repeat is None or self.count <= self._repeat: - next_timer = start + (self.count * self._interval) - sleep_time = max(0, next_timer - time()) + count = 0 + _repeat = self._repeat + _interval = self._interval + _wait = self._stop_event.wait + start = monotonic() + while _repeat is None or count <= _repeat: + next_timer = start + (count * _interval) try: - if await wait_for(self._stop_event.wait(), sleep_time): + if await wait_for(_wait(), max(0, next_timer - monotonic())): break except TimeoutError: pass - event = TimerEvent(callback=self._callback, timer=self) + event = events.Timer( + self.sender, timer=self, count=count, callback=self._callback + ) try: - self.target.post_event(event) + await self.target.post_message(event) except EventTargetGone: break - self.count += 1 + count += 1 diff --git a/rich/tui/events.py b/rich/tui/events.py index bd59dd5d0..af9d78cf6 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -1,22 +1,22 @@ from dataclasses import dataclass, field import re from enum import auto, Enum -from time import time +from time import monotonic from typing import ClassVar, Optional, TYPE_CHECKING from ..repr import rich_repr, RichReprResult -from .case import camel_to_snake -from .types import Callback + +from .message import Message +from .types import Callback, MessageTarget if TYPE_CHECKING: - from .timer import Timer, TimerCallback + from ._timer import Timer, TimerCallback class EventType(Enum): """Event type enumeration.""" - CUSTOM = auto() LOAD = auto() STARTUP = auto() MOUNT = auto() @@ -26,24 +26,27 @@ class EventType(Enum): EXIT = auto() REFRESH = auto() TIMER = auto() - INTERVAL = auto() + FOCUS = auto() + BLUR = auto() KEY = auto() + CUSTOM = 1000 + -class Event: +class Event(Message): type: ClassVar[EventType] - bubble: bool = False - default_priority: Optional[int] = None - def __init__(self) -> None: - self.time = time() + def __init__(self, sender: MessageTarget) -> None: + super().__init__(sender) + self.sender = sender + self.time = monotonic() self._suppressed = False def __rich_repr__(self) -> RichReprResult: return yield - def __init_subclass__(cls, type: EventType, priority: Optional[int] = None) -> None: + def __init_subclass__(cls, type: EventType, priority: int = 0) -> None: super().__init_subclass__() cls.type = type cls.default_priority = priority @@ -52,15 +55,6 @@ def __init_subclass__(cls, type: EventType, priority: Optional[int] = None) -> N def is_suppressed(self) -> bool: return self._suppressed - @property - def name(self) -> str: - if not hasattr(self, "_name"): - _name = camel_to_snake(self.__class__.__name__) - if _name.endswith("_event"): - _name = _name[:-6] - self._name = _name - return self._name - def __enter__(self) -> "Event": return self @@ -73,40 +67,40 @@ def suppress(self, suppress: bool = True) -> None: self._suppressed = suppress -class ShutdownRequestEvent(Event, type=EventType.SHUTDOWN_REQUEST): +class ShutdownRequest(Event, type=EventType.SHUTDOWN_REQUEST): pass -class LoadEvent(Event, type=EventType.SHUTDOWN_REQUEST): +class Load(Event, type=EventType.SHUTDOWN_REQUEST): pass -class StartupEvent(Event, type=EventType.SHUTDOWN_REQUEST): +class Startup(Event, type=EventType.SHUTDOWN_REQUEST): pass -class MountEvent(Event, type=EventType.MOUNT): +class Mount(Event, type=EventType.MOUNT): pass -class UnmountEvent(Event, type=EventType.UNMOUNT): +class Unmount(Event, type=EventType.UNMOUNT): pass -class ShutdownEvent(Event, type=EventType.SHUTDOWN): +class Shutdown(Event, type=EventType.SHUTDOWN): pass -class RefreshEvent(Event, type=EventType.REFRESH): +class Refresh(Event, type=EventType.REFRESH): pass @rich_repr -class KeyEvent(Event, type=EventType.KEY): +class Key(Event, type=EventType.KEY): code: int = 0 - def __init__(self, code: int) -> None: - super().__init__() + def __init__(self, sender: MessageTarget, code: int) -> None: + super().__init__(sender) self.code = code def __rich_repr__(self) -> RichReprResult: @@ -118,7 +112,27 @@ def key(self) -> str: return chr(self.code) -@dataclass -class TimerEvent(Event, type=EventType.TIMER, priority=10): - timer: "Timer" - callback: Optional["TimerCallback"] = None +@rich_repr +class Timer(Event, type=EventType.TIMER, priority=10): + def __init__( + self, + sender: MessageTarget, + timer: "Timer", + count: int = 0, + callback: Optional["TimerCallback"] = None, + ) -> None: + super().__init__(sender) + self.timer = timer + self.count = count + self.callback = callback + + def __rich_repr__(self) -> RichReprResult: + yield "timer", self.timer + + +class Focus(Event, type=EventType.FOCUS): + pass + + +class Blur(Event, type=EventType.BLUR): + pass \ No newline at end of file diff --git a/rich/tui/message.py b/rich/tui/message.py new file mode 100644 index 000000000..e92e30ed6 --- /dev/null +++ b/rich/tui/message.py @@ -0,0 +1,15 @@ +from .case import camel_to_snake + +from .types import MessageTarget + + +class Message: + """Base class for a message.""" + + sender: MessageTarget + bubble: bool = False + default_priority: int = 0 + + def __init__(self, sender: MessageTarget) -> None: + self.sender = sender + self.name = camel_to_snake(self.__class__.__name__) diff --git a/rich/tui/_event_loop.py b/rich/tui/message_pump.py similarity index 51% rename from rich/tui/_event_loop.py rename to rich/tui/message_pump.py index 1d3390a72..090131250 100644 --- a/rich/tui/_event_loop.py +++ b/rich/tui/message_pump.py @@ -4,23 +4,26 @@ from dataclasses import dataclass from . import events +from .message import Message from ._timer import Timer, TimerCallback @dataclass(order=True, frozen=True) -class EventQueueItem: - event: events.Event +class MessageQueueItem: + message: Message priority: int -class EventLoop: - def __init__(self) -> None: - self._event_queue: "PriorityQueue[Optional[EventQueueItem]]" = PriorityQueue() +class MessagePump: + def __init__(self, queue_size: int = 10) -> None: + self._message_queue: "PriorityQueue[Optional[MessageQueueItem]]" = ( + PriorityQueue(maxsize=queue_size) + ) self._closing: bool = False self._closed: bool = False self._done_event = AIOEvent() - async def get_event(self) -> Optional[events.Event]: + async def get_message(self) -> Optional[Message]: """Get the next event on the queue, or None if queue is closed. Returns: @@ -28,11 +31,11 @@ async def get_event(self) -> Optional[events.Event]: """ if self._closed: return None - queue_item = await self._event_queue.get() + queue_item = await self._message_queue.get() if queue_item is None: self._closed = True return None - return queue_item.event + return queue_item.message def set_timer( self, @@ -54,50 +57,65 @@ def set_interval( repeat: int = 0, ): timer = Timer( - self, interval, name=name, callback=callback, repeat=repeat or None + self, interval, self, name=name, callback=callback, repeat=repeat or None ) asyncio.get_event_loop().create_task(timer.run()) return timer - def close_events(self) -> None: + async def close_messages(self, wait: bool = True) -> None: self._closing = True - self._event_queue.put_nowait(None) - - async def wait_for_events(self) -> None: - await self._done_event.wait() + await self._message_queue.put(None) + if wait: + await self._done_event.wait() async def run(self) -> None: - async def stuff(timer: Timer): - print("TIMER", timer) + from time import time + + start = time() + + async def stuff(event: events.Timer): + print("TIMER", event) + print(time() - start) - self.set_interval(1, callback=stuff, repeat=1) + self.set_interval(0.1, callback=stuff) try: while not self._closed: - event = await self.get_event() - if event is None: + message = await self.get_message() + if message is None: break - dispatch_function = getattr(self, f"on_{event.name}", None) - if dispatch_function is not None: - await dispatch_function(event) + await self.dispatch_message(message) finally: self._done_event.set() - def post_event(self, event: events.Event, priority: Optional[int] = None) -> bool: + async def dispatch_messageage(self, message: Message) -> None: + if isinstance(message, events.Event): + dispatch_function = getattr(self, f"on_{message.name}", None) + if dispatch_function is not None: + await dispatch_function(message) + else: + await self.on_message(message) + + async def on_message(self, message: Message) -> None: + pass + + async def post_message( + self, event: Message, priority: Optional[int] = None + ) -> bool: if self._closing or self._closed: return False event_priority = priority if priority is not None else event.default_priority - item = EventQueueItem(event, priority=event_priority or 0) - self._event_queue.put_nowait(item) + item = MessageQueueItem(event, priority=event_priority) + await self._message_queue.put(item) return True - async def on_timer(self, event: events.TimerEvent) -> None: + async def on_timer(self, event: events.Timer) -> None: if event.callback is not None: - await event.callback(event.timer) + await event.callback(event) if __name__ == "__main__": - class Widget(EventLoop): + class Widget(MessagePump): pass widget1 = Widget() diff --git a/rich/tui/types.py b/rich/tui/types.py index 8e7227351..2717463b7 100644 --- a/rich/tui/types.py +++ b/rich/tui/types.py @@ -2,11 +2,17 @@ if TYPE_CHECKING: from .events import Event + from .message import Message Callback = Callable[[], None] # IntervalID = int +class MessageTarget(Protocol): + async def post_message(self, message: "Message", priority: int = 0) -> bool: + ... + + class EventTarget(Protocol): - def post_event(self, event: "Event", priority: int = 0) -> bool: + async def post_message(self, event: "Event", priority: int = 0) -> bool: ... \ No newline at end of file diff --git a/rich/tui/widget.py b/rich/tui/widget.py index 1ce87eeb2..872b23e57 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -1,17 +1,15 @@ from typing import TYPE_CHECKING from . import events +from .message_pump import MessagePump if TYPE_CHECKING: from .app import App class Widget: - async def run(self, events: events.EventBus) -> None: - async for event in events: - dispatch_function = getattr(self, f"on_{event.name}", None) - if dispatch_function is not None: - await dispatch_function(event) + def __init__(self, app: App) -> None: + self.app = app def focus(self): pass From 9febf2a4a464731cf4ce187cf890c284bc4dcb6e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 10 May 2021 14:38:18 +0100 Subject: [PATCH 07/33] typo --- rich/tui/message_pump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 090131250..cc8693eb8 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -87,7 +87,7 @@ async def stuff(event: events.Timer): finally: self._done_event.set() - async def dispatch_messageage(self, message: Message) -> None: + async def dispatch_message(self, message: Message) -> None: if isinstance(message, events.Event): dispatch_function = getattr(self, f"on_{message.name}", None) if dispatch_function is not None: From c719319d399d8c141384e0115b4fba184f9543a5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 16 May 2021 16:36:42 +0100 Subject: [PATCH 08/33] message pump app --- examples/e.py | 2 - rich/tui/_timer.py | 6 +-- rich/tui/app.py | 95 +++++++++++-------------------------- rich/tui/driver.py | 25 ++++++---- rich/tui/events.py | 5 +- rich/tui/examples/simple.py | 2 +- rich/tui/message_pump.py | 50 ++++++++++--------- 7 files changed, 76 insertions(+), 109 deletions(-) delete mode 100644 examples/e.py diff --git a/examples/e.py b/examples/e.py deleted file mode 100644 index 8f6ba3824..000000000 --- a/examples/e.py +++ /dev/null @@ -1,2 +0,0 @@ -a, *_, b = range(100000) -print(len(_)) diff --git a/rich/tui/_timer.py b/rich/tui/_timer.py index 593bdb95d..6819356a7 100644 --- a/rich/tui/_timer.py +++ b/rich/tui/_timer.py @@ -5,7 +5,7 @@ import weakref from . import events -from .types import Callback, EventTarget, MessageTarget +from .types import MessageTarget TimerCallback = Callable[[events.Timer], None] @@ -20,7 +20,7 @@ class Timer: def __init__( self, - event_target: EventTarget, + event_target: MessageTarget, interval: float, sender: MessageTarget, *, @@ -42,7 +42,7 @@ def __repr__(self) -> str: return f"Timer({self._target_repr}, {self._interval}, name={self.name!r}, repeat={self._repeat})" @property - def target(self) -> EventTarget: + def target(self) -> MessageTarget: target = self._target() if target is None: raise EventTargetGone() diff --git a/rich/tui/app.py b/rich/tui/app.py index b0e9121ae..b9c4b7007 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -1,96 +1,59 @@ import asyncio -from contextlib import contextmanager -from dis import dis +from contextvars import ContextVar import logging import signal -from typing import AsyncGenerator, ClassVar, Dict, Iterable, List, Optional +from typing import AsyncGenerator, ClassVar, Optional -from .events import Event, KeyEvent, ShutdownRequestEvent +from . import events from .. import get_console from ..console import Console from .driver import Driver, CursesDriver -from .events import EventBus +from .message_pump import MessagePump from .types import Callback -from .widget import Widget log = logging.getLogger("rich") -# def signal_handler(sig, frame): -# print("You pressed Ctrl+C!") -# App.on_keyboard_interupt() +active_app: ContextVar["App"] = ContextVar("active_app") -# signal.signal(signal.SIGINT, signal_handler) - - -class App: - - _active_app: ClassVar[Optional["App"]] = None - +class App(MessagePump): def __init__(self, console: Console = None, screen: bool = True): + super().__init__() self.console = console or get_console() self._screen = screen - self._events: Optional[EventBus] = None - self._widget: Optional[Widget] = None - - @property - def events(self) -> EventBus: - assert self._events is not None - return self._events @classmethod - def on_keyboard_interupt(cls) -> None: - if App._active_app is not None: - App._active_app.events.post_event(ShutdownRequestEvent()) + def run(cls, console: Console = None, screen: bool = True): + async def run_app() -> None: + app = cls(console=console, screen=screen) + await app.process_messages() - def mount(self, widget: Widget) -> None: - Bus() - self._widget = widget + asyncio.run(run_app()) - def mount(self, widget: Widget, parent: Optional[Widget] = None) -> None: - pass + def on_keyboard_interupt(self) -> None: + loop = asyncio.get_event_loop() + event = events.ShutdownRequest(sender=self) + asyncio.run_coroutine_threadsafe(self.post_message(event), loop=loop) - async def __aiter__(self) -> AsyncGenerator[Event, None]: + async def process_messages(self) -> None: loop = asyncio.get_event_loop() - self._events = EventBus() - driver = CursesDriver(self.console, self.events) + driver = CursesDriver(self.console, self) driver.start_application_mode() loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt) - App._active_app = self + active_app.set(self) + try: - while True: - event = await self.events.get() - if event is None: - break - yield event + await super().process_messages() + log.debug("Message loop exited") finally: - App._active_app = None loop.remove_signal_handler(signal.SIGINT) driver.stop_application_mode() - def run(self) -> None: - asyncio.run(self._run()) - - async def _run(self) -> None: - async for event in self: - log.debug(event) - dispatch_function = getattr(self, f"on_{event.name}", None) - if dispatch_function is not None: - log.debug(await dispatch_function(event)) - else: - log.debug("No handler for %r", event) - - async def on_shutdown_request(self, event: ShutdownRequestEvent) -> None: + async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: log.debug("%r shutting down", self) - self.events.close() - - # def add_interval(self, delay: float, callback: Callback = None) -> IntervalID: - # pass - - # def add_timer(self, period: float, callback: Callback = None) -> TimerID: - # pass + await self.close_messages() if __name__ == "__main__": @@ -105,9 +68,9 @@ async def on_shutdown_request(self, event: ShutdownRequestEvent) -> None: ) class MyApp(App): - async def on_key(self, event: KeyEvent) -> None: - if event.key == ord("q"): - raise ValueError() + async def on_key(self, event: events.Key) -> None: + log.debug("on_key %r", event) + if event.key == "q": + await self.close_messages() - app = MyApp() - app.run() \ No newline at end of file + MyApp.run() diff --git a/rich/tui/driver.py b/rich/tui/driver.py index 813bb4c40..465df2fe7 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -1,21 +1,25 @@ from abc import ABC, abstractmethod import asyncio +import logging import curses -from functools import partial from threading import Event, Thread from typing import Optional, TYPE_CHECKING -from .events import KeyEvent +from . import events +from .types import MessageTarget if TYPE_CHECKING: - from .events import EventBus + from ..console import Console +log = logging.getLogger("rich") + + class Driver(ABC): - def __init__(self, console: "Console", events: "EventBus") -> None: + def __init__(self, console: "Console", target: "MessageTarget") -> None: self.console = console - self.events = events + self._target = target @abstractmethod def start_application_mode(self): @@ -27,8 +31,8 @@ def stop_application_mode(self): class CursesDriver(Driver): - def __init__(self, console: "Console", events: "EventBus") -> None: - super().__init__(console, events) + def __init__(self, console: "Console", target: "MessageTarget") -> None: + super().__init__(console, target) self._stdscr = None self._exit_event = Event() @@ -62,6 +66,9 @@ def run_key_thread(self, loop) -> None: while not exit_event.is_set(): code = stdscr.getch() if code != -1: - loop.call_soon_threadsafe( - partial(self.events.post, KeyEvent(code=code)) + key_event = events.Key(sender=self._target, code=code) + log.debug("KEY=%r", key_event) + asyncio.run_coroutine_threadsafe( + self._target.post_message(key_event), + loop=loop, ) diff --git a/rich/tui/events.py b/rich/tui/events.py index af9d78cf6..735e81c49 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -11,7 +11,8 @@ if TYPE_CHECKING: - from ._timer import Timer, TimerCallback + from ._timer import Timer as TimerClass + from ._timer import TimerCallback class EventType(Enum): @@ -117,7 +118,7 @@ class Timer(Event, type=EventType.TIMER, priority=10): def __init__( self, sender: MessageTarget, - timer: "Timer", + timer: "TimerClass", count: int = 0, callback: Optional["TimerCallback"] = None, ) -> None: diff --git a/rich/tui/examples/simple.py b/rich/tui/examples/simple.py index f2c003b9e..88a01d345 100644 --- a/rich/tui/examples/simple.py +++ b/rich/tui/examples/simple.py @@ -25,4 +25,4 @@ def visualize(self): if __name__ == "__main__": app = SimpleApp() - app.run() + app.run_message_loop() diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index cc8693eb8..a6aaa1d4c 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -2,11 +2,14 @@ from asyncio import ensure_future, Task, PriorityQueue from asyncio import Event as AIOEvent from dataclasses import dataclass +import logging from . import events from .message import Message from ._timer import Timer, TimerCallback +log = logging.getLogger("rich") + @dataclass(order=True, frozen=True) class MessageQueueItem: @@ -16,12 +19,12 @@ class MessageQueueItem: class MessagePump: def __init__(self, queue_size: int = 10) -> None: + self._queue_size = queue_size self._message_queue: "PriorityQueue[Optional[MessageQueueItem]]" = ( - PriorityQueue(maxsize=queue_size) + PriorityQueue(queue_size) ) self._closing: bool = False self._closed: bool = False - self._done_event = AIOEvent() async def get_message(self) -> Optional[Message]: """Get the next event on the queue, or None if queue is closed. @@ -62,34 +65,29 @@ def set_interval( asyncio.get_event_loop().create_task(timer.run()) return timer - async def close_messages(self, wait: bool = True) -> None: + async def close_messages(self) -> None: self._closing = True await self._message_queue.put(None) - if wait: - await self._done_event.wait() - - async def run(self) -> None: - from time import time - - start = time() - async def stuff(event: events.Timer): - print("TIMER", event) - print(time() - start) + async def process_messages(self) -> None: - self.set_interval(0.1, callback=stuff) - try: - while not self._closed: + while not self._closed: + try: message = await self.get_message() - if message is None: - break - await self.dispatch_message(message) - finally: - self._done_event.set() + except Exception as error: + log.exception("error getting message") + raise + log.debug("message=%r", message) + if message is None: + break + await self.dispatch_message(message) async def dispatch_message(self, message: Message) -> None: if isinstance(message, events.Event): - dispatch_function = getattr(self, f"on_{message.name}", None) + method_name = f"on_{message.name}" + log.debug("method=%s", method_name) + dispatch_function = getattr(self, method_name, None) + log.debug("dispatch=%r", dispatch_function) if dispatch_function is not None: await dispatch_function(message) else: @@ -99,12 +97,12 @@ async def on_message(self, message: Message) -> None: pass async def post_message( - self, event: Message, priority: Optional[int] = None + self, message: Message, priority: Optional[int] = None ) -> bool: if self._closing or self._closed: return False - event_priority = priority if priority is not None else event.default_priority - item = MessageQueueItem(event, priority=event_priority) + event_priority = priority if priority is not None else message.default_priority + item = MessageQueueItem(message, priority=event_priority) await self._message_queue.put(item) return True @@ -123,4 +121,4 @@ class Widget(MessagePump): import asyncio - asyncio.get_event_loop().run_until_complete(widget1.run()) \ No newline at end of file + asyncio.get_event_loop().run_until_complete(widget1.run_message_loop()) \ No newline at end of file From 6fa66dafd577b3677d5ad758c5f96d53d08e770e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 17 May 2021 22:18:08 +0100 Subject: [PATCH 09/33] manager --- rich/tui/app.py | 53 +++++++++++++++++++++++++++++++++++++--- rich/tui/events.py | 19 ++++---------- rich/tui/message.py | 13 ++++++++-- rich/tui/message_pump.py | 41 +++++++++++++++++-------------- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/rich/tui/app.py b/rich/tui/app.py index b9c4b7007..fe3791efd 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -9,7 +9,6 @@ from ..console import Console from .driver import Driver, CursesDriver from .message_pump import MessagePump -from .types import Callback log = logging.getLogger("rich") @@ -44,21 +43,26 @@ async def process_messages(self) -> None: loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt) active_app.set(self) + await self.post_message(events.Startup(sender=self)) + try: await super().process_messages() - log.debug("Message loop exited") finally: loop.remove_signal_handler(signal.SIGINT) driver.stop_application_mode() + async def on_startup(self, event: events.Startup) -> None: + pass + async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: - log.debug("%r shutting down", self) await self.close_messages() if __name__ == "__main__": import asyncio from logging import FileHandler + from rich.layout import Layout + from rich.panel import Panel logging.basicConfig( level="NOTSET", @@ -67,7 +71,50 @@ async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: handlers=[FileHandler("richtui.log")], ) + layout = { + "split": "column", + "children": [ + {"name": "title", "height": 3}, + { + "name": "main", + "children": [ + {"name": "left", "ratio": 1, "visible": False}, + {"name": "right", "ratio": 2}, + ], + }, + {"name": "footer", "height": "1"}, + ], + } + + layout = """ + + + + + + + + + """ + class MyApp(App): + def get_layout(self) -> Layout: + return Layout.from_xml( + """ + + + + + + + + + """ + ) + # layout = Layout() + # layout.split_column(Layout(name="title", height=3), Layout(name="body")) + # return layout + async def on_key(self, event: events.Key) -> None: log.debug("on_key %r", event) if event.key == "q": diff --git a/rich/tui/events.py b/rich/tui/events.py index 735e81c49..ae32733e3 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -40,21 +40,15 @@ class Event(Message): def __init__(self, sender: MessageTarget) -> None: super().__init__(sender) self.sender = sender - self.time = monotonic() - self._suppressed = False def __rich_repr__(self) -> RichReprResult: return yield - def __init_subclass__(cls, type: EventType, priority: int = 0) -> None: - super().__init_subclass__() - cls.type = type - cls.default_priority = priority - - @property - def is_suppressed(self) -> bool: - return self._suppressed + def __init_subclass__( + cls, type: EventType, priority: int = 0, bubble: bool = False + ) -> None: + super().__init_subclass__(priority=priority, bubble=bubble) def __enter__(self) -> "Event": return self @@ -64,9 +58,6 @@ def __exit__(self, exc_type, exc_value, exc_tb) -> Optional[bool]: # Log and suppress exception return True - def suppress(self, suppress: bool = True) -> None: - self._suppressed = suppress - class ShutdownRequest(Event, type=EventType.SHUTDOWN_REQUEST): pass @@ -97,7 +88,7 @@ class Refresh(Event, type=EventType.REFRESH): @rich_repr -class Key(Event, type=EventType.KEY): +class Key(Event, type=EventType.KEY, bubble=True): code: int = 0 def __init__(self, sender: MessageTarget, code: int) -> None: diff --git a/rich/tui/message.py b/rich/tui/message.py index e92e30ed6..9ac6bd9ef 100644 --- a/rich/tui/message.py +++ b/rich/tui/message.py @@ -1,3 +1,6 @@ +from time import monotonic +from typing import ClassVar + from .case import camel_to_snake from .types import MessageTarget @@ -7,9 +10,15 @@ class Message: """Base class for a message.""" sender: MessageTarget - bubble: bool = False - default_priority: int = 0 + bubble: ClassVar[bool] = False + default_priority: ClassVar[int] = 0 def __init__(self, sender: MessageTarget) -> None: self.sender = sender self.name = camel_to_snake(self.__class__.__name__) + self.time = monotonic() + + def __init_subclass__(cls, bubble: bool = False, priority: int = 0) -> None: + super().__init_subclass__() + cls.bubble = bubble + cls.default_priority = priority \ No newline at end of file diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index a6aaa1d4c..239356909 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -1,4 +1,4 @@ -from typing import AsyncIterable, Optional, TYPE_CHECKING +from typing import AsyncIterable, Optional, Tuple, TYPE_CHECKING from asyncio import ensure_future, Task, PriorityQueue from asyncio import Event as AIOEvent from dataclasses import dataclass @@ -17,28 +17,34 @@ class MessageQueueItem: priority: int +class MessagePumpClosed(Exception): + pass + + class MessagePump: - def __init__(self, queue_size: int = 10) -> None: - self._queue_size = queue_size + def __init__( + self, queue_size: int = 10, parent: Optional["MessagePump"] = None + ) -> None: self._message_queue: "PriorityQueue[Optional[MessageQueueItem]]" = ( PriorityQueue(queue_size) ) + self._parent = parent self._closing: bool = False self._closed: bool = False - async def get_message(self) -> Optional[Message]: + async def get_message(self) -> Tuple[Message, int]: """Get the next event on the queue, or None if queue is closed. Returns: Optional[Event]: Event object or None. """ if self._closed: - return None + raise MessagePumpClosed("The message pump is closed") queue_item = await self._message_queue.get() if queue_item is None: self._closed = True - return None - return queue_item.message + raise MessagePumpClosed("The message pump is now closed") + return queue_item.message, queue_item.priority def set_timer( self, @@ -47,7 +53,7 @@ def set_timer( name: Optional[str] = None, callback: TimerCallback = None, ) -> Timer: - timer = Timer(self, delay, name=name, callback=callback, repeat=0) + timer = Timer(self, delay, self, name=name, callback=callback, repeat=0) asyncio.get_event_loop().create_task(timer.run()) return timer @@ -73,25 +79,24 @@ async def process_messages(self) -> None: while not self._closed: try: - message = await self.get_message() - except Exception as error: + message, priority = await self.get_message() + except MessagePumpClosed: log.exception("error getting message") - raise - log.debug("message=%r", message) - if message is None: break - await self.dispatch_message(message) + log.debug("message=%r", message) + await self.dispatch_message(message, priority) - async def dispatch_message(self, message: Message) -> None: + async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]: if isinstance(message, events.Event): method_name = f"on_{message.name}" - log.debug("method=%s", method_name) dispatch_function = getattr(self, method_name, None) - log.debug("dispatch=%r", dispatch_function) if dispatch_function is not None: await dispatch_function(message) + if message.bubble and self._parent: + await self._parent.post_message(message, priority=priority) else: - await self.on_message(message) + return await self.on_message(message) + return False async def on_message(self, message: Message) -> None: pass From 91cd7aec2ad56fb3bce5964bceeb9c136c9cc898 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 18 May 2021 11:21:36 +0100 Subject: [PATCH 10/33] layout def --- rich/tui/app.py | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/rich/tui/app.py b/rich/tui/app.py index fe3791efd..82cefc791 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -2,7 +2,8 @@ from contextvars import ContextVar import logging import signal -from typing import AsyncGenerator, ClassVar, Optional +from typing import Any, Dict + from . import events from .. import get_console @@ -17,6 +18,9 @@ active_app: ContextVar["App"] = ContextVar("active_app") +LayoutDefinition = Dict[str, Any] + + class App(MessagePump): def __init__(self, console: Console = None, screen: bool = True): super().__init__() @@ -51,6 +55,9 @@ async def process_messages(self) -> None: loop.remove_signal_handler(signal.SIGINT) driver.stop_application_mode() + def set_layout(self, layout: LayoutDefinition) -> None: + pass + async def on_startup(self, event: events.Startup) -> None: pass @@ -98,26 +105,29 @@ async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: """ class MyApp(App): - def get_layout(self) -> Layout: - return Layout.from_xml( - """ - - - - - - - - - """ - ) - # layout = Layout() - # layout.split_column(Layout(name="title", height=3), Layout(name="body")) - # return layout - async def on_key(self, event: events.Key) -> None: log.debug("on_key %r", event) if event.key == "q": await self.close_messages() + async def on_startup(self) -> None: + + self.set_layout( + { + "split": "column", + "children": [ + {"name": "header", "height": 3, "mount": TitleBar()}, + { + "name": "main", + "children": [ + {"name": "left", "ratio": 1, "visible": False}, + {"name": "right", "ratio": 2}, + ], + }, + {"name": "footer", "height": 1}, + ], + } + ) + # self.mount(TitleBar(clock=True), slot="header") + MyApp.run() From 4f632ef7b1560c5ce8ec6430863834a10faa4c42 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 May 2021 16:40:16 +0100 Subject: [PATCH 11/33] widgets --- rich/tui/app.py | 38 ++++++++++++++--------------- rich/tui/events.py | 4 ---- rich/tui/manager.py | 6 +++++ rich/tui/message_pump.py | 38 ++++++++++++++++++++--------- rich/tui/types.py | 19 +++++++++++---- rich/tui/widget.py | 12 +++------- rich/tui/widgets/__init__.py | 0 rich/tui/widgets/header.py | 46 ++++++++++++++++++++++++++++++++++++ 8 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 rich/tui/manager.py create mode 100644 rich/tui/widgets/__init__.py create mode 100644 rich/tui/widgets/header.py diff --git a/rich/tui/app.py b/rich/tui/app.py index 82cefc791..ee4b66529 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -110,24 +110,24 @@ async def on_key(self, event: events.Key) -> None: if event.key == "q": await self.close_messages() - async def on_startup(self) -> None: - - self.set_layout( - { - "split": "column", - "children": [ - {"name": "header", "height": 3, "mount": TitleBar()}, - { - "name": "main", - "children": [ - {"name": "left", "ratio": 1, "visible": False}, - {"name": "right", "ratio": 2}, - ], - }, - {"name": "footer", "height": 1}, - ], - } - ) - # self.mount(TitleBar(clock=True), slot="header") + # async def on_startup(self, event: events.Startup) -> None: + + # self.set_layout( + # { + # "split": "column", + # "children": [ + # {"name": "header", "height": 3, "mount": TitleBar()}, + # { + # "name": "main", + # "children": [ + # {"name": "left", "ratio": 1, "visible": False}, + # {"name": "right", "ratio": 2}, + # ], + # }, + # {"name": "footer", "height": 1}, + # ], + # } + # ) + # self.mount(TitleBar(clock=True), slot="header") MyApp.run() diff --git a/rich/tui/events.py b/rich/tui/events.py index ae32733e3..86e963299 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -37,10 +37,6 @@ class EventType(Enum): class Event(Message): type: ClassVar[EventType] - def __init__(self, sender: MessageTarget) -> None: - super().__init__(sender) - self.sender = sender - def __rich_repr__(self) -> RichReprResult: return yield diff --git a/rich/tui/manager.py b/rich/tui/manager.py new file mode 100644 index 000000000..cd4173b85 --- /dev/null +++ b/rich/tui/manager.py @@ -0,0 +1,6 @@ +from rich.layout import Layout + + +class LayoutManager: + def __init__(self) -> None: + self.layout = Layout() \ No newline at end of file diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 239356909..5ba38f0b3 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -1,20 +1,30 @@ -from typing import AsyncIterable, Optional, Tuple, TYPE_CHECKING +from functools import total_ordering +from typing import AsyncIterable, Optional, NamedTuple, Tuple, TYPE_CHECKING +import asyncio from asyncio import ensure_future, Task, PriorityQueue from asyncio import Event as AIOEvent from dataclasses import dataclass +from functools import total_ordering import logging from . import events from .message import Message from ._timer import Timer, TimerCallback +from .types import MessageTarget, MessageHandler log = logging.getLogger("rich") -@dataclass(order=True, frozen=True) -class MessageQueueItem: - message: Message +@total_ordering +class MessageQueueItem(NamedTuple): priority: int + message: Message + + def __gt__(self, other: "MessageQueueItem") -> bool: + return self.priority > other.priority + + def __eq__(self, other: "MessageQueueItem") -> bool: + return self.priority == other.priority class MessagePumpClosed(Exception): @@ -32,7 +42,7 @@ def __init__( self._closing: bool = False self._closed: bool = False - async def get_message(self) -> Tuple[Message, int]: + async def get_message(self) -> MessageQueueItem: """Get the next event on the queue, or None if queue is closed. Returns: @@ -44,7 +54,7 @@ async def get_message(self) -> Tuple[Message, int]: if queue_item is None: self._closed = True raise MessagePumpClosed("The message pump is now closed") - return queue_item.message, queue_item.priority + return queue_item def set_timer( self, @@ -79,7 +89,7 @@ async def process_messages(self) -> None: while not self._closed: try: - message, priority = await self.get_message() + priority, message = await self.get_message() except MessagePumpClosed: log.exception("error getting message") break @@ -89,11 +99,11 @@ async def process_messages(self) -> None: async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]: if isinstance(message, events.Event): method_name = f"on_{message.name}" - dispatch_function = getattr(self, method_name, None) + dispatch_function: MessageHandler = getattr(self, method_name, None) if dispatch_function is not None: await dispatch_function(message) if message.bubble and self._parent: - await self._parent.post_message(message, priority=priority) + await self._parent.post_message(message, priority) else: return await self.on_message(message) return False @@ -102,15 +112,21 @@ async def on_message(self, message: Message) -> None: pass async def post_message( - self, message: Message, priority: Optional[int] = None + self, + message: Message, + priority: Optional[int] = None, ) -> bool: if self._closing or self._closed: return False event_priority = priority if priority is not None else message.default_priority - item = MessageQueueItem(message, priority=event_priority) + item = MessageQueueItem(event_priority, message) await self._message_queue.put(item) return True + async def emit(self, message: Message, priority: Optional[int] = None) -> None: + if self._parent: + await self._parent.post_message(message, priority=priority) + async def on_timer(self, event: events.Timer) -> None: if event.callback is not None: await event.callback(event) diff --git a/rich/tui/types.py b/rich/tui/types.py index 2717463b7..94359cfd9 100644 --- a/rich/tui/types.py +++ b/rich/tui/types.py @@ -1,4 +1,4 @@ -from typing import Callable, Protocol, TYPE_CHECKING +from typing import Awaitable, Callable, Optional, Protocol, TYPE_CHECKING if TYPE_CHECKING: from .events import Event @@ -9,10 +9,21 @@ class MessageTarget(Protocol): - async def post_message(self, message: "Message", priority: int = 0) -> bool: + async def post_message( + self, + message: "Message", + priority: Optional[int] = None, + ) -> bool: ... class EventTarget(Protocol): - async def post_message(self, event: "Event", priority: int = 0) -> bool: - ... \ No newline at end of file + async def post_message( + self, + message: "Message", + priority: Optional[int] = None, + ) -> bool: + ... + + +MessageHandler = Callable[["Message"], Awaitable] \ No newline at end of file diff --git a/rich/tui/widget.py b/rich/tui/widget.py index 872b23e57..762285a90 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -7,12 +7,6 @@ from .app import App -class Widget: - def __init__(self, app: App) -> None: - self.app = app - - def focus(self): - pass - - def blur(self): - pass \ No newline at end of file +class Widget(MessagePump): + async def refresh(self) -> None: + await self.emit(events.Refresh(self)) \ No newline at end of file diff --git a/rich/tui/widgets/__init__.py b/rich/tui/widgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rich/tui/widgets/header.py b/rich/tui/widgets/header.py new file mode 100644 index 000000000..1c9a703a4 --- /dev/null +++ b/rich/tui/widgets/header.py @@ -0,0 +1,46 @@ +from datetime import datetime + +from rich.console import RenderableType +from rich.panel import Panel +from rich.style import StyleType +from rich.table import Table +from rich.text import TextType + +from .. import events +from ..widget import Widget + + +class Header(Widget): + def __init__( + self, + title: TextType, + *, + panel: bool = True, + style: StyleType = "white on blue", + clock: bool = True + ) -> None: + self.title = title + self.panel = panel + self.style = style + self.clock = clock + + def get_clock(self) -> str: + return datetime.now().ctime() + + def __rich__(self) -> RenderableType: + header_table = Table.grid() + header_table.style = self.style + header_table.add_column("title", justify="center") + if self.clock: + header_table.add_column("clock", justify="right") + header_table.add_row(self.title, self.get_clock()) + else: + header_table.add_row(self.title) + if self.panel: + header = Panel(header_table, style=self.style) + else: + header = header_table + return header + + async def on_mount(self, event: events.Mount) -> None: + self.set_interval(1.0, callback=self.refresh) \ No newline at end of file From 9d97507b7de76a9e87c9c92bd75ccc1e49a85a43 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 22 May 2021 17:06:43 +0100 Subject: [PATCH 12/33] view and widgets --- rich/cells.py | 1 + rich/repr.py | 9 ++-- rich/screen.py | 18 +++++-- rich/tui/_context.py | 3 ++ rich/tui/_timer.py | 9 +++- rich/tui/app.py | 96 ++++++++++++++++---------------------- rich/tui/driver.py | 1 + rich/tui/events.py | 6 +++ rich/tui/message_pump.py | 38 +++++++++++---- rich/tui/view.py | 63 +++++++++++++++++++++++++ rich/tui/widget.py | 61 +++++++++++++++++++++++- rich/tui/widgets/header.py | 17 +++++-- 12 files changed, 242 insertions(+), 80 deletions(-) create mode 100644 rich/tui/_context.py create mode 100644 rich/tui/view.py diff --git a/rich/cells.py b/rich/cells.py index 1a0ebccec..38507c0eb 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -25,6 +25,7 @@ def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int: return total_size +@lru_cache(maxsize=4096) def get_character_cell_size(character: str) -> int: """Get the cell size of a character. diff --git a/rich/repr.py b/rich/repr.py index 157090f72..988b42958 100644 --- a/rich/repr.py +++ b/rich/repr.py @@ -19,9 +19,12 @@ def auto_repr(self: Any) -> str: append(repr(arg[0])) else: key, value, *default = arg - if len(default) and default[0] == value: - continue - append(f"{key}={value!r}") + if key is None: + append(value) + else: + if len(default) and default[0] == value: + continue + append(f"{key}={value!r}") else: append(repr(arg)) return f"{self.__class__.__name__}({', '.join(repr_str)})" diff --git a/rich/screen.py b/rich/screen.py index 189fdc3de..dfbff7f3d 100644 --- a/rich/screen.py +++ b/rich/screen.py @@ -7,7 +7,13 @@ if TYPE_CHECKING: - from .console import Console, ConsoleOptions, RenderResult, RenderableType + from .console import ( + Console, + ConsoleOptions, + RenderResult, + RenderableType, + RenderGroup, + ) class Screen: @@ -20,11 +26,15 @@ class Screen: def __init__( self, - renderable: Optional["RenderableType"] = None, + *renderables: "RenderableType", style: Optional[StyleType] = None, + application_mode: bool = False, ) -> None: - self.renderable = renderable + from rich.console import RenderGroup + + self.renderable = RenderGroup(*renderables) self.style = style + self.application_mode = application_mode def __rich_console__( self, console: "Console", options: "ConsoleOptions" @@ -36,7 +46,7 @@ def __rich_console__( self.renderable or "", render_options, style=style, pad=True ) lines = Segment.set_shape(lines, width, height, style=style) - new_line = Segment.line() + new_line = Segment("\n\r") if self.application_mode else Segment.line() for last, line in loop_last(lines): yield from line if not last: diff --git a/rich/tui/_context.py b/rich/tui/_context.py new file mode 100644 index 000000000..cd8ee5db1 --- /dev/null +++ b/rich/tui/_context.py @@ -0,0 +1,3 @@ +from contextvars import ContextVar + +active_app = ContextVar("active_app") diff --git a/rich/tui/_timer.py b/rich/tui/_timer.py index 6819356a7..f089de76f 100644 --- a/rich/tui/_timer.py +++ b/rich/tui/_timer.py @@ -4,6 +4,8 @@ from asyncio import Event, wait_for, TimeoutError import weakref +from rich.repr import rich_repr, RichReprResult + from . import events from .types import MessageTarget @@ -15,6 +17,7 @@ class EventTargetGone(Exception): pass +@rich_repr class Timer: _timer_count: int = 1 @@ -38,8 +41,10 @@ def __init__( self._repeat = repeat self._stop_event = Event() - def __repr__(self) -> str: - return f"Timer({self._target_repr}, {self._interval}, name={self.name!r}, repeat={self._repeat})" + def __rich_repr__(self) -> RichReprResult: + yield self._interval + yield "name", self.name + yield "repeat", self._repeat, None @property def target(self) -> MessageTarget: diff --git a/rich/tui/app.py b/rich/tui/app.py index ee4b66529..ddeeeeb92 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -1,31 +1,43 @@ import asyncio -from contextvars import ContextVar + import logging import signal -from typing import Any, Dict +from typing import Any, Dict, Set +from rich.live import Live +from rich.control import Control +from rich.screen import Screen from . import events +from ._context import active_app from .. import get_console from ..console import Console from .driver import Driver, CursesDriver from .message_pump import MessagePump - +from .view import View, LayoutView log = logging.getLogger("rich") -active_app: ContextVar["App"] = ContextVar("active_app") - - LayoutDefinition = Dict[str, Any] class App(MessagePump): - def __init__(self, console: Console = None, screen: bool = True): + def __init__( + self, + console: Console = None, + view: View = None, + screen: bool = True, + auto_refresh=4, + title: str = "Megasoma Application", + ): super().__init__() self.console = console or get_console() self._screen = screen + self._auto_refresh = auto_refresh + self.title = title + self.view = view or LayoutView() + self.children: Set[MessagePump] = set() @classmethod def run(cls, console: Console = None, screen: bool = True): @@ -47,16 +59,30 @@ async def process_messages(self) -> None: loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt) active_app.set(self) - await self.post_message(events.Startup(sender=self)) + await self.add(self.view) + await self.post_message(events.Startup(sender=self)) + self.refresh() try: await super().process_messages() finally: loop.remove_signal_handler(signal.SIGINT) driver.stop_application_mode() - def set_layout(self, layout: LayoutDefinition) -> None: - pass + await asyncio.gather(*(child.close_messages() for child in self.children)) + self.children.clear() + + async def add(self, child: MessagePump) -> None: + self.children.add(child) + asyncio.create_task(child.process_messages()) + await child.post_message(events.Created(sender=self)) + + def refresh(self) -> None: + console = self.console + with console: + console.print( + Screen(Control.home(), self.view, Control.home(), application_mode=True) + ) async def on_startup(self, event: events.Startup) -> None: pass @@ -71,6 +97,8 @@ async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: from rich.layout import Layout from rich.panel import Panel + from .widgets.header import Header + logging.basicConfig( level="NOTSET", format="%(message)s", @@ -78,56 +106,14 @@ async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: handlers=[FileHandler("richtui.log")], ) - layout = { - "split": "column", - "children": [ - {"name": "title", "height": 3}, - { - "name": "main", - "children": [ - {"name": "left", "ratio": 1, "visible": False}, - {"name": "right", "ratio": 2}, - ], - }, - {"name": "footer", "height": "1"}, - ], - } - - layout = """ - - - - - - - - - """ - class MyApp(App): async def on_key(self, event: events.Key) -> None: log.debug("on_key %r", event) if event.key == "q": await self.close_messages() - # async def on_startup(self, event: events.Startup) -> None: - - # self.set_layout( - # { - # "split": "column", - # "children": [ - # {"name": "header", "height": 3, "mount": TitleBar()}, - # { - # "name": "main", - # "children": [ - # {"name": "left", "ratio": 1, "visible": False}, - # {"name": "right", "ratio": 2}, - # ], - # }, - # {"name": "footer", "height": 1}, - # ], - # } - # ) - # self.mount(TitleBar(clock=True), slot="header") + async def on_startup(self, event: events.Startup) -> None: + await self.view.mount(Header(self.title), slot="header") + self.refresh() MyApp.run() diff --git a/rich/tui/driver.py b/rich/tui/driver.py index 465df2fe7..9003ea462 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -43,6 +43,7 @@ def start_application_mode(self): curses.noecho() curses.cbreak() curses.halfdelay(1) + self._stdscr.keypad(True) self.console.show_cursor(False) self._key_thread = Thread( diff --git a/rich/tui/events.py b/rich/tui/events.py index 86e963299..53ee2343f 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -20,6 +20,7 @@ class EventType(Enum): LOAD = auto() STARTUP = auto() + CREATED = auto() MOUNT = auto() UNMOUNT = auto() SHUTDOWN_REQUEST = auto() @@ -34,6 +35,7 @@ class EventType(Enum): CUSTOM = 1000 +@rich_repr class Event(Message): type: ClassVar[EventType] @@ -67,6 +69,10 @@ class Startup(Event, type=EventType.SHUTDOWN_REQUEST): pass +class Created(Event, type=EventType.CREATED): + pass + + class Mount(Event, type=EventType.MOUNT): pass diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 5ba38f0b3..11971de91 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -1,31 +1,41 @@ from functools import total_ordering from typing import AsyncIterable, Optional, NamedTuple, Tuple, TYPE_CHECKING import asyncio -from asyncio import ensure_future, Task, PriorityQueue -from asyncio import Event as AIOEvent -from dataclasses import dataclass +from asyncio import PriorityQueue + from functools import total_ordering import logging from . import events from .message import Message from ._timer import Timer, TimerCallback -from .types import MessageTarget, MessageHandler +from .types import MessageHandler log = logging.getLogger("rich") -@total_ordering class MessageQueueItem(NamedTuple): priority: int message: Message + def __lt__(self, other: "MessageQueueItem") -> bool: + return self.priority < other.priority + + def __le__(self, other: "MessageQueueItem") -> bool: + return self.priority <= other.priority + def __gt__(self, other: "MessageQueueItem") -> bool: return self.priority > other.priority + def __ge__(self, other: "MessageQueueItem") -> bool: + return self.priority >= other.priority + def __eq__(self, other: "MessageQueueItem") -> bool: return self.priority == other.priority + def __ne__(self, other: "MessageQueueItem") -> bool: + return self.priority != other.priority + class MessagePumpClosed(Exception): pass @@ -86,11 +96,13 @@ async def close_messages(self) -> None: await self._message_queue.put(None) async def process_messages(self) -> None: - + """Process messages until the queue is closed.""" while not self._closed: try: priority, message = await self.get_message() except MessagePumpClosed: + break + except Exception: log.exception("error getting message") break log.debug("message=%r", message) @@ -123,13 +135,21 @@ async def post_message( await self._message_queue.put(item) return True - async def emit(self, message: Message, priority: Optional[int] = None) -> None: + async def post_message_from_child( + self, message: Message, priority: Optional[int] = None + ) -> None: + await self.post_message(message, priority=priority) + + async def emit(self, message: Message, priority: Optional[int] = None) -> bool: if self._parent: - await self._parent.post_message(message, priority=priority) + await self._parent.post_message_from_child(message, priority=priority) + return True + else: + return False async def on_timer(self, event: events.Timer) -> None: if event.callback is not None: - await event.callback(event) + await event.callback() if __name__ == "__main__": diff --git a/rich/tui/view.py b/rich/tui/view.py new file mode 100644 index 000000000..2b7cbc496 --- /dev/null +++ b/rich/tui/view.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from rich.console import Console, RenderableType +from rich.layout import Layout +from rich.live import Live + +from . import events +from ._context import active_app +from .message_pump import MessagePump +from .widget import Widget +from .widgets.header import Header + +if TYPE_CHECKING: + from .app import App + + +class View(MessagePump): + pass + + +class LayoutView(View): + layout: Layout + + def __init__( + self, layout: Layout = None, title: str = "Layout Application" + ) -> None: + self.title = title + if layout is None: + layout = Layout() + layout.split_column( + Layout(name="header", size=3, ratio=0), + Layout(name="main", ratio=1), + Layout(name="footer", size=1, ratio=0), + ) + layout["main"].split_row( + Layout(name="left", size=30, visible=True), + Layout(name="body", ratio=1), + Layout(name="right", size=30, visible=False), + ) + self.layout = layout + super().__init__() + + def __rich__(self) -> RenderableType: + return self.layout + + @property + def app(self) -> "App": + return active_app.get() + + @property + def console(self) -> Console: + return active_app.get().console + + async def on_create(self, event: events.Created) -> None: + await self.mount(Header(self.title)) + + async def mount(self, widget: Widget, *, slot: str = "main") -> None: + self.layout[slot].update(widget) + await self.app.add(widget) + await widget.post_message(events.Mount(sender=self)) + + async def on_startup(self, event: events.Startup) -> None: + await self.mount(Header(self.title), slot="header") \ No newline at end of file diff --git a/rich/tui/widget.py b/rich/tui/widget.py index 762285a90..28a7ff48c 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -1,12 +1,69 @@ -from typing import TYPE_CHECKING +from rich.align import Align +from rich.console import Console, ConsoleOptions, RenderResult, RenderableType +from rich.pretty import Pretty +from rich.panel import Panel +from rich.repr import rich_repr, RichReprResult + +from typing import ClassVar, NamedTuple, Optional, TYPE_CHECKING from . import events +from ._context import active_app from .message_pump import MessagePump if TYPE_CHECKING: from .app import App +class WidgetDimensions(NamedTuple): + width: int + height: int + + +class WidgetPlaceholder: + def __init__(self, widget: "Widget") -> None: + self.widget = widget + + def __rich__(self) -> Panel: + return Panel( + Align.center(Pretty(self.widget), vertical="middle"), title="Widget" + ) + + +@rich_repr class Widget(MessagePump): + _count: ClassVar[int] = 0 + + def __init__(self, name: Optional[str] = None) -> None: + self.name = name or f"Widget#{self._count}" + Widget._count += 1 + self.size = WidgetDimensions(0, 0) + self.size_changed = False + super().__init__() + + @property + def app(self) -> "App": + return active_app.get() + + @property + def console(self) -> Console: + return active_app.get().console + async def refresh(self) -> None: - await self.emit(events.Refresh(self)) \ No newline at end of file + self.app.refresh() + # await self.emit(events.Refresh(self)) + + def __rich_repr__(self) -> RichReprResult: + yield "name", self.name + + def render( + self, console: Console, options: ConsoleOptions, new_size: WidgetDimensions + ) -> RenderableType: + return WidgetPlaceholder(self) + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + new_size = WidgetDimensions(options.max_width, options.height or console.height) + renderable = self.render(console, options, new_size) + self.size = new_size + yield renderable \ No newline at end of file diff --git a/rich/tui/widgets/header.py b/rich/tui/widgets/header.py index 1c9a703a4..691a88067 100644 --- a/rich/tui/widgets/header.py +++ b/rich/tui/widgets/header.py @@ -2,6 +2,7 @@ from rich.console import RenderableType from rich.panel import Panel +from rich.repr import rich_repr, RichReprResult from rich.style import StyleType from rich.table import Table from rich.text import TextType @@ -10,6 +11,7 @@ from ..widget import Widget +@rich_repr class Header(Widget): def __init__( self, @@ -23,19 +25,24 @@ def __init__( self.panel = panel self.style = style self.clock = clock + super().__init__() + + def __rich_repr__(self) -> RichReprResult: + yield self.title def get_clock(self) -> str: - return datetime.now().ctime() + return datetime.now().time().strftime("%X") def __rich__(self) -> RenderableType: - header_table = Table.grid() + header_table = Table.grid(padding=(0, 1), expand=True) header_table.style = self.style - header_table.add_column("title", justify="center") + header_table.add_column(justify="left", ratio=0) + header_table.add_column("title", justify="center", ratio=1) if self.clock: header_table.add_column("clock", justify="right") - header_table.add_row(self.title, self.get_clock()) + header_table.add_row("🐞", self.title, self.get_clock()) else: - header_table.add_row(self.title) + header_table.add_row("🐞", self.title) if self.panel: header = Panel(header_table, style=self.style) else: From 068cb86555d4c38fe53f082ec2cabe7181d34b17 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 23 May 2021 16:44:40 +0100 Subject: [PATCH 13/33] terminal resize events --- rich/console.py | 12 ++++++++++ rich/tui/_context.py | 7 +++++- rich/tui/_timer.py | 4 ++-- rich/tui/app.py | 5 ++++ rich/tui/driver.py | 47 +++++++++++++++++++++++++++++++++++--- rich/tui/events.py | 15 ++++++++++++ rich/tui/message.py | 12 +++++++++- rich/tui/message_pump.py | 2 +- rich/tui/view.py | 19 +++++++-------- rich/tui/widgets/header.py | 1 - 10 files changed, 106 insertions(+), 18 deletions(-) diff --git a/rich/console.py b/rich/console.py index 08dc9108a..56de7719a 100644 --- a/rich/console.py +++ b/rich/console.py @@ -26,6 +26,7 @@ Optional, TextIO, Type, + Tuple, Union, cast, ) @@ -943,6 +944,17 @@ def size(self) -> ConsoleDimensions: height if self._height is None else self._height, ) + @size.setter + def size(self, new_size: Tuple[int, int]) -> None: + """Set a new size for the terminal. + + Args: + new_size (Tuple[int, int]): New width and height. + """ + width, height = new_size + self._width = width + self._height = height + @property def width(self) -> int: """Get the width of the console. diff --git a/rich/tui/_context.py b/rich/tui/_context.py index cd8ee5db1..e16817631 100644 --- a/rich/tui/_context.py +++ b/rich/tui/_context.py @@ -1,3 +1,8 @@ +from typing import TYPE_CHECKING + from contextvars import ContextVar -active_app = ContextVar("active_app") +if TYPE_CHECKING: + from .app import App + +active_app: ContextVar["App"] = ContextVar("active_app") diff --git a/rich/tui/_timer.py b/rich/tui/_timer.py index f089de76f..1fdeddd07 100644 --- a/rich/tui/_timer.py +++ b/rich/tui/_timer.py @@ -1,5 +1,5 @@ from time import monotonic -from typing import Optional, Callable +from typing import Awaitable, Optional, Callable from asyncio import Event, wait_for, TimeoutError import weakref @@ -10,7 +10,7 @@ from .types import MessageTarget -TimerCallback = Callable[[events.Timer], None] +TimerCallback = Callable[[], Awaitable[None]] class EventTargetGone(Exception): diff --git a/rich/tui/app.py b/rich/tui/app.py index ddeeeeb92..207bde67c 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -90,6 +90,11 @@ async def on_startup(self, event: events.Startup) -> None: async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: await self.close_messages() + async def on_resize(self, event: events.Resize) -> None: + await self.view.post_message(event) + if not event.suppressed: + self.refresh() + if __name__ == "__main__": import asyncio diff --git a/rich/tui/driver.py b/rich/tui/driver.py index 9003ea462..cf93ec8b1 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -1,20 +1,26 @@ from abc import ABC, abstractmethod import asyncio import logging +import os +import signal import curses +import platform +import sys +import shutil from threading import Event, Thread -from typing import Optional, TYPE_CHECKING +from typing import Optional, Tuple, TYPE_CHECKING from . import events from .types import MessageTarget if TYPE_CHECKING: - from ..console import Console log = logging.getLogger("rich") +WINDOWS = platform.system() == "Windows" + class Driver(ABC): def __init__(self, console: "Console", target: "MessageTarget") -> None: @@ -35,10 +41,39 @@ def __init__(self, console: "Console", target: "MessageTarget") -> None: super().__init__(console, target) self._stdscr = None self._exit_event = Event() - self._key_thread: Optional[Thread] = None + def _get_terminal_size(self) -> Tuple[int, int]: + width: Optional[int] = 80 + height: Optional[int] = 25 + if WINDOWS: # pragma: no cover + width, height = shutil.get_terminal_size() + else: + try: + width, height = os.get_terminal_size(sys.stdin.fileno()) + except (AttributeError, ValueError, OSError): + try: + width, height = os.get_terminal_size(sys.stdout.fileno()) + except (AttributeError, ValueError, OSError): + pass + width = width or 80 + height = height or 25 + return width, height + def start_application_mode(self): + loop = asyncio.get_event_loop() + + def on_terminal_resize(signum, stack) -> None: + terminal_size = self.console.size = self._get_terminal_size() + width, height = terminal_size + event = events.Resize(self._target, width, height) + self.console.size = terminal_size + asyncio.run_coroutine_threadsafe( + self._target.post_message(event), + loop=loop, + ) + + signal.signal(signal.SIGWINCH, on_terminal_resize) self._stdscr = curses.initscr() curses.noecho() curses.cbreak() @@ -49,9 +84,15 @@ def start_application_mode(self): self._key_thread = Thread( target=self.run_key_thread, args=(asyncio.get_event_loop(),) ) + + self.console.size = self._get_terminal_size() + self._key_thread.start() def stop_application_mode(self): + + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + self._exit_event.set() self._key_thread.join() curses.nocbreak() diff --git a/rich/tui/events.py b/rich/tui/events.py index 53ee2343f..fd9e7ccc1 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -21,6 +21,7 @@ class EventType(Enum): LOAD = auto() STARTUP = auto() CREATED = auto() + RESIZE = auto() MOUNT = auto() UNMOUNT = auto() SHUTDOWN_REQUEST = auto() @@ -73,6 +74,20 @@ class Created(Event, type=EventType.CREATED): pass +class Resize(Event, type=EventType.RESIZE): + width: int + height: int + + def __init__(self, sender: MessageTarget, width: int, height: int) -> None: + self.width = width + self.height = height + super().__init__(sender) + + def __rich_repr__(self) -> RichReprResult: + yield self.width + yield self.height + + class Mount(Event, type=EventType.MOUNT): pass diff --git a/rich/tui/message.py b/rich/tui/message.py index 9ac6bd9ef..64eeed715 100644 --- a/rich/tui/message.py +++ b/rich/tui/message.py @@ -12,6 +12,7 @@ class Message: sender: MessageTarget bubble: ClassVar[bool] = False default_priority: ClassVar[int] = 0 + suppressed: bool = False def __init__(self, sender: MessageTarget) -> None: self.sender = sender @@ -21,4 +22,13 @@ def __init__(self, sender: MessageTarget) -> None: def __init_subclass__(cls, bubble: bool = False, priority: int = 0) -> None: super().__init_subclass__() cls.bubble = bubble - cls.default_priority = priority \ No newline at end of file + cls.default_priority = priority + + def suppress_default(self, suppress: bool = True) -> None: + """Suppress the default action. + + Args: + suppress (bool, optional): True if the default action should be suppressed, + or False if the default actions should be performed. Defaults to True. + """ + self.suppress = suppress \ No newline at end of file diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 11971de91..90ccc1cc5 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -105,7 +105,7 @@ async def process_messages(self) -> None: except Exception: log.exception("error getting message") break - log.debug("message=%r", message) + log.debug(repr(message)) await self.dispatch_message(message, priority) async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]: diff --git a/rich/tui/view.py b/rich/tui/view.py index 2b7cbc496..807e5bc63 100644 --- a/rich/tui/view.py +++ b/rich/tui/view.py @@ -15,7 +15,16 @@ class View(MessagePump): - pass + @property + def app(self) -> "App": + return active_app.get() + + @property + def console(self) -> Console: + return active_app.get().console + + async def on_resize(self, event: events.Resize) -> None: + pass class LayoutView(View): @@ -43,14 +52,6 @@ def __init__( def __rich__(self) -> RenderableType: return self.layout - @property - def app(self) -> "App": - return active_app.get() - - @property - def console(self) -> Console: - return active_app.get().console - async def on_create(self, event: events.Created) -> None: await self.mount(Header(self.title)) diff --git a/rich/tui/widgets/header.py b/rich/tui/widgets/header.py index 691a88067..fcb8d25da 100644 --- a/rich/tui/widgets/header.py +++ b/rich/tui/widgets/header.py @@ -11,7 +11,6 @@ from ..widget import Widget -@rich_repr class Header(Widget): def __init__( self, From 6aac8da6984482ad767c06dd592ffa019a0b5318 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 May 2021 16:09:56 +0100 Subject: [PATCH 14/33] scrollbars --- CHANGELOG.md | 8 ++++ rich/console.py | 24 +++++++++-- rich/screen.py | 2 + rich/segment.py | 30 +++++++++++++- rich/tui/app.py | 12 +++--- rich/tui/events.py | 2 +- rich/tui/manager.py | 6 --- rich/tui/message_pump.py | 15 +------ rich/tui/scrollbar.py | 85 ++++++++++++++++++++++++++++++++++++++ rich/tui/view.py | 31 ++++++++++++-- rich/tui/widgets/window.py | 14 +++++++ 11 files changed, 194 insertions(+), 35 deletions(-) delete mode 100644 rich/tui/manager.py create mode 100644 rich/tui/scrollbar.py create mode 100644 rich/tui/widgets/window.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a0a53dc..6fe252fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.3.0] - Unreleased + +### Added + +- Added Console.size setter +- Added Console.width setter +- Added Console.height setter + ## [10.2.2] - 2021-05-19 ### Fixed diff --git a/rich/console.py b/rich/console.py index 56de7719a..e6873bbbe 100644 --- a/rich/console.py +++ b/rich/console.py @@ -962,8 +962,16 @@ def width(self) -> int: Returns: int: The width (in characters) of the console. """ - width, _ = self.size - return width + return self.size.width + + @width.setter + def width(self, width: int) -> None: + """Set width. + + Args: + width (int): New width. + """ + self._width = width @property def height(self) -> int: @@ -972,8 +980,16 @@ def height(self) -> int: Returns: int: The height (in lines) of the console. """ - _, height = self.size - return height + return self.size.height + + @height.setter + def height(self, height: int) -> None: + """Set height. + + Args: + height (int): new height. + """ + self._height = height def bell(self) -> None: """Play a 'bell' sound (if supported by the terminal).""" diff --git a/rich/screen.py b/rich/screen.py index dfbff7f3d..e1245467b 100644 --- a/rich/screen.py +++ b/rich/screen.py @@ -24,6 +24,8 @@ class Screen: style (StyleType, optional): Optional background style. Defaults to None. """ + renderable: "RenderableType" + def __init__( self, *renderables: "RenderableType", diff --git a/rich/segment.py b/rich/segment.py index 094a7b25a..62197a1c1 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -7,7 +7,11 @@ from itertools import filterfalse from operator import attrgetter -from typing import Iterable, List, Sequence, Union, Tuple +from typing import Iterable, List, Sequence, Union, Tuple, TYPE_CHECKING + + +if TYPE_CHECKING: + from .console import Console, ConsoleOptions, RenderResult class ControlType(IntEnum): @@ -404,6 +408,30 @@ def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: yield cls(text, None, control) +class Segments: + """A simple renderable to render an iterable of segments. + + Args: + segments (Iterable[Segment]): An iterable of segments. + new_lines (bool, optional): Add new lines between segments. Defaults to False. + """ + + def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None: + self.segments = list(segments) + self.new_lines = new_lines + + def __rich_console__( + self, console: "Console", options: "ConsoleOptions" + ) -> "RenderResult": + if self.new_lines: + line = Segment.line() + for segment in self.segments: + yield segment + yield line + else: + yield from self.segments + + if __name__ == "__main__": # pragma: no cover from rich.syntax import Syntax from rich.text import Text diff --git a/rich/tui/app.py b/rich/tui/app.py index 207bde67c..58629e24f 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -4,8 +4,8 @@ import signal from typing import Any, Dict, Set -from rich.live import Live from rich.control import Control +from rich.repr import rich_repr, RichReprResult from rich.screen import Screen from . import events @@ -22,23 +22,27 @@ LayoutDefinition = Dict[str, Any] +@rich_repr class App(MessagePump): + view: View + def __init__( self, console: Console = None, view: View = None, screen: bool = True, - auto_refresh=4, title: str = "Megasoma Application", ): super().__init__() self.console = console or get_console() self._screen = screen - self._auto_refresh = auto_refresh self.title = title self.view = view or LayoutView() self.children: Set[MessagePump] = set() + def __rich_repr__(self) -> RichReprResult: + yield "title", self.title + @classmethod def run(cls, console: Console = None, screen: bool = True): async def run_app() -> None: @@ -99,8 +103,6 @@ async def on_resize(self, event: events.Resize) -> None: if __name__ == "__main__": import asyncio from logging import FileHandler - from rich.layout import Layout - from rich.panel import Panel from .widgets.header import Header diff --git a/rich/tui/events.py b/rich/tui/events.py index fd9e7ccc1..975c803d3 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -136,7 +136,7 @@ def __init__( self.callback = callback def __rich_repr__(self) -> RichReprResult: - yield "timer", self.timer + yield self.timer.name class Focus(Event, type=EventType.FOCUS): diff --git a/rich/tui/manager.py b/rich/tui/manager.py deleted file mode 100644 index cd4173b85..000000000 --- a/rich/tui/manager.py +++ /dev/null @@ -1,6 +0,0 @@ -from rich.layout import Layout - - -class LayoutManager: - def __init__(self) -> None: - self.layout = Layout() \ No newline at end of file diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 90ccc1cc5..a271f4124 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -105,7 +105,7 @@ async def process_messages(self) -> None: except Exception: log.exception("error getting message") break - log.debug(repr(message)) + log.debug("%r -> %r", message, self) await self.dispatch_message(message, priority) async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]: @@ -150,16 +150,3 @@ async def emit(self, message: Message, priority: Optional[int] = None) -> bool: async def on_timer(self, event: events.Timer) -> None: if event.callback is not None: await event.callback() - - -if __name__ == "__main__": - - class Widget(MessagePump): - pass - - widget1 = Widget() - widget2 = Widget() - - import asyncio - - asyncio.get_event_loop().run_until_complete(widget1.run_message_loop()) \ No newline at end of file diff --git a/rich/tui/scrollbar.py b/rich/tui/scrollbar.py new file mode 100644 index 000000000..9a8d4a18b --- /dev/null +++ b/rich/tui/scrollbar.py @@ -0,0 +1,85 @@ +from typing import List, Optional + +from rich.segment import Segment +from rich.style import Style + + +def render_bar( + height: int = 25, + size: float = 100, + window_size: float = 25, + position: float = 0, + bar_style: Optional[Style] = None, + back_style: Optional[Style] = None, + ascii_only: bool = False, + vertical: bool = True, +) -> List[Segment]: + if vertical: + if ascii_only: + solid = "|" + half_start = "|" + half_end = "|" + else: + solid = "┃" + half_start = "╻" + half_end = "╹" + else: + if ascii_only: + solid = "-" + half_start = "-" + half_end = "-" + else: + solid = "━" + half_start = "╺" + half_end = "╸" + + _bar_style = bar_style or Style.parse("bright_magenta") + _back_style = back_style or Style.parse("#555555") + + _Segment = Segment + + start_bar_segment = _Segment(half_start, _bar_style) + end_bar_segment = _Segment(half_end, _bar_style) + bar_segment = _Segment(solid, _bar_style) + + start_back_segment = _Segment(half_end, _back_style) + end_back_segment = _Segment(half_end, _back_style) + back_segment = _Segment(solid, _back_style) + + segments = [back_segment] * height + + step_size = size / height + + start = position / step_size + end = (position + window_size) / step_size + + start_index = int(start) + end_index = int(end) + bar_height = (end_index - start_index) + 1 + + segments[start_index:end_index] = [bar_segment] * bar_height + + sub_position = start % 1.0 + if sub_position >= 0.5: + segments[start_index] = start_bar_segment + elif start_index: + segments[start_index - 1] = end_back_segment + + sub_position = end % 1.0 + if sub_position < 0.5: + segments[end_index] = end_bar_segment + elif end_index + 1 < len(segments): + segments[end_index + 1] = start_back_segment + + return segments + + +if __name__ == "__main__": + from rich.console import Console + from rich.segment import Segments + + console = Console() + + bar = render_bar(height=20, position=10, vertical=False, ascii_only=False) + + console.print(Segments(bar, new_lines=False)) diff --git a/rich/tui/view.py b/rich/tui/view.py index 807e5bc63..7de89dacd 100644 --- a/rich/tui/view.py +++ b/rich/tui/view.py @@ -1,8 +1,9 @@ +from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from rich.console import Console, RenderableType +from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.layout import Layout -from rich.live import Live +from rich.repr import rich_repr, RichReprResult from . import events from ._context import active_app @@ -14,7 +15,8 @@ from .app import App -class View(MessagePump): +@rich_repr +class View(ABC, MessagePump): @property def app(self) -> "App": return active_app.get() @@ -23,16 +25,34 @@ def app(self) -> "App": def console(self) -> Console: return active_app.get().console + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + return + yield + + def __rich_repr__(self) -> RichReprResult: + return + yield + async def on_resize(self, event: events.Resize) -> None: pass + @abstractmethod + async def mount(self, widget: Widget, *, slot: str = "main") -> None: + ... + class LayoutView(View): layout: Layout def __init__( - self, layout: Layout = None, title: str = "Layout Application" + self, + layout: Layout = None, + name: str = "default", + title: str = "Layout Application", ) -> None: + self.name = name self.title = title if layout is None: layout = Layout() @@ -49,6 +69,9 @@ def __init__( self.layout = layout super().__init__() + def __rich_repr__(self) -> RichReprResult: + yield "name", self.name + def __rich__(self) -> RenderableType: return self.layout diff --git a/rich/tui/widgets/window.py b/rich/tui/widgets/window.py new file mode 100644 index 000000000..985952323 --- /dev/null +++ b/rich/tui/widgets/window.py @@ -0,0 +1,14 @@ +from typing import Optional + +from rich.console import RenderableType +from ..widget import Widget + + +class Window(Widget): + renderable: Optional[RenderableType] + + def __init__(self, renderable: RenderableType): + self.renderable = renderable + + def update(self, renderable: RenderableType) -> None: + self.renderable = renderable \ No newline at end of file From 8e6dbe5406f9d05dae630aac6ec3a75b1c6fb98d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 May 2021 18:28:26 +0100 Subject: [PATCH 15/33] mouse events --- rich/tui/driver.py | 68 ++++++++++++++++++++++++++++++++++---- rich/tui/events.py | 48 +++++++++++++++++++++++++-- rich/tui/widgets/header.py | 3 +- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/rich/tui/driver.py b/rich/tui/driver.py index cf93ec8b1..3471a0f2d 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -37,6 +37,42 @@ def stop_application_mode(self): class CursesDriver(Driver): + + _MOUSE_PRESSED = [ + curses.BUTTON1_PRESSED, + curses.BUTTON2_PRESSED, + curses.BUTTON3_PRESSED, + curses.BUTTON4_PRESSED, + ] + + _MOUSE_RELEASED = [ + curses.BUTTON1_RELEASED, + curses.BUTTON2_RELEASED, + curses.BUTTON3_RELEASED, + curses.BUTTON4_RELEASED, + ] + + _MOUSE_CLICKED = [ + curses.BUTTON1_CLICKED, + curses.BUTTON2_CLICKED, + curses.BUTTON3_CLICKED, + curses.BUTTON4_CLICKED, + ] + + _MOUSE_DOUBLE_CLICKED = [ + curses.BUTTON1_DOUBLE_CLICKED, + curses.BUTTON2_DOUBLE_CLICKED, + curses.BUTTON3_DOUBLE_CLICKED, + curses.BUTTON4_DOUBLE_CLICKED, + ] + + _MOUSE = [ + (events.MousePressed, _MOUSE_PRESSED), + (events.MouseReleased, _MOUSE_RELEASED), + (events.MouseClicked, _MOUSE_CLICKED), + (events.MouseDoubleClicked, _MOUSE_DOUBLE_CLICKED), + ] + def __init__(self, console: "Console", target: "MessageTarget") -> None: super().__init__(console, target) self._stdscr = None @@ -78,6 +114,7 @@ def on_terminal_resize(signum, stack) -> None: curses.noecho() curses.cbreak() curses.halfdelay(1) + curses.mousemask(-1) self._stdscr.keypad(True) self.console.show_cursor(False) @@ -105,12 +142,29 @@ def run_key_thread(self, loop) -> None: stdscr = self._stdscr assert stdscr is not None exit_event = self._exit_event + + def send_event(event: events.Event) -> None: + log.debug(event) + asyncio.run_coroutine_threadsafe( + self._target.post_message(event), + loop=loop, + ) + while not exit_event.is_set(): code = stdscr.getch() - if code != -1: - key_event = events.Key(sender=self._target, code=code) - log.debug("KEY=%r", key_event) - asyncio.run_coroutine_threadsafe( - self._target.post_message(key_event), - loop=loop, - ) + if code == -1: + continue + + if code == curses.KEY_MOUSE: + + try: + _id, x, y, _z, button_state = curses.getmouse() + except Exception: + log.exception("getmouse") + else: + for event_type, masks in self._MOUSE: + for button, mask in enumerate(masks, 1): + if button_state & mask: + send_event(event_type(self._target, x, y, button)) + else: + send_event(events.Key(self._target, code=code)) diff --git a/rich/tui/events.py b/rich/tui/events.py index 975c803d3..a891a134a 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -2,7 +2,7 @@ import re from enum import auto, Enum from time import monotonic -from typing import ClassVar, Optional, TYPE_CHECKING +from typing import ClassVar, Optional, Set, TYPE_CHECKING from ..repr import rich_repr, RichReprResult @@ -32,7 +32,10 @@ class EventType(Enum): FOCUS = auto() BLUR = auto() KEY = auto() - + MOUSE_PRESSED = auto() + MOUSE_RELEASED = auto() + MOUSE_CLICKED = auto() + MOUSE_DOUBLE_CLICKED = auto() CUSTOM = 1000 @@ -121,6 +124,47 @@ def key(self) -> str: return chr(self.code) +@rich_repr +class MousePressed(Event, type=EventType.MOUSE_PRESSED): + def __init__( + self, + sender: MessageTarget, + x: int, + y: int, + button: int, + alt: bool = False, + ctrl: bool = False, + shift: bool = False, + ) -> None: + super().__init__(sender) + self.x = x + self.y = y + self.button = button + self.alt = alt + self.ctrl = ctrl + self.shift = shift + + def __rich_repr__(self) -> RichReprResult: + yield "x", self.x + yield "y", self.y + yield "button", self.button, + yield "alt", self.alt, False + yield "ctrl", self.ctrl, False + yield "shift", self.shift, False + + +class MouseReleased(MousePressed, type=EventType.MOUSE_RELEASED): + pass + + +class MouseClicked(MousePressed, type=EventType.MOUSE_CLICKED): + pass + + +class MouseDoubleClicked(MousePressed, type=EventType.MOUSE_DOUBLE_CLICKED): + pass + + @rich_repr class Timer(Event, type=EventType.TIMER, priority=10): def __init__( diff --git a/rich/tui/widgets/header.py b/rich/tui/widgets/header.py index fcb8d25da..2aec3dbf0 100644 --- a/rich/tui/widgets/header.py +++ b/rich/tui/widgets/header.py @@ -49,4 +49,5 @@ def __rich__(self) -> RenderableType: return header async def on_mount(self, event: events.Mount) -> None: - self.set_interval(1.0, callback=self.refresh) \ No newline at end of file + pass + # self.set_interval(1.0, callback=self.refresh) \ No newline at end of file From c7427baaed9c4a5b84c0c0cad0efad7545d31e6b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 May 2021 22:19:41 +0100 Subject: [PATCH 16/33] more mouseevents --- rich/tui/driver.py | 22 ++++++++++++++++++++-- rich/tui/events.py | 13 +++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/rich/tui/driver.py b/rich/tui/driver.py index 3471a0f2d..413132906 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -114,10 +114,12 @@ def on_terminal_resize(signum, stack) -> None: curses.noecho() curses.cbreak() curses.halfdelay(1) - curses.mousemask(-1) + curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) + # curses.mousemask(-1) self._stdscr.keypad(True) self.console.show_cursor(False) + self.console.file.write("\033[?1003h\n") self._key_thread = Thread( target=self.run_key_thread, args=(asyncio.get_event_loop(),) ) @@ -159,12 +161,28 @@ def send_event(event: events.Event) -> None: try: _id, x, y, _z, button_state = curses.getmouse() + log.debug("%s %s", _id, curses.REPORT_MOUSE_POSITION) except Exception: log.exception("getmouse") else: + if button_state & curses.REPORT_MOUSE_POSITION: + send_event(events.MouseMove(self._target, x, y)) + alt = bool(button_state & curses.BUTTON_ALT) + ctrl = bool(button_state & curses.BUTTON_CTRL) + shift = bool(button_state & curses.BUTTON_SHIFT) for event_type, masks in self._MOUSE: for button, mask in enumerate(masks, 1): if button_state & mask: - send_event(event_type(self._target, x, y, button)) + send_event( + event_type( + self._target, + x, + y, + button, + alt=alt, + ctrl=ctrl, + shift=shift, + ) + ) else: send_event(events.Key(self._target, code=code)) diff --git a/rich/tui/events.py b/rich/tui/events.py index a891a134a..fe42e6352 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -32,6 +32,7 @@ class EventType(Enum): FOCUS = auto() BLUR = auto() KEY = auto() + MOUSE_MOVE = auto() MOUSE_PRESSED = auto() MOUSE_RELEASED = auto() MOUSE_CLICKED = auto() @@ -124,6 +125,18 @@ def key(self) -> str: return chr(self.code) +@rich_repr +class MouseMove(Event, type=EventType.MOUSE_MOVE): + def __init__(self, sender: MessageTarget, x: int, y: int) -> None: + super().__init__(sender) + self.x = x + self.y = y + + def __rich_repr__(self) -> RichReprResult: + yield "x", self.x + yield "y", self.y + + @rich_repr class MousePressed(Event, type=EventType.MOUSE_PRESSED): def __init__( From 12b2a047a379ce4ebadc6a736ad6e9a4ef5aa5aa Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 May 2021 11:30:48 +0100 Subject: [PATCH 17/33] added mouse enter leave and idle --- rich/layout.py | 7 +++++++ rich/region.py | 14 +++++++++++++ rich/tui/app.py | 3 +++ rich/tui/driver.py | 4 +--- rich/tui/events.py | 24 +++++++++++++++++++++ rich/tui/message_pump.py | 6 +++++- rich/tui/view.py | 45 ++++++++++++++++++++++++++++++++++++++-- rich/tui/widget.py | 3 +-- 8 files changed, 98 insertions(+), 8 deletions(-) diff --git a/rich/layout.py b/rich/layout.py index 8b9ff8215..2d617c738 100644 --- a/rich/layout.py +++ b/rich/layout.py @@ -181,6 +181,8 @@ def __rich_repr__(self) -> RichReprResult: yield "minimum_size", self.minimum_size, 1 yield "ratio", self.ratio, 1 + __rich_repr__.meta = {"angular": True} + @property def renderable(self) -> RenderableType: """Layout renderable.""" @@ -191,6 +193,11 @@ def children(self) -> List["Layout"]: """Gets (visible) layout children.""" return [child for child in self._children if child.visible] + @property + def map(self) -> RenderMap: + """Get a map of the last render.""" + return self._render_map + def get(self, name: str) -> Optional["Layout"]: """Get a named layout, or None if it doesn't exist. diff --git a/rich/region.py b/rich/region.py index 75b3631c3..3a6afa750 100644 --- a/rich/region.py +++ b/rich/region.py @@ -8,3 +8,17 @@ class Region(NamedTuple): y: int width: int height: int + + def contains(self, x: int, y: int) -> bool: + """Check if a point is in the region. + + Args: + x (int): X coordinate (column) + y (int): Y coordinate (row) + + Returns: + bool: True if the point is within the region. + """ + return ((self.x + self.width) > x >= self.x) and ( + ((self.y + self.height) > y >= self.y) + ) \ No newline at end of file diff --git a/rich/tui/app.py b/rich/tui/app.py index 58629e24f..e366c6757 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -99,6 +99,9 @@ async def on_resize(self, event: events.Resize) -> None: if not event.suppressed: self.refresh() + async def on_mouse_move(self, event: events.MouseMove) -> None: + await self.view.post_message(event) + if __name__ == "__main__": import asyncio diff --git a/rich/tui/driver.py b/rich/tui/driver.py index 413132906..94647e135 100644 --- a/rich/tui/driver.py +++ b/rich/tui/driver.py @@ -146,7 +146,6 @@ def run_key_thread(self, loop) -> None: exit_event = self._exit_event def send_event(event: events.Event) -> None: - log.debug(event) asyncio.run_coroutine_threadsafe( self._target.post_message(event), loop=loop, @@ -161,9 +160,8 @@ def send_event(event: events.Event) -> None: try: _id, x, y, _z, button_state = curses.getmouse() - log.debug("%s %s", _id, curses.REPORT_MOUSE_POSITION) except Exception: - log.exception("getmouse") + log.exception("error in curses.getmouse") else: if button_state & curses.REPORT_MOUSE_POSITION: send_event(events.MouseMove(self._target, x, y)) diff --git a/rich/tui/events.py b/rich/tui/events.py index fe42e6352..f2a088b3f 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -21,6 +21,7 @@ class EventType(Enum): LOAD = auto() STARTUP = auto() CREATED = auto() + IDLE = auto() RESIZE = auto() MOUNT = auto() UNMOUNT = auto() @@ -37,6 +38,8 @@ class EventType(Enum): MOUSE_RELEASED = auto() MOUSE_CLICKED = auto() MOUSE_DOUBLE_CLICKED = auto() + MOUSE_ENTER = auto() + MOUSE_LEAVE = auto() CUSTOM = 1000 @@ -78,6 +81,10 @@ class Created(Event, type=EventType.CREATED): pass +class Idle(Event, type=EventType.IDLE): + """Sent when there are no more items in the message queue.""" + + class Resize(Event, type=EventType.RESIZE): width: int height: int @@ -196,6 +203,23 @@ def __rich_repr__(self) -> RichReprResult: yield self.timer.name +@rich_repr +class MouseEnter(Event, type=EventType.MOUSE_ENTER): + def __init__(self, sender: MessageTarget, x: int, y: int) -> None: + super().__init__(sender) + self.x = x + self.y = y + + def __rich_repr__(self) -> RichReprResult: + yield "x", self.x + yield "y", self.y + + +@rich_repr +class MouseLeave(Event, type=EventType.MOUSE_LEAVE): + pass + + class Focus(Event, type=EventType.FOCUS): pass diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index a271f4124..0c7ad03d0 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -107,8 +107,12 @@ async def process_messages(self) -> None: break log.debug("%r -> %r", message, self) await self.dispatch_message(message, priority) + if self._message_queue.empty(): + await self.dispatch_message(events.Idle(self)) - async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]: + async def dispatch_message( + self, message: Message, priority: int = 0 + ) -> Optional[bool]: if isinstance(message, events.Event): method_name = f"on_{message.name}" dispatch_function: MessageHandler = getattr(self, method_name, None) diff --git a/rich/tui/view.py b/rich/tui/view.py index 7de89dacd..fe1bfc63b 100644 --- a/rich/tui/view.py +++ b/rich/tui/view.py @@ -1,8 +1,10 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING +import logging +from typing import Optional, Tuple, TYPE_CHECKING from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.layout import Layout +from rich.region import Region from rich.repr import rich_repr, RichReprResult from . import events @@ -14,6 +16,12 @@ if TYPE_CHECKING: from .app import App +log = logging.getLogger("rich") + + +class NoWidget(Exception): + pass + @rich_repr class View(ABC, MessagePump): @@ -67,6 +75,7 @@ def __init__( Layout(name="right", size=30, visible=False), ) self.layout = layout + self.mouse_over: Optional[MessagePump] = None super().__init__() def __rich_repr__(self) -> RichReprResult: @@ -75,6 +84,15 @@ def __rich_repr__(self) -> RichReprResult: def __rich__(self) -> RenderableType: return self.layout + def get_widget_at(self, x: int, y: int) -> Tuple[MessagePump, Region]: + for layout, (region, render) in self.layout.map.items(): + if region.contains(x, y): + if isinstance(layout.renderable, MessagePump): + return layout.renderable, region + else: + break + raise NoWidget(f"No widget at ${x}, ${y}") + async def on_create(self, event: events.Created) -> None: await self.mount(Header(self.title)) @@ -84,4 +102,27 @@ async def mount(self, widget: Widget, *, slot: str = "main") -> None: await widget.post_message(events.Mount(sender=self)) async def on_startup(self, event: events.Startup) -> None: - await self.mount(Header(self.title), slot="header") \ No newline at end of file + await self.mount(Header(self.title), slot="header") + + async def on_mouse_move(self, event: events.MouseMove) -> None: + try: + widget, region = self.get_widget_at(event.x, event.y) + except NoWidget: + if self.mouse_over is not None: + try: + await self.mouse_over.post_message(events.MouseLeave(self)) + finally: + self.mouse_over = None + else: + if self.mouse_over != widget: + try: + if self.mouse_over is not None: + await self.mouse_over.post_message(events.MouseLeave(self)) + if widget is not None: + await widget.post_message( + events.MouseEnter( + self, event.x - region.x, event.y - region.x + ) + ) + finally: + self.mouse_over = widget diff --git a/rich/tui/widget.py b/rich/tui/widget.py index 28a7ff48c..a6217bcf2 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -50,7 +50,6 @@ def console(self) -> Console: async def refresh(self) -> None: self.app.refresh() - # await self.emit(events.Refresh(self)) def __rich_repr__(self) -> RichReprResult: yield "name", self.name @@ -66,4 +65,4 @@ def __rich_console__( new_size = WidgetDimensions(options.max_width, options.height or console.height) renderable = self.render(console, options, new_size) self.size = new_size - yield renderable \ No newline at end of file + yield renderable From 4d0bb200e1bd0a01e3575b461f5e96d4bbf3f0a2 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 May 2021 12:01:47 +0100 Subject: [PATCH 18/33] disable messages --- rich/tui/message_pump.py | 16 +++++++++++++++- rich/tui/widget.py | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 0c7ad03d0..fe1a584a5 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -1,5 +1,5 @@ from functools import total_ordering -from typing import AsyncIterable, Optional, NamedTuple, Tuple, TYPE_CHECKING +from typing import AsyncIterable, Optional, NamedTuple, Set, Type, Tuple, TYPE_CHECKING import asyncio from asyncio import PriorityQueue @@ -51,6 +51,18 @@ def __init__( self._parent = parent self._closing: bool = False self._closed: bool = False + self._disabled_messages: Set[Message] = set() + + def check_message_enabled(self, message: Message) -> bool: + return type(message) not in self._disabled_messages + + def disable_messages(self, *messages: Type[Message]) -> None: + """Disable message types from being proccessed.""" + self._disabled_messages.intersection_update(messages) + + def enable_messages(self, *messages: Type[Message]) -> None: + """Enable processing of messages types.""" + self._disabled_messages.difference_update(messages) async def get_message(self) -> MessageQueueItem: """Get the next event on the queue, or None if queue is closed. @@ -134,6 +146,8 @@ async def post_message( ) -> bool: if self._closing or self._closed: return False + if not self.check_message_enabled(message): + return True event_priority = priority if priority is not None else message.default_priority item = MessageQueueItem(event_priority, message) await self._message_queue.put(item) diff --git a/rich/tui/widget.py b/rich/tui/widget.py index a6217bcf2..7dab2c2a5 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -32,6 +32,8 @@ def __rich__(self) -> Panel: @rich_repr class Widget(MessagePump): _count: ClassVar[int] = 0 + can_focus: bool = False + mouse_events: bool = False def __init__(self, name: Optional[str] = None) -> None: self.name = name or f"Widget#{self._count}" @@ -39,6 +41,8 @@ def __init__(self, name: Optional[str] = None) -> None: self.size = WidgetDimensions(0, 0) self.size_changed = False super().__init__() + if not self.mouse_events: + self.disable_messages(events.MouseMove) @property def app(self) -> "App": From 6d4227bb532557b9d6659afbf24c573fdfbbc82d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 May 2021 12:10:38 +0100 Subject: [PATCH 19/33] idle events disable --- rich/tui/view.py | 3 +++ rich/tui/widget.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/rich/tui/view.py b/rich/tui/view.py index fe1bfc63b..a03a4fa37 100644 --- a/rich/tui/view.py +++ b/rich/tui/view.py @@ -126,3 +126,6 @@ async def on_mouse_move(self, event: events.MouseMove) -> None: ) finally: self.mouse_over = widget + await widget.post_message( + events.MouseMove(self, event.x - region.x, event.y - region.x) + ) diff --git a/rich/tui/widget.py b/rich/tui/widget.py index 7dab2c2a5..7eea69470 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -34,6 +34,7 @@ class Widget(MessagePump): _count: ClassVar[int] = 0 can_focus: bool = False mouse_events: bool = False + idle_events: bool = False def __init__(self, name: Optional[str] = None) -> None: self.name = name or f"Widget#{self._count}" @@ -43,6 +44,8 @@ def __init__(self, name: Optional[str] = None) -> None: super().__init__() if not self.mouse_events: self.disable_messages(events.MouseMove) + if not self.idle_events: + self.disable_messages(events.Idle) @property def app(self) -> "App": From d13b133a37af57a664ca0e09105954de9d77d538 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 May 2021 13:59:47 +0100 Subject: [PATCH 20/33] idle events disable --- rich/tui/app.py | 2 ++ rich/tui/message_pump.py | 15 +++++++++------ rich/tui/view.py | 2 +- rich/tui/widget.py | 32 +++++++++++++++++++------------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/rich/tui/app.py b/rich/tui/app.py index e366c6757..7f6a8f8e7 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -108,6 +108,7 @@ async def on_mouse_move(self, event: events.MouseMove) -> None: from logging import FileHandler from .widgets.header import Header + from .widgets.placeholder import Placeholder logging.basicConfig( level="NOTSET", @@ -124,6 +125,7 @@ async def on_key(self, event: events.Key) -> None: async def on_startup(self, event: events.Startup) -> None: await self.view.mount(Header(self.title), slot="header") + await self.view.mount(Placeholder(), slot="body") self.refresh() MyApp.run() diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index fe1a584a5..eac09fff6 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -126,16 +126,19 @@ async def dispatch_message( self, message: Message, priority: int = 0 ) -> Optional[bool]: if isinstance(message, events.Event): - method_name = f"on_{message.name}" - dispatch_function: MessageHandler = getattr(self, method_name, None) - if dispatch_function is not None: - await dispatch_function(message) - if message.bubble and self._parent: - await self._parent.post_message(message, priority) + await self.on_event(message, priority) else: return await self.on_message(message) return False + async def on_event(self, event: events.Event, priority: int) -> None: + method_name = f"on_{event.name}" + dispatch_function: MessageHandler = getattr(self, method_name, None) + if dispatch_function is not None: + await dispatch_function(event) + if event.bubble and self._parent: + await self._parent.post_message(event, priority) + async def on_message(self, message: Message) -> None: pass diff --git a/rich/tui/view.py b/rich/tui/view.py index a03a4fa37..762ee446b 100644 --- a/rich/tui/view.py +++ b/rich/tui/view.py @@ -127,5 +127,5 @@ async def on_mouse_move(self, event: events.MouseMove) -> None: finally: self.mouse_over = widget await widget.post_message( - events.MouseMove(self, event.x - region.x, event.y - region.x) + events.MouseMove(self, event.x - region.x, event.y - region.y) ) diff --git a/rich/tui/widget.py b/rich/tui/widget.py index 7eea69470..c8fc62b96 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -1,11 +1,13 @@ +from logging import getLogger +from typing import ClassVar, NamedTuple, Optional, TYPE_CHECKING + + from rich.align import Align from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.pretty import Pretty from rich.panel import Panel from rich.repr import rich_repr, RichReprResult -from typing import ClassVar, NamedTuple, Optional, TYPE_CHECKING - from . import events from ._context import active_app from .message_pump import MessagePump @@ -13,22 +15,14 @@ if TYPE_CHECKING: from .app import App +log = getLogger("rich") + class WidgetDimensions(NamedTuple): width: int height: int -class WidgetPlaceholder: - def __init__(self, widget: "Widget") -> None: - self.widget = widget - - def __rich__(self) -> Panel: - return Panel( - Align.center(Pretty(self.widget), vertical="middle"), title="Widget" - ) - - @rich_repr class Widget(MessagePump): _count: ClassVar[int] = 0 @@ -41,6 +35,7 @@ def __init__(self, name: Optional[str] = None) -> None: Widget._count += 1 self.size = WidgetDimensions(0, 0) self.size_changed = False + self.mouse_over = False super().__init__() if not self.mouse_events: self.disable_messages(events.MouseMove) @@ -64,7 +59,11 @@ def __rich_repr__(self) -> RichReprResult: def render( self, console: Console, options: ConsoleOptions, new_size: WidgetDimensions ) -> RenderableType: - return WidgetPlaceholder(self) + return Panel( + Align.center(Pretty(self), vertical="middle"), + title=self.__class__.__name__, + border_style="green" if self.mouse_over else "blue", + ) def __rich_console__( self, console: Console, options: ConsoleOptions @@ -73,3 +72,10 @@ def __rich_console__( renderable = self.render(console, options, new_size) self.size = new_size yield renderable + + async def on_event(self, event: events.Event, priority: int) -> None: + if isinstance(event, (events.MouseEnter, events.MouseLeave)): + self.mouse_over = isinstance(event, events.MouseEnter) + log.debug("%r", self.mouse_over) + await self.refresh() + await super().on_event(event, priority) From 58845970c309677555200808886d64a5a12ae2bd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 May 2021 14:00:02 +0100 Subject: [PATCH 21/33] placeholder widget --- rich/tui/widgets/placeholder.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 rich/tui/widgets/placeholder.py diff --git a/rich/tui/widgets/placeholder.py b/rich/tui/widgets/placeholder.py new file mode 100644 index 000000000..048fb5f85 --- /dev/null +++ b/rich/tui/widgets/placeholder.py @@ -0,0 +1,9 @@ +from ..widget import Widget + +from rich.repr import RichReprResult + + +class Placeholder(Widget): + def __rich_repr__(self) -> RichReprResult: + yield "name", self.name + yield "mouse_over", self.mouse_over \ No newline at end of file From 34630b02b60c1c1cce4b072410cd93f6a6385039 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 30 May 2021 17:41:47 +0100 Subject: [PATCH 22/33] remove bus.py --- rich/tui/bus.py | 92 -------------------------------------------- rich/tui/dispatch.py | 0 rich/tui/widget.py | 2 + 3 files changed, 2 insertions(+), 92 deletions(-) delete mode 100644 rich/tui/bus.py delete mode 100644 rich/tui/dispatch.py diff --git a/rich/tui/bus.py b/rich/tui/bus.py deleted file mode 100644 index b0fbef8fc..000000000 --- a/rich/tui/bus.py +++ /dev/null @@ -1,92 +0,0 @@ -from asyncio import Queue, PriorityQueue -from dataclasses import dataclass - -from typing import ( - AsyncGenerator, - Generic, - NamedTuple, - Optional, - Protocol, - TypeVar, - TYPE_CHECKING, -) - -from .events import Event -from .actions import Action - -if TYPE_CHECKING: - from .widget import Widget - - -class Actionable: - def post_action(self, sender: Widget, action: Action): - ... - - -@dataclass(order=True, frozen=True) -class EventQueueItem: - """An item and meta data on the event queue.""" - - event: Event - priority: int - - -class Bus: - def __init__(self, actions_queue: Queue[Action]) -> None: - self.action_queue: "Queue[Action]" = actions_queue - self.event_queue: "PriorityQueue[Optional[EventQueueItem]]" = PriorityQueue() - self._closing = False - self._closed = False - - @property - def is_closing(self) -> bool: - return self._closing - - @property - def is_closed(self) -> bool: - return self._closed - - def post_event(self, event: Event, priority: int = 0) -> bool: - """Post an item on the bus. - - If the event pump is closing or closed, the event will not be posted, and the method - will return ``False``. - - Args: - event (Event): An Event object - priority (int, optional): Priority of event (greater priority processed first). Defaults to 0. - - Returns: - bool: Return True if the event was posted, otherwise False. - """ - if self._closing or self._closed: - return False - item = EventQueueItem(event, priority=-priority) - self.queue.put_nowait(item) - return True - - def close(self) -> None: - """Close the event pump after processing remaining events.""" - self._closing = True - self.event_queue.put_nowait(None) - - async def get(self) -> Optional[Event]: - """Get the next event on the queue, or None if queue is closed. - - Returns: - Optional[Event]: Event object or None. - """ - if self._closed: - return None - queue_item = await self.event_queue.get() - if queue_item is None: - self._closed = True - return None - return queue_item.event - - async def __aiter__(self) -> AsyncGenerator[Event, None]: - while not self._closed: - event = await self.get() - if event is None: - break - yield event diff --git a/rich/tui/dispatch.py b/rich/tui/dispatch.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/rich/tui/widget.py b/rich/tui/widget.py index c8fc62b96..b1b53ecb4 100644 --- a/rich/tui/widget.py +++ b/rich/tui/widget.py @@ -3,6 +3,7 @@ from rich.align import Align +from rich import box from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.pretty import Pretty from rich.panel import Panel @@ -63,6 +64,7 @@ def render( Align.center(Pretty(self), vertical="middle"), title=self.__class__.__name__, border_style="green" if self.mouse_over else "blue", + box=box.HEAVY if self.mouse_over else box.ROUNDED, ) def __rich_console__( From 803651999e50ac2df1a60f1c0dd85729d7b556ce Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 1 Jun 2021 17:13:43 +0100 Subject: [PATCH 23/33] remote tui submodule --- rich/tui/__init__.py | 0 rich/tui/_context.py | 8 -- rich/tui/_timer.py | 79 ----------- rich/tui/actions.py | 34 ----- rich/tui/app.py | 131 ------------------ rich/tui/case.py | 22 --- rich/tui/driver.py | 186 -------------------------- rich/tui/events.py | 228 -------------------------------- rich/tui/examples/simple.py | 28 ---- rich/tui/message.py | 34 ----- rich/tui/message_pump.py | 173 ------------------------ rich/tui/scrollbar.py | 85 ------------ rich/tui/types.py | 29 ---- rich/tui/view.py | 131 ------------------ rich/tui/widget.py | 83 ------------ rich/tui/widgets/__init__.py | 0 rich/tui/widgets/header.py | 53 -------- rich/tui/widgets/placeholder.py | 9 -- rich/tui/widgets/window.py | 14 -- 19 files changed, 1327 deletions(-) delete mode 100644 rich/tui/__init__.py delete mode 100644 rich/tui/_context.py delete mode 100644 rich/tui/_timer.py delete mode 100644 rich/tui/actions.py delete mode 100644 rich/tui/app.py delete mode 100644 rich/tui/case.py delete mode 100644 rich/tui/driver.py delete mode 100644 rich/tui/events.py delete mode 100644 rich/tui/examples/simple.py delete mode 100644 rich/tui/message.py delete mode 100644 rich/tui/message_pump.py delete mode 100644 rich/tui/scrollbar.py delete mode 100644 rich/tui/types.py delete mode 100644 rich/tui/view.py delete mode 100644 rich/tui/widget.py delete mode 100644 rich/tui/widgets/__init__.py delete mode 100644 rich/tui/widgets/header.py delete mode 100644 rich/tui/widgets/placeholder.py delete mode 100644 rich/tui/widgets/window.py diff --git a/rich/tui/__init__.py b/rich/tui/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/rich/tui/_context.py b/rich/tui/_context.py deleted file mode 100644 index e16817631..000000000 --- a/rich/tui/_context.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import TYPE_CHECKING - -from contextvars import ContextVar - -if TYPE_CHECKING: - from .app import App - -active_app: ContextVar["App"] = ContextVar("active_app") diff --git a/rich/tui/_timer.py b/rich/tui/_timer.py deleted file mode 100644 index 1fdeddd07..000000000 --- a/rich/tui/_timer.py +++ /dev/null @@ -1,79 +0,0 @@ -from time import monotonic -from typing import Awaitable, Optional, Callable - -from asyncio import Event, wait_for, TimeoutError -import weakref - -from rich.repr import rich_repr, RichReprResult - -from . import events -from .types import MessageTarget - - -TimerCallback = Callable[[], Awaitable[None]] - - -class EventTargetGone(Exception): - pass - - -@rich_repr -class Timer: - _timer_count: int = 1 - - def __init__( - self, - event_target: MessageTarget, - interval: float, - sender: MessageTarget, - *, - name: Optional[str] = None, - callback: Optional[TimerCallback] = None, - repeat: int = None, - ) -> None: - self._target_repr = repr(event_target) - self._target = weakref.ref(event_target) - self._interval = interval - self.sender = sender - self.name = f"Timer#{self._timer_count}" if name is None else name - self._timer_count += 1 - self._callback = callback - self._repeat = repeat - self._stop_event = Event() - - def __rich_repr__(self) -> RichReprResult: - yield self._interval - yield "name", self.name - yield "repeat", self._repeat, None - - @property - def target(self) -> MessageTarget: - target = self._target() - if target is None: - raise EventTargetGone() - return target - - def stop(self) -> None: - self._stop_event.set() - - async def run(self) -> None: - count = 0 - _repeat = self._repeat - _interval = self._interval - _wait = self._stop_event.wait - start = monotonic() - while _repeat is None or count <= _repeat: - next_timer = start + (count * _interval) - try: - if await wait_for(_wait(), max(0, next_timer - monotonic())): - break - except TimeoutError: - pass - event = events.Timer( - self.sender, timer=self, count=count, callback=self._callback - ) - try: - await self.target.post_message(event) - except EventTargetGone: - break - count += 1 diff --git a/rich/tui/actions.py b/rich/tui/actions.py deleted file mode 100644 index 3a567dc24..000000000 --- a/rich/tui/actions.py +++ /dev/null @@ -1,34 +0,0 @@ -from time import time -from typing import ClassVar -from enum import auto, Enum - -from .case import camel_to_snake - - -class ActionType(Enum): - CUSTOM = auto() - QUIT = auto() - - -class Action: - type: ClassVar[ActionType] - - def __init__(self) -> None: - self.time = time() - - def __init_subclass__(cls, type: ActionType) -> None: - super().__init_subclass__() - cls.type = type - - @property - def name(self) -> str: - if not hasattr(self, "_name"): - _name = camel_to_snake(self.__class__.__name__) - if _name.endswith("_event"): - _name = _name[:-6] - self._name = _name - return self._name - - -class QuitAction(Action, type=ActionType.QUIT): - pass diff --git a/rich/tui/app.py b/rich/tui/app.py deleted file mode 100644 index 7f6a8f8e7..000000000 --- a/rich/tui/app.py +++ /dev/null @@ -1,131 +0,0 @@ -import asyncio - -import logging -import signal -from typing import Any, Dict, Set - -from rich.control import Control -from rich.repr import rich_repr, RichReprResult -from rich.screen import Screen - -from . import events -from ._context import active_app -from .. import get_console -from ..console import Console -from .driver import Driver, CursesDriver -from .message_pump import MessagePump -from .view import View, LayoutView - -log = logging.getLogger("rich") - - -LayoutDefinition = Dict[str, Any] - - -@rich_repr -class App(MessagePump): - view: View - - def __init__( - self, - console: Console = None, - view: View = None, - screen: bool = True, - title: str = "Megasoma Application", - ): - super().__init__() - self.console = console or get_console() - self._screen = screen - self.title = title - self.view = view or LayoutView() - self.children: Set[MessagePump] = set() - - def __rich_repr__(self) -> RichReprResult: - yield "title", self.title - - @classmethod - def run(cls, console: Console = None, screen: bool = True): - async def run_app() -> None: - app = cls(console=console, screen=screen) - await app.process_messages() - - asyncio.run(run_app()) - - def on_keyboard_interupt(self) -> None: - loop = asyncio.get_event_loop() - event = events.ShutdownRequest(sender=self) - asyncio.run_coroutine_threadsafe(self.post_message(event), loop=loop) - - async def process_messages(self) -> None: - loop = asyncio.get_event_loop() - driver = CursesDriver(self.console, self) - driver.start_application_mode() - loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt) - active_app.set(self) - - await self.add(self.view) - - await self.post_message(events.Startup(sender=self)) - self.refresh() - try: - await super().process_messages() - finally: - loop.remove_signal_handler(signal.SIGINT) - driver.stop_application_mode() - - await asyncio.gather(*(child.close_messages() for child in self.children)) - self.children.clear() - - async def add(self, child: MessagePump) -> None: - self.children.add(child) - asyncio.create_task(child.process_messages()) - await child.post_message(events.Created(sender=self)) - - def refresh(self) -> None: - console = self.console - with console: - console.print( - Screen(Control.home(), self.view, Control.home(), application_mode=True) - ) - - async def on_startup(self, event: events.Startup) -> None: - pass - - async def on_shutdown_request(self, event: events.ShutdownRequest) -> None: - await self.close_messages() - - async def on_resize(self, event: events.Resize) -> None: - await self.view.post_message(event) - if not event.suppressed: - self.refresh() - - async def on_mouse_move(self, event: events.MouseMove) -> None: - await self.view.post_message(event) - - -if __name__ == "__main__": - import asyncio - from logging import FileHandler - - from .widgets.header import Header - from .widgets.placeholder import Placeholder - - logging.basicConfig( - level="NOTSET", - format="%(message)s", - datefmt="[%X]", - handlers=[FileHandler("richtui.log")], - ) - - class MyApp(App): - async def on_key(self, event: events.Key) -> None: - log.debug("on_key %r", event) - if event.key == "q": - await self.close_messages() - - async def on_startup(self, event: events.Startup) -> None: - await self.view.mount(Header(self.title), slot="header") - await self.view.mount(Placeholder(), slot="body") - self.refresh() - - MyApp.run() diff --git a/rich/tui/case.py b/rich/tui/case.py deleted file mode 100644 index c3db63854..000000000 --- a/rich/tui/case.py +++ /dev/null @@ -1,22 +0,0 @@ -import re - - -def camel_to_snake(name: str, _re_snake=re.compile("[a-z][A-Z]")) -> str: - """Convert name from CamelCase to snake_case. - - Args: - name (str): A symbol name, such as a class name. - - Returns: - str: Name in camel case. - """ - - def repl(match) -> str: - lower, upper = match.group() - return f"{lower}_{upper.lower()}" - - return _re_snake.sub(repl, name).lower() - - -if __name__ == "__main__": - print(camel_to_snake("HelloWorldEvent")) \ No newline at end of file diff --git a/rich/tui/driver.py b/rich/tui/driver.py deleted file mode 100644 index 94647e135..000000000 --- a/rich/tui/driver.py +++ /dev/null @@ -1,186 +0,0 @@ -from abc import ABC, abstractmethod -import asyncio -import logging -import os -import signal -import curses -import platform -import sys -import shutil -from threading import Event, Thread -from typing import Optional, Tuple, TYPE_CHECKING - -from . import events -from .types import MessageTarget - -if TYPE_CHECKING: - from ..console import Console - - -log = logging.getLogger("rich") - -WINDOWS = platform.system() == "Windows" - - -class Driver(ABC): - def __init__(self, console: "Console", target: "MessageTarget") -> None: - self.console = console - self._target = target - - @abstractmethod - def start_application_mode(self): - ... - - @abstractmethod - def stop_application_mode(self): - ... - - -class CursesDriver(Driver): - - _MOUSE_PRESSED = [ - curses.BUTTON1_PRESSED, - curses.BUTTON2_PRESSED, - curses.BUTTON3_PRESSED, - curses.BUTTON4_PRESSED, - ] - - _MOUSE_RELEASED = [ - curses.BUTTON1_RELEASED, - curses.BUTTON2_RELEASED, - curses.BUTTON3_RELEASED, - curses.BUTTON4_RELEASED, - ] - - _MOUSE_CLICKED = [ - curses.BUTTON1_CLICKED, - curses.BUTTON2_CLICKED, - curses.BUTTON3_CLICKED, - curses.BUTTON4_CLICKED, - ] - - _MOUSE_DOUBLE_CLICKED = [ - curses.BUTTON1_DOUBLE_CLICKED, - curses.BUTTON2_DOUBLE_CLICKED, - curses.BUTTON3_DOUBLE_CLICKED, - curses.BUTTON4_DOUBLE_CLICKED, - ] - - _MOUSE = [ - (events.MousePressed, _MOUSE_PRESSED), - (events.MouseReleased, _MOUSE_RELEASED), - (events.MouseClicked, _MOUSE_CLICKED), - (events.MouseDoubleClicked, _MOUSE_DOUBLE_CLICKED), - ] - - def __init__(self, console: "Console", target: "MessageTarget") -> None: - super().__init__(console, target) - self._stdscr = None - self._exit_event = Event() - self._key_thread: Optional[Thread] = None - - def _get_terminal_size(self) -> Tuple[int, int]: - width: Optional[int] = 80 - height: Optional[int] = 25 - if WINDOWS: # pragma: no cover - width, height = shutil.get_terminal_size() - else: - try: - width, height = os.get_terminal_size(sys.stdin.fileno()) - except (AttributeError, ValueError, OSError): - try: - width, height = os.get_terminal_size(sys.stdout.fileno()) - except (AttributeError, ValueError, OSError): - pass - width = width or 80 - height = height or 25 - return width, height - - def start_application_mode(self): - loop = asyncio.get_event_loop() - - def on_terminal_resize(signum, stack) -> None: - terminal_size = self.console.size = self._get_terminal_size() - width, height = terminal_size - event = events.Resize(self._target, width, height) - self.console.size = terminal_size - asyncio.run_coroutine_threadsafe( - self._target.post_message(event), - loop=loop, - ) - - signal.signal(signal.SIGWINCH, on_terminal_resize) - self._stdscr = curses.initscr() - curses.noecho() - curses.cbreak() - curses.halfdelay(1) - curses.mousemask(curses.REPORT_MOUSE_POSITION | curses.ALL_MOUSE_EVENTS) - # curses.mousemask(-1) - - self._stdscr.keypad(True) - self.console.show_cursor(False) - self.console.file.write("\033[?1003h\n") - self._key_thread = Thread( - target=self.run_key_thread, args=(asyncio.get_event_loop(),) - ) - - self.console.size = self._get_terminal_size() - - self._key_thread.start() - - def stop_application_mode(self): - - signal.signal(signal.SIGWINCH, signal.SIG_DFL) - - self._exit_event.set() - self._key_thread.join() - curses.nocbreak() - self._stdscr.keypad(False) - curses.echo() - curses.endwin() - self.console.show_cursor(True) - - def run_key_thread(self, loop) -> None: - stdscr = self._stdscr - assert stdscr is not None - exit_event = self._exit_event - - def send_event(event: events.Event) -> None: - asyncio.run_coroutine_threadsafe( - self._target.post_message(event), - loop=loop, - ) - - while not exit_event.is_set(): - code = stdscr.getch() - if code == -1: - continue - - if code == curses.KEY_MOUSE: - - try: - _id, x, y, _z, button_state = curses.getmouse() - except Exception: - log.exception("error in curses.getmouse") - else: - if button_state & curses.REPORT_MOUSE_POSITION: - send_event(events.MouseMove(self._target, x, y)) - alt = bool(button_state & curses.BUTTON_ALT) - ctrl = bool(button_state & curses.BUTTON_CTRL) - shift = bool(button_state & curses.BUTTON_SHIFT) - for event_type, masks in self._MOUSE: - for button, mask in enumerate(masks, 1): - if button_state & mask: - send_event( - event_type( - self._target, - x, - y, - button, - alt=alt, - ctrl=ctrl, - shift=shift, - ) - ) - else: - send_event(events.Key(self._target, code=code)) diff --git a/rich/tui/events.py b/rich/tui/events.py deleted file mode 100644 index f2a088b3f..000000000 --- a/rich/tui/events.py +++ /dev/null @@ -1,228 +0,0 @@ -from dataclasses import dataclass, field -import re -from enum import auto, Enum -from time import monotonic -from typing import ClassVar, Optional, Set, TYPE_CHECKING - -from ..repr import rich_repr, RichReprResult - -from .message import Message -from .types import Callback, MessageTarget - - -if TYPE_CHECKING: - from ._timer import Timer as TimerClass - from ._timer import TimerCallback - - -class EventType(Enum): - """Event type enumeration.""" - - LOAD = auto() - STARTUP = auto() - CREATED = auto() - IDLE = auto() - RESIZE = auto() - MOUNT = auto() - UNMOUNT = auto() - SHUTDOWN_REQUEST = auto() - SHUTDOWN = auto() - EXIT = auto() - REFRESH = auto() - TIMER = auto() - FOCUS = auto() - BLUR = auto() - KEY = auto() - MOUSE_MOVE = auto() - MOUSE_PRESSED = auto() - MOUSE_RELEASED = auto() - MOUSE_CLICKED = auto() - MOUSE_DOUBLE_CLICKED = auto() - MOUSE_ENTER = auto() - MOUSE_LEAVE = auto() - CUSTOM = 1000 - - -@rich_repr -class Event(Message): - type: ClassVar[EventType] - - def __rich_repr__(self) -> RichReprResult: - return - yield - - def __init_subclass__( - cls, type: EventType, priority: int = 0, bubble: bool = False - ) -> None: - super().__init_subclass__(priority=priority, bubble=bubble) - - def __enter__(self) -> "Event": - return self - - def __exit__(self, exc_type, exc_value, exc_tb) -> Optional[bool]: - if exc_type is not None: - # Log and suppress exception - return True - - -class ShutdownRequest(Event, type=EventType.SHUTDOWN_REQUEST): - pass - - -class Load(Event, type=EventType.SHUTDOWN_REQUEST): - pass - - -class Startup(Event, type=EventType.SHUTDOWN_REQUEST): - pass - - -class Created(Event, type=EventType.CREATED): - pass - - -class Idle(Event, type=EventType.IDLE): - """Sent when there are no more items in the message queue.""" - - -class Resize(Event, type=EventType.RESIZE): - width: int - height: int - - def __init__(self, sender: MessageTarget, width: int, height: int) -> None: - self.width = width - self.height = height - super().__init__(sender) - - def __rich_repr__(self) -> RichReprResult: - yield self.width - yield self.height - - -class Mount(Event, type=EventType.MOUNT): - pass - - -class Unmount(Event, type=EventType.UNMOUNT): - pass - - -class Shutdown(Event, type=EventType.SHUTDOWN): - pass - - -class Refresh(Event, type=EventType.REFRESH): - pass - - -@rich_repr -class Key(Event, type=EventType.KEY, bubble=True): - code: int = 0 - - def __init__(self, sender: MessageTarget, code: int) -> None: - super().__init__(sender) - self.code = code - - def __rich_repr__(self) -> RichReprResult: - yield "code", self.code - yield "key", self.key - - @property - def key(self) -> str: - return chr(self.code) - - -@rich_repr -class MouseMove(Event, type=EventType.MOUSE_MOVE): - def __init__(self, sender: MessageTarget, x: int, y: int) -> None: - super().__init__(sender) - self.x = x - self.y = y - - def __rich_repr__(self) -> RichReprResult: - yield "x", self.x - yield "y", self.y - - -@rich_repr -class MousePressed(Event, type=EventType.MOUSE_PRESSED): - def __init__( - self, - sender: MessageTarget, - x: int, - y: int, - button: int, - alt: bool = False, - ctrl: bool = False, - shift: bool = False, - ) -> None: - super().__init__(sender) - self.x = x - self.y = y - self.button = button - self.alt = alt - self.ctrl = ctrl - self.shift = shift - - def __rich_repr__(self) -> RichReprResult: - yield "x", self.x - yield "y", self.y - yield "button", self.button, - yield "alt", self.alt, False - yield "ctrl", self.ctrl, False - yield "shift", self.shift, False - - -class MouseReleased(MousePressed, type=EventType.MOUSE_RELEASED): - pass - - -class MouseClicked(MousePressed, type=EventType.MOUSE_CLICKED): - pass - - -class MouseDoubleClicked(MousePressed, type=EventType.MOUSE_DOUBLE_CLICKED): - pass - - -@rich_repr -class Timer(Event, type=EventType.TIMER, priority=10): - def __init__( - self, - sender: MessageTarget, - timer: "TimerClass", - count: int = 0, - callback: Optional["TimerCallback"] = None, - ) -> None: - super().__init__(sender) - self.timer = timer - self.count = count - self.callback = callback - - def __rich_repr__(self) -> RichReprResult: - yield self.timer.name - - -@rich_repr -class MouseEnter(Event, type=EventType.MOUSE_ENTER): - def __init__(self, sender: MessageTarget, x: int, y: int) -> None: - super().__init__(sender) - self.x = x - self.y = y - - def __rich_repr__(self) -> RichReprResult: - yield "x", self.x - yield "y", self.y - - -@rich_repr -class MouseLeave(Event, type=EventType.MOUSE_LEAVE): - pass - - -class Focus(Event, type=EventType.FOCUS): - pass - - -class Blur(Event, type=EventType.BLUR): - pass \ No newline at end of file diff --git a/rich/tui/examples/simple.py b/rich/tui/examples/simple.py deleted file mode 100644 index 88a01d345..000000000 --- a/rich/tui/examples/simple.py +++ /dev/null @@ -1,28 +0,0 @@ -from rich.layout import Layout -from rich.table import Table -from rich.tui.app import App - -from rich.widgets.color_changer import ColorChanger - - -class SimpleApp(App): - table: Table - - def __init__(self): - super().__init__() - - self.table = table = Table("foo", "bar", "baz") - table.add_row("1", "2", "3") - - def visualize(self): - layout = Layout() - layout.split_column( - Layout(self.table, name="top"), Layout(ColorChanger(), name="bottom") - ) - layout["bottom"].split_row(Layout(name="left"), Layout(name="right")) - return layout - - -if __name__ == "__main__": - app = SimpleApp() - app.run_message_loop() diff --git a/rich/tui/message.py b/rich/tui/message.py deleted file mode 100644 index 64eeed715..000000000 --- a/rich/tui/message.py +++ /dev/null @@ -1,34 +0,0 @@ -from time import monotonic -from typing import ClassVar - -from .case import camel_to_snake - -from .types import MessageTarget - - -class Message: - """Base class for a message.""" - - sender: MessageTarget - bubble: ClassVar[bool] = False - default_priority: ClassVar[int] = 0 - suppressed: bool = False - - def __init__(self, sender: MessageTarget) -> None: - self.sender = sender - self.name = camel_to_snake(self.__class__.__name__) - self.time = monotonic() - - def __init_subclass__(cls, bubble: bool = False, priority: int = 0) -> None: - super().__init_subclass__() - cls.bubble = bubble - cls.default_priority = priority - - def suppress_default(self, suppress: bool = True) -> None: - """Suppress the default action. - - Args: - suppress (bool, optional): True if the default action should be suppressed, - or False if the default actions should be performed. Defaults to True. - """ - self.suppress = suppress \ No newline at end of file diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py deleted file mode 100644 index eac09fff6..000000000 --- a/rich/tui/message_pump.py +++ /dev/null @@ -1,173 +0,0 @@ -from functools import total_ordering -from typing import AsyncIterable, Optional, NamedTuple, Set, Type, Tuple, TYPE_CHECKING -import asyncio -from asyncio import PriorityQueue - -from functools import total_ordering -import logging - -from . import events -from .message import Message -from ._timer import Timer, TimerCallback -from .types import MessageHandler - -log = logging.getLogger("rich") - - -class MessageQueueItem(NamedTuple): - priority: int - message: Message - - def __lt__(self, other: "MessageQueueItem") -> bool: - return self.priority < other.priority - - def __le__(self, other: "MessageQueueItem") -> bool: - return self.priority <= other.priority - - def __gt__(self, other: "MessageQueueItem") -> bool: - return self.priority > other.priority - - def __ge__(self, other: "MessageQueueItem") -> bool: - return self.priority >= other.priority - - def __eq__(self, other: "MessageQueueItem") -> bool: - return self.priority == other.priority - - def __ne__(self, other: "MessageQueueItem") -> bool: - return self.priority != other.priority - - -class MessagePumpClosed(Exception): - pass - - -class MessagePump: - def __init__( - self, queue_size: int = 10, parent: Optional["MessagePump"] = None - ) -> None: - self._message_queue: "PriorityQueue[Optional[MessageQueueItem]]" = ( - PriorityQueue(queue_size) - ) - self._parent = parent - self._closing: bool = False - self._closed: bool = False - self._disabled_messages: Set[Message] = set() - - def check_message_enabled(self, message: Message) -> bool: - return type(message) not in self._disabled_messages - - def disable_messages(self, *messages: Type[Message]) -> None: - """Disable message types from being proccessed.""" - self._disabled_messages.intersection_update(messages) - - def enable_messages(self, *messages: Type[Message]) -> None: - """Enable processing of messages types.""" - self._disabled_messages.difference_update(messages) - - async def get_message(self) -> MessageQueueItem: - """Get the next event on the queue, or None if queue is closed. - - Returns: - Optional[Event]: Event object or None. - """ - if self._closed: - raise MessagePumpClosed("The message pump is closed") - queue_item = await self._message_queue.get() - if queue_item is None: - self._closed = True - raise MessagePumpClosed("The message pump is now closed") - return queue_item - - def set_timer( - self, - delay: float, - *, - name: Optional[str] = None, - callback: TimerCallback = None, - ) -> Timer: - timer = Timer(self, delay, self, name=name, callback=callback, repeat=0) - asyncio.get_event_loop().create_task(timer.run()) - return timer - - def set_interval( - self, - interval: float, - *, - name: Optional[str] = None, - callback: TimerCallback = None, - repeat: int = 0, - ): - timer = Timer( - self, interval, self, name=name, callback=callback, repeat=repeat or None - ) - asyncio.get_event_loop().create_task(timer.run()) - return timer - - async def close_messages(self) -> None: - self._closing = True - await self._message_queue.put(None) - - async def process_messages(self) -> None: - """Process messages until the queue is closed.""" - while not self._closed: - try: - priority, message = await self.get_message() - except MessagePumpClosed: - break - except Exception: - log.exception("error getting message") - break - log.debug("%r -> %r", message, self) - await self.dispatch_message(message, priority) - if self._message_queue.empty(): - await self.dispatch_message(events.Idle(self)) - - async def dispatch_message( - self, message: Message, priority: int = 0 - ) -> Optional[bool]: - if isinstance(message, events.Event): - await self.on_event(message, priority) - else: - return await self.on_message(message) - return False - - async def on_event(self, event: events.Event, priority: int) -> None: - method_name = f"on_{event.name}" - dispatch_function: MessageHandler = getattr(self, method_name, None) - if dispatch_function is not None: - await dispatch_function(event) - if event.bubble and self._parent: - await self._parent.post_message(event, priority) - - async def on_message(self, message: Message) -> None: - pass - - async def post_message( - self, - message: Message, - priority: Optional[int] = None, - ) -> bool: - if self._closing or self._closed: - return False - if not self.check_message_enabled(message): - return True - event_priority = priority if priority is not None else message.default_priority - item = MessageQueueItem(event_priority, message) - await self._message_queue.put(item) - return True - - async def post_message_from_child( - self, message: Message, priority: Optional[int] = None - ) -> None: - await self.post_message(message, priority=priority) - - async def emit(self, message: Message, priority: Optional[int] = None) -> bool: - if self._parent: - await self._parent.post_message_from_child(message, priority=priority) - return True - else: - return False - - async def on_timer(self, event: events.Timer) -> None: - if event.callback is not None: - await event.callback() diff --git a/rich/tui/scrollbar.py b/rich/tui/scrollbar.py deleted file mode 100644 index 9a8d4a18b..000000000 --- a/rich/tui/scrollbar.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import List, Optional - -from rich.segment import Segment -from rich.style import Style - - -def render_bar( - height: int = 25, - size: float = 100, - window_size: float = 25, - position: float = 0, - bar_style: Optional[Style] = None, - back_style: Optional[Style] = None, - ascii_only: bool = False, - vertical: bool = True, -) -> List[Segment]: - if vertical: - if ascii_only: - solid = "|" - half_start = "|" - half_end = "|" - else: - solid = "┃" - half_start = "╻" - half_end = "╹" - else: - if ascii_only: - solid = "-" - half_start = "-" - half_end = "-" - else: - solid = "━" - half_start = "╺" - half_end = "╸" - - _bar_style = bar_style or Style.parse("bright_magenta") - _back_style = back_style or Style.parse("#555555") - - _Segment = Segment - - start_bar_segment = _Segment(half_start, _bar_style) - end_bar_segment = _Segment(half_end, _bar_style) - bar_segment = _Segment(solid, _bar_style) - - start_back_segment = _Segment(half_end, _back_style) - end_back_segment = _Segment(half_end, _back_style) - back_segment = _Segment(solid, _back_style) - - segments = [back_segment] * height - - step_size = size / height - - start = position / step_size - end = (position + window_size) / step_size - - start_index = int(start) - end_index = int(end) - bar_height = (end_index - start_index) + 1 - - segments[start_index:end_index] = [bar_segment] * bar_height - - sub_position = start % 1.0 - if sub_position >= 0.5: - segments[start_index] = start_bar_segment - elif start_index: - segments[start_index - 1] = end_back_segment - - sub_position = end % 1.0 - if sub_position < 0.5: - segments[end_index] = end_bar_segment - elif end_index + 1 < len(segments): - segments[end_index + 1] = start_back_segment - - return segments - - -if __name__ == "__main__": - from rich.console import Console - from rich.segment import Segments - - console = Console() - - bar = render_bar(height=20, position=10, vertical=False, ascii_only=False) - - console.print(Segments(bar, new_lines=False)) diff --git a/rich/tui/types.py b/rich/tui/types.py deleted file mode 100644 index 94359cfd9..000000000 --- a/rich/tui/types.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Awaitable, Callable, Optional, Protocol, TYPE_CHECKING - -if TYPE_CHECKING: - from .events import Event - from .message import Message - -Callback = Callable[[], None] -# IntervalID = int - - -class MessageTarget(Protocol): - async def post_message( - self, - message: "Message", - priority: Optional[int] = None, - ) -> bool: - ... - - -class EventTarget(Protocol): - async def post_message( - self, - message: "Message", - priority: Optional[int] = None, - ) -> bool: - ... - - -MessageHandler = Callable[["Message"], Awaitable] \ No newline at end of file diff --git a/rich/tui/view.py b/rich/tui/view.py deleted file mode 100644 index 762ee446b..000000000 --- a/rich/tui/view.py +++ /dev/null @@ -1,131 +0,0 @@ -from abc import ABC, abstractmethod -import logging -from typing import Optional, Tuple, TYPE_CHECKING - -from rich.console import Console, ConsoleOptions, RenderResult, RenderableType -from rich.layout import Layout -from rich.region import Region -from rich.repr import rich_repr, RichReprResult - -from . import events -from ._context import active_app -from .message_pump import MessagePump -from .widget import Widget -from .widgets.header import Header - -if TYPE_CHECKING: - from .app import App - -log = logging.getLogger("rich") - - -class NoWidget(Exception): - pass - - -@rich_repr -class View(ABC, MessagePump): - @property - def app(self) -> "App": - return active_app.get() - - @property - def console(self) -> Console: - return active_app.get().console - - def __rich_console__( - self, console: Console, options: ConsoleOptions - ) -> RenderResult: - return - yield - - def __rich_repr__(self) -> RichReprResult: - return - yield - - async def on_resize(self, event: events.Resize) -> None: - pass - - @abstractmethod - async def mount(self, widget: Widget, *, slot: str = "main") -> None: - ... - - -class LayoutView(View): - layout: Layout - - def __init__( - self, - layout: Layout = None, - name: str = "default", - title: str = "Layout Application", - ) -> None: - self.name = name - self.title = title - if layout is None: - layout = Layout() - layout.split_column( - Layout(name="header", size=3, ratio=0), - Layout(name="main", ratio=1), - Layout(name="footer", size=1, ratio=0), - ) - layout["main"].split_row( - Layout(name="left", size=30, visible=True), - Layout(name="body", ratio=1), - Layout(name="right", size=30, visible=False), - ) - self.layout = layout - self.mouse_over: Optional[MessagePump] = None - super().__init__() - - def __rich_repr__(self) -> RichReprResult: - yield "name", self.name - - def __rich__(self) -> RenderableType: - return self.layout - - def get_widget_at(self, x: int, y: int) -> Tuple[MessagePump, Region]: - for layout, (region, render) in self.layout.map.items(): - if region.contains(x, y): - if isinstance(layout.renderable, MessagePump): - return layout.renderable, region - else: - break - raise NoWidget(f"No widget at ${x}, ${y}") - - async def on_create(self, event: events.Created) -> None: - await self.mount(Header(self.title)) - - async def mount(self, widget: Widget, *, slot: str = "main") -> None: - self.layout[slot].update(widget) - await self.app.add(widget) - await widget.post_message(events.Mount(sender=self)) - - async def on_startup(self, event: events.Startup) -> None: - await self.mount(Header(self.title), slot="header") - - async def on_mouse_move(self, event: events.MouseMove) -> None: - try: - widget, region = self.get_widget_at(event.x, event.y) - except NoWidget: - if self.mouse_over is not None: - try: - await self.mouse_over.post_message(events.MouseLeave(self)) - finally: - self.mouse_over = None - else: - if self.mouse_over != widget: - try: - if self.mouse_over is not None: - await self.mouse_over.post_message(events.MouseLeave(self)) - if widget is not None: - await widget.post_message( - events.MouseEnter( - self, event.x - region.x, event.y - region.x - ) - ) - finally: - self.mouse_over = widget - await widget.post_message( - events.MouseMove(self, event.x - region.x, event.y - region.y) - ) diff --git a/rich/tui/widget.py b/rich/tui/widget.py deleted file mode 100644 index b1b53ecb4..000000000 --- a/rich/tui/widget.py +++ /dev/null @@ -1,83 +0,0 @@ -from logging import getLogger -from typing import ClassVar, NamedTuple, Optional, TYPE_CHECKING - - -from rich.align import Align -from rich import box -from rich.console import Console, ConsoleOptions, RenderResult, RenderableType -from rich.pretty import Pretty -from rich.panel import Panel -from rich.repr import rich_repr, RichReprResult - -from . import events -from ._context import active_app -from .message_pump import MessagePump - -if TYPE_CHECKING: - from .app import App - -log = getLogger("rich") - - -class WidgetDimensions(NamedTuple): - width: int - height: int - - -@rich_repr -class Widget(MessagePump): - _count: ClassVar[int] = 0 - can_focus: bool = False - mouse_events: bool = False - idle_events: bool = False - - def __init__(self, name: Optional[str] = None) -> None: - self.name = name or f"Widget#{self._count}" - Widget._count += 1 - self.size = WidgetDimensions(0, 0) - self.size_changed = False - self.mouse_over = False - super().__init__() - if not self.mouse_events: - self.disable_messages(events.MouseMove) - if not self.idle_events: - self.disable_messages(events.Idle) - - @property - def app(self) -> "App": - return active_app.get() - - @property - def console(self) -> Console: - return active_app.get().console - - async def refresh(self) -> None: - self.app.refresh() - - def __rich_repr__(self) -> RichReprResult: - yield "name", self.name - - def render( - self, console: Console, options: ConsoleOptions, new_size: WidgetDimensions - ) -> RenderableType: - return Panel( - Align.center(Pretty(self), vertical="middle"), - title=self.__class__.__name__, - border_style="green" if self.mouse_over else "blue", - box=box.HEAVY if self.mouse_over else box.ROUNDED, - ) - - def __rich_console__( - self, console: Console, options: ConsoleOptions - ) -> RenderResult: - new_size = WidgetDimensions(options.max_width, options.height or console.height) - renderable = self.render(console, options, new_size) - self.size = new_size - yield renderable - - async def on_event(self, event: events.Event, priority: int) -> None: - if isinstance(event, (events.MouseEnter, events.MouseLeave)): - self.mouse_over = isinstance(event, events.MouseEnter) - log.debug("%r", self.mouse_over) - await self.refresh() - await super().on_event(event, priority) diff --git a/rich/tui/widgets/__init__.py b/rich/tui/widgets/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/rich/tui/widgets/header.py b/rich/tui/widgets/header.py deleted file mode 100644 index 2aec3dbf0..000000000 --- a/rich/tui/widgets/header.py +++ /dev/null @@ -1,53 +0,0 @@ -from datetime import datetime - -from rich.console import RenderableType -from rich.panel import Panel -from rich.repr import rich_repr, RichReprResult -from rich.style import StyleType -from rich.table import Table -from rich.text import TextType - -from .. import events -from ..widget import Widget - - -class Header(Widget): - def __init__( - self, - title: TextType, - *, - panel: bool = True, - style: StyleType = "white on blue", - clock: bool = True - ) -> None: - self.title = title - self.panel = panel - self.style = style - self.clock = clock - super().__init__() - - def __rich_repr__(self) -> RichReprResult: - yield self.title - - def get_clock(self) -> str: - return datetime.now().time().strftime("%X") - - def __rich__(self) -> RenderableType: - header_table = Table.grid(padding=(0, 1), expand=True) - header_table.style = self.style - header_table.add_column(justify="left", ratio=0) - header_table.add_column("title", justify="center", ratio=1) - if self.clock: - header_table.add_column("clock", justify="right") - header_table.add_row("🐞", self.title, self.get_clock()) - else: - header_table.add_row("🐞", self.title) - if self.panel: - header = Panel(header_table, style=self.style) - else: - header = header_table - return header - - async def on_mount(self, event: events.Mount) -> None: - pass - # self.set_interval(1.0, callback=self.refresh) \ No newline at end of file diff --git a/rich/tui/widgets/placeholder.py b/rich/tui/widgets/placeholder.py deleted file mode 100644 index 048fb5f85..000000000 --- a/rich/tui/widgets/placeholder.py +++ /dev/null @@ -1,9 +0,0 @@ -from ..widget import Widget - -from rich.repr import RichReprResult - - -class Placeholder(Widget): - def __rich_repr__(self) -> RichReprResult: - yield "name", self.name - yield "mouse_over", self.mouse_over \ No newline at end of file diff --git a/rich/tui/widgets/window.py b/rich/tui/widgets/window.py deleted file mode 100644 index 985952323..000000000 --- a/rich/tui/widgets/window.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Optional - -from rich.console import RenderableType -from ..widget import Widget - - -class Window(Widget): - renderable: Optional[RenderableType] - - def __init__(self, renderable: RenderableType): - self.renderable = renderable - - def update(self, renderable: RenderableType) -> None: - self.renderable = renderable \ No newline at end of file From efa324a5942c003cb1e3cb273cfc8f363e8647c5 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Sun, 23 May 2021 18:23:22 +0100 Subject: [PATCH 24/33] perf(console): avoid call to inspect.stack This change improves the performance of the `Console.log` method by avoiding an expensive call to `stack()` and using `currentframe` instead. The same information is then available by navigating backward as many levels as required and peeking at the `code` object. --- CHANGELOG.md | 6 ++++++ CONTRIBUTORS.md | 1 + rich/console.py | 51 +++++++++++++++++++++++++++++++++++++---------- tests/test_log.py | 7 +++++++ 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a0a53dc..9871c4589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.2.3] - 2021-06-05 + +### Changed + +- Changed the logic for retrieving the calling frame in console logs to a faster one for the Python implementations that support it. + ## [10.2.2] - 2021-05-19 ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4d66ed70d..491c79b61 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,3 +12,4 @@ The following people have contributed to the development of Rich: - [Will McGugan](https://github.com/willmcgugan) - [Nathan Page](https://github.com/nathanrpage97) - [Clément Robert](https://github.com/neutrinoceros) +- [Gabriele N. Tornetta](https://github.com/p403n1x87) \ No newline at end of file diff --git a/rich/console.py b/rich/console.py index 08dc9108a..f581ecae7 100644 --- a/rich/console.py +++ b/rich/console.py @@ -12,7 +12,7 @@ from getpass import getpass from itertools import islice from time import monotonic -from types import TracebackType +from types import FrameType, TracebackType from typing import ( IO, TYPE_CHECKING, @@ -25,6 +25,7 @@ NamedTuple, Optional, TextIO, + Tuple, Type, Union, cast, @@ -1637,6 +1638,41 @@ def print_exception( ) self.print(traceback) + @staticmethod + def _caller_frame_info( + offset: int, + currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe, + ) -> Tuple[str, int, Dict[str, Any]]: + """Get caller frame information. + + Args: + offset (int): the caller offset within the current frame stack. + currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to + retrieve the current frame. Defaults to ``inspect.currentframe``. + + Returns: + Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and + the dictionary of local variables associated with the caller frame. + + Raises: + RuntimeError: If the stack offset is invalid. + """ + # Ignore the frame of this local helper + offset += 1 + + frame = currentframe() + if frame is not None: + # Use the faster currentframe where implemented + while offset and frame: + frame = frame.f_back + offset -= 1 + assert frame is not None + return frame.f_code.co_filename, frame.f_lineno, frame.f_locals + else: + # Fallback to the slower stack + frame_info = inspect.stack()[offset] + return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals + def log( self, *objects: Any, @@ -1682,18 +1718,13 @@ def log( if style is not None: renderables = [Styled(renderable, style) for renderable in renderables] - caller = inspect.stack()[_stack_offset] - link_path = ( - None - if caller.filename.startswith("<") - else os.path.abspath(caller.filename) - ) - path = caller.filename.rpartition(os.sep)[-1] - line_no = caller.lineno + filename, line_no, locals = self._caller_frame_info(_stack_offset) + link_path = None if filename.startswith("<") else os.path.abspath(filename) + path = filename.rpartition(os.sep)[-1] if log_locals: locals_map = { key: value - for key, value in caller.frame.f_locals.items() + for key, value in locals.items() if not key.startswith("__") } renderables.append(render_scope(locals_map, title="[i]locals")) diff --git a/tests/test_log.py b/tests/test_log.py index 7bcc05841..84da7a445 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -45,6 +45,13 @@ def test_log(): assert rendered == expected +def test_log_caller_frame_info(): + for i in range(2): + assert Console._caller_frame_info(i) == Console._caller_frame_info( + i, lambda: None + ) + + def test_justify(): console = Console(width=20, log_path=False, log_time=False, color_system=None) console.begin_capture() From 5d18450bba45faafc71812a74bd39fc9409a7a31 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 8 Jun 2021 20:17:09 +0100 Subject: [PATCH 25/33] remove widgets --- rich/widgets/__init__.py | 0 rich/widgets/color_changer.py | 18 ------------------ 2 files changed, 18 deletions(-) delete mode 100644 rich/widgets/__init__.py delete mode 100644 rich/widgets/color_changer.py diff --git a/rich/widgets/__init__.py b/rich/widgets/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/rich/widgets/color_changer.py b/rich/widgets/color_changer.py deleted file mode 100644 index ccf1236b0..000000000 --- a/rich/widgets/color_changer.py +++ /dev/null @@ -1,18 +0,0 @@ -from rich.align import Align -from rich.padding import Padding - -from rich.tui.widget import Widget -from rich.tui.events import KeyEvent - - -class ColorChanger(Widget): - def __init__(self) -> None: - self.color = 0 - - async def render(self): - return Align.center( - "Press any key", vertical="middle", style=f"color({self.color})" - ) - - async def on_key(self, event: KeyEvent) -> None: - self.color = ord(event.key) % 255 \ No newline at end of file From d491809f44a582403b7429b9aa2aa66e4a066d7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 14:33:14 +0000 Subject: [PATCH 26/33] Bump mypy from 0.812 to 0.901 Bumps [mypy](https://github.com/python/mypy) from 0.812 to 0.901. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.812...v0.901) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 53 ++++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index c7439824d..de1edb915 100644 --- a/poetry.lock +++ b/poetry.lock @@ -402,7 +402,7 @@ python-versions = "*" [[package]] name = "mypy" -version = "0.812" +version = "0.901" description = "Optional static typing for Python" category = "dev" optional = false @@ -410,11 +410,13 @@ python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" +toml = "*" +typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" @@ -892,7 +894,7 @@ jupyter = ["ipywidgets"] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "8a167cf4bdd118255c01f262d7b9b32b7b84056221c8a64a42acf9b6c0921941" +content-hash = "47deae496958478ace99be70b29a967965eb1cceb25f06b0284148778fa34722" [metadata.files] appdirs = [ @@ -1194,28 +1196,29 @@ mistune = [ {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, ] mypy = [ - {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, - {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, - {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, - {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, - {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, - {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, - {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, - {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, - {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, - {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, - {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, - {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, - {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, - {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, - {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, - {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, - {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, - {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, - {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, - {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, - {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, - {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, + {file = "mypy-0.901-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:91211acf1485a1db0b1261bc5f9ed450cba3c0dfd8da0a6680e94827591e34d7"}, + {file = "mypy-0.901-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c8bc628961cca4335ac7d1f2ed59b7125d9252fe4c78c3d66d30b50162359c99"}, + {file = "mypy-0.901-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4a622faa3be76114cdce009f8ec173401494cf9e8f22713e7ae75fee9d906ab3"}, + {file = "mypy-0.901-cp35-cp35m-win_amd64.whl", hash = "sha256:8183561bfd950e93eeab8379ae5ec65873c856f5b58498d23aa8691f74c86030"}, + {file = "mypy-0.901-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:da914faaa80c25f463913da6db12adba703822a768f452f29f75b40bb4357139"}, + {file = "mypy-0.901-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:307a6c047596d768c3d689734307e47a91596eb9dbb67cfdf7d1fd9117b27f13"}, + {file = "mypy-0.901-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a85c6759dcc6a9884131fa06a037bd34352aa3947e7f5d9d5a35652cc3a44bcd"}, + {file = "mypy-0.901-cp36-cp36m-win_amd64.whl", hash = "sha256:9941b685807b60c58020bb67b3217c9df47820dcd00425f55cdf71f31d3c42d9"}, + {file = "mypy-0.901-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:08cf1f31029612e1008a9432337ca4b1fbac989ff7c8200e2c9ec42705cd4c7b"}, + {file = "mypy-0.901-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bc61153eb4df769538bb4a6e1045f59c2e6119339690ec719feeacbfc3809e89"}, + {file = "mypy-0.901-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:1cd241966a35036f936d4739bd71a1c64e15f02bf7d12bb2815cccfb2993a9de"}, + {file = "mypy-0.901-cp37-cp37m-win_amd64.whl", hash = "sha256:97be0e8ed116f7f79472a49cf06dd45dd806771142401f684d4f13ee652a63c0"}, + {file = "mypy-0.901-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79beb6741df15395908ecc706b3a593a98804c1d5b5b6bd0c5b03b67c7ac03a0"}, + {file = "mypy-0.901-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf347c327c48d963bdef5bf365215d3e98b5fddbe5069fc796cec330e8235a20"}, + {file = "mypy-0.901-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:053b92ebae901fc7954677949049f70133f2f63e3e83dc100225c26d6a46fe95"}, + {file = "mypy-0.901-cp38-cp38-win_amd64.whl", hash = "sha256:f208cc967e566698c4e30a1f65843fc88d8da05a8693bac8b975417e0aee9ced"}, + {file = "mypy-0.901-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c86e3f015bfe7958646825d41c0691c6e5a5cd4015e3409b5c29c18a3c712534"}, + {file = "mypy-0.901-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8577d30daf1b7b6582020f539f76e78ee1ed64a0323b28c8e0333c45db9369f"}, + {file = "mypy-0.901-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:5ddd8f4096d5fc2e7d7bb3924ac22758862163ad2c1cdc902c4b85568160e90a"}, + {file = "mypy-0.901-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:4b54518e399c3f4dc53380d4252c83276b2e60623cfc5274076eb8aae57572ac"}, + {file = "mypy-0.901-cp39-cp39-win_amd64.whl", hash = "sha256:7845ad3a31407bfbd64c76d032c16ab546d282930f747023bf07c17b054bebc5"}, + {file = "mypy-0.901-py3-none-any.whl", hash = "sha256:61b10ba18a01d05fc46adbf4f18b0e92178f6b5fd0f45926ffc2a408b5419728"}, + {file = "mypy-0.901.tar.gz", hash = "sha256:18753a8bb9bcf031ff10009852bd48d781798ecbccf45be5449892e6af4e3f9f"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, diff --git a/pyproject.toml b/pyproject.toml index 2dbc4b837..ae61853b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ jupyter = ["ipywidgets"] [tool.poetry.dev-dependencies] pytest = "^6.2.3" black = "^20.8b1" -mypy = "^0.812" +mypy = "^0.901" pytest-cov = "^2.12.1" attrs = "^21.2.0" From 1af5eee96f9c444e046d5d08bd37a19818410651 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 15:39:58 +0100 Subject: [PATCH 27/33] pretty docs --- CHANGELOG.md | 1 + docs/source/index.rst | 1 + docs/source/pretty.rst | 162 +++++++++++++++++++++++++++++++++++++++++ examples/repr.py | 25 +++++++ rich/highlighter.py | 2 +- rich/layout.py | 2 - rich/pretty.py | 57 ++++++++++----- rich/region.py | 16 +--- rich/repr.py | 41 ++++++++++- rich/segment.py | 3 +- tests/test_layout.py | 4 +- tests/test_pretty.py | 5 +- tests/test_repr.py | 27 ++++++- tests/test_segment.py | 13 +++- 14 files changed, 314 insertions(+), 45 deletions(-) create mode 100644 docs/source/pretty.rst create mode 100644 examples/repr.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe252fb4..ae6b8a26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Console.size setter - Added Console.width setter - Added Console.height setter +- Added angular style Rich reprs ## [10.2.2] - 2021-05-19 diff --git a/docs/source/index.rst b/docs/source/index.rst index 12198de48..bad2459e2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,6 +16,7 @@ Welcome to Rich's documentation! markup.rst text.rst highlighting.rst + pretty.rst logging.rst traceback.rst prompt.rst diff --git a/docs/source/pretty.rst b/docs/source/pretty.rst new file mode 100644 index 000000000..e70e968b3 --- /dev/null +++ b/docs/source/pretty.rst @@ -0,0 +1,162 @@ +Pretty +====== + +In addition to syntax highlighting, Rich will automatically format any containers (lists, dicts, sets etc) you print to the console. The formatting method used by Rich follows well established conventions in the Python world to present data in a more readable way. + +Run the following command to see an example of pretty printed output:: + + python -m rich.pretty + +Note how the output will change if you change the width of the terminal. + +pprint method +------------- + +The :func:`~rich.pretty.pprint` method offers a few more argument you can use to tweak how objects are pretty printed. Here's how you would import it:: + + >>> from rich.pretty import pprint + >>> pprint(locals()) + +Indent guides +~~~~~~~~~~~~~ + +Rich can draw *intent guides* which are vertical lines to highlight the indent level of a data structure. These can make it easier to read more deeply nested output. The pprint method enables indent guides by default, but you can set ``indent_guides=False`` to disable this feature. + +Expand all +~~~~~~~~~~ + +Rich is quite conservative about expanding data structures and will try to fit as much in each line as it can. If you prefer, you can tell Rich to fully expand all data structures by setting ``expand_all=True``. Here's an example:: + + >>> pprint(["eggs", "ham"], expand_all=True) + +Truncating pretty output +~~~~~~~~~~~~~~~~~~~~~~~~ + +Very long data structures can be difficult to read and you may find yourself scrolling through multiple pages to find the output you are interested in. Rich can truncate containers and long strings if you just need a general overview a data structure. + +If you set the ``max_length`` argument to an integer then Rich will truncate containers with more than the given number of elements. If data is truncated, Rich will display an ellipsis ``...`` and the number of elements not shown. + +Here's an example:: + + >>> pprint(locals(), max_length=2) + +Truncating long strings +~~~~~~~~~~~~~~~~~~~~~~~ + +If you set the `max_string` argument to an integer, Rich will truncate strings over that length. Truncated string will be appended with the number of characters that have not been shown. Here's an example:: + + >>> pprint("Where there is a Will, there is a Way", max_string=21) + +Pretty renderable +----------------- + +Rich offers a :class:`~rich.pretty.Pretty` class which you can user to insert pretty printed data in to another renderable. + +The following example, prints pretty printed output within a simple panel:: + + from rich import print + from rich.pretty import Pretty + from rich.panel import Panel + + pretty = Pretty(locals()) + panel = Panel(pretty) + print(panel) + +There are a large number of options to tweak the pretty formatting, See the :class:`~rich.pretty.Pretty` reference for details. + +Rich Repr +--------- + +Rich is able to syntax highlight any output, but the extra formatting done by the pretty printing is restricted to builtin containers, dataclasses, and other objects Rich knows about, such as objects generated by the `attrs `_ library. Fortunately Rich offers a simple protocol you can use to add pretty printable output to any object. + +First, let's look at a class that might benefit from a Rich repr:: + + class Bird: + def __init__(self, name, eats=None, fly=True, extinct=False): + self.name = name + self.eats = list(eats) if eats else [] + self.fly = fly + self.extinct = extinct + + def __repr__(self): + return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})" + + BIRDS = { + "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]), + "penguin": Bird("penguin", eats=["fish"], fly=False), + "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True) + } + print(BIRDS) + +The result of this script would be:: + + {'gull': Bird('gull', eats=['fish', 'chips', 'ice cream', 'sausage rolls'], fly=True, extinct=False), 'penguin': Bird('penguin', eats=['fish'], fly=False, extinct=False), 'dodo': Bird('dodo', eats=['fruit'], fly=False, extinct=True)} + +The output is long enough to wrap on to the next line, which can make it hard to read. The repr strings are informative but a little verbose since they include default arguments. If we print this with Rich, things are improved somewhat:: + + { + 'gull': Bird('gull', eats=['fish', 'chips', 'ice cream', 'sausage rolls'], + fly=True, extinct=False), + 'penguin': Bird('penguin', eats=['fish'], fly=False, extinct=False), + 'dodo': Bird('dodo', eats=['fruit'], fly=False, extinct=True) + } + +Rich knows how to format the container dict, but the repr strings are still verbose, and there is some wrapping of the output (assuming an 80 character terminal). + +We can solve both these issues by adding the following ``__rich_repr__`` method:: + + def __rich_repr__(self): + yield self.name + yield "eats", self.eats + yield "fly", self.fly, True + yield "extinct", self.extinct, False + +Now if we print the same object with Rich we would get the following:: + + { + 'gull': Bird( + 'gull', + eats=['fish', 'chips', 'ice cream', 'sausage rolls'] + ), + 'penguin': Bird('penguin', eats=['fish'], fly=False), + 'dodo': Bird('dodo', eats=['fruit'], fly=False, extinct=True) + } + +The default arguments have been omitted, and the output has been formatted nicely. Even if we have less room in the terminal, the result is still quite readable:: + + { + 'gull': Bird( + 'gull', + eats=[ + 'fish', + 'chips', + 'ice cream', + 'sausage rolls' + ] + ), + 'penguin': Bird( + 'penguin', + eats=['fish'], + fly=False + ), + 'dodo': Bird( + 'dodo', + eats=['fruit'], + fly=False, + extinct=True + ) + } + +You can add a ``__rich_repr__`` method to any class to enable Rich reprs. This method should return an iterable of tuples. You could return a list of tuples, but it's easier to express with the ``yield`` keywords, making it a *generator*. + +Each tuple specifies an element in the output. + +- ``yield value`` will generate a positional argument. +- ``yield name, value`` will generate a keyword argument. +- ``yield name, value, default`` will generate a keyword argument *if* ``value`` is not equal to ``default``. + +You can also tell Rich to generate the *angular bracket* style of repr, which tend to be used where there is no easy way to recreate the object's constructor. To do this set the function attribute ``"angluar"`` to ``True`` immediately after your ``__rich_repr__`` methods. For example:: + + __rich_repr__.angular = True + +Note that you can add ``__rich_repr__`` methods to third-party libraries *without* including Rich as a dependency. If Rich is not installed, then nothing will break. Hopefully many more libraries will adopt Rich repr methods in the future, which will aid debugging! \ No newline at end of file diff --git a/examples/repr.py b/examples/repr.py new file mode 100644 index 000000000..b6e5a47a6 --- /dev/null +++ b/examples/repr.py @@ -0,0 +1,25 @@ +class Bird: + def __init__(self, name, eats=None, fly=True, extinct=False): + self.name = name + self.eats = list(eats) if eats else [] + self.fly = fly + self.extinct = extinct + + def __repr__(self): + return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})" + + def __rich_repr__(self): + yield self.name + yield "eats", self.eats + yield "fly", self.fly, True + yield "extinct", self.extinct, False + + __rich_repr__.angular = True + +from rich import print +BIRDS = { + "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]), + "penguin": Bird("penguin", eats=["fish"], fly=False), + "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True) +} +print(BIRDS) diff --git a/rich/highlighter.py b/rich/highlighter.py index 59eae5858..9c3d87681 100644 --- a/rich/highlighter.py +++ b/rich/highlighter.py @@ -81,7 +81,7 @@ class ReprHighlighter(RegexHighlighter): base_style = "repr." highlights = [ - r"(?P\<)(?P[\w\-\.\:]*)(?P.*?)(?P\>)", + r"(?P\<)(?P[\w\-\.\:]*)(?P[\w\W]*?)(?P\>)", r"(?P[\w_]{1,50})=(?P\"?[\w_]+\"?)?", r"(?P[\{\[\(\)\]\}])", _combine_regex( diff --git a/rich/layout.py b/rich/layout.py index 2d617c738..3f93534d6 100644 --- a/rich/layout.py +++ b/rich/layout.py @@ -181,8 +181,6 @@ def __rich_repr__(self) -> RichReprResult: yield "minimum_size", self.minimum_size, 1 yield "ratio", self.ratio, 1 - __rich_repr__.meta = {"angular": True} - @property def renderable(self) -> RenderableType: """Layout renderable.""" diff --git a/rich/pretty.py b/rich/pretty.py index 827faba55..4695e9b60 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -301,11 +301,7 @@ class Node: is_tuple: bool = False children: Optional[List["Node"]] = None key_separator = ": " - - @property - def separator(self) -> str: - """Get separator between items.""" - return "" if self.last else "," + separator: str = ", " def iter_tokens(self) -> Iterable[str]: """Generate tokens for this node.""" @@ -324,7 +320,7 @@ def iter_tokens(self) -> Iterable[str]: for child in self.children: yield from child.iter_tokens() if not child.last: - yield ", " + yield self.separator yield self.close_brace else: yield self.empty @@ -380,12 +376,14 @@ def render( class _Line: """A line in repr output.""" + parent: Optional["_Line"] = None is_root: bool = False node: Optional[Node] = None text: str = "" suffix: str = "" whitespace: str = "" expanded: bool = False + last: bool = False @property def expandable(self) -> bool: @@ -407,31 +405,39 @@ def expand(self, indent_size: int) -> Iterable["_Line"]: whitespace = self.whitespace assert node.children if node.key_repr: - yield _Line( + new_line = yield _Line( text=f"{node.key_repr}{node.key_separator}{node.open_brace}", whitespace=whitespace, ) else: - yield _Line(text=node.open_brace, whitespace=whitespace) + new_line = yield _Line(text=node.open_brace, whitespace=whitespace) child_whitespace = self.whitespace + " " * indent_size tuple_of_one = node.is_tuple and len(node.children) == 1 - for child in node.children: - separator = "," if tuple_of_one else child.separator + for last, child in loop_last(node.children): + separator = "," if tuple_of_one else node.separator line = _Line( + parent=new_line, node=child, whitespace=child_whitespace, suffix=separator, + last=last and not tuple_of_one, ) yield line yield _Line( text=node.close_brace, whitespace=whitespace, - suffix="," if (tuple_of_one and not self.is_root) else node.separator, + suffix=self.suffix, + last=self.last, ) def __str__(self) -> str: - return f"{self.whitespace}{self.text}{self.node or ''}{self.suffix}" + if self.last: + return f"{self.whitespace}{self.text}{self.node or ''}" + else: + return ( + f"{self.whitespace}{self.text}{self.node or ''}{self.suffix.rstrip()}" + ) def traverse( @@ -493,17 +499,28 @@ def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]: yield arg if hasattr(obj, "__rich_repr__"): + angular = getattr(obj.__rich_repr__, "angular", False) args = list(iter_rich_args(obj.__rich_repr__())) + class_name = obj.__class__.__name__ if args: children = [] append = children.append - node = Node( - open_brace=f"{obj.__class__.__name__}(", - close_brace=")", - children=children, - last=root, - ) + if angular: + node = Node( + open_brace=f"<{class_name} ", + close_brace=">", + children=children, + last=root, + separator=" ", + ) + else: + node = Node( + open_brace=f"{class_name}(", + close_brace=")", + children=children, + last=root, + ) for last, arg in loop_last(args): if isinstance(arg, tuple): key, child = arg @@ -518,7 +535,9 @@ def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]: append(child_node) else: node = Node( - value_repr=f"{obj.__class__.__name__}()", children=[], last=root + value_repr=f"<{class_name}>" if angular else f"{class_name}()", + children=[], + last=root, ) elif _is_attr_object(obj): children = [] diff --git a/rich/region.py b/rich/region.py index 3a6afa750..d82fe7bd6 100644 --- a/rich/region.py +++ b/rich/region.py @@ -7,18 +7,4 @@ class Region(NamedTuple): x: int y: int width: int - height: int - - def contains(self, x: int, y: int) -> bool: - """Check if a point is in the region. - - Args: - x (int): X coordinate (column) - y (int): Y coordinate (row) - - Returns: - bool: True if the point is within the region. - """ - return ((self.x + self.width) > x >= self.x) and ( - ((self.y + self.height) > y >= self.y) - ) \ No newline at end of file + height: int \ No newline at end of file diff --git a/rich/repr.py b/rich/repr.py index 988b42958..afd1a5d5b 100644 --- a/rich/repr.py +++ b/rich/repr.py @@ -13,6 +13,7 @@ def rich_repr(cls: Type[T]) -> Type[T]: def auto_repr(self: Any) -> str: repr_str: List[str] = [] append = repr_str.append + angular = getattr(self.__rich_repr__, "angular", False) for arg in self.__rich_repr__(): if isinstance(arg, tuple): if len(arg) == 1: @@ -20,16 +21,52 @@ def auto_repr(self: Any) -> str: else: key, value, *default = arg if key is None: - append(value) + append(repr(value)) else: if len(default) and default[0] == value: continue append(f"{key}={value!r}") else: append(repr(arg)) - return f"{self.__class__.__name__}({', '.join(repr_str)})" + if angular: + return f"<{self.__class__.__name__} {' '.join(repr_str)}>" + else: + return f"{self.__class__.__name__}({', '.join(repr_str)})" auto_repr.__doc__ = "Return repr(self)" cls.__repr__ = auto_repr # type: ignore return cls + + +if __name__ == "__main__": + + @rich_repr + class Foo: + def __rich_repr__(self) -> RichReprResult: + + yield "foo" + yield "bar", {"shopping": ["eggs", "ham", "pineapple"]} + yield "buy", "hand sanitizer" + + __rich_repr__.angular = False + + foo = Foo() + from rich.console import Console + from rich import print + + console = Console() + + console.rule("Standard repr") + console.print(foo) + + console.print(foo, width=60) + console.print(foo, width=30) + + console.rule("Angular repr") + Foo.__rich_repr__.angular = True + + console.print(foo) + + console.print(foo, width=60) + console.print(foo, width=30) diff --git a/rich/segment.py b/rich/segment.py index 62197a1c1..60989e592 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -409,7 +409,8 @@ def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: class Segments: - """A simple renderable to render an iterable of segments. + """A simple renderable to render an iterable of segments. This class may be useful if + you want to print segments outside of a __rich_console__ method. Args: segments (Iterable[Segment]): An iterable of segments. diff --git a/tests/test_layout.py b/tests/test_layout.py index e233a7b9c..5eae0f735 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -59,6 +59,7 @@ def test_render(): result = capture.get() print(repr(result)) expected = "╭──────────────────────────────────────────────────────────╮\n│ foo │\n│ │\n│ │\n╰──────────────────────────────────────────────────────────╯\nfoobar ╭───── 'right' (30 x 5) ─────╮\n │ │\n │ Layout(name='right') │\n │ │\n ╰────────────────────────────╯\n" + assert result == expected @@ -74,7 +75,7 @@ def test_tree(): result = capture.get() print(repr(result)) expected = "⬍ Layout(name='root') \n├── ⬍ Layout(size=2) \n└── ⬌ Layout(name='bar') \n ├── ⬍ Layout() \n └── ⬍ Layout() \n" - + print(result, "\n", expected) assert result == expected @@ -92,5 +93,4 @@ def test_refresh_screen(): print() print(repr(result)) expected = "\x1b[1;1H\x1b[34m╭─\x1b[0m\x1b[34m \x1b[0m\x1b[32m'foo'\x1b[0m\x1b[34m─╮\x1b[0m\x1b[2;1H\x1b[34m│\x1b[0m \x1b[1;35mLayout\x1b[0m \x1b[34m│\x1b[0m\x1b[3;1H\x1b[34m│\x1b[0m \x1b[1m(\x1b[0m \x1b[34m│\x1b[0m\x1b[4;1H\x1b[34m│\x1b[0m \x1b[33mna\x1b[0m \x1b[34m│\x1b[0m\x1b[5;1H\x1b[34m╰────────╯\x1b[0m" - assert result == expected diff --git a/tests/test_pretty.py b/tests/test_pretty.py index ec12a058f..cf838db20 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -38,7 +38,7 @@ def test_pretty(): result = pretty_repr(test, max_width=80) print(result) - print(repr(result)) + # print(repr(result)) expected = "{\n 'foo': [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}],\n 'bar': {\n 'egg': 'baz',\n 'words': [\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World',\n 'Hello World'\n ]\n },\n False: 'foo',\n True: '',\n 'text': ('Hello World', 'foo bar baz egg')\n}" print(expected) assert result == expected @@ -172,6 +172,9 @@ def test_tuples(): result = console.end_capture() print(repr(result)) expected = "(1,)\n(\n│ 1,\n)\n(\n│ (\n│ │ 1,\n│ ),\n)\n" + print(result) + print("--") + print(expected) assert result == expected diff --git a/tests/test_repr.py b/tests/test_repr.py index 2cc7aeb07..473be3709 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -13,16 +13,32 @@ def __init__(self, foo: str, bar: Optional[int] = None, egg: int = 1): def __rich_repr__(self): yield self.foo - yield self.foo, + yield None, self.foo, yield "bar", self.bar, None yield "egg", self.egg +@rich_repr +class Bar(Foo): + def __rich_repr__(self): + yield (self.foo,) + yield None, self.foo, + yield "bar", self.bar, None + yield "egg", self.egg + + __rich_repr__.angular = True + + def test_rich_repr() -> None: assert (repr(Foo("hello"))) == "Foo('hello', 'hello', egg=1)" assert (repr(Foo("hello", bar=3))) == "Foo('hello', 'hello', bar=3, egg=1)" +def test_rich_angular() -> None: + assert (repr(Bar("hello"))) == "" + assert (repr(Bar("hello", bar=3))) == "" + + def test_rich_pretty() -> None: console = Console() with console.capture() as capture: @@ -30,3 +46,12 @@ def test_rich_pretty() -> None: result = capture.get() expected = "Foo('hello', 'hello', bar=3, egg=1)\n" assert result == expected + + +def test_rich_pretty_angular() -> None: + console = Console() + with console.capture() as capture: + console.print(Bar("hello", bar=3)) + result = capture.get() + expected = "\n" + assert result == expected \ No newline at end of file diff --git a/tests/test_segment.py b/tests/test_segment.py index 3aa0cb11f..edf8d3a74 100644 --- a/tests/test_segment.py +++ b/tests/test_segment.py @@ -1,7 +1,7 @@ import sys from rich.segment import ControlType -from rich.segment import Segment +from rich.segment import Segment, Segments from rich.style import Style @@ -127,3 +127,14 @@ def test_is_control(): assert Segment("foo", Style(bold=True)).is_control == False assert Segment("foo", Style(bold=True), []).is_control == True assert Segment("foo", Style(bold=True), [(ControlType.HOME, 0)]).is_control == True + + +def test_segments_renderable(): + segments = Segments([Segment("foo")]) + assert list(segments.__rich_console__(None, None)) == [Segment("foo")] + + segments = Segments([Segment("foo")], new_lines=True) + assert list(segments.__rich_console__(None, None)) == [ + Segment("foo"), + Segment.line(), + ] From 928d8efbc465a28a2415bd6999cb967ee93c640a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 18:20:11 +0100 Subject: [PATCH 28/33] ununsed ignores --- rich/logging.py | 2 +- rich/progress.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/logging.py b/rich/logging.py index 87b648b69..f59f1f2f9 100644 --- a/rich/logging.py +++ b/rich/logging.py @@ -142,7 +142,7 @@ def emit(self, record: LogRecord) -> None: if self.formatter: record.message = record.getMessage() formatter = self.formatter - if hasattr(formatter, "usesTime") and formatter.usesTime(): # type: ignore + if hasattr(formatter, "usesTime") and formatter.usesTime(): record.asctime = formatter.formatTime(record, formatter.datefmt) message = formatter.formatMessage(record) diff --git a/rich/progress.py b/rich/progress.py index e67173d3f..0c53837b3 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -484,7 +484,7 @@ class Task: def get_time(self) -> float: """float: Get the current time, in seconds.""" - return self._get_time() # type: ignore + return self._get_time() @property def started(self) -> bool: From 5b5e57715626648c929dfd01701e478e3d71b619 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 18:22:24 +0100 Subject: [PATCH 29/33] typing and blackening --- rich/region.py | 2 +- rich/repr.py | 4 ++-- tests/test_repr.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rich/region.py b/rich/region.py index d82fe7bd6..75b3631c3 100644 --- a/rich/region.py +++ b/rich/region.py @@ -7,4 +7,4 @@ class Region(NamedTuple): x: int y: int width: int - height: int \ No newline at end of file + height: int diff --git a/rich/repr.py b/rich/repr.py index afd1a5d5b..a6d164c97 100644 --- a/rich/repr.py +++ b/rich/repr.py @@ -49,7 +49,7 @@ def __rich_repr__(self) -> RichReprResult: yield "bar", {"shopping": ["eggs", "ham", "pineapple"]} yield "buy", "hand sanitizer" - __rich_repr__.angular = False + __rich_repr__.angular = False # type: ignore foo = Foo() from rich.console import Console @@ -64,7 +64,7 @@ def __rich_repr__(self) -> RichReprResult: console.print(foo, width=30) console.rule("Angular repr") - Foo.__rich_repr__.angular = True + Foo.__rich_repr__.angular = True # type: ignore console.print(foo) diff --git a/tests/test_repr.py b/tests/test_repr.py index 473be3709..704147f51 100644 --- a/tests/test_repr.py +++ b/tests/test_repr.py @@ -54,4 +54,4 @@ def test_rich_pretty_angular() -> None: console.print(Bar("hello", bar=3)) result = capture.get() expected = "\n" - assert result == expected \ No newline at end of file + assert result == expected From 74efdcd860e1de266ee09857d3c55d7df27de3a3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 18:24:25 +0100 Subject: [PATCH 30/33] Version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 68cbe2a91..66fa635ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rich" homepage = "https://github.com/willmcgugan/rich" documentation = "https://rich.readthedocs.io/en/latest/" -version = "10.2.2" +version = "10.3.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" From 932c451dd9c66b8938676105d0ffc1823f131cf8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 18:28:10 +0100 Subject: [PATCH 31/33] repr example --- examples/repr.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/repr.py b/examples/repr.py index b6e5a47a6..628d7d096 100644 --- a/examples/repr.py +++ b/examples/repr.py @@ -1,3 +1,7 @@ +from rich.repr import rich_repr + + +@rich_repr class Bird: def __init__(self, name, eats=None, fly=True, extinct=False): self.name = name @@ -5,9 +9,6 @@ def __init__(self, name, eats=None, fly=True, extinct=False): self.fly = fly self.extinct = extinct - def __repr__(self): - return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})" - def __rich_repr__(self): yield self.name yield "eats", self.eats @@ -15,11 +16,13 @@ def __rich_repr__(self): yield "extinct", self.extinct, False __rich_repr__.angular = True - -from rich import print + + +from rich import print + BIRDS = { "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]), "penguin": Bird("penguin", eats=["fish"], fly=False), - "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True) + "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True), } print(BIRDS) From ed2e4cc71b11b3947c7c70b1de3a9daf1ef92e7b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 18:38:14 +0100 Subject: [PATCH 32/33] mypy upgrade --- examples/repr.py | 2 -- poetry.lock | 48 ++++++++++++------------------------------------ pyproject.toml | 3 ++- 3 files changed, 14 insertions(+), 39 deletions(-) diff --git a/examples/repr.py b/examples/repr.py index 628d7d096..1d5085b1a 100644 --- a/examples/repr.py +++ b/examples/repr.py @@ -15,8 +15,6 @@ def __rich_repr__(self): yield "fly", self.fly, True yield "extinct", self.extinct, False - __rich_repr__.angular = True - from rich import print diff --git a/poetry.lock b/poetry.lock index c7439824d..6f1fdfe1f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -839,6 +839,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "types-dataclasses" +version = "0.1.3" +description = "Typing stubs for dataclasses" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "3.10.0.0" @@ -892,7 +900,7 @@ jupyter = ["ipywidgets"] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "8a167cf4bdd118255c01f262d7b9b32b7b84056221c8a64a42acf9b6c0921941" +content-hash = "4384858628de11da9a05f6fb94a6172426785cb02130f2cfa2180cfcb1e5c673" [metadata.files] appdirs = [ @@ -922,10 +930,6 @@ argon2-cffi = [ {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"}, {file = "argon2_cffi-20.1.0-cp39-cp39-win32.whl", hash = "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be"}, {file = "argon2_cffi-20.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32"}, - {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b94042e5dcaa5d08cf104a54bfae614be502c6f44c9c89ad1535b2ebdaacbd4c"}, - {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:8282b84ceb46b5b75c3a882b28856b8cd7e647ac71995e71b6705ec06fc232c3"}, - {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3aa804c0e52f208973845e8b10c70d8957c9e5a666f702793256242e9167c4e0"}, - {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:36320372133a003374ef4275fbfce78b7ab581440dfca9f9471be3dd9a522428"}, ] async-generator = [ {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, @@ -967,36 +971,24 @@ cffi = [ {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"}, - {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"}, {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"}, - {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"}, {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"}, - {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"}, {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"}, - {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"}, {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, @@ -1154,39 +1146,20 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mistune = [ @@ -1506,6 +1479,9 @@ typed-ast = [ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] +types-dataclasses = [ + {file = "types_dataclasses-0.1.3-py2.py3-none-any.whl", hash = "sha256:133fd65e2475817486e4e0c78302a049210f2ee6d69146f0d73155e6934c06ee"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, diff --git a/pyproject.toml b/pyproject.toml index b7b389c0a..345f6b0a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,10 @@ jupyter = ["ipywidgets"] [tool.poetry.dev-dependencies] pytest = "^6.2.3" black = "^20.8b1" -mypy = "^0.812" +mypy = "^0.901" pytest-cov = "^2.12.1" attrs = "^21.2.0" +types-dataclasses = "^0.1.3" [build-system] requires = ["poetry-core>=1.0.0"] From d5d08a2c4a938b35a1436a1f9e0d2f3373ceac4c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 9 Jun 2021 18:42:33 +0100 Subject: [PATCH 33/33] lock update --- poetry.lock | 251 +++++++++++++++++++++++++++------------------------- 1 file changed, 128 insertions(+), 123 deletions(-) diff --git a/poetry.lock b/poetry.lock index a1313b280..81774fca7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -118,11 +118,15 @@ pycparser = "*" [[package]] name = "click" -version = "7.1.2" +version = "8.0.1" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -164,7 +168,7 @@ python-versions = ">=3.6, <3.7" [[package]] name = "decorator" -version = "5.0.7" +version = "5.0.9" description = "Decorators for Humans" category = "main" optional = true @@ -188,7 +192,7 @@ python-versions = ">=2.7" [[package]] name = "importlib-metadata" -version = "4.0.1" +version = "4.5.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -212,7 +216,7 @@ python-versions = "*" [[package]] name = "ipykernel" -version = "5.5.4" +version = "5.5.5" description = "IPython Kernel for Jupyter" category = "main" optional = true @@ -303,17 +307,17 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [[package]] name = "jinja2" -version = "2.11.3" +version = "3.0.1" description = "A very fast and expressive template engine." category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -MarkupSafe = ">=0.23" +MarkupSafe = ">=2.0" [package.extras] -i18n = ["Babel (>=0.8)"] +i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" @@ -386,11 +390,11 @@ python-versions = ">=3.6" [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = true -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "mistune" @@ -504,7 +508,7 @@ python-versions = ">=3.5" [[package]] name = "notebook" -version = "6.3.0" +version = "6.4.0" description = "A web-based notebook environment for interactive computing" category = "main" optional = true @@ -527,7 +531,7 @@ tornado = ">=6.1" traitlets = ">=4.2.1" [package.extras] -docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme"] +docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"] json-logging = ["json-logging"] test = ["pytest", "coverage", "requests", "nbval", "selenium", "pytest-cov", "requests-unixsocket"] @@ -605,7 +609,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "prometheus-client" -version = "0.10.1" +version = "0.11.0" description = "Python client for the Prometheus monitoring system." category = "main" optional = true @@ -724,7 +728,7 @@ six = ">=1.5" [[package]] name = "pywin32" -version = "300" +version = "301" description = "Python for Window Extensions" category = "main" optional = true @@ -732,15 +736,15 @@ python-versions = "*" [[package]] name = "pywinpty" -version = "1.0.1" +version = "1.1.1" description = "Pseudo terminal support for Windows from Python." category = "main" optional = true -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "pyzmq" -version = "22.0.3" +version = "22.1.0" description = "Python bindings for 0MQ" category = "main" optional = true @@ -776,7 +780,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "terminado" -version = "0.9.4" +version = "0.10.0" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." category = "main" optional = true @@ -784,7 +788,7 @@ python-versions = ">=3.6" [package.dependencies] ptyprocess = {version = "*", markers = "os_name != \"nt\""} -pywinpty = {version = ">=0.5", markers = "os_name == \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} tornado = ">=4" [package.extras] @@ -792,14 +796,14 @@ test = ["pytest"] [[package]] name = "testpath" -version = "0.4.4" +version = "0.5.0" description = "Test utilities for code working with files and commands" category = "main" optional = true -python-versions = "*" +python-versions = ">= 3.5" [package.extras] -test = ["pathlib2"] +test = ["pytest", "pathlib2"] [[package]] name = "toml" @@ -845,7 +849,7 @@ python-versions = "*" name = "types-dataclasses" version = "0.1.3" description = "Typing stubs for dataclasses" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -902,7 +906,7 @@ jupyter = ["ipywidgets"] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "4384858628de11da9a05f6fb94a6172426785cb02130f2cfa2180cfcb1e5c673" +content-hash = "60c041afde141aae92314140c1284d42f71045545a2e6a71314dcd3a02d50469" [metadata.files] appdirs = [ @@ -996,8 +1000,8 @@ cffi = [ {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, ] click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1066,8 +1070,8 @@ dataclasses = [ {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] decorator = [ - {file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"}, - {file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"}, + {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, + {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, ] defusedxml = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, @@ -1078,16 +1082,16 @@ entrypoints = [ {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"}, - {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"}, + {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, + {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] ipykernel = [ - {file = "ipykernel-5.5.4-py3-none-any.whl", hash = "sha256:f57739bf26d7396549562c0c888b96be896385ce099fb34ca89af359b7436b25"}, - {file = "ipykernel-5.5.4.tar.gz", hash = "sha256:1ce0e83672cc3bfdc1ffb5603e1d77ab125f24b41abc4612e22bfb3e994c0db2"}, + {file = "ipykernel-5.5.5-py3-none-any.whl", hash = "sha256:29eee66548ee7c2edb7941de60c0ccf0a7a8dd957341db0a49c5e8e6a0fcb712"}, + {file = "ipykernel-5.5.5.tar.gz", hash = "sha256:e976751336b51082a89fc2099fb7f96ef20f535837c398df6eab1283c2070884"}, ] ipython = [ {file = "ipython-7.16.1-py3-none-any.whl", hash = "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64"}, @@ -1106,8 +1110,8 @@ jedi = [ {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] jinja2 = [ - {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, - {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -1130,39 +1134,40 @@ jupyterlab-widgets = [ {file = "jupyterlab_widgets-1.0.0.tar.gz", hash = "sha256:5c1a29a84d3069208cb506b10609175b249b6486d6b1cbae8fcde2a11584fb78"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, @@ -1214,8 +1219,8 @@ nest-asyncio = [ {file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"}, ] notebook = [ - {file = "notebook-6.3.0-py3-none-any.whl", hash = "sha256:cb271af1e8134e3d6fc6d458bdc79c40cbfc84c1eb036a493f216d58f0880e92"}, - {file = "notebook-6.3.0.tar.gz", hash = "sha256:cbc9398d6c81473e9cdb891d2cae9c0d3718fca289dda6d26df5cb660fcadc7d"}, + {file = "notebook-6.4.0-py3-none-any.whl", hash = "sha256:f7f0a71a999c7967d9418272ae4c3378a220bd28330fbfb49860e46cf8a5838a"}, + {file = "notebook-6.4.0.tar.gz", hash = "sha256:9c4625e2a2aa49d6eae4ce20cbc3d8976db19267e32d2a304880e0c10bf8aef9"}, ] packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, @@ -1245,8 +1250,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] prometheus-client = [ - {file = "prometheus_client-0.10.1-py2.py3-none-any.whl", hash = "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa"}, - {file = "prometheus_client-0.10.1.tar.gz", hash = "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"}, + {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"}, + {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"}, ] prompt-toolkit = [ {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, @@ -1288,57 +1293,57 @@ python-dateutil = [ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] pywin32 = [ - {file = "pywin32-300-cp35-cp35m-win32.whl", hash = "sha256:1c204a81daed2089e55d11eefa4826c05e604d27fe2be40b6bf8db7b6a39da63"}, - {file = "pywin32-300-cp35-cp35m-win_amd64.whl", hash = "sha256:350c5644775736351b77ba68da09a39c760d75d2467ecec37bd3c36a94fbed64"}, - {file = "pywin32-300-cp36-cp36m-win32.whl", hash = "sha256:a3b4c48c852d4107e8a8ec980b76c94ce596ea66d60f7a697582ea9dce7e0db7"}, - {file = "pywin32-300-cp36-cp36m-win_amd64.whl", hash = "sha256:27a30b887afbf05a9cbb05e3ffd43104a9b71ce292f64a635389dbad0ed1cd85"}, - {file = "pywin32-300-cp37-cp37m-win32.whl", hash = "sha256:d7e8c7efc221f10d6400c19c32a031add1c4a58733298c09216f57b4fde110dc"}, - {file = "pywin32-300-cp37-cp37m-win_amd64.whl", hash = "sha256:8151e4d7a19262d6694162d6da85d99a16f8b908949797fd99c83a0bfaf5807d"}, - {file = "pywin32-300-cp38-cp38-win32.whl", hash = "sha256:fbb3b1b0fbd0b4fc2a3d1d81fe0783e30062c1abed1d17c32b7879d55858cfae"}, - {file = "pywin32-300-cp38-cp38-win_amd64.whl", hash = "sha256:60a8fa361091b2eea27f15718f8eb7f9297e8d51b54dbc4f55f3d238093d5190"}, - {file = "pywin32-300-cp39-cp39-win32.whl", hash = "sha256:638b68eea5cfc8def537e43e9554747f8dee786b090e47ead94bfdafdb0f2f50"}, - {file = "pywin32-300-cp39-cp39-win_amd64.whl", hash = "sha256:b1609ce9bd5c411b81f941b246d683d6508992093203d4eb7f278f4ed1085c3f"}, + {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, + {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, + {file = "pywin32-301-cp36-cp36m-win32.whl", hash = "sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0"}, + {file = "pywin32-301-cp36-cp36m-win_amd64.whl", hash = "sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78"}, + {file = "pywin32-301-cp37-cp37m-win32.whl", hash = "sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b"}, + {file = "pywin32-301-cp37-cp37m-win_amd64.whl", hash = "sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a"}, + {file = "pywin32-301-cp38-cp38-win32.whl", hash = "sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17"}, + {file = "pywin32-301-cp38-cp38-win_amd64.whl", hash = "sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96"}, + {file = "pywin32-301-cp39-cp39-win32.whl", hash = "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe"}, + {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"}, ] pywinpty = [ - {file = "pywinpty-1.0.1-cp36-none-win_amd64.whl", hash = "sha256:739094e8d0d685a64c92ff91424cf43da9296110349036161ab294268e444d05"}, - {file = "pywinpty-1.0.1-cp37-none-win_amd64.whl", hash = "sha256:5447b8c158e5807237f80ea4e14262f0c05ff7c4d39f1c4b697ea6e8920786b2"}, - {file = "pywinpty-1.0.1-cp38-none-win_amd64.whl", hash = "sha256:aa3e4178503ff6be3e8a1d9ae4ce77de9058308562dbf26b505a51583be9f02d"}, - {file = "pywinpty-1.0.1-cp39-none-win_amd64.whl", hash = "sha256:58e23d59891e624d478ec7bcc42ced0ecfbf0a4e7cb0217de714f785f71c2461"}, - {file = "pywinpty-1.0.1.tar.gz", hash = "sha256:b3512d4a964a0abae1b77b6908917c62ea0ad7d8178696e4e973877fe9e820f9"}, + {file = "pywinpty-1.1.1-cp36-none-win_amd64.whl", hash = "sha256:fa2a0af28eaaacc59227c6edbc0f1525704d68b2dfa3e5b47ae21c5aa25d6d78"}, + {file = "pywinpty-1.1.1-cp37-none-win_amd64.whl", hash = "sha256:0fe3f538860c6b06e6fbe63da0ee5dab5194746b0df1be7ed65b4fce5da21d21"}, + {file = "pywinpty-1.1.1-cp38-none-win_amd64.whl", hash = "sha256:12c89765b3102d2eea3d39d191d1b0baea68fb5e3bd094c67b2575b3c9ebfa12"}, + {file = "pywinpty-1.1.1-cp39-none-win_amd64.whl", hash = "sha256:50bce6f7d9857ffe9694847af7e8bf989b198d0ebc2bf30e26d54c4622cb5c50"}, + {file = "pywinpty-1.1.1.tar.gz", hash = "sha256:4a3ffa2444daf15c5f65a76b5b2864447cc915564e41e2876816b9e4fe849070"}, ] pyzmq = [ - {file = "pyzmq-22.0.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0cde362075ee8f3d2b0353b283e203c2200243b5a15d5c5c03b78112a17e7d4"}, - {file = "pyzmq-22.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:ff1ea14075bbddd6f29bf6beb8a46d0db779bcec6b9820909584081ec119f8fd"}, - {file = "pyzmq-22.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:26380487eae4034d6c2a3fb8d0f2dff6dd0d9dd711894e8d25aa2d1938950a33"}, - {file = "pyzmq-22.0.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:3e29f9cf85a40d521d048b55c63f59d6c772ac1c4bf51cdfc23b62a62e377c33"}, - {file = "pyzmq-22.0.3-cp36-cp36m-win32.whl", hash = "sha256:4f34a173f813b38b83f058e267e30465ed64b22cd0cf6bad21148d3fa718f9bb"}, - {file = "pyzmq-22.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:30df70f81fe210506aa354d7fd486a39b87d9f7f24c3d3f4f698ec5d96b8c084"}, - {file = "pyzmq-22.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7026f0353977431fc884abd4ac28268894bd1a780ba84bb266d470b0ec26d2ed"}, - {file = "pyzmq-22.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6d4163704201fff0f3ab0cd5d7a0ea1514ecfffd3926d62ec7e740a04d2012c7"}, - {file = "pyzmq-22.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:763c175294d861869f18eb42901d500eda7d3fa4565f160b3b2fd2678ea0ebab"}, - {file = "pyzmq-22.0.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:61e4bb6cd60caf1abcd796c3f48395e22c5b486eeca6f3a8797975c57d94b03e"}, - {file = "pyzmq-22.0.3-cp37-cp37m-win32.whl", hash = "sha256:b25e5d339550a850f7e919fe8cb4c8eabe4c917613db48dab3df19bfb9a28969"}, - {file = "pyzmq-22.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3ef50d74469b03725d781a2a03c57537d86847ccde587130fe35caafea8f75c6"}, - {file = "pyzmq-22.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60e63577b85055e4cc43892fecd877b86695ee3ef12d5d10a3c5d6e77a7cc1a3"}, - {file = "pyzmq-22.0.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:f5831eff6b125992ec65d973f5151c48003b6754030094723ac4c6e80a97c8c4"}, - {file = "pyzmq-22.0.3-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:9221783dacb419604d5345d0e097bddef4459a9a95322de6c306bf1d9896559f"}, - {file = "pyzmq-22.0.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b62ea18c0458a65ccd5be90f276f7a5a3f26a6dea0066d948ce2fa896051420f"}, - {file = "pyzmq-22.0.3-cp38-cp38-win32.whl", hash = "sha256:81e7df0da456206201e226491aa1fc449da85328bf33bbeec2c03bb3a9f18324"}, - {file = "pyzmq-22.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:f52070871a0fd90a99130babf21f8af192304ec1e995bec2a9533efc21ea4452"}, - {file = "pyzmq-22.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:c5e29fe4678f97ce429f076a2a049a3d0b2660ada8f2c621e5dc9939426056dd"}, - {file = "pyzmq-22.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d18ddc6741b51f3985978f2fda57ddcdae359662d7a6b395bc8ff2292fca14bd"}, - {file = "pyzmq-22.0.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4231943514812dfb74f44eadcf85e8dd8cf302b4d0bce450ce1357cac88dbfdc"}, - {file = "pyzmq-22.0.3-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:23a74de4b43c05c3044aeba0d1f3970def8f916151a712a3ac1e5cd9c0bc2902"}, - {file = "pyzmq-22.0.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:532af3e6dddea62d9c49062ece5add998c9823c2419da943cf95589f56737de0"}, - {file = "pyzmq-22.0.3-cp39-cp39-win32.whl", hash = "sha256:33acd2b9790818b9d00526135acf12790649d8d34b2b04d64558b469c9d86820"}, - {file = "pyzmq-22.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:a558c5bc89d56d7253187dccc4e81b5bb0eac5ae9511eb4951910a1245d04622"}, - {file = "pyzmq-22.0.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:581787c62eaa0e0db6c5413cedc393ebbadac6ddfd22e1cf9a60da23c4f1a4b2"}, - {file = "pyzmq-22.0.3-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:38e3dca75d81bec4f2defa14b0a65b74545812bb519a8e89c8df96bbf4639356"}, - {file = "pyzmq-22.0.3-pp36-pypy36_pp73-win32.whl", hash = "sha256:2f971431aaebe0a8b54ac018e041c2f0b949a43745444e4dadcc80d0f0ef8457"}, - {file = "pyzmq-22.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da7d4d4c778c86b60949d17531e60c54ed3726878de8a7f8a6d6e7f8cc8c3205"}, - {file = "pyzmq-22.0.3-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:13465c1ff969cab328bc92f7015ce3843f6e35f8871ad79d236e4fbc85dbe4cb"}, - {file = "pyzmq-22.0.3-pp37-pypy37_pp73-win32.whl", hash = "sha256:279cc9b51db48bec2db146f38e336049ac5a59e5f12fb3a8ad864e238c1c62e3"}, - {file = "pyzmq-22.0.3.tar.gz", hash = "sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d"}, + {file = "pyzmq-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e9b9a2f6944acdaf57316436c1acdcb30b8df76726bcf570ad9342bc5001654"}, + {file = "pyzmq-22.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24fb5bb641f0b2aa25fc3832f4b6fc62430f14a7d328229fe994b2bcdc07c93a"}, + {file = "pyzmq-22.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c4674004ed64685a38bee222cd75afa769424ec603f9329f0dd4777138337f48"}, + {file = "pyzmq-22.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:461ed80d741692d9457ab820b1cc057ba9c37c394e67b647b639f623c8b321f6"}, + {file = "pyzmq-22.1.0-cp36-cp36m-win32.whl", hash = "sha256:de5806be66c9108e4dcdaced084e8ceae14100aa559e2d57b4f0cceb98c462de"}, + {file = "pyzmq-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a1c77796f395804d6002ff56a6a8168c1f98579896897ad7e35665a9b4a9eec5"}, + {file = "pyzmq-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a81c9e6754465d09a87e3acd74d9bb1f0039b2d785c6899622f0afdb41d760"}, + {file = "pyzmq-22.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f0f27eaab9ba7b92d73d71c51d1a04464a1da6097a252d007922103253d2313"}, + {file = "pyzmq-22.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4b8fb1b3174b56fd020e4b10232b1764e52cf7f3babcfb460c5253bdc48adad0"}, + {file = "pyzmq-22.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c8fff75af4c7af92dce9f81fa2a83ed009c3e1f33ee8b5222db2ef80b94e242e"}, + {file = "pyzmq-22.1.0-cp37-cp37m-win32.whl", hash = "sha256:cb9f9fe1305ef69b65794655fd89b2209b11bff3e837de981820a8aa051ef914"}, + {file = "pyzmq-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bf80b2cec42d96117248b99d3c86e263a00469c840a778e6cb52d916f4fdf82c"}, + {file = "pyzmq-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0ea7f4237991b0f745a4432c63e888450840bf8cb6c48b93fb7d62864f455529"}, + {file = "pyzmq-22.1.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:12ffcf33db6ba7c0e5aaf901e65517f5e2b719367b80bcbfad692f546a297c7a"}, + {file = "pyzmq-22.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d3ecfee2ee8d91ab2e08d2d8e89302c729b244e302bbc39c5b5dde42306ff003"}, + {file = "pyzmq-22.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:68e2c4505992ab5b89f976f89a9135742b18d60068f761bef994a6805f1cae0c"}, + {file = "pyzmq-22.1.0-cp38-cp38-win32.whl", hash = "sha256:285514956c08c7830da9d94e01f5414661a987831bd9f95e4d89cc8aaae8da10"}, + {file = "pyzmq-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5e5be93e1714a59a535bbbc086b9e4fd2448c7547c5288548f6fd86353cad9e"}, + {file = "pyzmq-22.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b2f707b52e09098a7770503e39294ca6e22ae5138ffa1dd36248b6436d23d78e"}, + {file = "pyzmq-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:18dd2ca4540c476558099891c129e6f94109971d110b549db2a9775c817cedbd"}, + {file = "pyzmq-22.1.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:c6d0c32532a0519997e1ded767e184ebb8543bdb351f8eff8570bd461e874efc"}, + {file = "pyzmq-22.1.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9ee48413a2d3cd867fd836737b4c89c24cea1150a37f4856d82d20293fa7519f"}, + {file = "pyzmq-22.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4c4fe69c7dc0d13d4ae180ad650bb900854367f3349d3c16f0569f6c6447f698"}, + {file = "pyzmq-22.1.0-cp39-cp39-win32.whl", hash = "sha256:fc712a90401bcbf3fa25747f189d6dcfccbecc32712701cad25c6355589dac57"}, + {file = "pyzmq-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:68be16107f41563b9f67d93dff1c9f5587e0f76aa8fd91dc04c83d813bcdab1f"}, + {file = "pyzmq-22.1.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:734ea6565c71fc2d03d5b8c7d0d7519c96bb5567e0396da1b563c24a4ac66f0c"}, + {file = "pyzmq-22.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:1389b615917d4196962a9b469e947ba862a8ec6f5094a47da5e7a8d404bc07a4"}, + {file = "pyzmq-22.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:41049cff5265e9cd75606aa2c90a76b9c80b98d8fe70ee08cf4af3cedb113358"}, + {file = "pyzmq-22.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f49755684a963731479ff3035d45a8185545b4c9f662d368bd349c419839886d"}, + {file = "pyzmq-22.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:6355f81947e1fe6e7bb9e123aeb3067264391d3ebe8402709f824ef8673fa6f3"}, + {file = "pyzmq-22.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:089b974ec04d663b8685ac90e86bfe0e4da9d911ff3cf52cb765ff22408b102d"}, + {file = "pyzmq-22.1.0.tar.gz", hash = "sha256:7040d6dd85ea65703904d023d7f57fab793d7ffee9ba9e14f3b897f34ff2415d"}, ] regex = [ {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, @@ -1392,12 +1397,12 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] terminado = [ - {file = "terminado-0.9.4-py3-none-any.whl", hash = "sha256:daed77f9fad7b32558fa84b226a76f45a02242c20813502f36c4e1ade6d8f1ad"}, - {file = "terminado-0.9.4.tar.gz", hash = "sha256:9a7dbcfbc2778830eeb70261bf7aa9d98a3eac8631a3afe3febeb57c12f798be"}, + {file = "terminado-0.10.0-py3-none-any.whl", hash = "sha256:048ce7b271ad1f94c48130844af1de163e54913b919f8c268c89b36a6d468d7c"}, + {file = "terminado-0.10.0.tar.gz", hash = "sha256:46fd07c9dc7db7321922270d544a1f18eaa7a02fd6cd4438314f27a687cabbea"}, ] testpath = [ - {file = "testpath-0.4.4-py2.py3-none-any.whl", hash = "sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4"}, - {file = "testpath-0.4.4.tar.gz", hash = "sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e"}, + {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, + {file = "testpath-0.5.0.tar.gz", hash = "sha256:1acf7a0bcd3004ae8357409fc33751e16d37ccc650921da1094a86581ad1e417"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},