From 828b383b9968d94e649e234665ed15ae8b1ee99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:09:57 +0000 Subject: [PATCH 01/58] Support nested selector lists. Partially fix #3969. --- CHANGELOG.md | 1 + src/textual/css/tokenize.py | 1 + tests/css/test_tokenize.py | 89 +++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f838be7d..ffd94ab9bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `SelectionList` option IDs are usable as soon as the widget is instantiated https://github.com/Textualize/textual/issues/3903 - Fix issue with `Strip.crop` when crop window start aligned with strip end https://github.com/Textualize/textual/pull/3998 - Fixed Strip.crop_extend https://github.com/Textualize/textual/pull/4011 +- Improve support for selector lists in nested CSS https://github.com/Textualize/textual/issues/3969 ## [0.47.1] - 2023-01-05 diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index 7cf1556613..76d97a851a 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -105,6 +105,7 @@ new_selector=r",", declaration_set_start=r"\{", declaration_set_end=r"\}", + nested=r"\&", ).expect_eof(True) # A rule declaration e.g. "text: red;" diff --git a/tests/css/test_tokenize.py b/tests/css/test_tokenize.py index d4dfba888e..adbb47dc63 100644 --- a/tests/css/test_tokenize.py +++ b/tests/css/test_tokenize.py @@ -898,3 +898,92 @@ def test_allow_new_lines(): ), ] assert list(tokenize(css, ("", ""))) == expected + + +def test_nested_css_selector_list_with_ampersand(): + """Regression test for https://github.com/Textualize/textual/issues/3969.""" + css = "Label{&.foo,&.bar{border:solid red;}}" + tokens = list(tokenize(css, ("", ""))) + assert tokens == [ + Token( + name="selector_start", + value="Label", + read_from=("", ""), + code=css, + location=(0, 0), + ), + Token( + name="declaration_set_start", + value="{", + read_from=("", ""), + code=css, + location=(0, 5), + ), + Token(name="nested", value="&", read_from=("", ""), code=css, location=(0, 6)), + Token( + name="selector_class", + value=".foo", + read_from=("", ""), + code=css, + location=(0, 7), + ), + Token( + name="new_selector", + value=",", + read_from=("", ""), + code=css, + location=(0, 11), + ), + Token(name="nested", value="&", read_from=("", ""), code=css, location=(0, 12)), + Token( + name="selector_class", + value=".bar", + read_from=("", ""), + code=css, + location=(0, 13), + ), + Token( + name="declaration_set_start", + value="{", + read_from=("", ""), + code=css, + location=(0, 17), + ), + Token( + name="declaration_name", + value="border:", + read_from=("", ""), + code=css, + location=(0, 18), + ), + Token( + name="token", value="solid", read_from=("", ""), code=css, location=(0, 25) + ), + Token( + name="whitespace", value=" ", read_from=("", ""), code=css, location=(0, 30) + ), + Token( + name="token", value="red", read_from=("", ""), code=css, location=(0, 31) + ), + Token( + name="declaration_end", + value=";", + read_from=("", ""), + code=css, + location=(0, 34), + ), + Token( + name="declaration_set_end", + value="}", + read_from=("", ""), + code=css, + location=(0, 35), + ), + Token( + name="declaration_set_end", + value="}", + read_from=("", ""), + code=css, + location=(0, 36), + ), + ] From 18b8a23fd0bf3ee2012ea9608540b35470e992ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:11:30 +0000 Subject: [PATCH 02/58] Improve support for declarations after nested rule sets. Partially fixes #3999. --- CHANGELOG.md | 3 +- src/textual/css/tokenize.py | 1 + tests/css/test_tokenize.py | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd94ab9bd..a6086f2d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `SelectionList` option IDs are usable as soon as the widget is instantiated https://github.com/Textualize/textual/issues/3903 - Fix issue with `Strip.crop` when crop window start aligned with strip end https://github.com/Textualize/textual/pull/3998 - Fixed Strip.crop_extend https://github.com/Textualize/textual/pull/4011 -- Improve support for selector lists in nested CSS https://github.com/Textualize/textual/issues/3969 +- Improve support for selector lists in nested TCSS https://github.com/Textualize/textual/issues/3969 +- Improve support for rule declarations after nested TCSS rule sets https://github.com/Textualize/textual/issues/3999 ## [0.47.1] - 2023-01-05 diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index 76d97a851a..ec88144f1a 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -64,6 +64,7 @@ whitespace=r"\s+", comment_start=COMMENT_START, comment_line=COMMENT_LINE, + declaration_name=r"[a-zA-Z_\-]+\:", selector_start_id=r"\#" + IDENTIFIER, selector_start_class=r"\." + IDENTIFIER, selector_start_universal=r"\*", diff --git a/tests/css/test_tokenize.py b/tests/css/test_tokenize.py index adbb47dc63..fab79ed4f6 100644 --- a/tests/css/test_tokenize.py +++ b/tests/css/test_tokenize.py @@ -987,3 +987,95 @@ def test_nested_css_selector_list_with_ampersand(): location=(0, 36), ), ] + + +def test_declaration_after_nested_declaration_set(): + """Regression test for https://github.com/Textualize/textual/issues/3999.""" + css = "Screen{Label{background:red;}background:green;}" + tokens = list(tokenize(css, ("", ""))) + assert tokens == [ + Token( + name="selector_start", + value="Screen", + read_from=("", ""), + code=css, + location=(0, 0), + ), + Token( + name="declaration_set_start", + value="{", + read_from=("", ""), + code=css, + location=(0, 6), + ), + Token( + name="selector_start", + value="Label", + read_from=("", ""), + code=css, + location=(0, 7), + ), + Token( + name="declaration_set_start", + value="{", + read_from=("", ""), + code=css, + location=(0, 12), + ), + Token( + name="declaration_name", + value="background:", + read_from=("", ""), + code=css, + location=(0, 13), + ), + Token( + name="token", + value="red", + read_from=("", ""), + code=css, + location=(0, 24), + ), + Token( + name="declaration_end", + value=";", + read_from=("", ""), + code=css, + location=(0, 27), + ), + Token( + name="declaration_set_end", + value="}", + read_from=("", ""), + code=css, + location=(0, 28), + ), + Token( + name="declaration_name", + value="background:", + read_from=("", ""), + code=css, + location=(0, 29), + ), + Token( + name="token", + value="green", + read_from=("", ""), + code=css, + location=(0, 40), + ), + Token( + name="declaration_end", + value=";", + read_from=("", ""), + code=css, + location=(0, 45), + ), + Token( + name="declaration_set_end", + value="}", + read_from=("", ""), + code=css, + location=(0, 46), + ), + ] From 599feb06831d1f60403da768f2c872c946ba73e3 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Wed, 10 Jan 2024 13:55:23 +0100 Subject: [PATCH 03/58] Make MarkdownFence respond to app theme changes. Closes #3997 --- CHANGELOG.md | 1 + src/textual/widgets/_markdown.py | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 705eb5dd3e..00dd0c6a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Ensuring `TextArea.SelectionChanged` message only sends when the updated selection is different https://github.com/Textualize/textual/pull/3933 - Fixed declaration after nested rule set causing a parse error https://github.com/Textualize/textual/pull/4012 - ID and class validation was too lenient https://github.com/Textualize/textual/issues/3954 +- Fixed `MarkdownFence` not adapting to app theme changes https://github.com/Textualize/textual/issues/3997 ## [0.47.1] - 2023-01-05 diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index f8ac0fdaff..cdfd5113df 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -7,6 +7,7 @@ from markdown_it.token import Token from rich import box from rich.style import Style +from rich.syntax import Syntax from rich.table import Table from rich.text import Text from typing_extensions import TypeAlias @@ -500,11 +501,27 @@ class MarkdownFence(MarkdownBlock): def __init__(self, markdown: Markdown, code: str, lexer: str) -> None: self.code = code self.lexer = lexer + self.theme = "solarized-dark" if self.app.dark else "solarized-light" super().__init__(markdown) - def compose(self) -> ComposeResult: - from rich.syntax import Syntax + def _retheme(self) -> None: + """Swap between a dark and light theme when the mode changes.""" + self.theme = "solarized-dark" if self.app.dark else "solarized-light" + code_block = self.query_one(Static) + code_block.renderable = Syntax( + self.code, + lexer=self.lexer, + word_wrap=False, + indent_guides=True, + padding=(1, 2), + theme=self.theme, + ) + def _on_mount(self, _: Mount) -> None: + """Watch app theme switching.""" + self.watch(self.app, "dark", self._retheme) + + def compose(self) -> ComposeResult: yield Static( Syntax( self.code, @@ -512,7 +529,7 @@ def compose(self) -> ComposeResult: word_wrap=False, indent_guides=True, padding=(1, 2), - theme="material", + theme=self.theme, ), expand=True, shrink=False, From 52ebfe73b79dd215bf3c633238d7a2bea4d70f70 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 19 Jan 2024 12:11:12 +0100 Subject: [PATCH 04/58] Introduce dark_theme, light_theme reactive properties to Markdown. Allows for code blocks within Markdown to be styled and redrawn following the app theme. --- src/textual/widgets/_markdown.py | 51 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index cdfd5113df..558b72dcae 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -18,7 +18,7 @@ from ..containers import Horizontal, Vertical, VerticalScroll from ..events import Mount from ..message import Message -from ..reactive import reactive, var +from ..reactive import Reactive, reactive, var from ..widget import Widget from ..widgets import Static, Tree @@ -499,16 +499,15 @@ class MarkdownFence(MarkdownBlock): """ def __init__(self, markdown: Markdown, code: str, lexer: str) -> None: + super().__init__(markdown) self.code = code self.lexer = lexer - self.theme = "solarized-dark" if self.app.dark else "solarized-light" - super().__init__(markdown) + self.theme = ( + self._markdown.dark_theme if self.app.dark else self._markdown.light_theme + ) - def _retheme(self) -> None: - """Swap between a dark and light theme when the mode changes.""" - self.theme = "solarized-dark" if self.app.dark else "solarized-light" - code_block = self.query_one(Static) - code_block.renderable = Syntax( + def _block(self) -> Syntax: + return Syntax( self.code, lexer=self.lexer, word_wrap=False, @@ -521,16 +520,17 @@ def _on_mount(self, _: Mount) -> None: """Watch app theme switching.""" self.watch(self.app, "dark", self._retheme) + def _retheme(self) -> None: + """Rerender when the theme changes.""" + self.theme = ( + self._markdown.dark_theme if self.app.dark else self._markdown.light_theme + ) + code_block = self.query_one(Static) + code_block.update(self._block()) + def compose(self) -> ComposeResult: yield Static( - Syntax( - self.code, - lexer=self.lexer, - word_wrap=False, - indent_guides=True, - padding=(1, 2), - theme=self.theme, - ), + self._block(), expand=True, shrink=False, ) @@ -584,6 +584,9 @@ class Markdown(Widget): BULLETS = ["\u25CF ", "▪ ", "‣ ", "• ", "⭑ "] + dark_theme: Reactive[str] = reactive("material", layout=True, repaint=True) + light_theme: Reactive[str] = reactive("material-light") + def __init__( self, markdown: str | None = None, @@ -670,6 +673,16 @@ def _on_mount(self, _: Mount) -> None: if self._markdown is not None: self.update(self._markdown) + def watch_dark_theme(self, dark_theme: str) -> None: + if self.app.dark: + for block in self.query(MarkdownFence): + block._retheme() + + def watch_light_theme(self, light_theme: str) -> None: + if not self.app.dark: + for block in self.query(MarkdownFence): + block._retheme() + @staticmethod def sanitize_location(location: str) -> tuple[Path, str]: """Given a location, break out the path and any anchor. @@ -875,11 +888,7 @@ def update(self, markdown: str) -> AwaitComplete: stack[-1].set_content(content) elif token.type in ("fence", "code_block"): (stack[-1]._blocks if stack else output).append( - MarkdownFence( - self, - token.content.rstrip(), - token.info, - ) + MarkdownFence(self, token.content.rstrip(), token.info) ) else: external = self.unhandled_token(token) From e5839f6d4cfecabf4f239af3d167b2d4363bc6d7 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Sun, 21 Jan 2024 11:16:50 +0100 Subject: [PATCH 05/58] Add snapshot tests to test handling of theme switching in markdown --- .../__snapshots__/test_snapshots.ambr | 493 ++++++++++++++++++ .../snapshot_apps/markdown_theme_switcher.py | 33 ++ tests/snapshot_tests/test_snapshots.py | 16 + 3 files changed, 542 insertions(+) create mode 100644 tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index af8bffb828..b7680fafc6 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -21690,6 +21690,170 @@ ''' # --- +# name: test_markdown_dark_theme_override + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MarkdownThemeSwitchertApp + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + This is a H1 + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + defmain(): + │   print("Hello world!") + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_markdown_example ''' @@ -21854,6 +22018,335 @@ ''' # --- +# name: test_markdown_light_theme_override + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MarkdownThemeSwitchertApp + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + This is a H1 + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + defmain(): + │   print("Hello world!") + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_markdown_theme_switching + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MarkdownThemeSwitchertApp + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + This is a H1 + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + defmain(): + │   print("Hello world!") + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_markdown_viewer_example ''' diff --git a/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py b/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py new file mode 100644 index 0000000000..510f2c8bf7 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py @@ -0,0 +1,33 @@ +from textual.app import App, ComposeResult +from textual.widgets import Markdown + +TEST_CODE_MARKDOWN = """ +# This is a H1 +```python +def main(): + print("Hello world!") +``` +""" + + +class MarkdownThemeSwitchertApp(App[None]): + BINDINGS = [ + ("t", "toggle_dark"), + ("d", "switch_dark"), + ("l", "switch_light"), + ] + + def action_switch_dark(self) -> None: + md = self.query_one(Markdown) + md.dark_theme = "solarized-dark" + + def action_switch_light(self) -> None: + md = self.query_one(Markdown) + md.light_theme = "solarized-light" + + def compose(self) -> ComposeResult: + yield Markdown(TEST_CODE_MARKDOWN) + + +if __name__ == "__main__": + MarkdownThemeSwitchertApp().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index ec51437e84..3d786fea9c 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -229,6 +229,22 @@ def test_markdown_viewer_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "markdown_viewer.py") +def test_markdown_theme_switching(snap_compare): + assert snap_compare(SNAPSHOT_APPS_DIR / "markdown_theme_switcher.py", press=["t"]) + + +def test_markdown_dark_theme_override(snap_compare): + assert snap_compare( + SNAPSHOT_APPS_DIR / "markdown_theme_switcher.py", press=["d", "wait:100"] + ) + + +def test_markdown_light_theme_override(snap_compare): + assert snap_compare( + SNAPSHOT_APPS_DIR / "markdown_theme_switcher.py", press=["l", "t", "wait:100"] + ) + + def test_checkbox_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "checkbox.py") From b2c373780edc1162a63bd6dd658e0919e993fc7d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 8 Feb 2024 11:38:54 +0000 Subject: [PATCH 06/58] Remove layout and repaint I'm not sure these are needed. --- src/textual/widgets/_markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 558b72dcae..706b47e6e2 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -584,8 +584,8 @@ class Markdown(Widget): BULLETS = ["\u25CF ", "▪ ", "‣ ", "• ", "⭑ "] - dark_theme: Reactive[str] = reactive("material", layout=True, repaint=True) light_theme: Reactive[str] = reactive("material-light") + dark_theme: reactive[str] = reactive("material") def __init__( self, From 4260ef505749a1b0be760bf1c0e44742fa109a4d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 8 Feb 2024 11:39:21 +0000 Subject: [PATCH 07/58] Add docstrings to the dark and light theme properties --- src/textual/widgets/_markdown.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 706b47e6e2..f4fe3a0cb3 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -584,8 +584,11 @@ class Markdown(Widget): BULLETS = ["\u25CF ", "▪ ", "‣ ", "• ", "⭑ "] - light_theme: Reactive[str] = reactive("material-light") dark_theme: reactive[str] = reactive("material") + """The theme to use for code blocks when in [dark mode][textual.app.App.dark].""" + + light_theme: reactive[str] = reactive("material-light") + """The theme to use for code blocks when in [light mode][textual.app.App.dark].""" def __init__( self, From 70d3af799cc3c0f2a75274c824748ae27615a86a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 8 Feb 2024 11:51:34 +0000 Subject: [PATCH 08/58] Rename the code theme reactives This helps make it clear that we're talking about themes for the code in Markdown, rather than a whole Markdown theme. --- src/textual/widgets/_markdown.py | 14 ++++++++------ .../snapshot_apps/markdown_theme_switcher.py | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index f4fe3a0cb3..635b97dc34 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -503,7 +503,7 @@ def __init__(self, markdown: Markdown, code: str, lexer: str) -> None: self.code = code self.lexer = lexer self.theme = ( - self._markdown.dark_theme if self.app.dark else self._markdown.light_theme + self._markdown.code_dark_theme if self.app.dark else self._markdown.code_light_theme ) def _block(self) -> Syntax: @@ -523,7 +523,7 @@ def _on_mount(self, _: Mount) -> None: def _retheme(self) -> None: """Rerender when the theme changes.""" self.theme = ( - self._markdown.dark_theme if self.app.dark else self._markdown.light_theme + self._markdown.code_dark_theme if self.app.dark else self._markdown.code_light_theme ) code_block = self.query_one(Static) code_block.update(self._block()) @@ -584,10 +584,10 @@ class Markdown(Widget): BULLETS = ["\u25CF ", "▪ ", "‣ ", "• ", "⭑ "] - dark_theme: reactive[str] = reactive("material") + code_dark_theme: reactive[str] = reactive("material") """The theme to use for code blocks when in [dark mode][textual.app.App.dark].""" - light_theme: reactive[str] = reactive("material-light") + code_light_theme: reactive[str] = reactive("material-light") """The theme to use for code blocks when in [light mode][textual.app.App.dark].""" def __init__( @@ -676,12 +676,14 @@ def _on_mount(self, _: Mount) -> None: if self._markdown is not None: self.update(self._markdown) - def watch_dark_theme(self, dark_theme: str) -> None: + def _watch_code_dark_theme(self) -> None: + """React to the dark theme being changed.""" if self.app.dark: for block in self.query(MarkdownFence): block._retheme() - def watch_light_theme(self, light_theme: str) -> None: + def _watch_code_light_theme(self) -> None: + """React to the light theme being changed.""" if not self.app.dark: for block in self.query(MarkdownFence): block._retheme() diff --git a/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py b/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py index 510f2c8bf7..698d0c2c78 100644 --- a/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py +++ b/tests/snapshot_tests/snapshot_apps/markdown_theme_switcher.py @@ -19,11 +19,11 @@ class MarkdownThemeSwitchertApp(App[None]): def action_switch_dark(self) -> None: md = self.query_one(Markdown) - md.dark_theme = "solarized-dark" + md.code_dark_theme = "solarized-dark" def action_switch_light(self) -> None: md = self.query_one(Markdown) - md.light_theme = "solarized-light" + md.code_light_theme = "solarized-light" def compose(self) -> ComposeResult: yield Markdown(TEST_CODE_MARKDOWN) From 88dfe45dcc85a357fd85f3a9b63cc34f80335253 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 8 Feb 2024 13:46:23 +0000 Subject: [PATCH 09/58] Keep Black happy --- src/textual/widgets/_markdown.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 635b97dc34..ce950cd7bf 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -503,7 +503,9 @@ def __init__(self, markdown: Markdown, code: str, lexer: str) -> None: self.code = code self.lexer = lexer self.theme = ( - self._markdown.code_dark_theme if self.app.dark else self._markdown.code_light_theme + self._markdown.code_dark_theme + if self.app.dark + else self._markdown.code_light_theme ) def _block(self) -> Syntax: @@ -523,7 +525,9 @@ def _on_mount(self, _: Mount) -> None: def _retheme(self) -> None: """Rerender when the theme changes.""" self.theme = ( - self._markdown.code_dark_theme if self.app.dark else self._markdown.code_light_theme + self._markdown.code_dark_theme + if self.app.dark + else self._markdown.code_light_theme ) code_block = self.query_one(Static) code_block.update(self._block()) From 29fad94c3e09f145010bef62613200da80b8bd92 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 8 Feb 2024 13:58:09 +0000 Subject: [PATCH 10/58] Simplify the retheme method Also use get_child_by_type to ensure it's just our child (not really necessary but more in keeping with the makeup of the widget). --- src/textual/widgets/_markdown.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index ce950cd7bf..69d8a066d2 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -529,8 +529,7 @@ def _retheme(self) -> None: if self.app.dark else self._markdown.code_light_theme ) - code_block = self.query_one(Static) - code_block.update(self._block()) + self.get_child_by_type(Static).update(self._block()) def compose(self) -> ComposeResult: yield Static( From 690ca0dc43f8360c07875ebb987e2ca70acf8d90 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 8 Feb 2024 14:15:35 +0000 Subject: [PATCH 11/58] Remove unused import --- src/textual/widgets/_markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 69d8a066d2..24cde3dd91 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -18,7 +18,7 @@ from ..containers import Horizontal, Vertical, VerticalScroll from ..events import Mount from ..message import Message -from ..reactive import Reactive, reactive, var +from ..reactive import reactive, var from ..widget import Widget from ..widgets import Static, Tree From d857b649054cd64f293e5f1f2e2ead6369e44aed Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 8 Feb 2024 17:33:53 +0000 Subject: [PATCH 12/58] version bump --- CHANGELOG.md | 11 +++++------ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d46af308e..a0f1da3158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -### Unreleased -### Added - -- Added support for configuring dark and light themes for code in `Markdown` https://github.com/Textualize/textual/issues/3997 - -## [0.49.1] - 2023-02-08 +## [0.50.0] - 2023-02-08 ### Fixed @@ -19,6 +14,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed duplicate watch methods being attached to DOM nodes https://github.com/Textualize/textual/pull/4030 - Fixed using `watch` to create additional watchers would trigger other watch methods https://github.com/Textualize/textual/issues/3878 +### Added + +- Added support for configuring dark and light themes for code in `Markdown` https://github.com/Textualize/textual/issues/3997 + ## [0.49.0] - 2024-02-07 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index f19b6025f0..1131d4feca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.49.1" +version = "0.50.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" From 7d8efa01ed8be6b1e13a99f4960135196f28073e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 8 Feb 2024 17:34:44 +0000 Subject: [PATCH 13/58] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f1da3158..7f56f39065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1677,6 +1677,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.50.0]: https://github.com/Textualize/textual/compare/v0.49.0...v0.50.0 [0.49.1]: https://github.com/Textualize/textual/compare/v0.49.0...v0.49.1 [0.49.0]: https://github.com/Textualize/textual/compare/v0.48.2...v0.49.0 [0.48.2]: https://github.com/Textualize/textual/compare/v0.48.1...v0.48.2 From fcc96c323814691c999b9955801483744544a6f8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 9 Feb 2024 18:09:31 +0000 Subject: [PATCH 14/58] ansi fix --- CHANGELOG.md | 7 +++++++ src/textual/renderables/tint.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f56f39065..43a77fbb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.51.1] - 2023-02-09 + +### Fixed + +- Fixed tint applied to ANSI colors + ## [0.50.0] - 2023-02-08 ### Fixed @@ -1677,6 +1683,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.50.1]: https://github.com/Textualize/textual/compare/v0.50.0...v0.50.1 [0.50.0]: https://github.com/Textualize/textual/compare/v0.49.0...v0.50.0 [0.49.1]: https://github.com/Textualize/textual/compare/v0.49.0...v0.49.1 [0.49.0]: https://github.com/Textualize/textual/compare/v0.48.2...v0.49.0 diff --git a/src/textual/renderables/tint.py b/src/textual/renderables/tint.py index c9fabe8477..358bbca3fc 100644 --- a/src/textual/renderables/tint.py +++ b/src/textual/renderables/tint.py @@ -2,11 +2,13 @@ from typing import Iterable +from rich import terminal_theme from rich.console import Console, ConsoleOptions, RenderableType, RenderResult from rich.segment import Segment from rich.style import Style from ..color import Color +from ..filter import ANSIToTruecolor class Tint: @@ -43,13 +45,15 @@ def process_segments( style_from_color = Style.from_color _Segment = Segment + truecolor_style = ANSIToTruecolor(terminal_theme.DIMMED_MONOKAI).truecolor_style + NULL_STYLE = Style() for segment in segments: text, style, control = segment if control: yield segment else: - style = style if style is not None else NULL_STYLE + style = truecolor_style(style) if style is not None else NULL_STYLE yield _Segment( text, ( From 40863d0c2d97535664a858127c338229aae929fb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 9 Feb 2024 18:10:23 +0000 Subject: [PATCH 15/58] version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1131d4feca..71c101fcb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.50.0" +version = "0.50.1" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" From e1749ac36a88ae7a015a5c0fc16923df9b6ee3a9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 9 Feb 2024 18:23:10 +0000 Subject: [PATCH 16/58] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a77fbb38..068d466723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed -- Fixed tint applied to ANSI colors +- Fixed tint applied to ANSI colors https://github.com/Textualize/textual/pull/4142 ## [0.50.0] - 2023-02-08 From adecf4aae868350c013be65a558cf36366ee5cfe Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 11 Feb 2024 11:40:30 +0000 Subject: [PATCH 17/58] fix year --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068d466723..0a104dd737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.51.1] - 2023-02-09 +## [0.51.1] - 2024-02-09 ### Fixed - Fixed tint applied to ANSI colors https://github.com/Textualize/textual/pull/4142 -## [0.50.0] - 2023-02-08 +## [0.50.0] - 2024-02-08 ### Fixed From 00eb1dcac0557e4fd4c58131bcb7dc788162748d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 11 Feb 2024 14:19:48 +0000 Subject: [PATCH 18/58] new blog --- docs/blog/posts/toolong-retrospective.md | 143 +++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/blog/posts/toolong-retrospective.md diff --git a/docs/blog/posts/toolong-retrospective.md b/docs/blog/posts/toolong-retrospective.md new file mode 100644 index 0000000000..29c843aa54 --- /dev/null +++ b/docs/blog/posts/toolong-retrospective.md @@ -0,0 +1,143 @@ +--- +draft: false +date: 2024-02-11 +categories: + - DevLog +authors: + - willmcgugan +--- + +# File magic with the Python standard library + +I recently published [Toolong](https://github.com/textualize/toolong), an app for viewing log files. +There were some interesting technical challenges in building Toolong that I'd like to cover in this post. + + + +!!! note "Python is awesome" + + This isn't specifically [Textual](https://github.com/textualize/textual/) related. These techniques could be employed in any Python project. + +These techniques aren't difficult, and shouldn't be beyond anyone with an intermediate understanding of Python. +They are the kind of "if you know it you kow it" knowledge that you may not need often, but can make a massive difference when you do! + +## Opening large files + +If you were to open a very large text file (multiple gigabyte in size) in an editor, you will almost certainly find that it takes a while. You may also find that it doesn't load at all because you don't have enough memory, or it disables features like syntax highlighting. + +This is because most app will do something analogous to this: + +```python +with open("access.log", "rb") as log_file: + log_data = log_file.read() +``` + +All the data is read in to memory, where it can be easily processed. +This is fine for most files of a reasonable size, but when you get in to the gigabyte territory the read and any additional processing will start to use a significant amount of time and memory. + +Yet Toolong can open a file of *any* size in a second or so, with syntax highlighting. +It can do this because it doesn't need to read the entire log file in to memory. +Toolong opens a file and reads only the portion of it required to display whatever is on screen at that moment. +When you scroll around the log file, Toolong reads the data off disk as required -- fast enough that you may never even notice it. + +### Scanning lines + +There is an additional bit of work that Toolong has to do up front in order to show the file. +If you open a large file you may see a progress bar and a message about "scanning". + +Toolong needs to know where every line starts and ends in a log file, so it can display a scrollbar bar and allow the user to navigate lines in the file. +In other words it needs to know the offset of every new line (`\n`) character within the file. + +This isn't a hard problem in itself. +You might have imagined a loop that reads a chunk at a time and searches for new lines characters. +And that would likely have worked just fine, but there is a bit of magic in the Python standard library that can speed that up. + +The [mmap](https://docs.python.org/3/library/mmap.html) module is a real gem for this kind of thing. +A *memory mapped file* is an OS-level construct that *appears* to load a file instantaneously. +In Python you get an object which behaves like a `bytearray`, but loads data from disk when it is accessed. +The beauty of this module is that you can work with files in much the same way as if you had read the entire file in to memory, while leaving the actually reading of the file to the OS. + +Here's the method that Toolong uses to scan for line breaks. +Forgive the micro-optimizations, I was going for raw execution speed here. + +```python + def scan_line_breaks( + self, batch_time: float = 0.25 + ) -> Iterable[tuple[int, list[int]]]: + """Scan the file for line breaks. + + Args: + batch_time: Time to group the batches. + + Returns: + An iterable of tuples, containing the scan position and a list of offsets of new lines. + """ + fileno = self.fileno + size = self.size + if not size: + return + log_mmap = mmap.mmap(fileno, size, prot=mmap.PROT_READ) + rfind = log_mmap.rfind + position = size + batch: list[int] = [] + append = batch.append + get_length = batch.__len__ + monotonic = time.monotonic + break_time = monotonic() + + while (position := rfind(b"\n", 0, position)) != -1: + append(position) + if get_length() % 1000 == 0 and monotonic() - break_time > batch_time: + break_time = monotonic() + yield (position, batch) + batch = [] + append = batch.append + yield (0, batch) + log_mmap.close() +``` + +This code runs in a thread (actually a [worker](https://textual.textualize.io/guide/workers/)), and will generate line breaks in batches. Without batching, it risks slowing down the UI with millions of rapid events. + +It's fast because most of the work is done in `rfind`, which runs at C speed, while the OS reads from the disk. + +## Watching a file for changes + +Toolong can tail files in realtime. +When something appends to the file, it will be read and displayed virtually instantly. +How is this done? + +You can easily *poll* a file for changes, by periodically querying the size or timestamp of a file until it changes. +The downside of this is that you don't get notified immediately if a file changes between polls. +You could poll at a very fast rate, but if you were to do that you would end up burning a lot of CPU for no good reason. + +There is a very good solution for this in the standard library. +The [selectors](https://docs.python.org/3/library/selectors.html) module is typically used for working with sockets (network data), but can also work with files (at least on macOS and Linux). + +!!! info "Software developers are an unimaginative bunch when it comes to naming things" + + Not to be confused with CSS [selectors](https://textual.textualize.io/guide/CSS/#selectors)! + +The selectors module can tell you precisely when a file can be read. +It can do this very efficiently, because it relies on the OS to tell us when a file can be read, and doesn't need to poll. + +You register a file with a `Selector` object, then call `select()` which returns as soon as there is new data available for reading. + +See [watcher.py](https://github.com/Textualize/toolong/blob/main/src/toolong/watcher.py) in Toolong, which runs a thread to monitors files for changes with a selector. + +## Textual learnings + +This project was a chance for me to "dogfood" Textual. +Other Textual devs have build some cool projects ([Trogon](https://github.com/Textualize/trogon) and [Frogmouth](https://github.com/Textualize/frogmouth)), but before Toolong I had only every written example apps for docs. + +I paid particular attention to Textual error messages when working on Toolong, and improved many of them in Textual. +Much of what I improved were general programming errors, and not Textual errors per se. +For instance, if you forget to call `super()` on a widget constructor, Textual used to give a fairly cryptic error. +It's a fairly common gotcha, even for experience devs, but now Textual will detect that and tell you how to fix it. + +There's a lot of other improvements which I thought about when working on this app. +Mostly quality of life features that will make implementing some features more intuitive. +Keep an eye out for those in the next few weeks. + +## Found this interesting? + +If you would like to talk about this post or anything Textual related, join us on the [Discord server](https://discord.gg/Enf6Z3qhVr). From 6b14305837898bb6e76d14625ec6ac7cabfef215 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 11 Feb 2024 15:46:15 +0000 Subject: [PATCH 19/58] typo --- docs/blog/posts/toolong-retrospective.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/posts/toolong-retrospective.md b/docs/blog/posts/toolong-retrospective.md index 29c843aa54..dff9b79220 100644 --- a/docs/blog/posts/toolong-retrospective.md +++ b/docs/blog/posts/toolong-retrospective.md @@ -127,7 +127,7 @@ See [watcher.py](https://github.com/Textualize/toolong/blob/main/src/toolong/wat ## Textual learnings This project was a chance for me to "dogfood" Textual. -Other Textual devs have build some cool projects ([Trogon](https://github.com/Textualize/trogon) and [Frogmouth](https://github.com/Textualize/frogmouth)), but before Toolong I had only every written example apps for docs. +Other Textual devs have build some cool projects ([Trogon](https://github.com/Textualize/trogon) and [Frogmouth](https://github.com/Textualize/frogmouth)), but before Toolong I had only ever written example apps for docs. I paid particular attention to Textual error messages when working on Toolong, and improved many of them in Textual. Much of what I improved were general programming errors, and not Textual errors per se. From db4760b3b7a35953bb48165f252be1d3ae47febb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 12 Feb 2024 11:37:07 +0000 Subject: [PATCH 20/58] Add default syntax mapping to CSS theme in TextArea (#4149) * Add default syntax mapping to CSS theme in TextArea * Update CHANGELOG --- CHANGELOG.md | 5 ++ src/textual/_text_area_theme.py | 2 +- src/textual/widgets/_text_area.py | 30 ++++----- .../__snapshots__/test_snapshots.ambr | 67 ++++++++++--------- 4 files changed, 57 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a104dd737..2241508c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Added + +- Add some syntax highlighting to TextArea default theme https://github.com/Textualize/textual/pull/4149 ## [0.51.1] - 2024-02-09 diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index da0ec60d4f..b9f920600b 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -379,7 +379,7 @@ def default(cls) -> TextAreaTheme: }, ) -_CSS_THEME = TextAreaTheme(name="css") +_CSS_THEME = TextAreaTheme(name="css", syntax_styles=_DARK_VS.syntax_styles) _BUILTIN_THEMES = { "css": _CSS_THEME, diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 2874c00d55..d5f4d8efef 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -983,21 +983,6 @@ def render_line(self, widget_y: int) -> Strip: selection_top_row, selection_top_column = selection_top selection_bottom_row, selection_bottom_column = selection_bottom - highlights = self._highlights - if highlights and theme: - line_bytes = _utf8_encode(line_string) - byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) - get_highlight_from_theme = theme.syntax_styles.get - line_highlights = highlights[line_index] - for highlight_start, highlight_end, highlight_name in line_highlights: - node_style = get_highlight_from_theme(highlight_name) - if node_style is not None: - line.stylize( - node_style, - byte_to_codepoint.get(highlight_start, 0), - byte_to_codepoint.get(highlight_end) if highlight_end else None, - ) - cursor_line_style = theme.cursor_line_style if theme else None if cursor_line_style and cursor_row == line_index: line.stylize(cursor_line_style) @@ -1032,6 +1017,21 @@ def render_line(self, widget_y: int) -> Strip: else: line.stylize(selection_style, end=line_character_count) + highlights = self._highlights + if highlights and theme: + line_bytes = _utf8_encode(line_string) + byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) + get_highlight_from_theme = theme.syntax_styles.get + line_highlights = highlights[line_index] + for highlight_start, highlight_end, highlight_name in line_highlights: + node_style = get_highlight_from_theme(highlight_name) + if node_style is not None: + line.stylize( + node_style, + byte_to_codepoint.get(highlight_start, 0), + byte_to_codepoint.get(highlight_end) if highlight_end else None, + ) + # Highlight the cursor matching_bracket = self._matching_bracket_location match_cursor_bracket = self.match_cursor_bracket diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 4c00834c2a..f73d96309d 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -37227,77 +37227,82 @@ font-weight: 700; } - .terminal-3978829874-matrix { + .terminal-421307802-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3978829874-title { + .terminal-421307802-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3978829874-r1 { fill: #1e1e1e } - .terminal-3978829874-r2 { fill: #0178d4 } - .terminal-3978829874-r3 { fill: #c5c8c6 } - .terminal-3978829874-r4 { fill: #7d7e7a } - .terminal-3978829874-r5 { fill: #f8f8f2 } - .terminal-3978829874-r6 { fill: #abaca9;font-weight: bold } - .terminal-3978829874-r7 { fill: #151515 } + .terminal-421307802-r1 { fill: #1e1e1e } + .terminal-421307802-r2 { fill: #0178d4 } + .terminal-421307802-r3 { fill: #c5c8c6 } + .terminal-421307802-r4 { fill: #7d7e7a } + .terminal-421307802-r5 { fill: #569cd6 } + .terminal-421307802-r6 { fill: #f8f8f2 } + .terminal-421307802-r7 { fill: #4ec9b0 } + .terminal-421307802-r8 { fill: #abaca9;font-weight: bold } + .terminal-421307802-r9 { fill: #b5cea8 } + .terminal-421307802-r10 { fill: #151515 } + .terminal-421307802-r11 { fill: #7daf9c } + .terminal-421307802-r12 { fill: #ce9178 } - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1  def hello(name): - 2      x = 123                               - 3      while not False:  - 4          print("hello " + name)  - 5          continue  - 6   - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1  defhello(name): + 2      x =123 + 3  whilenotFalse + 4  print("hello "+ name)  + 5  continue + 6   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ From b1ae27e92c6799aba5dda22e12ce045c82bc903c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 12 Feb 2024 15:19:18 +0000 Subject: [PATCH 21/58] Remove a type warning --- src/textual/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/command.py b/src/textual/command.py index 9136bf9d5b..41d464df58 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -794,7 +794,7 @@ def _refresh_command_list( else None ) command_list.clear_options().add_options(sorted(commands, reverse=True)) - if highlighted is not None: + if highlighted is not None and highlighted.id: command_list.highlighted = command_list.get_option_index(highlighted.id) self._list_visible = bool(command_list.option_count) From 6a6c04040ef400bd30bb7594bc5b497523992890 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 12 Feb 2024 17:32:44 +0000 Subject: [PATCH 22/58] Proof-of-concept "discovery" phase for the command palette --- src/textual/_system_commands.py | 32 ++++++++++++++- src/textual/command.py | 70 +++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/textual/_system_commands.py b/src/textual/_system_commands.py index 8cbb6ef017..0796deb891 100644 --- a/src/textual/_system_commands.py +++ b/src/textual/_system_commands.py @@ -4,7 +4,7 @@ actions available via the [command palette][textual.command.CommandPalette]. """ -from .command import Hit, Hits, Provider +from .command import DiscoveryHit, Hit, Hits, Provider class SystemCommands(Provider): @@ -53,3 +53,33 @@ async def search(self, query: str) -> Hits: runnable, help=help_text, ) + + async def discover(self) -> Hits: + """Handle a request for the discovery commands for this provider. + + Yields: + Commands that can be discovered. + """ + # TODO: Dedupe these from the above. + for name, runnable, help_text in ( + ( + "Toggle light/dark mode", + self.app.action_toggle_dark, + "Toggle the application between light and dark mode", + ), + ( + "Quit the application", + self.app.action_quit, + "Quit the application as soon as possible", + ), + ( + "Ring the bell", + self.app.action_bell, + "Ring the terminal's 'bell'", + ), + ): + yield DiscoveryHit( + name, + runnable, + help=help_text, + ) diff --git a/src/textual/command.py b/src/textual/command.py index 41d464df58..9df835391c 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -49,6 +49,7 @@ __all__ = [ "CommandPalette", + "DiscoveryHit", "Hit", "Hits", "Matcher", @@ -105,7 +106,52 @@ def __post_init__(self) -> None: ) -Hits: TypeAlias = AsyncIterator[Hit] +@dataclass +class DiscoveryHit: + """Holds the details of a single command search hit.""" + + match_display: RenderableType + """A string or Rich renderable representation of the hit.""" + + command: IgnoreReturnCallbackType + """The function to call when the command is chosen.""" + + text: str | None = None + """The command text associated with the hit, as plain text. + + If `match_display` is not simple text, this attribute should be provided by the + [Provider][textual.command.Provider] object. + """ + + help: str | None = None + """Optional help text for the command.""" + + def __lt__(self, other: object) -> bool: + if isinstance(other, DiscoveryHit): + assert self.text is not None + assert other.text is not None + return self.text < other.text + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Hit): + return self.text == other.text + return NotImplemented + + def __post_init__(self) -> None: + """Ensure 'text' is populated.""" + if self.text is None: + if isinstance(self.match_display, str): + self.text = self.match_display + elif isinstance(self.match_display, Text): + self.text = self.match_display.plain + else: + raise ValueError( + "A value for 'text' is required if 'match_display' is not a str or Text" + ) + + +Hits: TypeAlias = AsyncIterator[DiscoveryHit | Hit] """Return type for the command provider's `search` method.""" @@ -199,9 +245,10 @@ async def _search(self, query: str) -> Hits: """ await self._wait_init() if self._init_success: - hits = self.search(query) + hits = self.search(query) if query else self.discover() async for hit in hits: - yield hit + if hit is not NotImplemented: + yield hit @abstractmethod async def search(self, query: str) -> Hits: @@ -215,6 +262,22 @@ async def search(self, query: str) -> Hits: """ yield NotImplemented + async def discover(self) -> Hits: + """A default collection of hits for the provider. + + Yields: + Instances of [`Hit`][textual.command.Hit]. + + Note: + This is different from + [`search`][textual.command.Provider.search] in that it should + yield [`Hit`s][textual.command.Hit] that should be shown by + default; before user input. + + It is permitted to *not* implement this method. + """ + yield NotImplemented + async def _shutdown(self) -> None: """Internal method to call shutdown and log errors.""" try: @@ -561,6 +624,7 @@ def _on_mount(self, _: Mount) -> None: ] for provider in self._providers: provider._post_init() + self._gather_commands("") async def _on_unmount(self) -> None: """Shutdown providers when command palette is closed.""" From 3847b50e2b788b23126a3f1bde3042326c41666a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 11:37:16 +0000 Subject: [PATCH 23/58] Refine the command palette discovery system somewhat --- src/textual/command.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/textual/command.py b/src/textual/command.py index 9df835391c..2e13dc48fa 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -83,6 +83,11 @@ class Hit: help: str | None = None """Optional help text for the command.""" + @property + def prompt(self) -> RenderableType: + """The prompt to use when displaying the hit in the command palette.""" + return self.match_display + def __lt__(self, other: object) -> bool: if isinstance(other, Hit): return self.score < other.score @@ -110,7 +115,7 @@ def __post_init__(self) -> None: class DiscoveryHit: """Holds the details of a single command search hit.""" - match_display: RenderableType + display: RenderableType """A string or Rich renderable representation of the hit.""" command: IgnoreReturnCallbackType @@ -119,13 +124,18 @@ class DiscoveryHit: text: str | None = None """The command text associated with the hit, as plain text. - If `match_display` is not simple text, this attribute should be provided by the - [Provider][textual.command.Provider] object. + If `display` is not simple text, this attribute should be provided by + the [Provider][textual.command.Provider] object. """ help: str | None = None """Optional help text for the command.""" + @property + def prompt(self) -> RenderableType: + """The prompt to use when displaying the discovery hit in the command palette.""" + return self.display + def __lt__(self, other: object) -> bool: if isinstance(other, DiscoveryHit): assert self.text is not None @@ -141,17 +151,17 @@ def __eq__(self, other: object) -> bool: def __post_init__(self) -> None: """Ensure 'text' is populated.""" if self.text is None: - if isinstance(self.match_display, str): - self.text = self.match_display - elif isinstance(self.match_display, Text): - self.text = self.match_display.plain + if isinstance(self.display, str): + self.text = self.display + elif isinstance(self.display, Text): + self.text = self.display.plain else: raise ValueError( - "A value for 'text' is required if 'match_display' is not a str or Text" + "A value for 'text' is required if 'display' is not a str or Text" ) -Hits: TypeAlias = AsyncIterator[DiscoveryHit | Hit] +Hits: TypeAlias = AsyncIterator["DiscoveryHit | Hit"] """Return type for the command provider's `search` method.""" @@ -941,7 +951,7 @@ async def _gather_commands(self, search_value: str) -> None: while hit: # Turn the command into something for display, and add it to the # list of commands that have been gathered so far. - prompt = hit.match_display + prompt = hit.prompt if hit.help: prompt = Group(prompt, Text(hit.help, style=help_style)) gathered_commands.append(Command(prompt, hit, id=str(command_id))) From a272c6de96d74263c6060cb6f6d2de97fa350f84 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 11:50:08 +0000 Subject: [PATCH 24/58] Alpha sort discovered hits in the correct order --- src/textual/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/command.py b/src/textual/command.py index 2e13dc48fa..922204b9e6 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -140,7 +140,7 @@ def __lt__(self, other: object) -> bool: if isinstance(other, DiscoveryHit): assert self.text is not None assert other.text is not None - return self.text < other.text + return other.text < self.text return NotImplemented def __eq__(self, other: object) -> bool: From 97371c13a3bab3ecbf17870a20ad1cd5e5db70e9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 11:50:36 +0000 Subject: [PATCH 25/58] Tidy up some typing --- src/textual/command.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/textual/command.py b/src/textual/command.py index 922204b9e6..4f23dcc45f 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -313,7 +313,7 @@ class Command(Option): def __init__( self, prompt: RenderableType, - command: Hit, + command: DiscoveryHit | Hit, id: str | None = None, disabled: bool = False, ) -> None: @@ -538,7 +538,7 @@ class CommandPalette(_SystemModalScreen[CallbackType]): def __init__(self) -> None: """Initialise the command palette.""" super().__init__(id=self._PALETTE_ID) - self._selected_command: Hit | None = None + self._selected_command: DiscoveryHit | Hit | None = None """The command that was selected by the user.""" self._busy_timer: Timer | None = None """Keeps track of if there's a busy indication timer in effect.""" @@ -714,7 +714,7 @@ async def _watch__show_busy(self) -> None: self.query_one(CommandList).set_class(self._show_busy, "--populating") @staticmethod - async def _consume(hits: Hits, commands: Queue[Hit]) -> None: + async def _consume(hits: Hits, commands: Queue[DiscoveryHit | Hit]) -> None: """Consume a source of matching commands, feeding the given command queue. Args: @@ -724,7 +724,9 @@ async def _consume(hits: Hits, commands: Queue[Hit]) -> None: async for hit in hits: await commands.put(hit) - async def _search_for(self, search_value: str) -> AsyncGenerator[Hit, bool]: + async def _search_for( + self, search_value: str + ) -> AsyncGenerator[DiscoveryHit | Hit, bool]: """Search for a given search value amongst all of the command providers. Args: @@ -735,7 +737,7 @@ async def _search_for(self, search_value: str) -> AsyncGenerator[Hit, bool]: """ # Set up a queue to stream in the command hits from all the providers. - commands: Queue[Hit] = Queue() + commands: Queue[DiscoveryHit | Hit] = Queue() # Fire up an instance of each command provider, inside a task, and # have them go start looking for matches. From e59fd0e2a20892afca210ad3ab6a7b13fbbc3147 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 12:43:24 +0000 Subject: [PATCH 26/58] Deduplicate the collection of system commands in the provider --- src/textual/_system_commands.py | 77 +++++++++++++++------------------ 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/textual/_system_commands.py b/src/textual/_system_commands.py index 0796deb891..b164b75a26 100644 --- a/src/textual/_system_commands.py +++ b/src/textual/_system_commands.py @@ -4,7 +4,10 @@ actions available via the [command palette][textual.command.CommandPalette]. """ +from __future__ import annotations + from .command import DiscoveryHit, Hit, Hits, Provider +from .types import IgnoreReturnCallbackType class SystemCommands(Provider): @@ -13,22 +16,10 @@ class SystemCommands(Provider): Used by default in [`App.COMMANDS`][textual.app.App.COMMANDS]. """ - async def search(self, query: str) -> Hits: - """Handle a request to search for system commands that match the query. - - Args: - query: The user input to be matched. - - Yields: - Command hits for use in the command palette. - """ - # We're going to use Textual's builtin fuzzy matcher to find - # matching commands. - matcher = self.matcher(query) - - # Loop over all applicable commands, find those that match and offer - # them up to the command palette. - for name, runnable, help_text in ( + @property + def _system_commands(self) -> tuple[tuple[str, IgnoreReturnCallbackType, str], ...]: + """The system commands to reveal to the command palette.""" + return ( ( "Toggle light/dark mode", self.app.action_toggle_dark, @@ -44,15 +35,7 @@ async def search(self, query: str) -> Hits: self.app.action_bell, "Ring the terminal's 'bell'", ), - ): - match = matcher.match(name) - if match > 0: - yield Hit( - match, - matcher.highlight(name), - runnable, - help=help_text, - ) + ) async def discover(self) -> Hits: """Handle a request for the discovery commands for this provider. @@ -60,26 +43,34 @@ async def discover(self) -> Hits: Yields: Commands that can be discovered. """ - # TODO: Dedupe these from the above. - for name, runnable, help_text in ( - ( - "Toggle light/dark mode", - self.app.action_toggle_dark, - "Toggle the application between light and dark mode", - ), - ( - "Quit the application", - self.app.action_quit, - "Quit the application as soon as possible", - ), - ( - "Ring the bell", - self.app.action_bell, - "Ring the terminal's 'bell'", - ), - ): + for name, runnable, help_text in self._system_commands: yield DiscoveryHit( name, runnable, help=help_text, ) + + async def search(self, query: str) -> Hits: + """Handle a request to search for system commands that match the query. + + Args: + query: The user input to be matched. + + Yields: + Command hits for use in the command palette. + """ + # We're going to use Textual's builtin fuzzy matcher to find + # matching commands. + matcher = self.matcher(query) + + # Loop over all applicable commands, find those that match and offer + # them up to the command palette. + for name, runnable, help_text in self._system_commands: + match = matcher.match(name) + if match > 0: + yield Hit( + match, + matcher.highlight(name), + runnable, + help=help_text, + ) From ec32ed5032b579cc332b5877c1279885e995e9c9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 12:48:52 +0000 Subject: [PATCH 27/58] Throw in a walrus because reasons --- src/textual/_system_commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/_system_commands.py b/src/textual/_system_commands.py index b164b75a26..ffa73b263a 100644 --- a/src/textual/_system_commands.py +++ b/src/textual/_system_commands.py @@ -66,8 +66,7 @@ async def search(self, query: str) -> Hits: # Loop over all applicable commands, find those that match and offer # them up to the command palette. for name, runnable, help_text in self._system_commands: - match = matcher.match(name) - if match > 0: + if (match := matcher.match(name)) > 0: yield Hit( match, matcher.highlight(name), From e1be32cf34d4eb4f874e896559c37e531162ffd7 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 13:24:41 +0000 Subject: [PATCH 28/58] Handle showing discovery results when the input goes empty --- src/textual/command.py | 43 ++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/textual/command.py b/src/textual/command.py index 4f23dcc45f..228e497540 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -672,23 +672,36 @@ def _stop_no_matches_countdown(self) -> None: _NO_MATCHES_COUNTDOWN: Final[float] = 0.5 """How many seconds to wait before showing 'No matches found'.""" - def _start_no_matches_countdown(self) -> None: + def _start_no_matches_countdown(self, search_value: str) -> None: """Start a countdown to showing that there are no matches for the query. - Adds a 'No matches found' option to the command list after `_NO_MATCHES_COUNTDOWN` seconds. + Args: + search_value: The value being searched for. + + Adds a 'No matches found' option to the command list after + `_NO_MATCHES_COUNTDOWN` seconds. """ self._stop_no_matches_countdown() def _show_no_matches() -> None: - command_list = self.query_one(CommandList) - command_list.add_option( - Option( - Align.center(Text("No matches found")), - disabled=True, - id=self._NO_MATCHES, + # If we were actually searching for something, show that we + # found no matches. + if search_value: + command_list = self.query_one(CommandList) + command_list.add_option( + Option( + Align.center(Text("No matches found")), + disabled=True, + id=self._NO_MATCHES, + ) ) - ) - self._list_visible = True + self._list_visible = True + else: + # The search value was empty, which means we were in + # discover mode; in that case it makes no sense to show that + # no matches were found. Lack of commands that can be + # discovered is a situation we don't need to highlight. + self._list_visible = False self._no_matches_timer = self.set_timer( self._NO_MATCHES_COUNTDOWN, @@ -1002,7 +1015,7 @@ async def _gather_commands(self, search_value: str) -> None: # mean nothing was found. Give the user positive feedback to that # effect. if command_list.option_count == 0 and not worker.is_cancelled: - self._start_no_matches_countdown() + self._start_no_matches_countdown(search_value) def _cancel_gather_commands(self) -> None: """Cancel any operation that is gather commands.""" @@ -1018,13 +1031,7 @@ def _input(self, event: Input.Changed) -> None: event.stop() self._cancel_gather_commands() self._stop_no_matches_countdown() - - search_value = event.value.strip() - if search_value: - self._gather_commands(search_value) - else: - self._list_visible = False - self.query_one(CommandList).clear_options() + self._gather_commands(event.value.strip()) @on(OptionList.OptionSelected) def _select_command(self, event: OptionList.OptionSelected) -> None: From 9a8ab7d37408c2dc1cad1e789d96da613a1c9b17 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 14:52:26 +0000 Subject: [PATCH 29/58] Add a unit test for provider discovery --- tests/command_palette/test_discover.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/command_palette/test_discover.py diff --git a/tests/command_palette/test_discover.py b/tests/command_palette/test_discover.py new file mode 100644 index 0000000000..24849adf07 --- /dev/null +++ b/tests/command_palette/test_discover.py @@ -0,0 +1,34 @@ +from textual.app import App +from textual.command import CommandPalette, DiscoveryHit, Hit, Hits, Provider +from textual.widgets import OptionList + + +class SimpleSource(Provider): + + async def discover(self) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + yield DiscoveryHit("XD-1", goes_nowhere_does_nothing, "XD-1") + + async def search(self, query: str) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + yield Hit(1, query, goes_nowhere_does_nothing, query) + + +class CommandPaletteApp(App[None]): + COMMANDS = {SimpleSource} + + def on_mount(self) -> None: + self.action_command_palette() + + +async def test_discovery_visible() -> None: + """A provider with discovery should cause the command palette to be opened right away.""" + async with CommandPaletteApp().run_test() as pilot: + assert CommandPalette.is_open(pilot.app) + results = pilot.app.screen.query_one(OptionList) + assert results.visible is True + assert results.option_count == 1 From e6ad1bd991857e9ce74db62c7bb916c71f760433 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 13 Feb 2024 15:16:57 +0000 Subject: [PATCH 30/58] Text area read only (#4151) * Add read_only reactive * Using nested CSS in TextArea and adding COMPONENT_CLASS for read-only cursor * Applying/removing CSS class `.-read-only` in TextArea * Preventing some edits in read-only mode. * Clearer distinction between user/keyboard driven edits and programmatic edits * Ensure we refresh cursor correctly when pressing key in read-only mode * Add test of paste in read-only mode * Fix typo in docstring * Ensure "delete line" keybinding doesnt move cursor in read_only mode in TextArea * Add clarification to docs based on issue #4145 * Add test to ensure read-only cursor colour * Update CHANGELOG * Fix cursor styling in CSS on read-only * Fix a docstring * Improving docstrings * Improving docstrings * Simplify fixtures * Test to ensure API driven editing still works on TextArea.read_only=True --- CHANGELOG.md | 1 + src/textual/_text_area_theme.py | 2 +- src/textual/document/_document_navigator.py | 6 +- src/textual/widgets/_text_area.py | 186 +++++++++++++----- .../__snapshots__/test_snapshots.ambr | 83 ++++++++ tests/snapshot_tests/test_snapshots.py | 14 ++ tests/text_area/test_edit_via_api.py | 22 ++- tests/text_area/test_edit_via_bindings.py | 43 ++++ tests/text_area/test_selection_bindings.py | 87 ++++---- 9 files changed, 338 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2241508c9c..220e7e0f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- TextArea now has `read_only` mode https://github.com/Textualize/textual/pull/4151 - Add some syntax highlighting to TextArea default theme https://github.com/Textualize/textual/pull/4149 ## [0.51.1] - 2024-02-09 diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index b9f920600b..5fbcee9967 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -107,7 +107,7 @@ def apply_css(self, text_area: TextArea) -> None: self.cursor_style = cursor_style else: # There's no component style either, fallback to a default. - self.cursor_style = Style( + self.cursor_style = Style.from_color( color=background_color.rich_color, bgcolor=background_color.inverse.rich_color, ) diff --git a/src/textual/document/_document_navigator.py b/src/textual/document/_document_navigator.py index e265f03b2a..25b44e8422 100644 --- a/src/textual/document/_document_navigator.py +++ b/src/textual/document/_document_navigator.py @@ -101,11 +101,15 @@ def is_start_of_wrapped_line(self, location: Location) -> bool: def is_end_of_document_line(self, location: Location) -> bool: """True if the location is at the end of a line in the document. + Note that the "end" of a line is equal to its length (one greater + than the final index), since there is a space at the end of the line + for the cursor to rest. + Args: location: The location to examine. Returns: - True if and only if the document is on the last line of the document. + True if and only if the document is at the end of a line in the document. """ row, column = location row_length = len(self._document[row]) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index d5f4d8efef..085ce16a3f 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -91,41 +91,51 @@ class TextArea(ScrollView, can_focus=True): border: tall $background; padding: 0 1; + & .text-area--gutter { + color: $text 40%; + } + + & .text-area--cursor-gutter { + color: $text 60%; + background: $boost; + text-style: bold; + } + + & .text-area--cursor-line { + background: $boost; + } + + & .text-area--selection { + background: $accent-lighten-1 40%; + } + + & .text-area--matching-bracket { + background: $foreground 30%; + } + &:focus { border: tall $accent; } -} - -.text-area--cursor { - color: $text 90%; - background: $foreground 90%; -} - -TextArea:light .text-area--cursor { - color: $text 90%; - background: $foreground 70%; -} - -.text-area--gutter { - color: $text 40%; -} - -.text-area--cursor-line { - background: $boost; -} - -.text-area--cursor-gutter { - color: $text 60%; - background: $boost; - text-style: bold; -} - -.text-area--selection { - background: $accent-lighten-1 40%; -} - -.text-area--matching-bracket { - background: $foreground 30%; + + &:dark { + .text-area--cursor { + color: $text 90%; + background: $foreground 90%; + } + &.-read-only .text-area--cursor { + background: $warning-darken-1; + } + } + + &:light { + .text-area--cursor { + color: $text 90%; + background: $foreground 70%; + } + &.-read-only .text-area--cursor { + background: $warning-darken-1; + } + } } """ @@ -295,6 +305,14 @@ class TextArea(ScrollView, can_focus=True): soft_wrap: Reactive[bool] = reactive(True, init=False) """True if text should soft wrap.""" + read_only: Reactive[bool] = reactive(False) + """True if the content is read-only. + + Read-only means end users cannot insert, delete or replace content. + + The document can still be edited programmatically via the API. + """ + _cursor_visible: Reactive[bool] = reactive(False, repaint=False, init=False) """Indicates where the cursor is in the blink cycle. If it's currently not visible due to blinking, this is False.""" @@ -337,6 +355,7 @@ def __init__( language: str | None = None, theme: str | None = None, soft_wrap: bool = True, + read_only: bool = False, tab_behaviour: Literal["focus", "indent"] = "focus", show_line_numbers: bool = False, name: str | None = None, @@ -351,7 +370,9 @@ def __init__( language: The language to use. theme: The theme to use. soft_wrap: Enable soft wrapping. - tab_behaviour: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab. + read_only: Enable read-only mode. This prevents edits using the keyboard. + tab_behaviour: If 'focus', pressing tab will switch focus. + If 'indent', pressing tab will insert a tab. show_line_numbers: Show line numbers on the left edge. name: The name of the `TextArea` widget. id: The ID of the widget, used to refer to it from Textual CSS. @@ -414,9 +435,9 @@ def __init__( self.theme = theme - self._reactive_soft_wrap = soft_wrap - - self._reactive_show_line_numbers = show_line_numbers + self.set_reactive(TextArea.soft_wrap, soft_wrap) + self.set_reactive(TextArea.read_only, read_only) + self.set_reactive(TextArea.show_line_numbers, show_line_numbers) self.tab_behaviour = tab_behaviour @@ -561,6 +582,10 @@ def _watch_cursor_blink(self, blink: bool) -> None: else: self._pause_blink(visible=self.has_focus) + def _watch_read_only(self, read_only: bool) -> None: + self.set_class(read_only, "-read-only") + self._set_theme(self._theme.name) + def _recompute_cursor_offset(self): """Recompute the (x, y) coordinate of the cursor in the wrapped document.""" self._cursor_offset = self.wrapped_document.location_to_offset( @@ -656,10 +681,10 @@ def _watch_theme(self, theme: str | None) -> None: if padding is applied, the colours match.""" self._set_theme(theme) - def _app_dark_toggled(self): + def _app_dark_toggled(self) -> None: self._set_theme(self._theme.name) - def _set_theme(self, theme: str | None): + def _set_theme(self, theme: str | None) -> None: theme_object: TextAreaTheme | None if theme is None: # If the theme is None, use the default. @@ -1219,6 +1244,10 @@ def edit(self, edit: Edit) -> EditResult: async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" + self._restart_blink() + if self.read_only: + return + key = event.key insert_values = { "enter": "\n", @@ -1234,7 +1263,6 @@ async def _on_key(self, event: events.Key) -> None: else: insert_values["tab"] = " " * self._find_columns_to_next_tab_stop() - self._restart_blink() if event.is_printable or key in insert_values: event.stop() event.prevent_default() @@ -1243,7 +1271,7 @@ async def _on_key(self, event: events.Key) -> None: # None because we've checked that it's printable. assert insert is not None start, end = self.selection - self.replace(insert, start, end, maintain_selection_offset=False) + self._replace_via_keyboard(insert, start, end) def _find_columns_to_next_tab_stop(self) -> int: """Get the location of the next tab stop after the cursors position on the current line. @@ -1310,6 +1338,11 @@ def _toggle_cursor_blink_visible(self) -> None: _, cursor_y = self._cursor_offset self.refresh_lines(cursor_y) + def _watch__cursor_visible(self) -> None: + """When the cursor visibility is toggled, ensure the row is refreshed.""" + _, cursor_y = self._cursor_offset + self.refresh_lines(cursor_y) + def _restart_blink(self) -> None: """Reset the cursor blink timer.""" if self.cursor_blink: @@ -1347,7 +1380,9 @@ async def _on_mouse_up(self, event: events.MouseUp) -> None: async def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" - result = self.replace(event.text, *self.selection) + if self.read_only: + return + result = self._replace_via_keyboard(event.text, *self.selection) self.move_cursor(result.end_location) def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: @@ -1847,12 +1882,54 @@ def replace( """ return self.edit(Edit(insert, start, end, maintain_selection_offset)) - def clear(self) -> None: - """Delete all text from the document.""" + def clear(self) -> EditResult: + """Delete all text from the document. + + Returns: + An EditResult relating to the deletion of all content. + """ document = self.document last_line = document[-1] document_end = (document.line_count, len(last_line)) - self.delete((0, 0), document_end, maintain_selection_offset=False) + return self.delete((0, 0), document_end, maintain_selection_offset=False) + + def _delete_via_keyboard( + self, + start: Location, + end: Location, + ) -> EditResult | None: + """Handle a deletion performed using a keyboard (as opposed to the API). + + Args: + start: The start location of the text to delete. + end: The end location of the text to delete. + + Returns: + An EditResult or None if no edit was performed (e.g. on read-only mode). + """ + if self.read_only: + return None + return self.delete(start, end, maintain_selection_offset=False) + + def _replace_via_keyboard( + self, + insert: str, + start: Location, + end: Location, + ) -> EditResult | None: + """Handle a replacement performed using a keyboard (as opposed to the API). + + Args: + insert: The text to insert into the document. + start: The start location of the text to replace. + end: The end location of the text to replace. + + Returns: + An EditResult or None if no edit was performed (e.g. on read-only mode). + """ + if self.read_only: + return None + return self.replace(insert, start, end, maintain_selection_offset=False) def action_delete_left(self) -> None: """Deletes the character to the left of the cursor and updates the cursor location. @@ -1865,7 +1942,7 @@ def action_delete_left(self) -> None: if selection.is_empty: end = self.get_cursor_left_location() - self.delete(start, end, maintain_selection_offset=False) + self._delete_via_keyboard(start, end) def action_delete_right(self) -> None: """Deletes the character to the right of the cursor and keeps the cursor at the same location. @@ -1878,7 +1955,7 @@ def action_delete_right(self) -> None: if selection.is_empty: end = self.get_cursor_right_location() - self.delete(start, end, maintain_selection_offset=False) + self._delete_via_keyboard(start, end) def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" @@ -1895,20 +1972,21 @@ def action_delete_line(self) -> None: from_location = (start_row, 0) to_location = (end_row + 1, 0) - self.delete(from_location, to_location, maintain_selection_offset=False) - self.move_cursor_relative(columns=end_column, record_width=False) + deletion = self._delete_via_keyboard(from_location, to_location) + if deletion is not None: + self.move_cursor_relative(columns=end_column, record_width=False) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" from_location = self.selection.end to_location = self.get_cursor_line_start_location() - self.delete(from_location, to_location, maintain_selection_offset=False) + self._delete_via_keyboard(from_location, to_location) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" from_location = self.selection.end to_location = self.get_cursor_line_end_location() - self.delete(from_location, to_location, maintain_selection_offset=False) + self._delete_via_keyboard(from_location, to_location) def action_delete_word_left(self) -> None: """Deletes the word to the left of the cursor and updates the cursor location.""" @@ -1919,11 +1997,11 @@ def action_delete_word_left(self) -> None: # deletes the characters within the selection range, ignoring word boundaries. start, end = self.selection if start != end: - self.delete(start, end, maintain_selection_offset=False) + self._delete_via_keyboard(start, end) return to_location = self.get_cursor_word_left_location() - self.delete(self.selection.end, to_location, maintain_selection_offset=False) + self._delete_via_keyboard(self.selection.end, to_location) def action_delete_word_right(self) -> None: """Deletes the word to the right of the cursor and keeps the cursor at the same location. @@ -1937,7 +2015,7 @@ def action_delete_word_right(self) -> None: start, end = self.selection if start != end: - self.delete(start, end, maintain_selection_offset=False) + self._delete_via_keyboard(start, end) return cursor_row, cursor_column = end @@ -1957,7 +2035,7 @@ def action_delete_word_right(self) -> None: else: to_location = (cursor_row, current_row_length) - self.delete(end, to_location, maintain_selection_offset=False) + self._delete_via_keyboard(end, to_location) @dataclass diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index f73d96309d..f4e6208590 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -36636,6 +36636,89 @@ ''' # --- +# name: test_text_area_read_only_cursor_rendering + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1  Hello, world!           + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- # name: test_text_area_selection_rendering[selection0] ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 799027f92e..ceb08b05b6 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -879,6 +879,20 @@ def setup_selection(pilot): ) +def test_text_area_read_only_cursor_rendering(snap_compare): + def setup_selection(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.theme = "css" + text_area.text = "Hello, world!" + text_area.read_only = True + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_selection, + terminal_size=(30, 5), + ) + + @pytest.mark.syntax @pytest.mark.parametrize( "theme_name", [theme.name for theme in TextAreaTheme.builtin_themes()] diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py index 98072d35ae..e217bfa210 100644 --- a/tests/text_area/test_edit_via_api.py +++ b/tests/text_area/test_edit_via_api.py @@ -5,6 +5,7 @@ Note that more extensive testing for editing is done at the Document level. """ + import pytest from textual.app import App, ComposeResult @@ -521,10 +522,29 @@ async def test_replace_fully_within_selection(): ) assert text_area.selected_text == "XX56" + async def test_text_setter(): app = TextAreaApp() async with app.run_test(): text_area = app.query_one(TextArea) new_text = "hello\nworld\n" text_area.text = new_text - assert text_area.text == new_text \ No newline at end of file + assert text_area.text == new_text + + +async def test_edits_on_read_only_mode(): + """API edits should still be permitted on read-only mode.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.text = "0123456789" + text_area.read_only = True + + text_area.replace("X", (0, 1), (0, 5)) + assert text_area.text == "0X56789" + + text_area.insert("X") + assert text_area.text == "X0X56789" + + text_area.delete((0, 0), (0, 2)) + assert text_area.text == "X56789" diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 2cd5a41114..17d7f52f02 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -420,6 +420,37 @@ async def test_delete_word_right_at_end_of_line(): assert text_area.selection == Selection.cursor((0, 5)) +@pytest.mark.parametrize( + "binding", + [ + "enter", + "backspace", + "ctrl+u", + "ctrl+f", + "ctrl+w", + "ctrl+k", + "ctrl+x", + "space", + "1", + "tab", + ], +) +async def test_edit_read_only_mode_does_nothing(binding): + """Try out various key-presses and bindings and ensure they don't alter + the document when read_only=True.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.read_only = True + selection = Selection.cursor((0, 2)) + text_area.selection = selection + + await pilot.press(binding) + + assert text_area.text == TEXT + assert text_area.selection == selection + + @pytest.mark.parametrize( "selection", [ @@ -469,3 +500,15 @@ async def test_paste(selection): Z""" assert text_area.text == expected_text assert text_area.selection == Selection.cursor((1, 1)) + + +async def test_paste_read_only_does_nothing(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.read_only = True + + app.post_message(Paste("hello")) + await pilot.pause() + + assert text_area.text == TEXT # No change diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py index 7efdcef164..06b180485e 100644 --- a/tests/text_area/test_selection_bindings.py +++ b/tests/text_area/test_selection_bindings.py @@ -13,34 +13,41 @@ class TextAreaApp(App): + def __init__(self, read_only: bool = False): + super().__init__() + self.read_only = read_only + def compose(self) -> ComposeResult: - text_area = TextArea(show_line_numbers=True) - text_area.load_text(TEXT) - yield text_area + yield TextArea(TEXT, show_line_numbers=True, read_only=self.read_only) + +@pytest.fixture(params=[True, False]) +async def app(request): + """Each test that receives an `app` will execute twice. + Once with read_only=True, and once with read_only=False. + """ + return TextAreaApp(read_only=request.param) -async def test_mouse_click(): + +async def test_mouse_click(app: TextAreaApp): """When you click the TextArea, the cursor moves to the expected location.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) await pilot.click(TextArea, Offset(x=5, y=2)) assert text_area.selection == Selection.cursor((1, 0)) -async def test_mouse_click_clamp_from_right(): +async def test_mouse_click_clamp_from_right(app: TextAreaApp): """When you click to the right of the document bounds, the cursor is clamped to within the document bounds.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) await pilot.click(TextArea, Offset(x=8, y=20)) assert text_area.selection == Selection.cursor((4, 0)) -async def test_mouse_click_gutter_clamp(): +async def test_mouse_click_gutter_clamp(app: TextAreaApp): """When you click the gutter, it selects the start of the line.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) await pilot.click(TextArea, Offset(x=0, y=3)) @@ -66,19 +73,17 @@ async def test_cursor_movement_basic(): assert text_area.selection == Selection.cursor((0, 0)) -async def test_cursor_selection_right(): +async def test_cursor_selection_right(app: TextAreaApp): """When you press shift+right the selection is updated correctly.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) await pilot.press(*["shift+right"] * 3) assert text_area.selection == Selection((0, 0), (0, 3)) -async def test_cursor_selection_right_to_previous_line(): +async def test_cursor_selection_right_to_previous_line(app: TextAreaApp): """When you press shift+right resulting in the cursor moving to the next line, the selection is updated correctly.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((0, 15)) @@ -86,9 +91,8 @@ async def test_cursor_selection_right_to_previous_line(): assert text_area.selection == Selection((0, 15), (1, 2)) -async def test_cursor_selection_left(): +async def test_cursor_selection_left(app: TextAreaApp): """When you press shift+left the selection is updated correctly.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((2, 5)) @@ -96,10 +100,9 @@ async def test_cursor_selection_left(): assert text_area.selection == Selection((2, 5), (2, 2)) -async def test_cursor_selection_left_to_previous_line(): +async def test_cursor_selection_left_to_previous_line(app: TextAreaApp): """When you press shift+left resulting in the cursor moving back to the previous line, the selection is updated correctly.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((2, 2)) @@ -110,9 +113,8 @@ async def test_cursor_selection_left_to_previous_line(): assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) -async def test_cursor_selection_up(): +async def test_cursor_selection_up(app: TextAreaApp): """When you press shift+up the selection is updated correctly.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.move_cursor((2, 3)) @@ -121,9 +123,8 @@ async def test_cursor_selection_up(): assert text_area.selection == Selection((2, 3), (1, 3)) -async def test_cursor_selection_up_when_cursor_on_first_line(): +async def test_cursor_selection_up_when_cursor_on_first_line(app: TextAreaApp): """When you press shift+up the on the first line, it selects to the start.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.move_cursor((0, 4)) @@ -134,8 +135,7 @@ async def test_cursor_selection_up_when_cursor_on_first_line(): assert text_area.selection == Selection((0, 4), (0, 0)) -async def test_cursor_selection_down(): - app = TextAreaApp() +async def test_cursor_selection_down(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.move_cursor((2, 5)) @@ -144,8 +144,7 @@ async def test_cursor_selection_down(): assert text_area.selection == Selection((2, 5), (3, 5)) -async def test_cursor_selection_down_when_cursor_on_last_line(): - app = TextAreaApp() +async def test_cursor_selection_down_when_cursor_on_last_line(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("ABCDEF\nGHIJK") @@ -157,8 +156,7 @@ async def test_cursor_selection_down_when_cursor_on_last_line(): assert text_area.selection == Selection((1, 2), (1, 5)) -async def test_cursor_word_right(): - app = TextAreaApp() +async def test_cursor_word_right(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("ABC DEF\nGHIJK") @@ -168,8 +166,7 @@ async def test_cursor_word_right(): assert text_area.selection == Selection.cursor((0, 3)) -async def test_cursor_word_right_select(): - app = TextAreaApp() +async def test_cursor_word_right_select(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("ABC DEF\nGHIJK") @@ -179,8 +176,7 @@ async def test_cursor_word_right_select(): assert text_area.selection == Selection((0, 0), (0, 3)) -async def test_cursor_word_left(): - app = TextAreaApp() +async def test_cursor_word_left(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("ABC DEF\nGHIJK") @@ -191,8 +187,7 @@ async def test_cursor_word_left(): assert text_area.selection == Selection.cursor((0, 4)) -async def test_cursor_word_left_select(): - app = TextAreaApp() +async def test_cursor_word_left_select(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("ABC DEF\nGHIJK") @@ -204,9 +199,8 @@ async def test_cursor_word_left_select(): @pytest.mark.parametrize("key", ["end", "ctrl+e"]) -async def test_cursor_to_line_end(key): +async def test_cursor_to_line_end(key, app: TextAreaApp): """You can use the keyboard to jump the cursor to the end of the current line.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((2, 2)) @@ -217,9 +211,8 @@ async def test_cursor_to_line_end(key): @pytest.mark.parametrize("key", ["home", "ctrl+a"]) -async def test_cursor_to_line_home_basic_behaviour(key): +async def test_cursor_to_line_home_basic_behaviour(key, app: TextAreaApp): """You can use the keyboard to jump the cursor to the start of the current line.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.selection = Selection.cursor((2, 2)) @@ -239,11 +232,12 @@ async def test_cursor_to_line_home_basic_behaviour(key): ((0, 15), (0, 4)), ], ) -async def test_cursor_line_home_smart_home(cursor_start, cursor_destination): +async def test_cursor_line_home_smart_home( + cursor_start, cursor_destination, app: TextAreaApp +): """If the line begins with whitespace, pressing home firstly goes to the start of the (non-whitespace) content. Pressing it again takes you to column 0. If you press it again, it goes back to the first non-whitespace column.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text(" hello world") @@ -252,9 +246,8 @@ async def test_cursor_line_home_smart_home(cursor_start, cursor_destination): assert text_area.selection == Selection.cursor(cursor_destination) -async def test_cursor_page_down(): +async def test_cursor_page_down(app: TextAreaApp): """Pagedown moves the cursor down 1 page, retaining column index.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("XXX\n" * 200) @@ -266,9 +259,8 @@ async def test_cursor_page_down(): ) -async def test_cursor_page_up(): +async def test_cursor_page_up(app: TextAreaApp): """Pageup moves the cursor up 1 page, retaining column index.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.load_text("XXX\n" * 200) @@ -280,10 +272,9 @@ async def test_cursor_page_up(): ) -async def test_cursor_vertical_movement_visual_alignment_snapping(): +async def test_cursor_vertical_movement_visual_alignment_snapping(app: TextAreaApp): """When you move the cursor vertically, it should stay vertically aligned even when double-width characters are used.""" - app = TextAreaApp() async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.text = "こんにちは\n012345" @@ -301,8 +292,7 @@ async def test_cursor_vertical_movement_visual_alignment_snapping(): assert text_area.selection == Selection.cursor((1, 3)) -async def test_select_line_binding(): - app = TextAreaApp() +async def test_select_line_binding(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) text_area.move_cursor((2, 2)) @@ -312,8 +302,7 @@ async def test_select_line_binding(): assert text_area.selection == Selection((2, 0), (2, 56)) -async def test_select_all_binding(): - app = TextAreaApp() +async def test_select_all_binding(app: TextAreaApp): async with app.run_test() as pilot: text_area = app.query_one(TextArea) From 5ef45e9cffcb0d6cb0a775ab1844e1004f8b404e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 15:38:04 +0000 Subject: [PATCH 31/58] Add a snapshot test for command palette discovery --- .../__snapshots__/test_snapshots.ambr | 161 ++++++++++++++++++ .../command_palette_discovery.py | 38 +++++ tests/snapshot_tests/test_snapshots.py | 8 + 3 files changed, 207 insertions(+) create mode 100644 tests/snapshot_tests/snapshot_apps/command_palette_discovery.py diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 4c00834c2a..02eba3ca29 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -3130,6 +3130,167 @@ ''' # --- +# name: test_command_palette_discovery + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CommandPaletteApp + + + + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + 🔎Command Palette Search... + + + This is a test of this code 0 + This is a test of this code 1 + This is a test of this code 2 + This is a test of this code 3 + This is a test of this code 4 + This is a test of this code 5 + This is a test of this code 6 + This is a test of this code 7 + This is a test of this code 8 + This is a test of this code 9 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ''' +# --- # name: test_content_switcher_example_initial ''' diff --git a/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py b/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py new file mode 100644 index 0000000000..a989a86968 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py @@ -0,0 +1,38 @@ +from textual.app import App +from textual.command import DiscoveryHit, Hit, Hits, Provider + + +class TestSource(Provider): + def goes_nowhere_does_nothing(self) -> None: + pass + + async def discover(self) -> Hits: + for n in range(10): + command = f"This is a test of this code {n}" + yield DiscoveryHit( + command, + self.goes_nowhere_does_nothing, + command, + ) + + async def search(self, query: str) -> Hits: + matcher = self.matcher(query) + for n in range(10): + command = f"This is a test of this code {n}" + yield Hit( + n / 10, + matcher.highlight(command), + self.goes_nowhere_does_nothing, + command, + ) + + +class CommandPaletteApp(App[None]): + COMMANDS = {TestSource} + + def on_mount(self) -> None: + self.action_command_palette() + + +if __name__ == "__main__": + CommandPaletteApp().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 799027f92e..cce17d0192 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -757,6 +757,14 @@ async def run_before(pilot) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "command_palette.py", run_before=run_before) +def test_command_palette_discovery(snap_compare) -> None: + async def run_before(pilot) -> None: + pilot.app.screen.query_one(Input).cursor_blink = False + await pilot.app.screen.workers.wait_for_complete() + + assert snap_compare(SNAPSHOT_APPS_DIR / "command_palette_discovery.py", run_before=run_before) + + # --- textual-dev library preview tests --- From 267926d321e12d256508b19ca4603d461584f4d0 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Tue, 13 Feb 2024 15:44:01 +0000 Subject: [PATCH 32/58] Have the provider discovery snapshot differ in search and discovery While it would normally be the case that these things would be the same; for the purposes of this test have them different so we know if search ever leaks into discovery for some bizarre reason. --- tests/snapshot_tests/snapshot_apps/command_palette_discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py b/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py index a989a86968..3b8a03496a 100644 --- a/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py +++ b/tests/snapshot_tests/snapshot_apps/command_palette_discovery.py @@ -18,7 +18,7 @@ async def discover(self) -> Hits: async def search(self, query: str) -> Hits: matcher = self.matcher(query) for n in range(10): - command = f"This is a test of this code {n}" + command = f"This should not appear {n}" yield Hit( n / 10, matcher.highlight(command), From b736c93a5e34cfabf0e75bcca5d650f751f3da7f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 13 Feb 2024 16:24:16 +0000 Subject: [PATCH 33/58] words --- docs/blog/posts/toolong-retrospective.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/blog/posts/toolong-retrospective.md b/docs/blog/posts/toolong-retrospective.md index dff9b79220..0fa480d033 100644 --- a/docs/blog/posts/toolong-retrospective.md +++ b/docs/blog/posts/toolong-retrospective.md @@ -19,7 +19,7 @@ There were some interesting technical challenges in building Toolong that I'd li This isn't specifically [Textual](https://github.com/textualize/textual/) related. These techniques could be employed in any Python project. These techniques aren't difficult, and shouldn't be beyond anyone with an intermediate understanding of Python. -They are the kind of "if you know it you kow it" knowledge that you may not need often, but can make a massive difference when you do! +They are the kind of "if you know it you know it" knowledge that you may not need often, but can make a massive difference when you do! ## Opening large files @@ -55,7 +55,7 @@ And that would likely have worked just fine, but there is a bit of magic in the The [mmap](https://docs.python.org/3/library/mmap.html) module is a real gem for this kind of thing. A *memory mapped file* is an OS-level construct that *appears* to load a file instantaneously. In Python you get an object which behaves like a `bytearray`, but loads data from disk when it is accessed. -The beauty of this module is that you can work with files in much the same way as if you had read the entire file in to memory, while leaving the actually reading of the file to the OS. +The beauty of this module is that you can work with files in much the same way as if you had read the entire file in to memory, while leaving the actual reading of the file to the OS. Here's the method that Toolong uses to scan for line breaks. Forgive the micro-optimizations, I was going for raw execution speed here. From 92ed4bfaa9f90fa5920a4f429b0a1adf6d46d9b9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 09:35:39 +0000 Subject: [PATCH 34/58] Clarify a choice in the heart of the command palette search For the reader who has made it this far, highlight the point at which the crucial decision is made. --- src/textual/command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/command.py b/src/textual/command.py index 228e497540..6a7796bde2 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -255,6 +255,8 @@ async def _search(self, query: str) -> Hits: """ await self._wait_init() if self._init_success: + # An empty search string is a discovery search, anything else is + # a conventional search. hits = self.search(query) if query else self.discover() async for hit in hits: if hit is not NotImplemented: From 51d7f28e0936d40977122c0f7d624342974b7f4b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 09:45:57 +0000 Subject: [PATCH 35/58] Clarify what Provider.discover should yield up --- src/textual/command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/command.py b/src/textual/command.py index 6a7796bde2..215d78875b 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -278,13 +278,13 @@ async def discover(self) -> Hits: """A default collection of hits for the provider. Yields: - Instances of [`Hit`][textual.command.Hit]. + Instances of [`DiscoveryHit`][textual.command.DiscoveryHit]. Note: This is different from [`search`][textual.command.Provider.search] in that it should - yield [`Hit`s][textual.command.Hit] that should be shown by - default; before user input. + yield [`DiscoveryHit`s][textual.command.DiscoveryHit] that + should be shown by default; before user input. It is permitted to *not* implement this method. """ From a705c349651d56b533c8febb05992d478c14b00b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 10:05:38 +0000 Subject: [PATCH 36/58] Fix a case typo that already existed in the document --- docs/guide/command_palette.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md index bdfd45b398..d614475a86 100644 --- a/docs/guide/command_palette.md +++ b/docs/guide/command_palette.md @@ -99,7 +99,7 @@ In the example above, the callback is a lambda which calls the `open_file` metho This is a deliberate design decision taken to prevent a single broken `Provider` class from making the command palette unusable. Errors in command providers will be logged to the [console](./devtools.md). -### Shutdown method +### shutdown method The [`shutdown`][textual.command.Provider.shutdown] method is called when the command palette is closed. You can use this as a hook to gracefully close any objects you created in [`startup`][textual.command.Provider.startup]. From cbb44bb50cbfd4a2fefd7dc803ec9df32f8cceb3 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 10:06:00 +0000 Subject: [PATCH 37/58] Mention the discover method in the docs --- docs/guide/command_palette.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md index d614475a86..d6f2d30338 100644 --- a/docs/guide/command_palette.md +++ b/docs/guide/command_palette.md @@ -68,7 +68,7 @@ The following example will display a blank screen initially, but if you bring up 5. Highlights matching letters in the search. 6. Adds our custom command provider and the default command provider. -There are three methods you can override in a command provider: [`startup`][textual.command.Provider.startup], [`search`][textual.command.Provider.search], and [`shutdown`][textual.command.Provider.shutdown]. +There are four methods you can override in a command provider: [`startup`][textual.command.Provider.startup], [`search`][textual.command.Provider.search], [`discover`][textual.command.Provider.discover] and [`shutdown`][textual.command.Provider.shutdown]. All of these methods should be coroutines (`async def`). Only `search` is required, the other methods are optional. Let's explore those methods in detail. @@ -99,6 +99,17 @@ In the example above, the callback is a lambda which calls the `open_file` metho This is a deliberate design decision taken to prevent a single broken `Provider` class from making the command palette unusable. Errors in command providers will be logged to the [console](./devtools.md). +### discover method + +The [`discover`][textual.command.Provider.discover] method is responsible for providing results (or *hits*) that should be shown to the user when the command palette input is empty; +this is to aid in command discoverability. + +`discover` is similar to `search` but with the difference that no search value is passed to it, and it should *yield* instances of [`DiscoveryHit`][textual.command.DiscoveryHit]. +There is no matching and no match score is needed. +The [`DiscoveryHit`][textual.command.DiscoveryHit] contains information about how the hit should be displayed, and an optional help string; +discovery hits are sorted in ascending alphabetical order. +It also contains a callback, which will be run if the user selects that command. + ### shutdown method The [`shutdown`][textual.command.Provider.shutdown] method is called when the command palette is closed. From 4f6f15a06690d83dc6c89efe310786983c8a3716 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 10:08:54 +0000 Subject: [PATCH 38/58] Add a note about best practice of the user of discover --- docs/guide/command_palette.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md index d6f2d30338..2da2ec592f 100644 --- a/docs/guide/command_palette.md +++ b/docs/guide/command_palette.md @@ -104,6 +104,11 @@ In the example above, the callback is a lambda which calls the `open_file` metho The [`discover`][textual.command.Provider.discover] method is responsible for providing results (or *hits*) that should be shown to the user when the command palette input is empty; this is to aid in command discoverability. +!!! note + + Because `discover` hits are shown the moment the command palette is opened, these should ideally be quick to generate; + commands that might take time to generate are best left for `search` -- use `discover` to help the user easily find the most important commands. + `discover` is similar to `search` but with the difference that no search value is passed to it, and it should *yield* instances of [`DiscoveryHit`][textual.command.DiscoveryHit]. There is no matching and no match score is needed. The [`DiscoveryHit`][textual.command.DiscoveryHit] contains information about how the hit should be displayed, and an optional help string; From 66c19fab01b337eb460bfbb0db9ccde40b945062 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 10:20:46 +0000 Subject: [PATCH 39/58] Make it clear we're talking about discovery hits --- docs/guide/command_palette.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md index 2da2ec592f..1d7fb55423 100644 --- a/docs/guide/command_palette.md +++ b/docs/guide/command_palette.md @@ -101,7 +101,7 @@ In the example above, the callback is a lambda which calls the `open_file` metho ### discover method -The [`discover`][textual.command.Provider.discover] method is responsible for providing results (or *hits*) that should be shown to the user when the command palette input is empty; +The [`discover`][textual.command.Provider.discover] method is responsible for providing results (or *discovery hits*) that should be shown to the user when the command palette input is empty; this is to aid in command discoverability. !!! note From f982dd7672751fdc78b9d5160694c3978ca03b2b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 10:22:41 +0000 Subject: [PATCH 40/58] Update the ChangeLog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 220e7e0f2c..b87acd3178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - TextArea now has `read_only` mode https://github.com/Textualize/textual/pull/4151 - Add some syntax highlighting to TextArea default theme https://github.com/Textualize/textual/pull/4149 +- Added support for command palette command discoverability https://github.com/Textualize/textual/pull/4154 ## [0.51.1] - 2024-02-09 @@ -26,7 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed duplicate watch methods being attached to DOM nodes https://github.com/Textualize/textual/pull/4030 - Fixed using `watch` to create additional watchers would trigger other watch methods https://github.com/Textualize/textual/issues/3878 -### Added +### Added - Added support for configuring dark and light themes for code in `Markdown` https://github.com/Textualize/textual/issues/3997 From fa4f75fd25c625d150e11c476bf468bf506c3400 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 14 Feb 2024 13:11:14 +0000 Subject: [PATCH 41/58] Text area undo redo (#4124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial undo related machinery added to TextArea * Initial undo implementation * Basic undo and redo * Some more fleshing out of undo and redo * Skeleton code for managing TextArea history * Initial implementation of undo & redo checkpointing in TextArea * Increase checkpoint characters * Restoring the selection in the TextArea and then restoring it on undo * Adding docstrings to undo_batch and redo_batch in the TextArea * Batching edits of the same type * Batching edits of the same type * Keeping edits containing newlines in their own batch * Checking for newline characters in insertion or replacement during undo checkpoint creation. Updating docstrings in history.py * Fix mypy warning * Performance improvement * Add history checkpoint on cursor movement * Fixing merge conflict in Edit class * Fixing error in merge conflict resolution * Remove unused test file * Remove unused test file * Initial testing of undo and redo * Testing for undo redo * Updating lockfile * Add an extra test * Fix: setting the `text` property programmatically should invalidate the edit history * Improving docstrings * Rename EditHistory.reset() to EditHistory.clear() * Add docstring to an exception * Add a pause after focus/blur in a test * Forcing CI colour * Update focus checkpoint test * Try to force color in pytest by setting --color=yes in PYTEST_ADDOPTS in env var on Github Actions * Add extra assertion in a test * Toggle text_area has focus to trigger checkpoint in history * Apply grammar/wording suggestions from code review Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Making max checkpoints configurable in TextArea history * Improve a docstring * Update changelog * Spelling fixes * More spelling fixes * Americanize spelling of tab_behaviour (->tab_behavior) * Update CHANGELOG regarding `tab_behaviour`->`tab_behavior` --------- Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- .github/workflows/pythonpackage.yml | 3 + .pre-commit-config.yaml | 8 +- CHANGELOG.md | 5 + docs/widgets/text_area.md | 2 +- poetry.lock | 758 ++++++++++++------------- pyproject.toml | 1 - src/textual/document/_document.py | 2 +- src/textual/document/_edit.py | 143 +++++ src/textual/document/_history.py | 183 ++++++ src/textual/widgets/_data_table.py | 4 +- src/textual/widgets/_text_area.py | 282 +++++---- src/textual/widgets/text_area.py | 4 +- tests/text_area/test_escape_binding.py | 12 +- tests/text_area/test_history.py | 343 +++++++++++ 14 files changed, 1180 insertions(+), 570 deletions(-) create mode 100644 src/textual/document/_edit.py create mode 100644 src/textual/document/_history.py create mode 100644 tests/text_area/test_history.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 02e367aa44..1772620e98 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,6 +11,9 @@ on: - "**.lock" - "Makefile" +env: + PYTEST_ADDOPTS: "--color=yes" + jobs: build: runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d173178cf8..f53b9c445b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,20 +17,20 @@ repos: - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline - id: mixed-line-ending # replaces or checks mixed line ending - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: '5.12.0' hooks: - id: isort name: isort (python) language_version: '3.11' - args: ["--profile", "black", "--filter-files"] + args: ['--profile', 'black', '--filter-files'] - repo: https://github.com/psf/black - rev: 24.1.1 + rev: '24.1.1' hooks: - id: black - repo: https://github.com/hadialqattan/pycln # removes unused imports rev: v2.3.0 hooks: - id: pycln - language_version: "3.11" + language_version: '3.11' args: [--all] exclude: ^tests/snapshot_tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 220e7e0f2c..5826989c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - TextArea now has `read_only` mode https://github.com/Textualize/textual/pull/4151 - Add some syntax highlighting to TextArea default theme https://github.com/Textualize/textual/pull/4149 +- Add undo and redo to TextArea https://github.com/Textualize/textual/pull/4124 + +### Changed + +- Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124 ## [0.51.1] - 2024-02-09 diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 57fdc719f4..4449bcd668 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -288,7 +288,7 @@ This immediately updates the appearance of the `TextArea`: Pressing the ++tab++ key will shift focus to the next widget in your application by default. This matches how other widgets work in Textual. -To have ++tab++ insert a `\t` character, set the `tab_behaviour` attribute to the string value `"indent"`. +To have ++tab++ insert a `\t` character, set the `tab_behavior` attribute to the string value `"indent"`. While in this mode, you can shift focus by pressing the ++escape++ key. ### Indentation diff --git a/poetry.lock b/poetry.lock index f248950165..73ec9fbf67 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,88 +1,88 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" -version = "3.9.2" +version = "3.9.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:772fbe371788e61c58d6d3d904268e48a594ba866804d08c995ad71b144f94cb"}, - {file = "aiohttp-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:edd4f1af2253f227ae311ab3d403d0c506c9b4410c7fc8d9573dec6d9740369f"}, - {file = "aiohttp-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cfee9287778399fdef6f8a11c9e425e1cb13cc9920fd3a3df8f122500978292b"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc158466f6a980a6095ee55174d1de5730ad7dec251be655d9a6a9dd7ea1ff9"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54ec82f45d57c9a65a1ead3953b51c704f9587440e6682f689da97f3e8defa35"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abeb813a18eb387f0d835ef51f88568540ad0325807a77a6e501fed4610f864e"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc91d07280d7d169f3a0f9179d8babd0ee05c79d4d891447629ff0d7d8089ec2"}, - {file = "aiohttp-3.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b65e861f4bebfb660f7f0f40fa3eb9f2ab9af10647d05dac824390e7af8f75b7"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04fd8ffd2be73d42bcf55fd78cde7958eeee6d4d8f73c3846b7cba491ecdb570"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d8d962b439a859b3ded9a1e111a4615357b01620a546bc601f25b0211f2da81"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8ceb658afd12b27552597cf9a65d9807d58aef45adbb58616cdd5ad4c258c39e"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0e4ee4df741670560b1bc393672035418bf9063718fee05e1796bf867e995fad"}, - {file = "aiohttp-3.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2dec87a556f300d3211decf018bfd263424f0690fcca00de94a837949fbcea02"}, - {file = "aiohttp-3.9.2-cp310-cp310-win32.whl", hash = "sha256:3e1a800f988ce7c4917f34096f81585a73dbf65b5c39618b37926b1238cf9bc4"}, - {file = "aiohttp-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:ea510718a41b95c236c992b89fdfc3d04cc7ca60281f93aaada497c2b4e05c46"}, - {file = "aiohttp-3.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6aaa6f99256dd1b5756a50891a20f0d252bd7bdb0854c5d440edab4495c9f973"}, - {file = "aiohttp-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a27d8c70ad87bcfce2e97488652075a9bdd5b70093f50b10ae051dfe5e6baf37"}, - {file = "aiohttp-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54287bcb74d21715ac8382e9de146d9442b5f133d9babb7e5d9e453faadd005e"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb3d05569aa83011fcb346b5266e00b04180105fcacc63743fc2e4a1862a891"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8534e7d69bb8e8d134fe2be9890d1b863518582f30c9874ed7ed12e48abe3c4"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd9d5b989d57b41e4ff56ab250c5ddf259f32db17159cce630fd543376bd96b"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa6904088e6642609981f919ba775838ebf7df7fe64998b1a954fb411ffb4663"}, - {file = "aiohttp-3.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda42eb410be91b349fb4ee3a23a30ee301c391e503996a638d05659d76ea4c2"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:193cc1ccd69d819562cc7f345c815a6fc51d223b2ef22f23c1a0f67a88de9a72"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b9f1cb839b621f84a5b006848e336cf1496688059d2408e617af33e3470ba204"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:d22a0931848b8c7a023c695fa2057c6aaac19085f257d48baa24455e67df97ec"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4112d8ba61fbd0abd5d43a9cb312214565b446d926e282a6d7da3f5a5aa71d36"}, - {file = "aiohttp-3.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c4ad4241b52bb2eb7a4d2bde060d31c2b255b8c6597dd8deac2f039168d14fd7"}, - {file = "aiohttp-3.9.2-cp311-cp311-win32.whl", hash = "sha256:ee2661a3f5b529f4fc8a8ffee9f736ae054adfb353a0d2f78218be90617194b3"}, - {file = "aiohttp-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:4deae2c165a5db1ed97df2868ef31ca3cc999988812e82386d22937d9d6fed52"}, - {file = "aiohttp-3.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6f4cdba12539215aaecf3c310ce9d067b0081a0795dd8a8805fdb67a65c0572a"}, - {file = "aiohttp-3.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:84e843b33d5460a5c501c05539809ff3aee07436296ff9fbc4d327e32aa3a326"}, - {file = "aiohttp-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8008d0f451d66140a5aa1c17e3eedc9d56e14207568cd42072c9d6b92bf19b52"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61c47ab8ef629793c086378b1df93d18438612d3ed60dca76c3422f4fbafa792"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc71f748e12284312f140eaa6599a520389273174b42c345d13c7e07792f4f57"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1c3a4d0ab2f75f22ec80bca62385db2e8810ee12efa8c9e92efea45c1849133"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a87aa0b13bbee025faa59fa58861303c2b064b9855d4c0e45ec70182bbeba1b"}, - {file = "aiohttp-3.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2cc0d04688b9f4a7854c56c18aa7af9e5b0a87a28f934e2e596ba7e14783192"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1956e3ac376b1711c1533266dec4efd485f821d84c13ce1217d53e42c9e65f08"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:114da29f39eccd71b93a0fcacff178749a5c3559009b4a4498c2c173a6d74dff"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3f17999ae3927d8a9a823a1283b201344a0627272f92d4f3e3a4efe276972fe8"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f31df6a32217a34ae2f813b152a6f348154f948c83213b690e59d9e84020925c"}, - {file = "aiohttp-3.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7a75307ffe31329928a8d47eae0692192327c599113d41b278d4c12b54e1bd11"}, - {file = "aiohttp-3.9.2-cp312-cp312-win32.whl", hash = "sha256:972b63d589ff8f305463593050a31b5ce91638918da38139b9d8deaba9e0fed7"}, - {file = "aiohttp-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:200dc0246f0cb5405c80d18ac905c8350179c063ea1587580e3335bfc243ba6a"}, - {file = "aiohttp-3.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:158564d0d1020e0d3fe919a81d97aadad35171e13e7b425b244ad4337fc6793a"}, - {file = "aiohttp-3.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:da1346cd0ccb395f0ed16b113ebb626fa43b7b07fd7344fce33e7a4f04a8897a"}, - {file = "aiohttp-3.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eaa9256de26ea0334ffa25f1913ae15a51e35c529a1ed9af8e6286dd44312554"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1543e7fb00214fb4ccead42e6a7d86f3bb7c34751ec7c605cca7388e525fd0b4"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:186e94570433a004e05f31f632726ae0f2c9dee4762a9ce915769ce9c0a23d89"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d52d20832ac1560f4510d68e7ba8befbc801a2b77df12bd0cd2bcf3b049e52a4"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c45e4e815ac6af3b72ca2bde9b608d2571737bb1e2d42299fc1ffdf60f6f9a1"}, - {file = "aiohttp-3.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa906b9bdfd4a7972dd0628dbbd6413d2062df5b431194486a78f0d2ae87bd55"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:68bbee9e17d66f17bb0010aa15a22c6eb28583edcc8b3212e2b8e3f77f3ebe2a"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4c189b64bd6d9a403a1a3f86a3ab3acbc3dc41a68f73a268a4f683f89a4dec1f"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:8a7876f794523123bca6d44bfecd89c9fec9ec897a25f3dd202ee7fc5c6525b7"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d23fba734e3dd7b1d679b9473129cd52e4ec0e65a4512b488981a56420e708db"}, - {file = "aiohttp-3.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b141753be581fab842a25cb319f79536d19c2a51995d7d8b29ee290169868eab"}, - {file = "aiohttp-3.9.2-cp38-cp38-win32.whl", hash = "sha256:103daf41ff3b53ba6fa09ad410793e2e76c9d0269151812e5aba4b9dd674a7e8"}, - {file = "aiohttp-3.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:328918a6c2835861ff7afa8c6d2c70c35fdaf996205d5932351bdd952f33fa2f"}, - {file = "aiohttp-3.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5264d7327c9464786f74e4ec9342afbbb6ee70dfbb2ec9e3dfce7a54c8043aa3"}, - {file = "aiohttp-3.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07205ae0015e05c78b3288c1517afa000823a678a41594b3fdc870878d645305"}, - {file = "aiohttp-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0a1e638cffc3ec4d4784b8b4fd1cf28968febc4bd2718ffa25b99b96a741bd"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d43302a30ba1166325974858e6ef31727a23bdd12db40e725bec0f759abce505"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16a967685907003765855999af11a79b24e70b34dc710f77a38d21cd9fc4f5fe"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fa3ee92cd441d5c2d07ca88d7a9cef50f7ec975f0117cd0c62018022a184308"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b500c5ad9c07639d48615a770f49618130e61be36608fc9bc2d9bae31732b8f"}, - {file = "aiohttp-3.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c07327b368745b1ce2393ae9e1aafed7073d9199e1dcba14e035cc646c7941bf"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc7d6502c23a0ec109687bf31909b3fb7b196faf198f8cff68c81b49eb316ea9"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:07be2be7071723c3509ab5c08108d3a74f2181d4964e869f2504aaab68f8d3e8"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:122468f6fee5fcbe67cb07014a08c195b3d4c41ff71e7b5160a7bcc41d585a5f"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:00a9abcea793c81e7f8778ca195a1714a64f6d7436c4c0bb168ad2a212627000"}, - {file = "aiohttp-3.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a9825fdd64ecac5c670234d80bb52bdcaa4139d1f839165f548208b3779c6c6"}, - {file = "aiohttp-3.9.2-cp39-cp39-win32.whl", hash = "sha256:5422cd9a4a00f24c7244e1b15aa9b87935c85fb6a00c8ac9b2527b38627a9211"}, - {file = "aiohttp-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:7d579dcd5d82a86a46f725458418458fa43686f6a7b252f2966d359033ffc8ab"}, - {file = "aiohttp-3.9.2.tar.gz", hash = "sha256:b0ad0a5e86ce73f5368a164c10ada10504bf91869c05ab75d982c6048217fbf7"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, + {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, + {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, + {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, + {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, + {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, + {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, + {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, + {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, + {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, + {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, + {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, ] [package.dependencies] @@ -227,13 +227,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -763,13 +763,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "linkify-it-py" -version = "2.0.2" +version = "2.0.3" description = "Links recognition library with FULL unicode support." optional = false python-versions = ">=3.7" files = [ - {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, - {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, ] [package.dependencies] @@ -827,71 +827,71 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -996,13 +996,13 @@ mkdocs = "*" [[package]] name = "mkdocs-material" -version = "9.5.6" +version = "9.5.8" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.6-py3-none-any.whl", hash = "sha256:e115b90fccf5cd7f5d15b0c2f8e6246b21041628b8f590630e7fca66ed7fcf6c"}, - {file = "mkdocs_material-9.5.6.tar.gz", hash = "sha256:5b24df36d8ac6cecd611241ce6f6423ccde3e1ad89f8360c3f76d5565fc2d82a"}, + {file = "mkdocs_material-9.5.8-py3-none-any.whl", hash = "sha256:14563314bbf97da4bfafc69053772341babfaeb3329cde01d3e63cec03997af8"}, + {file = "mkdocs_material-9.5.8.tar.gz", hash = "sha256:2a429213e83f84eda7a588e2b186316d806aac602b7f93990042f7a1f3d3cf65"}, ] [package.dependencies] @@ -1019,7 +1019,7 @@ regex = ">=2022.4" requests = ">=2.26,<3.0" [package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] @@ -1163,85 +1163,101 @@ files = [ [[package]] name = "multidict" -version = "6.0.4" +version = "6.0.5" description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [[package]] @@ -1350,18 +1366,18 @@ files = [ [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" @@ -1543,6 +1559,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1550,8 +1567,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1568,6 +1592,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1575,6 +1600,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1835,74 +1861,6 @@ msgpack = ">=1.0.3" textual = ">=0.36.0" typing-extensions = ">=4.4.0,<5.0.0" -[[package]] -name = "time-machine" -version = "2.13.0" -description = "Travel through time in your tests." -optional = false -python-versions = ">=3.8" -files = [ - {file = "time_machine-2.13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:685d98593f13649ad5e7ce3e58efe689feca1badcf618ba397d3ab877ee59326"}, - {file = "time_machine-2.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ccbce292380ebf63fb9a52e6b03d91677f6a003e0c11f77473efe3913a75f289"}, - {file = "time_machine-2.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:679cbf9b15bfde1654cf48124128d3fbe52f821fa158a98fcee5fe7e05db1917"}, - {file = "time_machine-2.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a26bdf3462d5f12a4c1009fdbe54366c6ef22c7b6f6808705b51dedaaeba8296"}, - {file = "time_machine-2.13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dabb3b155819811b4602f7e9be936e2024e20dc99a90f103e36b45768badf9c3"}, - {file = "time_machine-2.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0db97f92be3efe0ac62fd3f933c91a78438cef13f283b6dfc2ee11123bfd7d8a"}, - {file = "time_machine-2.13.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:12eed2e9171c85b703d75c985dab2ecad4fe7025b7d2f842596fce1576238ece"}, - {file = "time_machine-2.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bdfe4a7f033e6783c3e9a7f8d8fc0b115367330762e00a03ff35fedf663994f3"}, - {file = "time_machine-2.13.0-cp310-cp310-win32.whl", hash = "sha256:3a7a0a49ce50d9c306c4343a7d6a3baa11092d4399a4af4355c615ccc321a9d3"}, - {file = "time_machine-2.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1812e48c6c58707db9988445a219a908a710ea065b2cc808d9a50636291f27d4"}, - {file = "time_machine-2.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:5aee23cd046abf9caeddc982113e81ba9097a01f3972e9560f5ed64e3495f66d"}, - {file = "time_machine-2.13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e9a9d150e098be3daee5c9f10859ab1bd14a61abebaed86e6d71f7f18c05b9d7"}, - {file = "time_machine-2.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2bd4169b808745d219a69094b3cb86006938d45e7293249694e6b7366225a186"}, - {file = "time_machine-2.13.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:8d526cdcaca06a496877cfe61cc6608df2c3a6fce210e076761964ebac7f77cc"}, - {file = "time_machine-2.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfef4ebfb4f055ce3ebc7b6c1c4d0dbfcffdca0e783ad8c6986c992915a57ed3"}, - {file = "time_machine-2.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f128db8997c3339f04f7f3946dd9bb2a83d15e0a40d35529774da1e9e501511"}, - {file = "time_machine-2.13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21bef5854d49b62e2c33848b5c3e8acf22a3b46af803ef6ff19529949cb7cf9f"}, - {file = "time_machine-2.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:32b71e50b07f86916ac04bd1eefc2bd2c93706b81393748b08394509ee6585dc"}, - {file = "time_machine-2.13.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ac8ff145c63cd0dcfd9590fe694b5269aacbc130298dc7209b095d101f8cdde"}, - {file = "time_machine-2.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:19a3b10161c91ca8e0fd79348665cca711fd2eac6ce336ff9e6b447783817f93"}, - {file = "time_machine-2.13.0-cp311-cp311-win32.whl", hash = "sha256:5f87787d562e42bf1006a87eb689814105b98c4d5545874a281280d0f8b9a2d9"}, - {file = "time_machine-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:62fd14a80b8b71726e07018628daaee0a2e00937625083f96f69ed6b8e3304c0"}, - {file = "time_machine-2.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:e9935aff447f5400a2665ab10ed2da972591713080e1befe1bb8954e7c0c7806"}, - {file = "time_machine-2.13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:34dcdbbd25c1e124e17fe58050452960fd16a11f9d3476aaa87260e28ecca0fd"}, - {file = "time_machine-2.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e58d82fe0e59d6e096ada3281d647a2e7420f7da5453b433b43880e1c2e8e0c5"}, - {file = "time_machine-2.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71acbc1febbe87532c7355eca3308c073d6e502ee4ce272b5028967847c8e063"}, - {file = "time_machine-2.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dec0ec2135a4e2a59623e40c31d6e8a8ae73305ade2634380e4263d815855750"}, - {file = "time_machine-2.13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e3a2611f8788608ebbcb060a5e36b45911bc3b8adc421b1dc29d2c81786ce4d"}, - {file = "time_machine-2.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:42ef5349135626ad6cd889a0a81400137e5c6928502b0817ea9e90bb10702000"}, - {file = "time_machine-2.13.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6c16d90a597a8c2d3ce22d6be2eb3e3f14786974c11b01886e51b3cf0d5edaf7"}, - {file = "time_machine-2.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f2ae8d0e359b216b695f1e7e7256f208c390db0480601a439c5dd1e1e4e16ce"}, - {file = "time_machine-2.13.0-cp312-cp312-win32.whl", hash = "sha256:f5fa9610f7e73fff42806a2ed8b06d862aa59ce4d178a52181771d6939c3e237"}, - {file = "time_machine-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:02b33a8c19768c94f7ffd6aa6f9f64818e88afce23250016b28583929d20fb12"}, - {file = "time_machine-2.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:0cc116056a8a2a917a4eec85661dfadd411e0d8faae604ef6a0e19fe5cd57ef1"}, - {file = "time_machine-2.13.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de01f33aa53da37530ad97dcd17e9affa25a8df4ab822506bb08101bab0c2673"}, - {file = "time_machine-2.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:67fa45cd813821e4f5bec0ac0820869e8e37430b15509d3f5fad74ba34b53852"}, - {file = "time_machine-2.13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a2d3db2c3b8e519d5ef436cd405abd33542a7b7761fb05ef5a5f782a8ce0b1"}, - {file = "time_machine-2.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7558622a62243be866a7e7c41da48eacd82c874b015ecf67d18ebf65ca3f7436"}, - {file = "time_machine-2.13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab04cf4e56e1ee65bee2adaa26a04695e92eb1ed1ccc65fbdafd0d114399595a"}, - {file = "time_machine-2.13.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0c8f24ae611a58782773af34dd356f1f26756272c04be2be7ea73b47e5da37d"}, - {file = "time_machine-2.13.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ca20f85a973a4ca8b00cf466cd72c27ccc72372549b138fd48d7e70e5a190ab"}, - {file = "time_machine-2.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9fad549521c4c13bdb1e889b2855a86ec835780d534ffd8f091c2647863243be"}, - {file = "time_machine-2.13.0-cp38-cp38-win32.whl", hash = "sha256:20205422fcf2caf9a7488394587df86e5b54fdb315c1152094fbb63eec4e9304"}, - {file = "time_machine-2.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:2dc76ee55a7d915a55960a726ceaca7b9097f67e4b4e681ef89871bcf98f00be"}, - {file = "time_machine-2.13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7693704c0f2f6b9beed912ff609781edf5fcf5d63aff30c92be4093e09d94b8e"}, - {file = "time_machine-2.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:918f8389de29b4f41317d121f1150176fae2cdb5fa41f68b2aee0b9dc88df5c3"}, - {file = "time_machine-2.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fe3fda5fa73fec74278912e438fce1612a79c36fd0cc323ea3dc2d5ce629f31"}, - {file = "time_machine-2.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6245db573863b335d9ca64b3230f623caf0988594ae554c0c794e7f80e3e66"}, - {file = "time_machine-2.13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e433827eccd6700a34a2ab28fd9361ff6e4d4923f718d2d1dac6d1dcd9d54da6"}, - {file = "time_machine-2.13.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:924377d398b1c48e519ad86a71903f9f36117f69e68242c99fb762a2465f5ad2"}, - {file = "time_machine-2.13.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66fb3877014dca0b9286b0f06fa74062357bd23f2d9d102d10e31e0f8fa9b324"}, - {file = "time_machine-2.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0c9829b2edfcf6b5d72a6ff330d4380f36a937088314c675531b43d3423dd8af"}, - {file = "time_machine-2.13.0-cp39-cp39-win32.whl", hash = "sha256:1a22be4df364f49a507af4ac9ea38108a0105f39da3f9c60dce62d6c6ea4ccdc"}, - {file = "time_machine-2.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:88601de1da06c7cab3d5ed3d5c3801ef683366e769e829e96383fdab6ae2fe42"}, - {file = "time_machine-2.13.0-cp39-cp39-win_arm64.whl", hash = "sha256:3c87856105dcb25b5bbff031d99f06ef4d1c8380d096222e1bc63b496b5258e6"}, - {file = "time_machine-2.13.0.tar.gz", hash = "sha256:c23b2408e3adcedec84ea1131e238f0124a5bc0e491f60d1137ad7239b37c01a"}, -] - -[package.dependencies] -python-dateutil = "*" - [[package]] name = "toml" version = "0.10.2" @@ -2015,79 +1973,70 @@ setuptools = {version = ">=60.0.0", markers = "python_version >= \"3.12\""} [[package]] name = "tree-sitter-languages" -version = "1.9.1" +version = "1.10.2" description = "Binary Python wheels for all tree sitter languages." optional = true python-versions = "*" files = [ - {file = "tree_sitter_languages-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5dee458cf1bd1e725470949124e24db842dc789039ea7ff5ba46b338e5f0dc60"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81921135fa15469586b1528088f78553e60a900d3045f4f37021ad3836219216"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edd60780d14c727179acb7bb48fbe4f79da9b830abdeb0d12c06a9f2c37928c7"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a28da3f60a6bc23195d6850836e477c149d4aaf58cdb0eb662741dca4f6401e2"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9778c00a58ee77006abc5af905b591551b158ce106c8cc6c3b4148d624ccabf"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6f68cfec0d74d6344db9c83414f401dcfc753916e71fac7d37f3a5e35b79e5ec"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:02142d81b2cd759b5fe246d403e4fba80b70268d108bd2b108301e64a84437a6"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ca4e0041c2ead2a8b354b9c229faee152bfd4617480c85cf2b352acf459db3cc"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-win32.whl", hash = "sha256:506ff5c3646e7b3a533f9e925221d4fe63b88dad0b7ffc1fb96db4c271994606"}, - {file = "tree_sitter_languages-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ac3899e05f2bf0a7c8da70ef5a077ab3dbd442f99eb7452aabbe67bc7b29ddf"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823426c3768eea88b6a4fd70dc668b72de90cc9f44d041a579c76d024d7d0697"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51f64b11f30cef3c5c9741e06221a46948f7c82d53ea2468139028eaf4858cca"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1c384bcd2695ebf873bc63eccfa0b9e1c3c944cd6a6ebdd1139a2528d2d6f"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fecf8553645fc1ad84921e97b03615d84aca22c35d020f629bb44cb6a28a302e"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1a499004189bf9f338f3412d4c1c05a643e86d4619a60ba4b3ae56bc4bf5db9"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:634ef22744b4af2ed9a43fea8309ec1171b062e37c609c3463364c790a08dae3"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9394eb34208abcfa9c26ece39778037a8d97da3ef59501185303fef0ab850290"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:221c367be0129da540fbb84170e18c5b8c56c09fd2f6143e116eebbef72c780e"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-win32.whl", hash = "sha256:15d03f54f913f47ac36277d8a521cd425415a25b020e0845d7b8843f5f5e1209"}, - {file = "tree_sitter_languages-1.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:7c565c18cebc72417ebc8f0f4cd5cb91dda51874164045cc274f47c913b194aa"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cde380cdc37594e7fcbade6a4b396dbeab52a1cecfe884cd814e1a1541ca6b93"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9c4f2409e5460bdec5921ee445f748ea7c319469e347a13373e3c7086dbf0315"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a17bbe91a78a29a9c14ab8bb07ed3761bb2708b58815bafc02d0965b15cb99e5"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:369402e2b395de2655d769e515401fe7c7df247a83aa28a6362e808b8a017fae"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a382d1e463e6ae60bbbd0c1f3db48e83b3c1a3af98d652af11de4c0e6171fc"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc60fb35f377143b30f4319fbaac0503b12cfb49de34082a479c7f0cc28927f1"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e953fb43767e327bf5c1d0585ee39236eaff47683cbda2811cbe0227fd41ad7"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c5a6df25eae23a5e2d448218b130207476cb8a613ac40570d49008243b0915bb"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-win32.whl", hash = "sha256:2720f9a639f5d5c17692135f3f2d60506c240699d0c1becdb895546e553f2339"}, - {file = "tree_sitter_languages-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:f19157c33ddc1e75ae7843b813e65575ed2040e1638643251bd603bb0f52046b"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:40880b5e774c3d5759b726273c36f83042d39c600c3aeefaf39248c3adec92d0"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad71366ee2458bda6df5a7476fc0e465a1e1579f53335ce901935efc5c67fdeb"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8000c6bf889e35e8b75407ea2d56153534b3f80c3b768378f4ca5a6fe286c0f"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7e20ead363d70b3f0f0b04cf6da30257d22a166700fa39e06c9f263b527688"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:444d2662912bc439c54c1b0ffe38354ae648f1f1ac8d1254b14fa768aa1a8587"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cceac9018359310fee46204b452860bfdcb3da00f4518d430790f909cbbf6b4c"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:332c182afbd9f7601e268426470e8c453740769a6227e7d1a9636d905cd7d707"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-win32.whl", hash = "sha256:25e993a41ad11fc433cb18ce0cc1d51eb7a285560c5cdddf781139312dac1881"}, - {file = "tree_sitter_languages-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:57419c215092ba9ba1964e07620dd386fc88ebb075b981fbb80f68f58004d4b4"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06747cac4789c436affa7c6b3483f68cc234e6a75b508a0f8369c77eb1faa04b"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b40bc82005543309c9cd4059f362c9d0d51277c942c71a5fdbed118389e5543a"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44920c9654ae03e94baa45c6e8c4b36a5f7bdd0c93877c72931bd77e862adaf1"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82e44f63a5449a41c5de3e9350967dc1c9183d9375881af5efb970c58c3fcfd8"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:df177fa87b655f6234e4dae540ba3917cf8e87c3646423b809415711e926765e"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:abdc8793328aa13fbd1cef3a0dff1c2e057a430fe2a64251628bbc97c4774eba"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b3f319f95f4464c35381755422f6dc0a518ad7d295d3cfe57bbaa564d225f3f"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-win32.whl", hash = "sha256:9f3a59bb4e8ec0a598566e02b7900eb8142236bda6c8b1069c4f3cdaf641950d"}, - {file = "tree_sitter_languages-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:517bdfe34bf24a05a496d441bee836fa77a6864f256508b82457ac28a9ac36bc"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9383331026f736bcbdf6b67f9b45417fe8fbb47225fe2517a1e4f974c319d9a8"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bba45ff3715e20e6e9a9b402f1ec2f2fc5ce11ce7b223584d0b5be5a4f8c60bb"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03558927c6e731d81706e3a8b26276eaa4fadba17e2fd83a5e0bc2a32b261975"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0231140e2d29fcf987216277483c93bc7ce4c2f88b8af77756d796e17a2957"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ead59b416f03da262df26e282cd40eb487f15384c90290f5105451e9a8ecfea"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fd27b7bdb95a2b35b730069d7dea60d0f6cc37e5ab2e900d2940a82d1db608bd"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d8b65a5fafd774a6c6dcacd9ac8b4c258c9f1efe2bfdca0a63818c83e591b949"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f32f7a7b8fd9952f82e2b881c1c8701a467b27db209590e0effb2fb4d71fe3d3"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-win32.whl", hash = "sha256:b52321e2a3a7cd1660cd7dadea16d7c7b9c981e177e0f77f9735e04cd89de015"}, - {file = "tree_sitter_languages-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8752bec9372937094a2557d9bfff357f30f5aa398e41e76e656baf53b4939d3"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:119f32cfc7c561e252e8958259ef997f2adfd4587ae43e82819b56f2810b8b42"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:582b04e11c67706b0a5ea64fd53ce4910fe11ad29d74ec7680c4014a02d09d4a"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a816f76c52f6c9fb3316c5d44195f8de48e09f2214b7fdb5f9232395033c789c"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a099b2f69cf22ab77de811b148de7d2d8ba8c51176a64bc56304cf42a627dd4"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:447b6c62c59255c89341ec0968e467e8c59c60fc5c2c3dc1f7dfe159a820dd3c"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41f4fee9b7de9646ef9711b6dbcdd5a4e7079e3d175089c8ef3f2c68b5adb5f4"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ee3b70594b79ff1155d5d9fea64e3af240d9327a52526d446e6bd792ac5b43cf"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:087b82cc3943fc5ffac30dc1b4192936a27c3c06fbd8718935a269e30dedc83b"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-win32.whl", hash = "sha256:155483058dc11de302f47922d31feec5e1bb9888e661aed7be0dad6f70bfe691"}, - {file = "tree_sitter_languages-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:5335405a937f788a2608d1b25c654461dddddbc6a1341672c833d2c8943397a8"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5580348f0b20233b1d5431fa178ccd3d07423ca4a3275df02a44608fd72344b9"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:103c7466644486b1e9e03850df46fc6aa12f13ca636c74f173270276220ac80b"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d13db84511c6f1a7dc40383b66deafa74dabd8b877e3d65ab253f3719eccafd6"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57adfa32be7e465b54aa72f915f6c78a2b66b227df4f656b5d4fbd1ca7a92b3f"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c6385e033e460ceb8f33f3f940335f422ef2b763700a04f0089391a68b56153"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dfa3f38cc5381c5aba01dd7494f59b8a9050e82ff6e06e1233e3a0cbae297e3c"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9f195155acf47f8bc5de7cee46ecd07b2f5697f007ba89435b51ef4c0b953ea5"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2de330e2ac6d7426ca025a3ec0f10d5640c3682c1d0c7702e812dcfb44b58120"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-win32.whl", hash = "sha256:c9731cf745f135d9770eeba9bb4e2ff4dabc107b5ae9b8211e919f6b9100ea6d"}, + {file = "tree_sitter_languages-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:6dd75851c41d0c3c4987a9b7692d90fa8848706c23115669d8224ffd6571e357"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7eb7d7542b2091c875fe52719209631fca36f8c10fa66970d2c576ae6a1b8289"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b41bcb00974b1c8a1800c7f1bb476a1d15a0463e760ee24872f2d53b08ee424"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f370cd7845c6c81df05680d5bd96db8a99d32b56f4728c5d05978911130a853"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1dc195c88ef4c72607e112a809a69190e096a2e5ebc6201548b3e05fdd169ad"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae34ac314a7170be24998a0f994c1ac80761d8d4bd126af27ee53a023d3b849"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:01b5742d5f5bd675489486b582bd482215880b26dde042c067f8265a6e925d9c"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ab1cbc46244d34fd16f21edaa20231b2a57f09f092a06ee3d469f3117e6eb954"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b1149e7467a4e92b8a70e6005fe762f880f493cf811fc003554b29f04f5e7c8"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-win32.whl", hash = "sha256:049276343962f4696390ee555acc2c1a65873270c66a6cbe5cb0bca83bcdf3c6"}, + {file = "tree_sitter_languages-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:7f3fdd468a577f04db3b63454d939e26e360229b53c80361920aa1ebf2cd7491"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c0f4c8b2734c45859edc7fcaaeaab97a074114111b5ba51ab4ec7ed52104763c"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eecd3c1244ac3425b7a82ba9125b4ddb45d953bbe61de114c0334fd89b7fe782"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15db3c8510bc39a80147ee7421bf4782c15c09581c1dc2237ea89cefbd95b846"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92c6487a6feea683154d3e06e6db68c30e0ae749a7ce4ce90b9e4e46b78c85c7"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2f1cd1d1bdd65332f9c2b67d49dcf148cf1ded752851d159ac3e5ee4f4d260"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:976c8039165b8e12f17a01ddee9f4e23ec6e352b165ad29b44d2bf04e2fbe77e"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:dafbbdf16bf668a580902e1620f4baa1913e79438abcce721a50647564c687b9"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1aeabd3d60d6d276b73cd8f3739d595b1299d123cc079a317f1a5b3c5461e2ca"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-win32.whl", hash = "sha256:fab8ee641914098e8933b87ea3d657bea4dd00723c1ee7038b847b12eeeef4f5"}, + {file = "tree_sitter_languages-1.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e606430d736367e5787fa5a7a0c5a1ec9b85eded0b3596bbc0d83532a40810b"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:838d5b48a7ed7a17658721952c77fda4570d2a069f933502653b17e15a9c39c9"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:987b3c71b1d278c2889e018ee77b8ee05c384e2e3334dec798f8b611c4ab2d1e"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faa00abcb2c819027df58472da055d22fa7dfcb77c77413d8500c32ebe24d38b"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e102fbbf02322d9201a86a814e79a9734ac80679fdb9682144479044f401a73"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0b87cf1a7b03174ba18dfd81582be82bfed26803aebfe222bd20e444aba003"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c0f1b9af9cb67f0b942b020da9fdd000aad5e92f2383ae0ba7a330b318d31912"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5a4076c921f7a4d31e643843de7dfe040b65b63a238a5aa8d31d93aabe6572aa"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-win32.whl", hash = "sha256:fa6391a3a5d83d32db80815161237b67d70576f090ce5f38339206e917a6f8bd"}, + {file = "tree_sitter_languages-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:55649d3f254585a064121513627cf9788c1cfdadbc5f097f33d5ba750685a4c0"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6f85d1edaa2d22d80d4ea5b6d12b95cf3644017b6c227d0d42854439e02e8893"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d78feed4a764ef3141cb54bf00fe94d514d8b6e26e09423e23b4c616fcb7938c"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1aca27531f9dd5308637d76643372856f0f65d0d28677d1bcf4211e8ed1ad0"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1031ea440dafb72237437d754eff8940153a3b051e3d18932ac25e75ce060a15"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99d3249beaef2c9fe558ecc9a97853c260433a849dcc68266d9770d196c2e102"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:59a4450f262a55148fb7e68681522f0c2a2f6b7d89666312a2b32708d8f416e1"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ce74eab0e430370d5e15a96b6c6205f93405c177a8b2e71e1526643b2fb9bab1"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9b4dd2b6b3d24c85dffe33d6c343448869eaf4f41c19ddba662eb5d65d8808f4"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-win32.whl", hash = "sha256:92d734fb968fe3927a7596d9f0459f81a8fa7b07e16569476b28e27d0d753348"}, + {file = "tree_sitter_languages-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:46a13f7d38f2eeb75f7cf127d1201346093748c270d686131f0cbc50e42870a1"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f8c6a936ae99fdd8857e91f86c11c2f5e507ff30631d141d98132bb7ab2c8638"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c283a61423f49cdfa7b5a5dfbb39221e3bd126fca33479cd80749d4d7a6b7349"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e60be6bdcff923386a54a5edcb6ff33fc38ab0118636a762024fa2bc98de55"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c00069f9575bd831eabcce2cdfab158dde1ed151e7e5614c2d985ff7d78a7de1"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475ff53203d8a43ccb19bb322fa2fb200d764001cc037793f1fadd714bb343da"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26fe7c9c412e4141dea87ea4b3592fd12e385465b5bdab106b0d5125754d4f60"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8fed27319957458340f24fe14daad467cd45021da034eef583519f83113a8c5e"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3657a491a7f96cc75a3568ddd062d25f3be82b6a942c68801a7b226ff7130181"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-win32.whl", hash = "sha256:33f7d584d01a7a3c893072f34cfc64ec031f3cfe57eebc32da2f8ac046e101a7"}, + {file = "tree_sitter_languages-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:1b944af3ee729fa70fc8ae82224a9ff597cdb63addea084e0ea2fa2b0ec39bb7"}, ] [package.dependencies] @@ -2117,13 +2066,13 @@ files = [ [[package]] name = "types-tree-sitter-languages" -version = "1.8.0.0" +version = "1.10.0.20240201" description = "Typing stubs for tree-sitter-languages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-tree-sitter-languages-1.8.0.0.tar.gz", hash = "sha256:a066d1c91d5fe8b8fce08669816d9e8c41bbe348085b3cb9799fa74070a30604"}, - {file = "types_tree_sitter_languages-1.8.0.0-py3-none-any.whl", hash = "sha256:9d4a8e2a435a4a0d356e643fb53993e3c491749ce0b7a628c22cb87904c6daca"}, + {file = "types-tree-sitter-languages-1.10.0.20240201.tar.gz", hash = "sha256:10822bc9d2b98f7e8019a97f0233c68555d5c447ba4ef24284e93fd866ec73de"}, + {file = "types_tree_sitter_languages-1.10.0.20240201-py3-none-any.whl", hash = "sha256:3cb72f9df4c9b92a8710f0c1966de2d2295584b7cbea3194d0ba577fa50be56c"}, ] [package.dependencies] @@ -2167,17 +2116,18 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2203,38 +2153,40 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "watchdog" -version = "3.0.0" +version = "4.0.0" description = "Filesystem events monitoring" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, - {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, - {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, - {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, - {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, - {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, - {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, - {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, - {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, + {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, + {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, + {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, + {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, + {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, + {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, + {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, ] [package.extras] @@ -2364,4 +2316,4 @@ syntax = ["tree-sitter", "tree_sitter_languages"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e82a92dbbd8916df2ee09402ca7c4bc4c8dddb713e2723dc6eb144b373c99d1c" +content-hash = "014186a223d4236fb0ace86bef24fb030d6921cf714a8b4d2b889ba066a26375" diff --git a/pyproject.toml b/pyproject.toml index 71c101fcb1..f8b4180db7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ mkdocstrings-python = "0.10.1" mkdocs-material = "^9.0.11" mkdocs-exclude = "^1.0.2" pre-commit = "^2.13.0" -time-machine = "^2.6.0" mkdocs-rss-plugin = "^1.5.0" httpx = "^0.23.1" types-setuptools = "^67.2.0.1" diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index afd4348e12..3a4e5729b2 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -52,7 +52,7 @@ def _detect_newline_style(text: str) -> Newline: text: The text to inspect. Returns: - The NewlineStyle used in the file. + The Newline used in the file. """ if "\r\n" in text: # Windows newline return "\r\n" diff --git a/src/textual/document/_edit.py b/src/textual/document/_edit.py new file mode 100644 index 0000000000..d0bd5ad83d --- /dev/null +++ b/src/textual/document/_edit.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from textual.document._document import EditResult, Location, Selection + +if TYPE_CHECKING: + from textual.widgets import TextArea + + +@dataclass +class Edit: + """Implements the Undoable protocol to replace text at some range within a document.""" + + text: str + """The text to insert. An empty string is equivalent to deletion.""" + + from_location: Location + """The start location of the insert.""" + + to_location: Location + """The end location of the insert""" + + maintain_selection_offset: bool + """If True, the selection will maintain its offset to the replacement range.""" + + _original_selection: Selection | None = field(init=False, default=None) + """The Selection when the edit was originally performed, to be restored on undo.""" + + _updated_selection: Selection | None = field(init=False, default=None) + """Where the selection should move to after the replace happens.""" + + _edit_result: EditResult | None = field(init=False, default=None) + """The result of doing the edit.""" + + def do(self, text_area: TextArea, record_selection: bool = True) -> EditResult: + """Perform the edit operation. + + Args: + text_area: The `TextArea` to perform the edit on. + record_selection: If True, record the current selection in the TextArea + so that it may be restored if this Edit is undone in the future. + + Returns: + An `EditResult` containing information about the replace operation. + """ + if record_selection: + self._original_selection = text_area.selection + + text = self.text + + # This code is mostly handling how we adjust TextArea.selection + # when an edit is made to the document programmatically. + # We want a user who is typing away to maintain their relative + # position in the document even if an insert happens before + # their cursor position. + + edit_bottom_row, edit_bottom_column = self.bottom + + selection_start, selection_end = text_area.selection + selection_start_row, selection_start_column = selection_start + selection_end_row, selection_end_column = selection_end + + edit_result = text_area.document.replace_range(self.top, self.bottom, text) + + new_edit_to_row, new_edit_to_column = edit_result.end_location + + column_offset = new_edit_to_column - edit_bottom_column + target_selection_start_column = ( + selection_start_column + column_offset + if edit_bottom_row == selection_start_row + and edit_bottom_column <= selection_start_column + else selection_start_column + ) + target_selection_end_column = ( + selection_end_column + column_offset + if edit_bottom_row == selection_end_row + and edit_bottom_column <= selection_end_column + else selection_end_column + ) + + row_offset = new_edit_to_row - edit_bottom_row + target_selection_start_row = selection_start_row + row_offset + target_selection_end_row = selection_end_row + row_offset + + if self.maintain_selection_offset: + self._updated_selection = Selection( + start=(target_selection_start_row, target_selection_start_column), + end=(target_selection_end_row, target_selection_end_column), + ) + else: + self._updated_selection = Selection.cursor(edit_result.end_location) + + self._edit_result = edit_result + return edit_result + + def undo(self, text_area: TextArea) -> EditResult: + """Undo the edit operation. + + Looks at the data stored in the edit, and performs the inverse operation of `Edit.do`. + + Args: + text_area: The `TextArea` to undo the insert operation on. + + Returns: + An `EditResult` containing information about the replace operation. + """ + replaced_text = self._edit_result.replaced_text + edit_end = self._edit_result.end_location + + # Replace the span of the edit with the text that was originally there. + undo_edit_result = text_area.document.replace_range( + self.top, edit_end, replaced_text + ) + self._updated_selection = self._original_selection + + return undo_edit_result + + def after(self, text_area: TextArea) -> None: + """Hook for running code after an Edit has been performed via `Edit.do` *and* + side effects such as re-wrapping the document and refreshing the display + have completed. + + For example, we can't record cursor visual offset until we know where the cursor will + land *after* wrapping has been performed, so we must wait until here to do it. + + Args: + text_area: The `TextArea` this operation was performed on. + """ + if self._updated_selection is not None: + text_area.selection = self._updated_selection + text_area.record_cursor_width() + + @property + def top(self) -> Location: + """The Location impacted by this edit that is nearest the start of the document.""" + return min([self.from_location, self.to_location]) + + @property + def bottom(self) -> Location: + """The Location impacted by this edit that is nearest the end of the document.""" + return max([self.from_location, self.to_location]) diff --git a/src/textual/document/_history.py b/src/textual/document/_history.py new file mode 100644 index 0000000000..c779fdd7ab --- /dev/null +++ b/src/textual/document/_history.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import time +from collections import deque +from dataclasses import dataclass, field + +from textual.document._edit import Edit + + +class HistoryException(Exception): + """Indicates misuse of the EditHistory API. + + For example, trying to undo() an Edit that has yet to be done. + """ + + +@dataclass +class EditHistory: + """Manages batching/checkpointing of Edits into groups that can be undone/redone in the TextArea.""" + + max_checkpoints: int + + checkpoint_timer: float + """Maximum number of seconds since last edit until a new batch is created.""" + + checkpoint_max_characters: int + """Maximum number of characters that can appear in a batch before a new batch is formed.""" + + _last_edit_time: float = field(init=False, default_factory=time.monotonic) + + _character_count: int = field(init=False, default=0) + """Track number of characters replaced + inserted since last batch creation.""" + + _force_end_batch: bool = field(init=False, default=False) + """Flag to force the creation of a new batch for the next recorded edit.""" + + _previously_replaced: bool = field(init=False, default=False) + """Records whether the most recent edit was a replacement or a pure insertion. + + If an edit removes any text from the document at all, it's considered a replacement. + Every other edit is considered a pure insertion. + """ + + def __post_init__(self) -> None: + self._undo_stack: deque[list[Edit]] = deque(maxlen=self.max_checkpoints) + """Batching Edit operations together (edits are simply grouped together in lists).""" + self._redo_stack: deque[list[Edit]] = deque() + """Stores batches that have been undone, allowing them to be redone.""" + + def record(self, edit: Edit) -> None: + """Record an Edit so that it may be undone and redone. + + Determines whether to batch the Edit with previous Edits, or create a new batch/checkpoint. + + This method must be called exactly once per edit, in chronological order. + + A new batch/checkpoint is created when: + + - The undo stack is empty. + - The checkpoint timer expires. + - The maximum number of characters permitted in a checkpoint is reached. + - A redo is performed (we should not add new edits to a batch that has been redone). + - The programmer has requested a new batch via a call to `force_new_batch`. + - e.g. the TextArea widget may call this method in some circumstances. + - Clicking to move the cursor elsewhere in the document should create a new batch. + - Movement of the cursor via a keyboard action that is NOT an edit. + - Blurring the TextArea creates a new checkpoint. + - The current edit involves a deletion/replacement and the previous edit did not. + - The current edit is a pure insertion and the previous edit was not. + - The edit involves insertion or deletion of one or more newline characters. + - An edit which inserts more than a single character (a paste) gets an isolated batch. + + Args: + edit: The edit to record. + """ + edit_result = edit._edit_result + if edit_result is None: + raise HistoryException( + "Cannot add an edit to history before it has been performed using `Edit.do`." + ) + + if edit.text == "" and edit_result.replaced_text == "": + return None + + is_replacement = bool(edit_result.replaced_text) + undo_stack = self._undo_stack + current_time = self._get_time() + edit_characters = len(edit.text) + contains_newline = "\n" in edit.text or "\n" in edit_result.replaced_text + + # Determine whether to create a new batch, or add to the latest batch. + if ( + not undo_stack + or self._force_end_batch + or edit_characters > 1 + or contains_newline + or is_replacement != self._previously_replaced + or current_time - self._last_edit_time > self.checkpoint_timer + or self._character_count + edit_characters > self.checkpoint_max_characters + ): + # Create a new batch (creating a "checkpoint"). + undo_stack.append([edit]) + self._character_count = edit_characters + self._last_edit_time = current_time + self._force_end_batch = False + else: + # Update the latest batch. + undo_stack[-1].append(edit) + self._character_count += edit_characters + self._last_edit_time = current_time + + self._previously_replaced = is_replacement + self._redo_stack.clear() + + # For some edits, we want to ensure the NEXT edit cannot be added to its batch, + # so enforce a checkpoint now. + if contains_newline or edit_characters > 1: + self.checkpoint() + + def _pop_undo(self) -> list[Edit] | None: + """Pop the latest batch from the undo stack and return it. + + This will also place it on the redo stack. + + Returns: + The batch of Edits from the top of the undo stack or None if it's empty. + """ + undo_stack = self._undo_stack + redo_stack = self._redo_stack + if undo_stack: + batch = undo_stack.pop() + redo_stack.append(batch) + return batch + return None + + def _pop_redo(self) -> list[Edit] | None: + """Redo the latest batch on the redo stack and return it. + + This will also place it on the undo stack (with a forced checkpoint to ensure + this undo does not get batched with other edits). + + Returns: + The batch of Edits from the top of the redo stack or None if it's empty. + """ + undo_stack = self._undo_stack + redo_stack = self._redo_stack + if redo_stack: + batch = redo_stack.pop() + undo_stack.append(batch) + # Ensure edits which follow cannot be added to the redone batch. + self.checkpoint() + return batch + return None + + def clear(self) -> None: + """Completely clear the history.""" + self._undo_stack.clear() + self._redo_stack.clear() + self._last_edit_time = time.monotonic() + self._force_end_batch = False + self._previously_replaced = False + + def checkpoint(self) -> None: + """Ensure the next recorded edit starts a new batch.""" + self._force_end_batch = True + + @property + def undo_stack(self) -> list[list[Edit]]: + """A copy of the undo stack, with references to the original Edits.""" + return list(self._undo_stack) + + @property + def redo_stack(self) -> list[list[Edit]]: + """A copy of the redo stack, with references to the original Edits.""" + return list(self._redo_stack) + + def _get_time(self) -> float: + """Get the time from the monotonic clock. + + Returns: + The result of `time.monotonic()` as a float. + """ + return time.monotonic() diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index d8b694797d..245af7938f 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -606,7 +606,7 @@ def __init__( classes: str | None = None, disabled: bool = False, ) -> None: - """Initialises a widget to display tabular data. + """Initializes a widget to display tabular data. Args: show_header: Whether the table header should be visible or not. @@ -662,7 +662,7 @@ def __init__( RowCacheKey, tuple[SegmentLines, SegmentLines] ] = LRUCache(1000) """For each row (a row can have a height of multiple lines), we maintain a - cache of the fixed and scrollable lines within that row to minimise how often + cache of the fixed and scrollable lines within that row to minimize how often we need to re-render it. """ self._cell_render_cache: LRUCache[CellCacheKey, SegmentLines] = LRUCache(10000) """Cache for individual cells.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 085ce16a3f..40b12f1768 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -3,14 +3,14 @@ import dataclasses import re from collections import defaultdict -from dataclasses import dataclass, field +from dataclasses import dataclass from functools import lru_cache from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Optional, Tuple +from typing import TYPE_CHECKING, ClassVar, Iterable, Optional, Sequence, Tuple from rich.style import Style from rich.text import Text -from typing_extensions import Literal, Protocol, runtime_checkable +from typing_extensions import Literal from textual._text_area_theme import TextAreaTheme from textual._tree_sitter import TREE_SITTER @@ -24,6 +24,8 @@ _utf8_encode, ) from textual.document._document_navigator import DocumentNavigator +from textual.document._edit import Edit +from textual.document._history import EditHistory from textual.document._languages import BUILTIN_LANGUAGES from textual.document._syntax_aware_document import ( SyntaxAwareDocument, @@ -355,9 +357,10 @@ def __init__( language: str | None = None, theme: str | None = None, soft_wrap: bool = True, + tab_behavior: Literal["focus", "indent"] = "focus", read_only: bool = False, - tab_behaviour: Literal["focus", "indent"] = "focus", show_line_numbers: bool = False, + max_checkpoints: int = 50, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -370,10 +373,10 @@ def __init__( language: The language to use. theme: The theme to use. soft_wrap: Enable soft wrapping. + tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab. read_only: Enable read-only mode. This prevents edits using the keyboard. - tab_behaviour: If 'focus', pressing tab will switch focus. - If 'indent', pressing tab will insert a tab. show_line_numbers: Show line numbers on the left edge. + max_checkpoints: The maximum number of undo history checkpoints to retain. name: The name of the `TextArea` widget. id: The ID of the widget, used to refer to it from Textual CSS. classes: One or more Textual CSS compatible class names separated by spaces. @@ -394,7 +397,11 @@ def __init__( self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") """Compiled regular expression for what we consider to be a 'word'.""" - self._undo_stack: list[Undoable] = [] + self.history: EditHistory = EditHistory( + max_checkpoints=max_checkpoints, + checkpoint_timer=2.0, + checkpoint_max_characters=100, + ) """A stack (the end of the list is the top of the stack) for tracking edits.""" self._selecting = False @@ -439,7 +446,7 @@ def __init__( self.set_reactive(TextArea.read_only, read_only) self.set_reactive(TextArea.show_line_numbers, show_line_numbers) - self.tab_behaviour = tab_behaviour + self.tab_behavior = tab_behavior # When `app.dark` is toggled, reset the theme (since it caches values). self.watch(self.app, "dark", self._app_dark_toggled, init=False) @@ -452,7 +459,7 @@ def code_editor( language: str | None = None, theme: str | None = "monokai", soft_wrap: bool = False, - tab_behaviour: Literal["focus", "indent"] = "indent", + tab_behavior: Literal["focus", "indent"] = "indent", show_line_numbers: bool = True, name: str | None = None, id: str | None = None, @@ -462,14 +469,14 @@ def code_editor( """Construct a new `TextArea` with sensible defaults for editing code. This instantiates a `TextArea` with line numbers enabled, soft wrapping - disabled, and "indent" tab behaviour. + disabled, "indent" tab behavior, and the "monokai" theme. Args: text: The initial text to load into the TextArea. language: The language to use. theme: The theme to use. soft_wrap: Enable soft wrapping. - tab_behaviour: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab. + tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab. show_line_numbers: Show line numbers on the left edge. name: The name of the `TextArea` widget. id: The ID of the widget, used to refer to it from Textual CSS. @@ -481,7 +488,7 @@ def code_editor( language=language, theme=theme, soft_wrap=soft_wrap, - tab_behaviour=tab_behaviour, + tab_behavior=tab_behavior, show_line_numbers=show_line_numbers, name=name, id=id, @@ -544,6 +551,7 @@ def _watch_has_focus(self, focus: bool) -> None: if focus: self._restart_blink() self.app.cursor_position = self.cursor_screen_offset + self.history.checkpoint() else: self._pause_blink(visible=False) @@ -870,11 +878,12 @@ def _watch_scroll_y(self) -> None: def load_text(self, text: str) -> None: """Load text into the TextArea. - This will replace the text currently in the TextArea. + This will replace the text currently in the TextArea and clear the edit history. Args: text: The text to load into the TextArea. """ + self.history.clear() self._set_document(text, self.language) def _on_resize(self) -> None: @@ -1189,6 +1198,8 @@ def text(self) -> str: def text(self, value: str) -> None: """Replace the text currently in the TextArea. This is an alias of `load_text`. + Setting this value will clear the edit history. + Args: value: The text to load into the TextArea. """ @@ -1225,6 +1236,7 @@ def edit(self, edit: Edit) -> EditResult: """ old_gutter_width = self.gutter_width result = edit.do(self) + self.history.record(edit) new_gutter_width = self.gutter_width if old_gutter_width != new_gutter_width: @@ -1242,6 +1254,108 @@ def edit(self, edit: Edit) -> EditResult: self.post_message(self.Changed(self)) return result + def undo(self) -> None: + """Undo the edits since the last checkpoint (the most recent batch of edits).""" + edits = self.history._pop_undo() + self._undo_batch(edits) + + def redo(self) -> None: + """Redo the most recently undone batch of edits.""" + edits = self.history._pop_redo() + self._redo_batch(edits) + + def _undo_batch(self, edits: Sequence[Edit]) -> None: + """Undo a batch of Edits. + + The sequence must be chronologically ordered by edit time. + + There must be no edits missing from the sequence, or the resulting content + will be incorrect. + + Args: + edits: The edits to undo, in the order they were originally performed. + """ + if not edits: + return + + old_gutter_width = self.gutter_width + minimum_from = edits[-1].from_location + maximum_old_end = (0, 0) + maximum_new_end = (0, 0) + for edit in reversed(edits): + edit.undo(self) + end_location = ( + edit._edit_result.end_location if edit._edit_result else (0, 0) + ) + if edit.from_location < minimum_from: + minimum_from = edit.from_location + if end_location > maximum_old_end: + maximum_old_end = end_location + if edit.to_location > maximum_new_end: + maximum_new_end = edit.to_location + + new_gutter_width = self.gutter_width + if old_gutter_width != new_gutter_width: + self.wrapped_document.wrap(self.wrap_width, self.indent_width) + else: + self.wrapped_document.wrap_range( + minimum_from, maximum_old_end, maximum_new_end + ) + + self._refresh_size() + for edit in reversed(edits): + edit.after(self) + self._build_highlight_map() + self.post_message(self.Changed(self)) + + def _redo_batch(self, edits: Sequence[Edit]) -> None: + """Redo a batch of Edits in order. + + The sequence must be chronologically ordered by edit time. + + Edits are applied from the start of the sequence to the end. + + There must be no edits missing from the sequence, or the resulting content + will be incorrect. + + Args: + edits: The edits to redo. + """ + if not edits: + return + + old_gutter_width = self.gutter_width + minimum_from = edits[0].from_location + maximum_old_end = (0, 0) + maximum_new_end = (0, 0) + for edit in edits: + edit.do(self, record_selection=False) + end_location = ( + edit._edit_result.end_location if edit._edit_result else (0, 0) + ) + if edit.from_location < minimum_from: + minimum_from = edit.from_location + if end_location > maximum_new_end: + maximum_new_end = end_location + if edit.to_location > maximum_old_end: + maximum_old_end = edit.to_location + + new_gutter_width = self.gutter_width + if old_gutter_width != new_gutter_width: + self.wrapped_document.wrap(self.wrap_width, self.indent_width) + else: + self.wrapped_document.wrap_range( + minimum_from, + maximum_old_end, + maximum_new_end, + ) + + self._refresh_size() + for edit in edits: + edit.after(self) + self._build_highlight_map() + self.post_message(self.Changed(self)) + async def _on_key(self, event: events.Key) -> None: """Handle key presses which correspond to document inserts.""" self._restart_blink() @@ -1252,7 +1366,7 @@ async def _on_key(self, event: events.Key) -> None: insert_values = { "enter": "\n", } - if self.tab_behaviour == "indent": + if self.tab_behavior == "indent": if key == "escape": event.stop() event.prevent_default() @@ -1363,6 +1477,7 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None: # TextArea widget while selecting, the widget still scrolls. self.capture_mouse() self._pause_blink(visible=True) + self.history.checkpoint() async def _on_mouse_move(self, event: events.MouseMove) -> None: """Handles click and drag to expand and contract the selection.""" @@ -1474,6 +1589,8 @@ def move_cursor( if center: self.scroll_cursor_visible(center) + self.history.checkpoint() + def move_cursor_relative( self, rows: int = 0, @@ -2038,143 +2155,6 @@ def action_delete_word_right(self) -> None: self._delete_via_keyboard(end, to_location) -@dataclass -class Edit: - """Implements the Undoable protocol to replace text at some range within a document.""" - - text: str - """The text to insert. An empty string is equivalent to deletion.""" - from_location: Location - """The start location of the insert.""" - to_location: Location - """The end location of the insert""" - maintain_selection_offset: bool - """If True, the selection will maintain its offset to the replacement range.""" - _updated_selection: Selection | None = field(init=False, default=None) - """Where the selection should move to after the replace happens.""" - - def do(self, text_area: TextArea) -> EditResult: - """Perform the edit operation. - - Args: - text_area: The `TextArea` to perform the edit on. - - Returns: - An `EditResult` containing information about the replace operation. - """ - text = self.text - - edit_from = self.from_location - edit_to = self.to_location - - # This code is mostly handling how we adjust TextArea.selection - # when an edit is made to the document programmatically. - # We want a user who is typing away to maintain their relative - # position in the document even if an insert happens before - # their cursor position. - - edit_bottom_row, edit_bottom_column = self.bottom - - selection_start, selection_end = text_area.selection - selection_start_row, selection_start_column = selection_start - selection_end_row, selection_end_column = selection_end - - replace_result = text_area.document.replace_range(self.top, self.bottom, text) - - new_edit_to_row, new_edit_to_column = replace_result.end_location - - # TODO: We could maybe improve the situation where the selection - # and the edit range overlap with each other. - column_offset = new_edit_to_column - edit_bottom_column - target_selection_start_column = ( - selection_start_column + column_offset - if edit_bottom_row == selection_start_row - and edit_bottom_column <= selection_start_column - else selection_start_column - ) - target_selection_end_column = ( - selection_end_column + column_offset - if edit_bottom_row == selection_end_row - and edit_bottom_column <= selection_end_column - else selection_end_column - ) - - row_offset = new_edit_to_row - edit_bottom_row - target_selection_start_row = selection_start_row + row_offset - target_selection_end_row = selection_end_row + row_offset - - if self.maintain_selection_offset: - self._updated_selection = Selection( - start=(target_selection_start_row, target_selection_start_column), - end=(target_selection_end_row, target_selection_end_column), - ) - else: - self._updated_selection = Selection.cursor(replace_result.end_location) - - return replace_result - - def undo(self, text_area: TextArea) -> EditResult: - """Undo the edit operation. - - Args: - text_area: The `TextArea` to undo the insert operation on. - - Returns: - An `EditResult` containing information about the replace operation. - """ - raise NotImplementedError() - - def after(self, text_area: TextArea) -> None: - """Possibly update the cursor location after the widget has been refreshed. - - Args: - text_area: The `TextArea` this operation was performed on. - """ - if self._updated_selection is not None: - text_area.selection = self._updated_selection - text_area.record_cursor_width() - - @property - def top(self) -> Location: - """The Location impacted by this edit that is nearest the start of the document.""" - return min([self.from_location, self.to_location]) - - @property - def bottom(self) -> Location: - """The Location impacted by this edit that is nearest the end of the document.""" - return max([self.from_location, self.to_location]) - - -@runtime_checkable -class Undoable(Protocol): - """Protocol for actions performed in the text editor which can be done and undone. - - These are typically actions which affect the document (e.g. inserting and deleting - text), but they can really be anything. - - To perform an edit operation, pass the Edit to `TextArea.edit()`""" - - def do(self, text_area: TextArea) -> Any: - """Do the action. - - Args: - The `TextArea` to perform the action on. - - Returns: - Anything. This protocol doesn't prescribe what is returned. - """ - - def undo(self, text_area: TextArea) -> Any: - """Undo the action. - - Args: - The `TextArea` to perform the action on. - - Returns: - Anything. This protocol doesn't prescribe what is returned. - """ - - @lru_cache(maxsize=128) def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: """Build a mapping of utf-8 byte offsets to codepoint offsets for the given data. diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py index 10683d4b09..058f823542 100644 --- a/src/textual/widgets/text_area.py +++ b/src/textual/widgets/text_area.py @@ -7,11 +7,12 @@ Selection, ) from textual.document._document_navigator import DocumentNavigator +from textual.document._edit import Edit +from textual.document._history import EditHistory from textual.document._languages import BUILTIN_LANGUAGES from textual.document._syntax_aware_document import SyntaxAwareDocument from textual.document._wrapped_document import WrappedDocument from textual.widgets._text_area import ( - Edit, EndColumn, Highlight, HighlightName, @@ -27,6 +28,7 @@ "DocumentNavigator", "Edit", "EditResult", + "EditHistory", "EndColumn", "Highlight", "HighlightName", diff --git a/tests/text_area/test_escape_binding.py b/tests/text_area/test_escape_binding.py index bc644d3085..8d837e5c75 100644 --- a/tests/text_area/test_escape_binding.py +++ b/tests/text_area/test_escape_binding.py @@ -8,7 +8,7 @@ class TextAreaDialog(ModalScreen): def compose(self) -> ComposeResult: yield TextArea( - tab_behaviour="focus", # the default + tab_behavior="focus", # the default ) yield Button("Submit") @@ -18,10 +18,10 @@ def on_mount(self) -> None: self.push_screen(TextAreaDialog()) -async def test_escape_key_when_tab_behaviour_is_focus(): +async def test_escape_key_when_tab_behavior_is_focus(): """Regression test for https://github.com/Textualize/textual/issues/4110 - When the `tab_behaviour` of TextArea is the default to shift focus, + When the `tab_behavior` of TextArea is the default to shift focus, pressing should not shift focus but instead skip and allow any parent bindings to run. """ @@ -37,8 +37,8 @@ async def test_escape_key_when_tab_behaviour_is_focus(): assert not isinstance(pilot.app.screen, TextAreaDialog) -async def test_escape_key_when_tab_behaviour_is_indent(): - """When the `tab_behaviour` of TextArea is indent rather than switch focus, +async def test_escape_key_when_tab_behavior_is_indent(): + """When the `tab_behavior` of TextArea is indent rather than switch focus, pressing should instead shift focus. """ @@ -48,7 +48,7 @@ async def test_escape_key_when_tab_behaviour_is_indent(): assert isinstance(pilot.app.screen, TextAreaDialog) assert isinstance(pilot.app.focused, TextArea) - pilot.app.query_one(TextArea).tab_behaviour = "indent" + pilot.app.query_one(TextArea).tab_behavior = "indent" # Pressing escape should focus the button, not dismiss the dialog screen await pilot.press("escape") assert isinstance(pilot.app.screen, TextAreaDialog) diff --git a/tests/text_area/test_history.py b/tests/text_area/test_history.py new file mode 100644 index 0000000000..b6a30e4132 --- /dev/null +++ b/tests/text_area/test_history.py @@ -0,0 +1,343 @@ +from __future__ import annotations + +import dataclasses + +import pytest + +from textual.app import App, ComposeResult +from textual.events import Paste +from textual.pilot import Pilot +from textual.widgets import TextArea +from textual.widgets.text_area import EditHistory, Selection + +MAX_CHECKPOINTS = 5 +SIMPLE_TEXT = """\ +ABCDE +FGHIJ +KLMNO +PQRST +UVWXY +Z +""" + + +@dataclasses.dataclass +class TimeMockableEditHistory(EditHistory): + mock_time: float | None = dataclasses.field(default=None, init=False) + + def _get_time(self) -> float: + """Return the mocked time if it is set, otherwise use default behaviour.""" + if self.mock_time is None: + return super()._get_time() + return self.mock_time + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + # Update the history object to a version that supports mocking the time. + text_area.history = TimeMockableEditHistory( + max_checkpoints=MAX_CHECKPOINTS, + checkpoint_timer=2.0, + checkpoint_max_characters=100, + ) + self.text_area = text_area + yield text_area + + +@pytest.fixture +async def pilot(): + app = TextAreaApp() + async with app.run_test() as pilot: + yield pilot + + +@pytest.fixture +async def text_area(pilot): + return pilot.app.text_area + + +async def test_simple_undo_redo(pilot, text_area: TextArea): + text_area.insert("123", (0, 0)) + + assert text_area.text == "123" + text_area.undo() + assert text_area.text == "" + text_area.redo() + assert text_area.text == "123" + + +async def test_undo_selection_retained(pilot: Pilot, text_area: TextArea): + # Select a range of text and press backspace. + text_area.text = SIMPLE_TEXT + text_area.selection = Selection((0, 0), (2, 3)) + await pilot.press("backspace") + assert text_area.text == "NO\nPQRST\nUVWXY\nZ\n" + assert text_area.selection == Selection.cursor((0, 0)) + + # Undo the deletion - the text comes back, and the selection is restored. + text_area.undo() + assert text_area.selection == Selection((0, 0), (2, 3)) + assert text_area.text == SIMPLE_TEXT + + # Redo the deletion - the text is gone again. The selection goes to the post-delete location. + text_area.redo() + assert text_area.text == "NO\nPQRST\nUVWXY\nZ\n" + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_undo_checkpoint_created_on_cursor_move( + pilot: Pilot, text_area: TextArea +): + text_area.text = SIMPLE_TEXT + # Characters are inserted on line 0 and 1. + checkpoint_one = text_area.text + checkpoint_one_selection = text_area.selection + await pilot.press("1") # Added to initial batch. + + # This cursor movement ensures a new checkpoint is created. + post_insert_one_location = text_area.selection + await pilot.press("down") + + checkpoint_two = text_area.text + checkpoint_two_selection = text_area.selection + await pilot.press("2") # Added to new batch. + + checkpoint_three = text_area.text + checkpoint_three_selection = text_area.selection + + # Going back to checkpoint two + text_area.undo() + assert text_area.text == checkpoint_two + assert text_area.selection == checkpoint_two_selection + + # Back again to checkpoint one (initial state) + text_area.undo() + assert text_area.text == checkpoint_one + assert text_area.selection == checkpoint_one_selection + + # Redo to move forward to checkpoint two. + text_area.redo() + assert text_area.text == checkpoint_two + assert text_area.selection == post_insert_one_location + + # Redo to move forward to checkpoint three. + text_area.redo() + assert text_area.text == checkpoint_three + assert text_area.selection == checkpoint_three_selection + + +async def test_setting_text_property_resets_history(pilot: Pilot, text_area: TextArea): + await pilot.press("1") + + # Programmatically setting text, which should invalidate the history + text = "Hello, world!" + text_area.text = text + + # The undo doesn't do anything, since we set the `text` property. + text_area.undo() + assert text_area.text == text + + +async def test_edits_batched_by_time(pilot: Pilot, text_area: TextArea): + # The first "12" is batched since they happen within 2 seconds. + text_area.history.mock_time = 0 + await pilot.press("1") + + text_area.history.mock_time = 1.0 + await pilot.press("2") + + # Since "3" appears 10 seconds later, it's in a separate batch. + text_area.history.mock_time += 10.0 + await pilot.press("3") + + assert text_area.text == "123" + + text_area.undo() + assert text_area.text == "12" + + text_area.undo() + assert text_area.text == "" + + +async def test_undo_checkpoint_character_limit_reached( + pilot: Pilot, text_area: TextArea +): + await pilot.press("1") + # Since the insertion below is > 100 characters it goes to a new batch. + text_area.insert("2" * 120) + + text_area.undo() + assert text_area.text == "1" + text_area.undo() + assert text_area.text == "" + + +async def test_redo_with_no_undo_is_noop(text_area: TextArea): + text_area.text = SIMPLE_TEXT + text_area.redo() + assert text_area.text == SIMPLE_TEXT + + +async def test_undo_with_empty_undo_stack_is_noop(text_area: TextArea): + text_area.text = SIMPLE_TEXT + text_area.undo() + assert text_area.text == SIMPLE_TEXT + + +async def test_redo_stack_cleared_on_edit(pilot: Pilot, text_area: TextArea): + text_area.text = "" + await pilot.press("1") + text_area.history.checkpoint() + await pilot.press("2") + text_area.history.checkpoint() + await pilot.press("3") + + text_area.undo() + text_area.undo() + text_area.undo() + assert text_area.text == "" + assert text_area.selection == Selection.cursor((0, 0)) + + # Redo stack has 3 edits in it now. + await pilot.press("f") + assert text_area.text == "f" + assert text_area.selection == Selection.cursor((0, 1)) + + # Redo stack is cleared because of the edit, so redo has no effect. + text_area.redo() + assert text_area.text == "f" + assert text_area.selection == Selection.cursor((0, 1)) + text_area.redo() + assert text_area.text == "f" + assert text_area.selection == Selection.cursor((0, 1)) + + +async def test_inserts_not_batched_with_deletes(pilot: Pilot, text_area: TextArea): + # 3 batches here: __1___ ___________2____________ __3__ + await pilot.press(*"123", "backspace", "backspace", *"23") + + assert text_area.text == "123" + + # Undo batch 1: the "23" insertion. + text_area.undo() + assert text_area.text == "1" + + # Undo batch 2: the double backspace. + text_area.undo() + assert text_area.text == "123" + + # Undo batch 3: the "123" insertion. + text_area.undo() + assert text_area.text == "" + + +async def test_paste_is_an_isolated_batch(pilot: Pilot, text_area: TextArea): + pilot.app.post_message(Paste("hello ")) + pilot.app.post_message(Paste("world")) + await pilot.pause() + + assert text_area.text == "hello world" + + await pilot.press("!") + + # The insertion of "!" does not get batched with the paste of "world". + text_area.undo() + assert text_area.text == "hello world" + + text_area.undo() + assert text_area.text == "hello " + + text_area.undo() + assert text_area.text == "" + + +async def test_focus_creates_checkpoint(pilot: Pilot, text_area: TextArea): + await pilot.press(*"123") + text_area.has_focus = False + text_area.has_focus = True + await pilot.press(*"456") + assert text_area.text == "123456" + + # Since we re-focused, a checkpoint exists between 123 and 456, + # so when we use undo, only the 456 is removed. + text_area.undo() + assert text_area.text == "123" + + +async def test_undo_redo_deletions_batched(pilot: Pilot, text_area: TextArea): + text_area.text = SIMPLE_TEXT + text_area.selection = Selection((0, 2), (1, 2)) + + # Perform a single delete of some selected text. It'll live in it's own + # batch since it's a multi-line operation. + await pilot.press("backspace") + checkpoint_one = "ABHIJ\nKLMNO\nPQRST\nUVWXY\nZ\n" + assert text_area.text == checkpoint_one + assert text_area.selection == Selection.cursor((0, 2)) + + # Pressing backspace a few times to delete more characters. + await pilot.press("backspace", "backspace", "backspace") + checkpoint_two = "HIJ\nKLMNO\nPQRST\nUVWXY\nZ\n" + assert text_area.text == checkpoint_two + assert text_area.selection == Selection.cursor((0, 0)) + + # When we undo, the 3 deletions above should be batched, but not + # the original deletion since it contains a newline character. + text_area.undo() + assert text_area.text == checkpoint_one + assert text_area.selection == Selection.cursor((0, 2)) + + # Undoing again restores us back to our initial text and selection. + text_area.undo() + assert text_area.text == SIMPLE_TEXT + assert text_area.selection == Selection((0, 2), (1, 2)) + + # At this point, the undo stack contains two items, so we can redo twice. + + # Redo to go back to checkpoint one. + text_area.redo() + assert text_area.text == checkpoint_one + assert text_area.selection == Selection.cursor((0, 2)) + + # Redo again to go back to checkpoint two + text_area.redo() + assert text_area.text == checkpoint_two + assert text_area.selection == Selection.cursor((0, 0)) + + # Redo again does nothing. + text_area.redo() + assert text_area.text == checkpoint_two + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_max_checkpoints(pilot: Pilot, text_area: TextArea): + assert len(text_area.history.undo_stack) == 0 + for index in range(MAX_CHECKPOINTS): + # Press enter since that will ensure a checkpoint is created. + await pilot.press("enter") + + assert len(text_area.history.undo_stack) == MAX_CHECKPOINTS + await pilot.press("enter") + # Ensure we don't go over the limit. + assert len(text_area.history.undo_stack) == MAX_CHECKPOINTS + + +async def test_redo_stack(pilot: Pilot, text_area: TextArea): + assert len(text_area.history.redo_stack) == 0 + await pilot.press("enter") + await pilot.press(*"123") + assert len(text_area.history.undo_stack) == 2 + assert len(text_area.history.redo_stack) == 0 + text_area.undo() + assert len(text_area.history.undo_stack) == 1 + assert len(text_area.history.redo_stack) == 1 + text_area.undo() + assert len(text_area.history.undo_stack) == 0 + assert len(text_area.history.redo_stack) == 2 + text_area.redo() + assert len(text_area.history.undo_stack) == 1 + assert len(text_area.history.redo_stack) == 1 + text_area.redo() + assert len(text_area.history.undo_stack) == 2 + assert len(text_area.history.redo_stack) == 0 From ed9a8f7b7c623585250c6b5751b4d6f562161e1b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 14 Feb 2024 13:49:38 +0000 Subject: [PATCH 42/58] Fix active tab not coming into view This fixes one of the issues reported in #4150. --- src/textual/widgets/_tabs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py index 0123e17688..276f7e58ec 100644 --- a/src/textual/widgets/_tabs.py +++ b/src/textual/widgets/_tabs.py @@ -583,6 +583,7 @@ def watch_active(self, previously_active: str, active: str) -> None: self.query("#tabs-list > Tab.-active").remove_class("-active") active_tab.add_class("-active") self._highlight_active(animate=previously_active != "") + self._scroll_active_tab() self.post_message(self.TabActivated(self, active_tab)) else: underline = self.query_one(Underline) From efb8b483473561fd7aded311a4a07584faacef51 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 09:28:45 +0000 Subject: [PATCH 43/58] Tweak the layout of the `discover` description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/command_palette.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md index 1d7fb55423..2d4b17506b 100644 --- a/docs/guide/command_palette.md +++ b/docs/guide/command_palette.md @@ -109,11 +109,11 @@ this is to aid in command discoverability. Because `discover` hits are shown the moment the command palette is opened, these should ideally be quick to generate; commands that might take time to generate are best left for `search` -- use `discover` to help the user easily find the most important commands. -`discover` is similar to `search` but with the difference that no search value is passed to it, and it should *yield* instances of [`DiscoveryHit`][textual.command.DiscoveryHit]. -There is no matching and no match score is needed. -The [`DiscoveryHit`][textual.command.DiscoveryHit] contains information about how the hit should be displayed, and an optional help string; -discovery hits are sorted in ascending alphabetical order. -It also contains a callback, which will be run if the user selects that command. +`discover` is similar to `search` but with these differences: + - `discover` accepts no parameters (instead of the search value); + - `discover` yields instances of [`DiscoveryHit`][textual.command.DiscoveryHit]` (instead of instances of xxx); + - discovery hits are sorted in ascending alphabetical order because there is no matching and no match score is generated. +Instances of [`DiscoveryHit`][textual.command.DiscoveryHit] contain information about how the hit should be displayed, an optional help string, and a callback which will be run if the user selects that command. ### shutdown method From 3971449c1690fd8faab0259675fc1674cd00d5b7 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 09:37:47 +0000 Subject: [PATCH 44/58] Tidy up a suggested edit to the discover docs --- docs/guide/command_palette.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md index 2d4b17506b..5fa1bebc08 100644 --- a/docs/guide/command_palette.md +++ b/docs/guide/command_palette.md @@ -110,9 +110,11 @@ this is to aid in command discoverability. commands that might take time to generate are best left for `search` -- use `discover` to help the user easily find the most important commands. `discover` is similar to `search` but with these differences: - - `discover` accepts no parameters (instead of the search value); - - `discover` yields instances of [`DiscoveryHit`][textual.command.DiscoveryHit]` (instead of instances of xxx); - - discovery hits are sorted in ascending alphabetical order because there is no matching and no match score is generated. + +- `discover` accepts no parameters (instead of the search value) +- `discover` yields instances of [`DiscoveryHit`][textual.command.DiscoveryHit] (instead of instances of [`Hit`][textual.command.Hit]) +- discovery hits are sorted in ascending alphabetical order because there is no matching and no match score is generated + Instances of [`DiscoveryHit`][textual.command.DiscoveryHit] contain information about how the hit should be displayed, an optional help string, and a callback which will be run if the user selects that command. ### shutdown method From 45f13255874b79be40ead788a667f08cf3c41c78 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 09:56:43 +0000 Subject: [PATCH 45/58] Accept docstring tweak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- src/textual/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/command.py b/src/textual/command.py index 215d78875b..94d3aa07dc 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -284,7 +284,7 @@ async def discover(self) -> Hits: This is different from [`search`][textual.command.Provider.search] in that it should yield [`DiscoveryHit`s][textual.command.DiscoveryHit] that - should be shown by default; before user input. + should be shown by default (before user input). It is permitted to *not* implement this method. """ From a2d6eec9790b081d4e40deb8f1852e205ce6680a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 10:29:17 +0000 Subject: [PATCH 46/58] Tidy up some typing errors in _on_tabs_tab_activated These have been kicking around for a wee while; made sense to clean them up now. --- src/textual/widgets/_tabbed_content.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index e7ae3eb25b..ad80c24641 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -511,13 +511,16 @@ def _on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None: if self._is_associated_tabs(event.tabs): # The message is relevant, so consume it and update state accordingly. event.stop() + assert event.tab.id is not None switcher = self.get_child_by_type(ContentSwitcher) switcher.current = ContentTab.sans_prefix(event.tab.id) self.active = ContentTab.sans_prefix(event.tab.id) self.post_message( TabbedContent.TabActivated( tabbed_content=self, - tab=event.tab, + tab=self.get_child_by_type(ContentTabs).get_content_tab( + self.active + ), ) ) From 5d9e563410e4ab45d591ea1132c09cc3783691db Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 10:33:20 +0000 Subject: [PATCH 47/58] Fix assignment to TabbedContent.active not posting the appropriate message Fixes #4150 --- src/textual/widgets/_tabbed_content.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index ad80c24641..410b317809 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -514,7 +514,13 @@ def _on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None: assert event.tab.id is not None switcher = self.get_child_by_type(ContentSwitcher) switcher.current = ContentTab.sans_prefix(event.tab.id) - self.active = ContentTab.sans_prefix(event.tab.id) + with self.prevent(self.TabActivated): + # We prevent TabbedContent.TabActivated because it is also + # posted from the watcher for active, we're also about to + # post it below too, which is valid as here we're reacting + # to what the Tabs are doing. This ensures we don't get + # doubled-up messages. + self.active = ContentTab.sans_prefix(event.tab.id) self.post_message( TabbedContent.TabActivated( tabbed_content=self, @@ -552,6 +558,12 @@ def _watch_active(self, active: str) -> None: with self.prevent(Tabs.TabActivated): self.get_child_by_type(ContentTabs).active = ContentTab.add_prefix(active) self.get_child_by_type(ContentSwitcher).current = active + self.post_message( + TabbedContent.TabActivated( + tabbed_content=self, + tab=self.get_child_by_type(ContentTabs).get_content_tab(active), + ) + ) @property def tab_count(self) -> int: From c4373dcdd85610baba16e0c1e6cbd0fb29fd6157 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 10:36:58 +0000 Subject: [PATCH 48/58] Only post TabbedContent.TabActivated if active is truthy --- src/textual/widgets/_tabbed_content.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index 410b317809..b53769802c 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -558,12 +558,13 @@ def _watch_active(self, active: str) -> None: with self.prevent(Tabs.TabActivated): self.get_child_by_type(ContentTabs).active = ContentTab.add_prefix(active) self.get_child_by_type(ContentSwitcher).current = active - self.post_message( - TabbedContent.TabActivated( - tabbed_content=self, - tab=self.get_child_by_type(ContentTabs).get_content_tab(active), + if active: + self.post_message( + TabbedContent.TabActivated( + tabbed_content=self, + tab=self.get_child_by_type(ContentTabs).get_content_tab(active), + ) ) - ) @property def tab_count(self) -> int: From 907eaffba25e2a52747afb99c19e58a48a56ca6a Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 10:49:37 +0000 Subject: [PATCH 49/58] Update the ChangeLog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5826989c77..48df1194d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add some syntax highlighting to TextArea default theme https://github.com/Textualize/textual/pull/4149 - Add undo and redo to TextArea https://github.com/Textualize/textual/pull/4124 +### Fixed + +- Fixed out-of-view `Tab` not being scrolled into view when `Tabs.active` is assigned https://github.com/Textualize/textual/issues/4150 +- Fixed `TabbedContent.TabActivate` not being posted when `TabbedContent.active` is assigned https://github.com/Textualize/textual/issues/4150 + ### Changed - Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124 @@ -31,7 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed duplicate watch methods being attached to DOM nodes https://github.com/Textualize/textual/pull/4030 - Fixed using `watch` to create additional watchers would trigger other watch methods https://github.com/Textualize/textual/issues/3878 -### Added +### Added - Added support for configuring dark and light themes for code in `Markdown` https://github.com/Textualize/textual/issues/3997 From 5cc1361da3787967e257803b0479a30c336f8d21 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 10:53:06 +0000 Subject: [PATCH 50/58] Tidy TabbedContent._watch_active The "with prevent" was covering more code than was necessary. While it doesn't make a whole load of difference, here I make it clear what bit of code actually needs the prevention. --- src/textual/widgets/_tabbed_content.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index b53769802c..0edced54b2 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -557,14 +557,14 @@ def _watch_active(self, active: str) -> None: """Switch tabs when the active attributes changes.""" with self.prevent(Tabs.TabActivated): self.get_child_by_type(ContentTabs).active = ContentTab.add_prefix(active) - self.get_child_by_type(ContentSwitcher).current = active - if active: - self.post_message( - TabbedContent.TabActivated( - tabbed_content=self, - tab=self.get_child_by_type(ContentTabs).get_content_tab(active), - ) + self.get_child_by_type(ContentSwitcher).current = active + if active: + self.post_message( + TabbedContent.TabActivated( + tabbed_content=self, + tab=self.get_child_by_type(ContentTabs).get_content_tab(active), ) + ) @property def tab_count(self) -> int: From 9ca997b1cdf932017e710f2306084e4eb6a676d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:58:40 +0000 Subject: [PATCH 51/58] Fix Changelog header version. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 579e3a6abd..2bb816e2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124 -## [0.51.1] - 2024-02-09 +## [0.50.1] - 2024-02-09 ### Fixed From 3c18e47466a644fc323798de0d64f2b386c26c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:08:42 +0000 Subject: [PATCH 52/58] Test nested CSS worked at higher level. Addresses review feedback. See the three comments starting at https://github.com/Textualize/textual/pull/4040#issuecomment-1934279826. --- tests/css/test_nested_css.py | 48 ++++++++++ tests/css/test_tokenize.py | 181 ----------------------------------- 2 files changed, 48 insertions(+), 181 deletions(-) diff --git a/tests/css/test_nested_css.py b/tests/css/test_nested_css.py index 821c655ff3..20424ad297 100644 --- a/tests/css/test_nested_css.py +++ b/tests/css/test_nested_css.py @@ -44,6 +44,54 @@ async def test_nest_app(): assert app.query_one("#foo .paul").styles.background == Color.parse("blue") +class ListOfNestedSelectorsApp(App[None]): + CSS = """ + Label { + &.foo, &.bar { + background: red; + } + } + """ + + def compose(self) -> ComposeResult: + yield Label("one", classes="foo") + yield Label("two", classes="bar") + yield Label("three", classes="heh") + + +async def test_lists_of_selectors_in_nested_css() -> None: + """Regression test for https://github.com/Textualize/textual/issues/3969.""" + app = ListOfNestedSelectorsApp() + red = Color.parse("red") + async with app.run_test(): + assert app.query_one(".foo").styles.background == red + assert app.query_one(".bar").styles.background == red + assert app.query_one(".heh").styles.background != red + + +class DeclarationAfterNestedApp(App[None]): + # css = "Screen{Label{background:red;}background:green;}" + CSS = """ + Screen { + Label { + background: red; + } + background: green; + } + """ + + def compose(self) -> ComposeResult: + yield Label("one") + + +async def test_rule_declaration_after_nested() -> None: + """Regression test for https://github.com/Textualize/textual/issues/3999.""" + app = DeclarationAfterNestedApp() + async with app.run_test(): + assert app.screen.styles.background == Color.parse("green") + assert app.query_one(Label).styles.background == Color.parse("red") + + @pytest.mark.parametrize( ("css", "exception"), [ diff --git a/tests/css/test_tokenize.py b/tests/css/test_tokenize.py index fab79ed4f6..d4dfba888e 100644 --- a/tests/css/test_tokenize.py +++ b/tests/css/test_tokenize.py @@ -898,184 +898,3 @@ def test_allow_new_lines(): ), ] assert list(tokenize(css, ("", ""))) == expected - - -def test_nested_css_selector_list_with_ampersand(): - """Regression test for https://github.com/Textualize/textual/issues/3969.""" - css = "Label{&.foo,&.bar{border:solid red;}}" - tokens = list(tokenize(css, ("", ""))) - assert tokens == [ - Token( - name="selector_start", - value="Label", - read_from=("", ""), - code=css, - location=(0, 0), - ), - Token( - name="declaration_set_start", - value="{", - read_from=("", ""), - code=css, - location=(0, 5), - ), - Token(name="nested", value="&", read_from=("", ""), code=css, location=(0, 6)), - Token( - name="selector_class", - value=".foo", - read_from=("", ""), - code=css, - location=(0, 7), - ), - Token( - name="new_selector", - value=",", - read_from=("", ""), - code=css, - location=(0, 11), - ), - Token(name="nested", value="&", read_from=("", ""), code=css, location=(0, 12)), - Token( - name="selector_class", - value=".bar", - read_from=("", ""), - code=css, - location=(0, 13), - ), - Token( - name="declaration_set_start", - value="{", - read_from=("", ""), - code=css, - location=(0, 17), - ), - Token( - name="declaration_name", - value="border:", - read_from=("", ""), - code=css, - location=(0, 18), - ), - Token( - name="token", value="solid", read_from=("", ""), code=css, location=(0, 25) - ), - Token( - name="whitespace", value=" ", read_from=("", ""), code=css, location=(0, 30) - ), - Token( - name="token", value="red", read_from=("", ""), code=css, location=(0, 31) - ), - Token( - name="declaration_end", - value=";", - read_from=("", ""), - code=css, - location=(0, 34), - ), - Token( - name="declaration_set_end", - value="}", - read_from=("", ""), - code=css, - location=(0, 35), - ), - Token( - name="declaration_set_end", - value="}", - read_from=("", ""), - code=css, - location=(0, 36), - ), - ] - - -def test_declaration_after_nested_declaration_set(): - """Regression test for https://github.com/Textualize/textual/issues/3999.""" - css = "Screen{Label{background:red;}background:green;}" - tokens = list(tokenize(css, ("", ""))) - assert tokens == [ - Token( - name="selector_start", - value="Screen", - read_from=("", ""), - code=css, - location=(0, 0), - ), - Token( - name="declaration_set_start", - value="{", - read_from=("", ""), - code=css, - location=(0, 6), - ), - Token( - name="selector_start", - value="Label", - read_from=("", ""), - code=css, - location=(0, 7), - ), - Token( - name="declaration_set_start", - value="{", - read_from=("", ""), - code=css, - location=(0, 12), - ), - Token( - name="declaration_name", - value="background:", - read_from=("", ""), - code=css, - location=(0, 13), - ), - Token( - name="token", - value="red", - read_from=("", ""), - code=css, - location=(0, 24), - ), - Token( - name="declaration_end", - value=";", - read_from=("", ""), - code=css, - location=(0, 27), - ), - Token( - name="declaration_set_end", - value="}", - read_from=("", ""), - code=css, - location=(0, 28), - ), - Token( - name="declaration_name", - value="background:", - read_from=("", ""), - code=css, - location=(0, 29), - ), - Token( - name="token", - value="green", - read_from=("", ""), - code=css, - location=(0, 40), - ), - Token( - name="declaration_end", - value=";", - read_from=("", ""), - code=css, - location=(0, 45), - ), - Token( - name="declaration_set_end", - value="}", - read_from=("", ""), - code=css, - location=(0, 46), - ), - ] From 3abc8ee577a18a4dc0d1c945646d2c417bb1d878 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 15 Feb 2024 14:21:13 +0000 Subject: [PATCH 53/58] Text area fixes (#4157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial undo related machinery added to TextArea * Initial undo implementation * Basic undo and redo * Some more fleshing out of undo and redo * Skeleton code for managing TextArea history * Initial implementation of undo & redo checkpointing in TextArea * Increase checkpoint characters * Restoring the selection in the TextArea and then restoring it on undo * Adding docstrings to undo_batch and redo_batch in the TextArea * Batching edits of the same type * Batching edits of the same type * Keeping edits containing newlines in their own batch * Checking for newline characters in insertion or replacement during undo checkpoint creation. Updating docstrings in history.py * Fix mypy warning * Performance improvement * Add history checkpoint on cursor movement * Fixing merge conflict in Edit class * Fixing error in merge conflict resolution * Remove unused test file * Remove unused test file * Initial testing of undo and redo * Testing for undo redo * Updating lockfile * Add an extra test * Fix: setting the `text` property programmatically should invalidate the edit history * Improving docstrings * Rename EditHistory.reset() to EditHistory.clear() * Add docstring to an exception * Add a pause after focus/blur in a test * Forcing CI colour * Update focus checkpoint test * Try to force color in pytest by setting --color=yes in PYTEST_ADDOPTS in env var on Github Actions * Add extra assertion in a test * Toggle text_area has focus to trigger checkpoint in history * Apply grammar/wording suggestions from code review Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Making max checkpoints configurable in TextArea history * Improve a docstring * Update changelog * Spelling fixes * More spelling fixes * Americanize spelling of tab_behaviour (->tab_behavior) * Update CHANGELOG regarding `tab_behaviour`->`tab_behavior` * Various fixes * Various fixes and improvements * Updating tests to account for themes always being non-None * Update CHANGELOG. * Add TextArea.read_only to reactive attr table in TextArea docs * Update TextArea docs regarding new features * Cleaning up some typing issues * Add actions for undo and redo * Fix a typo * Fix wording in docs/widgets/text_area.md Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Re-add return type hint * PR feedback and fixing typos * Mark breaking change in CHANGELOG * Add undo/redo to docstring * Add note on undo/redo bindings --------- Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- CHANGELOG.md | 3 +- docs/widgets/text_area.md | 55 +++++++++++---- src/textual/_text_area_theme.py | 15 ---- src/textual/widgets/_text_area.py | 98 +++++++++++++------------- tests/text_area/test_setting_themes.py | 17 +---- 5 files changed, 95 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb816e2ab..674eb30a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed -- Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124 +- Breaking change: Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124 +- `TextArea.theme` now defaults to `"css"` instead of None, and is no longer optional https://github.com/Textualize/textual/pull/4157 ## [0.50.1] - 2024-02-09 diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md index 4449bcd668..c0a95c265a 100644 --- a/docs/widgets/text_area.md +++ b/docs/widgets/text_area.md @@ -1,7 +1,6 @@ - # TextArea -!!! tip +!!! tip Added in version 0.38.0. Soft wrapping added in version 0.48.0. @@ -68,7 +67,6 @@ text_area.language = "markdown" !!! note More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages). - ### Reading content from `TextArea` There are a number of ways to retrieve content from the `TextArea`: @@ -136,7 +134,7 @@ There are a number of additional utility methods available for interacting with ##### Location information -A number of properties exist on `TextArea` which give information about the current cursor location. +Many properties exist on `TextArea` which give information about the current cursor location. These properties begin with `cursor_at_`, and return booleans. For example, [`cursor_at_start_of_line`][textual.widgets._text_area.TextArea.cursor_at_start_of_line] tells us if the cursor is at a start of line. @@ -175,12 +173,14 @@ the cursor, selection, gutter, and more. #### Default theme -The default `TextArea` theme is called `css`. -This a theme which takes values entirely from CSS. +The default `TextArea` theme is called `css`, which takes it's values entirely from CSS. This means that the default appearance of the widget fits nicely into a standard Textual application, and looks right on both dark and light mode. -More complex applications such as code editors will likely want to use pre-defined themes such as `monokai`. +When using the `css` theme, you can make use of [component classes][textual.widgets.TextArea.COMPONENT_CLASSES] to style elements of the `TextArea`. +For example, the CSS code `TextArea .text-area--cursor { background: green; }` will make the cursor `green`. + +More complex applications such as code editors may want to use pre-defined themes such as `monokai`. This involves using a `TextAreaTheme` object, which we cover in detail below. This allows full customization of the `TextArea`, including syntax highlighting, at the code level. @@ -190,7 +190,7 @@ The initial theme of the `TextArea` is determined by the `theme` parameter. ```python # Create a TextArea with the 'dracula' theme. -yield TextArea("print(123)", language="python", theme="dracula") +yield TextArea.code_editor("print(123)", language="python", theme="dracula") ``` You can check which themes are available using the [`available_themes`][textual.widgets._text_area.TextArea.available_themes] property. @@ -198,7 +198,7 @@ You can check which themes are available using the [`available_themes`][textual. ```python >>> text_area = TextArea() >>> print(text_area.available_themes) -{'dracula', 'github_light', 'monokai', 'vscode_dark'} +{'css', 'dracula', 'github_light', 'monokai', 'vscode_dark'} ``` After creating a `TextArea`, you can change the theme by setting the [`theme`][textual.widgets._text_area.TextArea.theme] @@ -212,7 +212,12 @@ On setting this attribute the `TextArea` will immediately refresh to display the #### Custom themes -Using custom (non-builtin) themes is two-step process: +!!! note + + Custom themes are only relevant for people who are looking to customize syntax highlighting. + If you're only editing plain text, and wish to recolor aspects of the `TextArea`, you should use the [provided component classes][textual.widgets.TextArea.COMPONENT_CLASSES]. + +Using custom (non-builtin) themes is a two-step process: 1. Create an instance of [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme]. 2. Register it using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme]. @@ -297,6 +302,27 @@ The character(s) inserted when you press tab is controlled by setting the `inden If `indent_type == "spaces"`, pressing ++tab++ will insert up to `indent_width` spaces in order to align with the next tab stop. +### Undo and redo + +`TextArea` offers `undo` and `redo` methods. +By default, `undo` is bound to Ctrl+Z and `redo` to Ctrl+Y. + +The `TextArea` uses a heuristic to place _checkpoints_ after certain types of edit. +When you call `undo`, all of the edits between now and the most recent checkpoint are reverted. +You can manually add a checkpoint by calling the [`TextArea.history.checkpoint()`][textual.widgets.text_area.EditHistory.checkpoint] instance method. + +The undo and redo history uses a stack-based system, where a single item on the stack represents a single checkpoint. +In memory-constrained environments, you may wish to reduce the maximum number of checkpoints that can exist. +You can do this by passing the `max_checkpoints` argument to the `TextArea` constructor. + +### Read-only mode + +`TextArea.read_only` is a boolean reactive attribute which, if `True`, will prevent users from modifying content in the `TextArea`. + +While `read_only=True`, you can still modify the content programmatically. + +While this mode is active, the `TextArea` receives the `-read-only` CSS class, which you can use to supply custom styles for read-only mode. + ### Line separators When content is loaded into `TextArea`, the content is scanned from beginning to end @@ -459,7 +485,6 @@ If you notice some highlights are missing after registering a language, the issu The names assigned in tree-sitter highlight queries are often reused across multiple languages. For example, `@string` is used in many languages to highlight strings. - #### Navigation and wrapping information If you're building functionality on top of `TextArea`, it may be useful to inspect the `navigator` and `wrapped_document` attributes. @@ -473,14 +498,15 @@ A detailed view of these classes is out of scope, but do note that a lot of the | Name | Type | Default | Description | |------------------------|--------------------------|---------------|------------------------------------------------------------------| -| `language` | `str | None` | `None` | The language to use for syntax highlighting. | -| `theme` | `str | None` | `TextAreaTheme.default()` | The theme to use for syntax highlighting. | +| `language` | `str | None` | `None` | The language to use for syntax highlighting. | +| `theme` | `str` | `"css"` | The theme to use. | | `selection` | `Selection` | `Selection()` | The current selection. | | `show_line_numbers` | `bool` | `False` | Show or hide line numbers. | | `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. | | `match_cursor_bracket` | `bool` | `True` | Enable/disable highlighting matching brackets under cursor. | | `cursor_blink` | `bool` | `True` | Enable/disable blinking of the cursor when the widget has focus. | | `soft_wrap` | `bool` | `True` | Enable/disable soft wrapping. | +| `read_only` | `bool` | `False` | Enable/disable read-only mode. | ## Messages @@ -496,7 +522,6 @@ The `TextArea` widget defines the following bindings: show_root_heading: false show_root_toc_entry: false - ## Component classes The `TextArea` defines component classes that can style various aspects of the widget. @@ -513,11 +538,11 @@ Styles from the `theme` attribute take priority. - [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme] - theming the `TextArea` - [`DocumentNavigator`][textual.widgets.text_area.DocumentNavigator] - guides cursor movement - [`WrappedDocument`][textual.widgets.text_area.WrappedDocument] - manages wrapping the document +- [`EditHistory`][textual.widgets.text_area.EditHistory] - manages the undo stack - The tree-sitter documentation [website](https://tree-sitter.github.io/tree-sitter/). - The tree-sitter Python bindings [repository](https://github.com/tree-sitter/py-tree-sitter). - `py-tree-sitter-languages` [repository](https://github.com/grantjenks/py-tree-sitter-languages) (provides binary wheels for a large variety of tree-sitter languages). - ## Additional notes - To remove the outline effect when the `TextArea` is focused, you can set `border: none; padding: 0;` in your CSS. diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 5fbcee9967..14bc6ffd37 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -119,9 +119,6 @@ def apply_css(self, text_area: TextArea) -> None: if self.cursor_line_gutter_style is None: self.cursor_line_gutter_style = get_style("text-area--cursor-gutter") - if self.cursor_line_gutter_style is None and self.cursor_line_style is not None: - self.cursor_line_gutter_style = self.cursor_line_style.copy() - if self.bracket_matching_style is None: matching_bracket_style = get_style("text-area--matching-bracket") if matching_bracket_style: @@ -182,15 +179,6 @@ def builtin_themes(cls) -> list[TextAreaTheme]: """ return list(_BUILTIN_THEMES.values()) - @classmethod - def default(cls) -> TextAreaTheme: - """Get the default syntax theme. - - Returns: - The default TextAreaTheme (probably "css"). - """ - return _CSS_THEME - _MONOKAI = TextAreaTheme( name="monokai", @@ -388,6 +376,3 @@ def default(cls) -> TextAreaTheme: "vscode_dark": _DARK_VS, "github_light": _GITHUB_LIGHT, } - -DEFAULT_THEME = TextAreaTheme.get_builtin_theme("basic") -"""The default TextAreaTheme used by Textual.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 40b12f1768..f700a742f3 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -85,7 +85,7 @@ class TextAreaLanguage: highlight_query: str -class TextArea(ScrollView, can_focus=True): +class TextArea(ScrollView): DEFAULT_CSS = """\ TextArea { width: 1fr; @@ -220,6 +220,8 @@ class TextArea(ScrollView, can_focus=True): "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False ), Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), + Binding("ctrl+z", "undo", "Undo", show=False), + Binding("ctrl+y", "redo", "Redo", show=False), ] """ | Key(s) | Description | @@ -251,6 +253,8 @@ class TextArea(ScrollView, can_focus=True): | ctrl+k | Delete from cursor to the end of the line. | | f6 | Select the current line. | | f7 | Select all text in the document. | + | ctrl+z | Undo. | + | ctrl+y | Redo. | """ language: Reactive[str | None] = reactive(None, always_update=True, init=False) @@ -264,7 +268,7 @@ class TextArea(ScrollView, can_focus=True): it first using [`TextArea.register_language`][textual.widgets._text_area.TextArea.register_language]. """ - theme: Reactive[str | None] = reactive(None, always_update=True, init=False) + theme: Reactive[str] = reactive("css", always_update=True, init=False) """The name of the theme to use. Themes must be registered using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] before they can be used. @@ -355,7 +359,7 @@ def __init__( text: str = "", *, language: str | None = None, - theme: str | None = None, + theme: str = "css", soft_wrap: bool = True, tab_behavior: Literal["focus", "indent"] = "focus", read_only: bool = False, @@ -383,7 +387,6 @@ def __init__( disabled: True if the widget is disabled. """ super().__init__(name=name, id=id, classes=classes, disabled=disabled) - self._initial_text = text self._languages: dict[str, TextAreaLanguage] = {} """Maps language names to TextAreaLanguage.""" @@ -415,7 +418,7 @@ def __init__( self._highlights: dict[int, list[Highlight]] = defaultdict(list) """Mapping line numbers to the set of highlights for that line.""" - self._highlight_query: "Query" | None = None + self._highlight_query: "Query | None" = None """The query that's currently being used for highlighting.""" self.document: DocumentBase = Document(text) @@ -433,15 +436,14 @@ def __init__( self._set_document(text, language) - self._theme: TextAreaTheme | None = None + self.language = language + self.theme = theme + + self._theme: TextAreaTheme """The `TextAreaTheme` corresponding to the set theme name. When the `theme` reactive is set as a string, the watcher will update this attribute to the corresponding `TextAreaTheme` object.""" - self.language = language - - self.theme = theme - self.set_reactive(TextArea.soft_wrap, soft_wrap) self.set_reactive(TextArea.read_only, read_only) self.set_reactive(TextArea.show_line_numbers, show_line_numbers) @@ -457,7 +459,7 @@ def code_editor( text: str = "", *, language: str | None = None, - theme: str | None = "monokai", + theme: str = "monokai", soft_wrap: bool = False, tab_behavior: Literal["focus", "indent"] = "indent", show_line_numbers: bool = True, @@ -614,7 +616,7 @@ def find_matching_bracket( If the character is not available for bracket matching, `None` is returned. """ match_location = None - bracket_stack = [] + bracket_stack: list[str] = [] if bracket in _OPENING_BRACKETS: for candidate, candidate_location in self._yield_character_locations( search_from @@ -664,11 +666,7 @@ def _watch_language(self, language: str | None) -> None: f"then switch to it by setting the `TextArea.language` attribute." ) - self._set_document( - self.document.text if self.document is not None else self._initial_text, - language, - ) - self._initial_text = "" + self._set_document(self.document.text, language) def _watch_show_line_numbers(self) -> None: """The line number gutter contributes to virtual size, so recalculate.""" @@ -684,32 +682,28 @@ def _watch_show_vertical_scrollbar(self) -> None: self._rewrap_and_refresh_virtual_size() self.scroll_cursor_visible() - def _watch_theme(self, theme: str | None) -> None: + def _watch_theme(self, theme: str) -> None: """We set the styles on this widget when the theme changes, to ensure that - if padding is applied, the colours match.""" + if padding is applied, the colors match.""" self._set_theme(theme) def _app_dark_toggled(self) -> None: self._set_theme(self._theme.name) - def _set_theme(self, theme: str | None) -> None: + def _set_theme(self, theme: str) -> None: theme_object: TextAreaTheme | None - if theme is None: - # If the theme is None, use the default. - theme_object = TextAreaTheme.default() - else: - # If the user supplied a string theme name, find it and apply it. - try: - theme_object = self._themes[theme] - except KeyError: - theme_object = TextAreaTheme.get_builtin_theme(theme) + # If the user supplied a string theme name, find it and apply it. + try: + theme_object = self._themes[theme] + except KeyError: + theme_object = TextAreaTheme.get_builtin_theme(theme) if theme_object is None: raise ThemeDoesNotExist( f"{theme!r} is not a builtin theme, or it has not been registered. " f"To use a custom theme, register it first using `register_theme`, " f"then switch to that theme by setting the `TextArea.theme` attribute." - ) + ) from None self._theme = dataclasses.replace(theme_object) if theme_object: @@ -768,7 +762,7 @@ def available_languages(self) -> set[str]: def register_language( self, - language: str | "Language", + language: "str | Language", highlight_query: str, ) -> None: """Register a language and corresponding highlight query. @@ -825,7 +819,7 @@ def _set_document(self, text: str, language: str | None) -> None: if TREE_SITTER and language: # Attempt to get the override language. text_area_language = self._languages.get(language, None) - document_language: str | "Language" + document_language: "str | Language" if text_area_language: document_language = text_area_language.language highlight_query = text_area_language.highlight_query @@ -964,11 +958,11 @@ def _refresh_size(self) -> None: width, height = self.document.get_size(self.indent_width) self.virtual_size = Size(width + self.gutter_width + 1, height) - def render_line(self, widget_y: int) -> Strip: + def render_line(self, y: int) -> Strip: """Render a single line of the TextArea. Called by Textual. Args: - widget_y: Y Coordinate of line relative to the widget region. + y: Y Coordinate of line relative to the widget region. Returns: A rendered line. @@ -982,7 +976,7 @@ def render_line(self, widget_y: int) -> Strip: scroll_x, scroll_y = self.scroll_offset # Account for how much the TextArea is scrolled. - y_offset = widget_y + scroll_y + y_offset = y + scroll_y # If we're beyond the height of the document, render blank lines out_of_bounds = y_offset >= wrapped_document.height @@ -1007,7 +1001,7 @@ def render_line(self, widget_y: int) -> Strip: line_character_count = len(line) line.tab_size = self.indent_width line.set_length(line_character_count + 1) # space at end for cursor - virtual_width, virtual_height = self.virtual_size + virtual_width, _virtual_height = self.virtual_size selection = self.selection start, end = selection @@ -1256,13 +1250,21 @@ def edit(self, edit: Edit) -> EditResult: def undo(self) -> None: """Undo the edits since the last checkpoint (the most recent batch of edits).""" - edits = self.history._pop_undo() - self._undo_batch(edits) + if edits := self.history._pop_undo(): + self._undo_batch(edits) + + def action_undo(self) -> None: + """Undo the edits since the last checkpoint (the most recent batch of edits).""" + self.undo() def redo(self) -> None: """Redo the most recently undone batch of edits.""" - edits = self.history._pop_redo() - self._redo_batch(edits) + if edits := self.history._pop_redo(): + self._redo_batch(edits) + + def action_redo(self) -> None: + """Redo the most recently undone batch of edits.""" + self.redo() def _undo_batch(self, edits: Sequence[Edit]) -> None: """Undo a batch of Edits. @@ -1439,7 +1441,7 @@ def gutter_width(self) -> int: ) return gutter_width - def _on_mount(self, _: events.Mount) -> None: + def _on_mount(self, event: events.Mount) -> None: self.blink_timer = self.set_interval( 0.5, self._toggle_cursor_blink_visible, @@ -1487,7 +1489,7 @@ async def _on_mouse_move(self, event: events.MouseMove) -> None: self.selection = Selection(selection_start, target) async def _on_mouse_up(self, event: events.MouseUp) -> None: - """Finalise the selection that has been made using the mouse.""" + """Finalize the selection that has been made using the mouse.""" self._selecting = False self.release_mouse() self.record_cursor_width() @@ -1497,8 +1499,8 @@ async def _on_paste(self, event: events.Paste) -> None: """When a paste occurs, insert the text from the paste event into the document.""" if self.read_only: return - result = self._replace_via_keyboard(event.text, *self.selection) - self.move_cursor(result.end_location) + if result := self._replace_via_keyboard(event.text, *self.selection): + self.move_cursor(result.end_location) def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: """Return the column that the cell width corresponds to on the given row. @@ -1578,7 +1580,7 @@ def move_cursor( that is wide enough. """ if select: - start, end = self.selection + start, _end = self.selection self.selection = Selection(start, location) else: self.selection = Selection.cursor(location) @@ -1611,7 +1613,7 @@ def move_cursor_relative( that is wide enough. """ clamp_visitable = self.clamp_visitable - start, end = self.selection + _start, end = self.selection current_row, current_column = end target = clamp_visitable((current_row + rows, current_column + columns)) self.move_cursor(target, select, center, record_width) @@ -2078,7 +2080,7 @@ def action_delete_line(self) -> None: """Deletes the lines which intersect with the selection.""" start, end = self.selection start, end = sorted((start, end)) - start_row, start_column = start + start_row, _start_column = start end_row, end_column = end # Generally editors will only delete line the end line of the @@ -2165,7 +2167,7 @@ def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: Returns: A `dict[int, int]` mapping byte indices to codepoint indices within `data`. """ - byte_to_codepoint = {} + byte_to_codepoint: dict[int, int] = {} current_byte_offset = 0 code_point_offset = 0 diff --git a/tests/text_area/test_setting_themes.py b/tests/text_area/test_setting_themes.py index 8d165a98a9..a3ee7aa35e 100644 --- a/tests/text_area/test_setting_themes.py +++ b/tests/text_area/test_setting_themes.py @@ -6,7 +6,7 @@ from textual.widgets._text_area import ThemeDoesNotExist -class TextAreaApp(App): +class TextAreaApp(App[None]): def compose(self) -> ComposeResult: yield TextArea("print('hello')", language="python") @@ -16,11 +16,11 @@ async def test_default_theme(): async with app.run_test(): text_area = app.query_one(TextArea) - assert text_area.theme is None + assert text_area.theme is "css" async def test_setting_builtin_themes(): - class MyTextAreaApp(App): + class MyTextAreaApp(App[None]): def compose(self) -> ComposeResult: yield TextArea("print('hello')", language="python", theme="vscode_dark") @@ -34,17 +34,6 @@ def compose(self) -> ComposeResult: assert text_area.theme == "monokai" -async def test_setting_theme_to_none(): - app = TextAreaApp() - - async with app.run_test(): - text_area = app.query_one(TextArea) - text_area.theme = None - assert text_area.theme is None - # When theme is None, we use the default theme. - assert text_area._theme.name == TextAreaTheme.default().name - - async def test_setting_unknown_theme_raises_exception(): app = TextAreaApp() async with app.run_test(): From d5a1a3ef8640494b8309c7b466cade72d119fedb Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 14:33:45 +0000 Subject: [PATCH 54/58] Improve the TabActivated history unit test --- tests/test_tabbed_content.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_tabbed_content.py b/tests/test_tabbed_content.py index 28765e8fc3..9dd7dad0b3 100644 --- a/tests/test_tabbed_content.py +++ b/tests/test_tabbed_content.py @@ -137,13 +137,12 @@ def compose(self) -> ComposeResult: async def test_tabbed_content_messages(): class TabbedApp(App): - message = None + activation_history: list[Tab] = [] def compose(self) -> ComposeResult: - with TabbedContent(initial="bar"): + with TabbedContent(): with TabPane("foo", id="foo"): yield Label("Foo", id="foo-label") - with TabPane("bar", id="bar"): yield Label("Bar", id="bar-label") with TabPane("baz", id="baz"): @@ -152,15 +151,19 @@ def compose(self) -> ComposeResult: def on_tabbed_content_tab_activated( self, event: TabbedContent.TabActivated ) -> None: - self.message = event + self.activation_history.append(event.tab) app = TabbedApp() async with app.run_test() as pilot: tabbed_content = app.query_one(TabbedContent) tabbed_content.active = "bar" await pilot.pause() - assert isinstance(app.message, TabbedContent.TabActivated) - assert app.message.tab.label.plain == "bar" + assert app.activation_history == [ + # foo was originally activated. + app.query_one(TabbedContent).get_tab("foo"), + # then we did bar "by hand" + app.query_one(TabbedContent).get_tab("bar"), + ] async def test_tabbed_content_add_later_from_empty(): From 8050acdf82aa15212ffbb098158881eabfcd2ed9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 15 Feb 2024 14:40:37 +0000 Subject: [PATCH 55/58] Allow for older Pythons --- tests/test_tabbed_content.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_tabbed_content.py b/tests/test_tabbed_content.py index 9dd7dad0b3..6d7b0976e9 100644 --- a/tests/test_tabbed_content.py +++ b/tests/test_tabbed_content.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from textual.app import App, ComposeResult From 159a54e109c2ae271db878eca43727597578d855 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 15 Feb 2024 16:34:54 +0000 Subject: [PATCH 56/58] Update tests/css/test_nested_css.py --- tests/css/test_nested_css.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/css/test_nested_css.py b/tests/css/test_nested_css.py index 20424ad297..32086f2e4d 100644 --- a/tests/css/test_nested_css.py +++ b/tests/css/test_nested_css.py @@ -70,7 +70,6 @@ async def test_lists_of_selectors_in_nested_css() -> None: class DeclarationAfterNestedApp(App[None]): - # css = "Screen{Label{background:red;}background:green;}" CSS = """ Screen { Label { From c2bb30957ac983e5f8bd33c9852f3cf3fed13995 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 15 Feb 2024 17:19:43 +0000 Subject: [PATCH 57/58] version bump --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f930d4baa5..c793d5f578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## [0.51.0] - 2024-02-15 ### Added @@ -1706,6 +1706,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.51.0]: https://github.com/Textualize/textual/compare/v0.50.1...v0.51.0 [0.50.1]: https://github.com/Textualize/textual/compare/v0.50.0...v0.50.1 [0.50.0]: https://github.com/Textualize/textual/compare/v0.49.0...v0.50.0 [0.49.1]: https://github.com/Textualize/textual/compare/v0.49.0...v0.49.1 diff --git a/pyproject.toml b/pyproject.toml index f8b4180db7..429bfa777a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.50.1" +version = "0.51.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" From c680bd790516d959ed87c30b4aea0deabaeee056 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:41:55 +0000 Subject: [PATCH 58/58] docs(events): add tcss to on decorator examples --- docs/guide/events.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/guide/events.md b/docs/guide/events.md index da18527f0c..bd526341c8 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -210,6 +210,12 @@ In the following example we have three buttons, each of which does something dif 1. The message handler is called when any button is pressed +=== "on_decorator.tcss" + + ```python title="on_decorator.tcss" + --8<-- "docs/examples/events/on_decorator.tcss" + ``` + === "Output" ```{.textual path="docs/examples/events/on_decorator01.py"} @@ -233,6 +239,12 @@ The following example uses the decorator approach to write individual message ha 2. Matches the button with class names "toggle" *and* "dark" 3. Matches the button with an id of "quit" +=== "on_decorator.tcss" + + ```python title="on_decorator.tcss" + --8<-- "docs/examples/events/on_decorator.tcss" + ``` + === "Output" ```{.textual path="docs/examples/events/on_decorator02.py"}