diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 01a5639af0..7eca7607da 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -55,4 +55,4 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: snapshot-report-textual
- path: tests/snapshot_tests/output/snapshot_report.html
+ path: snapshot_report.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ed4f37c2f..939a0d38bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
+### Fixed
+
+- Fixed application freeze when pasting an emoji into an application on Windows https://github.com/Textualize/textual/issues/3178
+
+## [0.38.1] - 2023-09-21
+
+### Fixed
+
+- Hotfix - added missing highlight files in build distribution https://github.com/Textualize/textual/pull/3370
+
+## [0.38.0] - 2023-09-21
+
+### Added
+
+- Added a TextArea https://github.com/Textualize/textual/pull/2931
+- Added :dark and :light pseudo classes
+
+### Fixed
+
+- Fixed `DataTable` not updating component styles on hot-reloading https://github.com/Textualize/textual/issues/3312
+
+### Changed
+
+- Breaking change: CSS in DEFAULT_CSS is now automatically scoped to the widget (set SCOPED_CSS=False) to disable
+
+## [0.37.1] - 2023-09-16
+
+### Fixed
+
+- Fixed the command palette crashing with a `TimeoutError` in any Python before 3.11 https://github.com/Textualize/textual/issues/3320
+- Fixed `Input` event leakage from `CommandPalette` to `App`.
+
+### Changed
+
+- Breaking change: Changed `Markdown.goto_anchor` to return a boolean (if the anchor was found) instead of `None` https://github.com/Textualize/textual/pull/3334
+
+## [0.37.0] - 2023-09-15
+
### Added
- Added the command palette https://github.com/Textualize/textual/pull/3058
@@ -19,12 +57,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `Screen.sub_title`
- Properties `Header.screen_title` and `Header.screen_sub_title` https://github.com/Textualize/textual/pull/3199
- Added `DirectoryTree.DirectorySelected` message https://github.com/Textualize/textual/issues/3200
+- Added `widgets.Collapsible` contributed by Sunyoung Yoo https://github.com/Textualize/textual/pull/2989
### Fixed
- Fixed a crash when removing an option from an `OptionList` while the mouse is hovering over the last option https://github.com/Textualize/textual/issues/3270
- Fixed a crash in `MarkdownViewer` when clicking on a link that contains an anchor https://github.com/Textualize/textual/issues/3094
-- Fixed application freeze when pasting an emoji into an application on Windows https://github.com/Textualize/textual/issues/3178
+- Fixed wrong message pump in pop_screen https://github.com/Textualize/textual/pull/3315
### Changed
@@ -32,6 +71,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Breaking change: Widget.notify and App.notify now return None https://github.com/Textualize/textual/pull/3275
- App.unnotify is now private (renamed to App._unnotify) https://github.com/Textualize/textual/pull/3275
- `Markdown.load` will now attempt to scroll to a related heading if an anchor is provided https://github.com/Textualize/textual/pull/3244
+- `ProgressBar` explicitly supports being set back to its indeterminate state https://github.com/Textualize/textual/pull/3286
## [0.36.0] - 2023-09-05
@@ -49,6 +89,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Callbacks scheduled with `call_next` will now have the same prevented messages as when the callback was scheduled https://github.com/Textualize/textual/pull/3065
- Added `cursor_type` to the `DataTable` constructor.
- Fixed `push_screen` not updating Screen.CSS styles https://github.com/Textualize/textual/issues/3217
+- `DataTable.add_row` accepts `height=None` to automatically compute optimal height for a row https://github.com/Textualize/textual/pull/3213
### Fixed
@@ -1275,6 +1316,8 @@ 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.37.1]: https://github.com/Textualize/textual/compare/v0.37.0...v0.37.1
+[0.37.0]: https://github.com/Textualize/textual/compare/v0.36.0...v0.37.0
[0.36.0]: https://github.com/Textualize/textual/compare/v0.35.1...v0.36.0
[0.35.1]: https://github.com/Textualize/textual/compare/v0.35.0...v0.35.1
[0.35.0]: https://github.com/Textualize/textual/compare/v0.34.0...v0.35.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 41b440244b..b7d3488111 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,120 +1,103 @@
-# Contributing Guidelines
+# Contributing to Textual
-🎉 **First of all, thanks for taking the time to contribute!** 🎉
+First of all, thanks for taking the time to contribute to Textual!
-## 🤔 How can I contribute?
+## How can I contribute?
-**1.** Fix issue
+You can contribute to Textual in many ways:
-**2.** Report bug
+ 1. [Report a bug](https://github.com/textualize/textual/issues/new?title=%5BBUG%5D%20short%20bug%20description&template=bug_report.md)
+ 2. Add a new feature
+ 3. Fix a bug
+ 4. Improve the documentation
-**3.** Improve Documentation
+## Setup
-## Setup 🚀
-You need to set up Textualize to make your contribution. Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows, and probably any OS where Python also runs.
+To make a code or documentation contribution you will need to set up Textual locally.
+You can follow these steps:
-### Installation
+ 1. Make sure you have Poetry installed ([see instructions here](https://python-poetry.org))
+ 2. Clone the Textual repository
+ 3. Run `poetry shell` to create a virtual environment for the dependencies
+ 4. Run `poetry install` to install all dependencies
+ 5. Make sure the latest version of Textual was installed by running the command `textual --version`
+ 6. Install the pre-commit hooks with the command `pre-commit install`
-**Install Texualize via pip:**
-```bash
-pip install textual
-```
-**Install [Poetry](https://python-poetry.org/)**
-```bash
-curl -sSL https://install.python-poetry.org | python3 -
-```
-**To install all dependencies, run:**
-```bash
-poetry install --all
-```
-**Make sure everything works fine:**
-```bash
-textual --version
-```
-### Demo
+## Demo
-Once you have Textual installed, run the following to get an impression of what it can do:
+Once you have Textual installed, run the Textual demo to get an impression of what Textual can do and to double check that everything was installed correctly:
```bash
python -m textual
```
-If Texualize is installed, you should see this:
-
-## Make contribution
-**1.** Fork [this](repo) repository.
+## Guidelines
-**2.** Clone the forked repository.
+- Read any issue instructions carefully. Feel free to ask for clarification if any details are missing.
-```bash
-git clone https://github.com//textual.git
-```
+- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from.
-**3.** Navigate to the project directory.
+- Write tests for your code.
+ - If you are fixing a bug, make sure to add regression tests that link to the original issue.
+ - If you are implementing a visual element, make sure to add _snapshot tests_. [See below](#snapshot-testing) for more details.
-```bash
-cd textual
-```
+## Before opening a PR
+
+Before you open your PR, please go through this checklist and make sure you've checked all the items that apply:
-**4.** Create a new [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)
+ - [ ] Update the `CHANGELOG.md`
+ - [ ] Format your code with black (`make format`)
+ - [ ] All your code has docstrings in the style of the rest of the codebase
+ - [ ] Your code passes all tests (`make test`)
+([Read this](#makefile-commands) if the command `make` doesn't work for you.)
-### 📣 Pull Requests(PRs)
+## Updating and building the documentation
-The process described here should check off these goals:
+If you change the documentation, you will want to build the documentation to make sure everything looks like it should.
+The command `make docs-serve-offline` should start a server that will let you preview the documentation locally and that should reload whenever you save changes to the documentation or the code files.
-- [x] Maintain the project's quality.
-- [x] Fix problems that are important to users.
-- [x] The CHANGELOG.md was updated;
-- [x] Your code was formatted with black (make format);
-- [x] All of your code has docstrings in the style of the rest of the codebase;
-- [x] your code passes all tests (make test); and
-- [x] You added documentation when needed.
+([Read this](#makefile-commands) if the command `make` doesn't work for you.)
+
+## After opening a PR
-### After the PR 🥳
When you open a PR, your code will be reviewed by one of the Textual maintainers.
In that review process,
-- We will take a look at all of the changes you are making;
-- We might ask for clarifications (why did you do X or Y?);
-- We might ask for more tests/more documentation; and
-- We might ask for some code changes.
+- We will take a look at all of the changes you are making
+- We might ask for clarifications (why did you do X or Y?)
+- We might ask for more tests/more documentation
+- We might ask for some code changes
The sole purpose of those interactions is to make sure that, in the long run, everyone has the best experience possible with Textual and with the feature you are implementing/fixing.
Don't be discouraged if a reviewer asks for code changes.
If you go through our history of pull requests, you will see that every single one of the maintainers has had to make changes following a review.
+## Snapshot testing
+Snapshot tests ensure that visual things (like widgets) look like they are supposed to.
+PR [#1969](https://github.com/Textualize/textual/pull/1969) is a good example of what adding snapshot tests looks like: it amounts to a change in the file `tests/snapshot_tests/test_snapshots.py` that should run an app that you write and compare it against a historic snapshot of what that app should look like.
-## 🛑 Important
-
-- Make sure to read the issue instructions carefully. If you are a newbie you should look out for some good first issues because they should be clear enough and sometimes even provide some hints. If something isn't clear, ask for clarification!
-
-- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from.
-
-- Write tests for your code.
-
-- If you are fixing a bug, make sure to add regression tests that link to the original issue.
-
-- If you are implementing a visual element, make sure to add snapshot tests. See below for more details.
-
-
-### Snapshot Testing
-Snapshot tests ensure that things like widgets look like they are supposed to.
-PR [#1969](https://github.com/Textualize/textual/pull/1969) is a good example of what adding snapshot tests means: it amounts to a change in the file ```tests/snapshot_tests/test_snapshots.py```, that should run an app that you write and compare it against a historic snapshot of what that app should look like.
-
-When you create a new snapshot test, run it with ```pytest -vv tests/snapshot_tests/test_snapshots.py.```
-Because you just created this snapshot test, there is no history to compare against and the test will fail automatically.
+When you create a new snapshot test, run it with `pytest -vv tests/snapshot_tests/test_snapshots.py`.
+Because you just created this snapshot test, there is no history to compare against and the test will fail.
After running the snapshot tests, you should see a link that opens an interface in your browser.
-This interface should show all failing snapshot tests and a side-by-side diff between what the app looked like when it ran VS the historic snapshot.
+This interface should show all failing snapshot tests and a side-by-side diff between what the app looked like when the test ran versus the historic snapshot.
Make sure your snapshot app looks like it is supposed to and that you didn't break any other snapshot tests.
-If that's the case, you can run ```make test-snapshot-update``` to update the snapshot history with your new snapshot.
-This will write to the file ```tests/snapshot_tests/__snapshots__/test_snapshots.ambr```, that you should NOT modify by hand
+If everything looks fine, you can run `make test-snapshot-update` to update the snapshot history with your new snapshot.
+This will write to the file `tests/snapshot_tests/__snapshots__/test_snapshots.ambr`, which you should NOT modify by hand.
+
+([Read this](#makefile-commands) if the command `make` doesn't work for you.)
+
+## Join the community
+Seems a little overwhelming?
+Join our community on [Discord](https://discord.gg/uNRPEGCV) to get help!
-### 📈Join the community
+## Makefile commands
-- 😕 Seems a little overwhelming? Join our community on [Discord](https://discord.gg/uNRPEGCV) to get help.
+Textual has a `Makefile` file that contains the most common commands used when developing Textual.
+([Read about Make and makefiles on Wikipedia.](https://en.wikipedia.org/wiki/Make_(software)))
+If you don't have Make and you're on Windows, you may want to [install Make](https://stackoverflow.com/q/32127524/2828287).
diff --git a/docs/api/command.md b/docs/api/command.md
new file mode 100644
index 0000000000..865a605910
--- /dev/null
+++ b/docs/api/command.md
@@ -0,0 +1 @@
+::: textual.command
diff --git a/docs/api/command_palette.md b/docs/api/command_palette.md
deleted file mode 100644
index c7aea72eb1..0000000000
--- a/docs/api/command_palette.md
+++ /dev/null
@@ -1,135 +0,0 @@
-!!! tip "Added in version 0.37.0"
-
-## Introduction
-
-The command palette provides a system-wide facility for searching for and
-executing commands. These commands are added by creating command source
-classes and declaring them on your [application](../../guide/app/) or your
-[screens](../../guide/screens/).
-
-Note that `CommandPalette` itself isn't designed to be used directly in your
-applications; it is instead something that is enabled by default and is made
-available by the Textual [`App`][textual.app.App] class. If you wish to
-disable the availability of the command palette you can set the
-[`use_command_palette`][textual.app.App.use_command_palette] switch to
-`False`.
-
-## Creating a command source
-
-To add your own command source to the Textual command palette you start by
-creating a class that inherits from
-[`CommandSource`][textual.command_palette.CommandSource]. Your new command
-source class should implement the
-[`search`][textual.command_palette.CommandSource.search] method. This
-should be an `async` method which `yield`s instances of
-[`CommandSourceHit`][textual.command_palette.CommandSourceHit].
-
-For example, suppose we wanted to create a command source that would look
-through the globals in a running application and use
-[`notify`][textual.app.App.notify] to show the docstring (admittedly not the
-most useful command source, but illustrative of a source of text to match
-and code to run).
-
-The command source might look something like this:
-
-```python
-from functools import partial
-
-# ...
-
-class PythonGlobalSource(CommandSource):
- """A command palette source for globals in an app."""
-
- async def search(self, query: str) -> CommandMatches:
- # Create a fuzzy matching object for the query.
- matcher = self.matcher(query)
- # Looping throught the available globals...
- for name, value in globals().items():
- # Get a match score for the name.
- match = matcher.match(name)
- # If the match is above 0...
- if match:
- # ...pass the command up to the palette.
- yield CommandSourceHit(
- # The match score.
- match,
- # A highlighted version of the matched item,
- # showing how and where it matched.
- matcher.highlight(name),
- # The code to run. Here we'll call the Textual
- # notification system and get it to show the
- # docstring for the chosen item, if there is
- # one.
- partial(
- self.app.notify,
- value.__doc__ or "[i]Undocumented[/i]",
- title=name
- ),
- # The plain text that was selected.
- name
- )
-```
-
-!!! important
-
- The command palette populates itself asynchronously, pulling matches from
- all of the active sources. Your command source `search` method must be
- `async`, and must not block in any way; doing so will affect the
- performance of the user's experience while using the command palette.
-
-The key point here is that the `search` method should look for matches,
-given the user input, and yield up a
-[`CommandSourceHit`][textual.command_palette.CommandSourceHit], which will
-contain the match score (which should be between 0 and 1), a Rich renderable
-(such as a [rich Text object][rich.text.Text]) to illustrate how the command
-was matched (this appears in the drop-down list of the command palette), a
-reference to a function to run when the user selects that command, and the
-plain text version of the command.
-
-## Unhandled exceptions in a command source
-
-When writing your command source `search` method you should attempt to
-handle all possible errors. In the event that there is an unhandled
-exception Textual will carry on working and carry on taking results from any
-other registered command sources.
-
-!!! important
-
- This is different from how Textual normally works. Under normal
- circumstances Textual would not "hide" your errors.
-
-Textual doesn't just throw the exception away though. If an exception isn't
-handled by your code it will be logged to [the Textual devtools
-console](../../guide/devtools#console).
-
-## Using a command source
-
-Once a command source has been created it can be used either on an `App` or
-a `Screen`; this is done with the [`COMMAND_SOURCES` class variable][textual.app.App.COMMAND_SOURCES]. One or more command sources can
-be given. For example:
-
-```python
-class MyApp(App[None]):
-
- COMMAND_SOURCES = {MyCommandSource, MyOtherCommandSource}
-```
-
-When the command palette is called by the user, those sources will be used
-to populate the list of search hits.
-
-!!! tip
-
- If you wish to use your own commands sources on your appliaction, and
- you wish to keep using the default Textual command sources, be sure to
- include the ones provided by [`App`][textual.app.App.COMMAND_SOURCES].
- For example:
-
- ```python
- class MyApp(App[None]):
-
- COMMAND_SOURCES = App.COMMAND_SOURCES | {MyCommandSource, MyOtherCommandSource}
- ```
-
-## API documentation
-
-::: textual.command_palette
diff --git a/docs/api/fuzzy_matcher.md b/docs/api/fuzzy_matcher.md
index 015e71351e..0269ad2db0 100644
--- a/docs/api/fuzzy_matcher.md
+++ b/docs/api/fuzzy_matcher.md
@@ -1 +1 @@
-::: textual._fuzzy
+::: textual.fuzzy
diff --git a/docs/api/logger.md b/docs/api/logger.md
index bd76afceca..096ca3011c 100644
--- a/docs/api/logger.md
+++ b/docs/api/logger.md
@@ -1 +1,5 @@
+# Logger
+
+A [logger class](/guide/devtools/#logging-handler) that logs to the Textual [console](/guide/devtools#console).
+
::: textual.Logger
diff --git a/docs/api/system_commands_source.md b/docs/api/system_commands_source.md
index 00fe759f57..4778761810 100644
--- a/docs/api/system_commands_source.md
+++ b/docs/api/system_commands_source.md
@@ -1 +1 @@
-::: textual._system_commands_source
+::: textual._system_commands
diff --git a/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png b/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png
new file mode 100644
index 0000000000..c10f78dc84
Binary files /dev/null and b/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png differ
diff --git a/docs/blog/images/text-area-learnings/maintain_offset.gif b/docs/blog/images/text-area-learnings/maintain_offset.gif
new file mode 100644
index 0000000000..d39bca5e0d
Binary files /dev/null and b/docs/blog/images/text-area-learnings/maintain_offset.gif differ
diff --git a/docs/blog/images/text-area-learnings/text-area-api-insert.gif b/docs/blog/images/text-area-learnings/text-area-api-insert.gif
new file mode 100644
index 0000000000..529eb01e3d
Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-api-insert.gif differ
diff --git a/docs/blog/images/text-area-learnings/text-area-pyinstrument.png b/docs/blog/images/text-area-learnings/text-area-pyinstrument.png
new file mode 100644
index 0000000000..2a8cc3609c
Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-pyinstrument.png differ
diff --git a/docs/blog/images/text-area-learnings/text-area-syntax-error.gif b/docs/blog/images/text-area-learnings/text-area-syntax-error.gif
new file mode 100644
index 0000000000..0a74cb649e
Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-syntax-error.gif differ
diff --git a/docs/blog/images/text-area-learnings/text-area-theme-cycle.gif b/docs/blog/images/text-area-learnings/text-area-theme-cycle.gif
new file mode 100644
index 0000000000..c73e9dd9eb
Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-theme-cycle.gif differ
diff --git a/docs/blog/images/text-area-learnings/text-area-welcome.gif b/docs/blog/images/text-area-learnings/text-area-welcome.gif
new file mode 100644
index 0000000000..baaf821edc
Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-welcome.gif differ
diff --git a/docs/blog/posts/release0-38-0.md b/docs/blog/posts/release0-38-0.md
new file mode 100644
index 0000000000..f08756b13e
--- /dev/null
+++ b/docs/blog/posts/release0-38-0.md
@@ -0,0 +1,107 @@
+---
+draft: false
+date: 2023-09-21
+categories:
+ - Release
+title: "Textual 0.38.0 adds a syntax aware TextArea"
+authors:
+ - willmcgugan
+---
+
+# Textual 0.38.0 adds a syntax aware TextArea
+
+This is the second big feature release this month after last week's [command palette](./release0.37.0.md).
+
+
+
+The [TextArea](../../widgets/text_area.md) has finally landed.
+I know a lot of folk have been waiting for this one.
+Textual's TextArea is a fully-featured widget for editing code, with syntax highlighting and line numbers.
+It is highly configurable, and looks great.
+
+Darren Burns (the author of this widget) has penned a terrific write-up on the TextArea.
+See [Things I learned while building Textual's TextArea](./text-area-learnings.md) for some of the challenges he faced.
+
+
+## Scoped CSS
+
+Another notable feature added in 0.38.0 is *scoped* CSS.
+A common gotcha in building Textual widgets is that you could write CSS that impacted styles outside of that widget.
+
+Consider the following widget:
+
+```python
+class MyWidget(Widget):
+ DEFAULT_CSS = """
+ MyWidget {
+ height: auto;
+ border: magenta;
+ }
+ Label {
+ border: solid green;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ yield Label("foo")
+ yield Label("bar")
+```
+
+The author has intended to style the labels in that widget by adding a green border.
+This does work for the widget in question, but (prior to 0.38.0) the `Label` rule would style *all* Labels (including any outside of the widget) — which was probably not intended.
+
+With version 0.38.0, the CSS is scoped so that only the widget's labels will be styled.
+This is almost always what you want, which is why it is enabled by default.
+If you do want to style something outside of the widget you can set `SCOPED_CSS=False` (as a classvar).
+
+
+## Light and Dark pseudo selectors
+
+We've also made a slight quality of life improvement to the CSS, by adding `:light` and `:dark` pseudo selectors.
+This allows you to change styles depending on whether you have dark mode enabled or not.
+
+This was possible before, just a little verbose.
+Here's how you would do it in 0.37.0:
+
+```css
+App.-dark-mode MyWidget Label {
+ ...
+}
+```
+
+In 0.38.0 it's a little more concise and readable:
+
+```css
+MyWidget:dark Label {
+ ...
+}
+```
+
+## Testing guide
+
+Not strictly part of the release, but we've added a [guide on testing](/guide/testing) Textual apps.
+
+As you may know, we are on a mission to make TUIs a serious proposition for critical apps, which makes testing essential.
+We've extracted and documented our internal testing tools, including our snapshot tests pytest plugin [pytest-textual-snapshot](https://pypi.org/project/pytest-textual-snapshot/).
+
+This gives devs powerful tools to ensure the quality of their apps.
+Let us know your thoughts on that!
+
+## Release notes
+
+See the [release](https://github.com/Textualize/textual/releases/tag/v0.38.0) page for the full details on this release.
+
+
+## What's next?
+
+There's lots of features planned over the next few months.
+One feature I am particularly excited by is a widget to generate plots by wrapping the awesome [Plotext](https://pypi.org/project/plotext/) library.
+Check out some early work on this feature:
+
+
+
+
+
+## Join us
+
+Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to discuss Textual with the Textualize devs, or the community.
diff --git a/docs/blog/posts/release0.37.0.md b/docs/blog/posts/release0.37.0.md
new file mode 100644
index 0000000000..fd6a55c16c
--- /dev/null
+++ b/docs/blog/posts/release0.37.0.md
@@ -0,0 +1,85 @@
+---
+draft: false
+date: 2023-09-15
+categories:
+ - Release
+title: "Textual 0.37.0 adds a command palette"
+authors:
+ - willmcgugan
+---
+
+
+# Textual 0.37.0 adds a command palette
+
+Textual version 0.37.0 has landed!
+The highlight of this release is the new command palette.
+
+
+
+A command palette gives users quick access to features in your app.
+If you hit ctrl+backslash in a Textual app, it will bring up the command palette where you can start typing commands.
+The commands are matched with a *fuzzy* search, so you only need to type two or three characters to get to any command.
+
+Here's a video of it in action:
+
+
+
+
+
+Adding your own commands to the command palette is a piece of cake.
+Here's the (command) Provider class used in the example above:
+
+```python
+class ColorCommands(Provider):
+ """A command provider to select colors."""
+
+ async def search(self, query: str) -> Hits:
+ """Called for each key."""
+ matcher = self.matcher(query)
+ for color in COLOR_NAME_TO_RGB.keys():
+ score = matcher.match(color)
+ if score > 0:
+ yield Hit(
+ score,
+ matcher.highlight(color),
+ partial(self.app.post_message, SwitchColor(color)),
+ )
+```
+
+And here is how you add a provider to your app:
+
+```python
+class ColorApp(App):
+ """Experiment with the command palette."""
+
+ COMMANDS = App.COMMANDS | {ColorCommands}
+```
+
+We're excited about this feature because it is a step towards bringing a common user interface to Textual apps.
+
+!!! quote
+
+ It's a Textual app. I know this.
+
+ — You, maybe.
+
+The goal is to be able to build apps that may look quite different, but take no time to learn, because once you learn how to use one Textual app, you can use them all.
+
+See the Guide for details on how to work with the [command palette](../../guide/command_palette.md).
+
+## What else?
+
+Also in 0.37.0 we have a new [Collapsible](/widget_gallery/#collapsible) widget, which is a great way of adding content while avoiding a cluttered screen.
+
+And of course, bug fixes and other updates. See the [release](https://github.com/Textualize/textual/releases/tag/v0.37.0) page for the full details.
+
+## What's next?
+
+Coming very soon, is a new TextEditor widget.
+This is a super powerful widget to enter arbitrary text, with beautiful syntax highlighting for a number of languages.
+We're expecting that to land next week.
+Watch this space, or join the [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to be the first to try it out.
+
+## Join us
+
+Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to discuss Textual with the Textualize devs, or the community.
diff --git a/docs/blog/posts/text-area-learnings.md b/docs/blog/posts/text-area-learnings.md
new file mode 100644
index 0000000000..552ee7997e
--- /dev/null
+++ b/docs/blog/posts/text-area-learnings.md
@@ -0,0 +1,211 @@
+---
+draft: false
+date: 2023-09-18
+categories:
+ - DevLog
+authors:
+ - darrenburns
+title: "Things I learned while building Textual's TextArea"
+---
+
+# Things I learned building a text editor for the terminal
+
+`TextArea` is the latest widget to be added to Textual's [growing collection](https://textual.textualize.io/widget_gallery/).
+It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.
+
+![text-area-welcome.gif](../images/text-area-learnings/text-area-welcome.gif)
+
+Adding a `TextArea` to your Textual app is as simple as adding this to your `compose` method:
+
+```python
+yield TextArea()
+```
+
+Enabling syntax highlighting for a language is as simple as:
+
+```python
+yield TextArea(language="python")
+```
+
+Working on the `TextArea` widget for Textual taught me a lot about Python and my general
+approach to software engineering. It gave me an appreciation for the subtle functionality behind
+the editors we use on a daily basis — features we may not even notice, despite
+some engineer spending hours perfecting it to provide a small boost to our development experience.
+
+This post is a tour of some of these learnings.
+
+
+
+## Vertical cursor movement is more than just `cursor_row++`
+
+When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line.
+Editors should maintain the visual column offset where possible,
+meaning they must account for double-width emoji (sigh 😔) and East-Asian characters.
+
+![maintain_offset.gif](../images/text-area-learnings/maintain_offset.gif){ loading=lazy }
+
+Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it
+arrives at line 3.
+This is because the 6th character of line 3 _visually_ aligns with the 11th character of line 1.
+
+
+## Edits from other sources may move my cursor
+
+There are two ways to interact with the `TextArea`:
+
+1. You can type into it.
+2. You can make API calls to edit the content in it.
+
+In the example below, `Hello, world!\n` is repeatedly inserted at the start of the document via the
+API.
+Notice that this updates the location of my cursor, ensuring that I don't lose my place.
+
+![text-area-api-insert.gif](../images/text-area-learnings/text-area-api-insert.gif){ loading=lazy }
+
+This subtle feature should aid those implementing collaborative and multi-cursor editing.
+
+This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result.
+
+Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way!
+
+
+
+Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks [Dave](https://fosstodon.org/@davep)!) is what's needed to finally crack a tough problem.
+
+Many thanks to [David Brochart](https://mastodon.top/@davidbrochart) for sending me down this rabbit hole!
+
+## Spending a few minutes running a profiler can be really beneficial
+
+While building the `TextArea` widget I avoided heavy optimisation work that may have affected
+readability or maintainability.
+
+However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were
+affecting the performance of my code.
+
+I spent around 30 minutes profiling `TextArea`
+using [pyinstrument](https://pyinstrument.readthedocs.io/en/latest/home.html), and the result was a
+**~97%** reduction in the time taken to handle a key press.
+What an amazing return on investment for such a minimal time commitment!
+
+
+
+
+pyinstrument unveiled two issues that were massively impacting performance.
+
+### 1. Reparsing highlighting queries on each key press
+
+I was constructing a tree-sitter `Query` object on each key press, incorrectly assuming it was a
+low-overhead call.
+This query was completely static, so I moved it into the constructor ensuring the object was created
+only once.
+This reduced key processing time by around 94% - a substantial and very much noticeable improvement.
+
+This seems obvious in hindsight, but the code in question was written earlier in the project and had
+been relegated in my mind to "code that works correctly and will receive less attention from here on
+out".
+pyinstrument quickly brought this code back to my attention and highlighted it as a glaring
+performance bug.
+
+### 2. NamedTuples are slower than I expected
+
+In Python, `NamedTuple`s are slow to create relative to `tuple`s, and this cost was adding up inside
+an extremely hot loop which was instantiating a large number of them.
+pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside `NamedTuple.__new__`.
+
+Here's a quick benchmark which constructs 10,000 `NamedTuple`s:
+
+```toml
+❯ hyperfine -w 2 'python sandbox/darren/make_namedtuples.py'
+Benchmark 1: python sandbox/darren/make_namedtuples.py
+ Time (mean ± σ): 15.9 ms ± 0.5 ms [User: 12.8 ms, System: 2.5 ms]
+ Range (min … max): 15.2 ms … 18.4 ms 165 runs
+```
+
+Here's the same benchmark using `tuple` instead:
+
+```toml
+❯ hyperfine -w 2 'python sandbox/darren/make_tuples.py'
+Benchmark 1: python sandbox/darren/make_tuples.py
+ Time (mean ± σ): 9.3 ms ± 0.5 ms [User: 6.8 ms, System: 2.0 ms]
+ Range (min … max): 8.7 ms … 12.3 ms 256 runs
+```
+
+Switching to `tuple` resulted in another noticeable increase in responsiveness.
+Key-press handling time dropped by almost 50%!
+Unfortunately, this change _does_ impact readability.
+However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off.
+
+
+## Syntax highlighting is very different from what I expected
+
+In order to support syntax highlighting, we make use of
+the [tree-sitter](https://tree-sitter.github.io/tree-sitter/) library, which maintains a syntax tree
+representing the structure of our document.
+
+To perform highlighting, we follow these steps:
+
+1. The user edits the document.
+2. We inform tree-sitter of the location of this edit.
+3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree.
+4. We run a query against the tree to retrieve ranges of text we wish to highlight.
+5. These ranges are mapped to styles (defined by the chosen "theme").
+6. These styles to the appropriate text ranges when rendering the widget.
+
+
+
+Another benefit that I didn't consider before working on this project is that tree-sitter
+parsers can also be used to highlight syntax errors in a document.
+This can be useful in some situations - for example, highlighting mismatched HTML closing tags:
+
+
+
+Before building this widget, I was oblivious as to how we might approach syntax highlighting.
+Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have
+been feasible.
+
+## Edits are replacements
+
+All single-cursor edits can be distilled into a single behaviour: `replace_range`.
+This replaces a range of characters with some text.
+We can use this one method to easily implement deletion, insertion, and replacement of text.
+
+- Inserting text is replacing a zero-width range with the text to insert.
+- Pressing backspace (delete left) is just replacing the character behind the cursor with an empty
+ string.
+- Selecting text and pressing delete is just replacing the selected text with an empty string.
+- Selecting text and pasting is replacing the selected text with some other text.
+
+This greatly simplified my initial approach, which involved unique implementations for inserting and
+deleting.
+
+
+## The line between "text area" and "VSCode in the terminal"
+
+A project like this has no clear finish line.
+There are always new features, optimisations, and refactors waiting to be made.
+
+So where do we draw the line?
+
+We want to provide a widget which can act as both a basic multiline text area that
+anyone can drop into their app, yet powerful and extensible enough to act as the foundation
+for a Textual-powered text editor.
+
+Yet, the more features we add, the more opinionated the widget becomes, and the less that users
+will feel like they can build it into their _own_ thing.
+Finding the sweet spot between feature-rich and flexible is no easy task.
+
+I don't think the answer is clear, and I don't believe it's possible to please everyone.
+
+Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using `TextArea` in the future!
diff --git a/docs/examples/guide/command_palette/command01.py b/docs/examples/guide/command_palette/command01.py
new file mode 100644
index 0000000000..b4026f3ec6
--- /dev/null
+++ b/docs/examples/guide/command_palette/command01.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+from functools import partial
+from pathlib import Path
+
+from rich.syntax import Syntax
+
+from textual.app import App, ComposeResult
+from textual.command import Hit, Hits, Provider
+from textual.containers import VerticalScroll
+from textual.widgets import Static
+
+
+class PythonFileCommands(Provider):
+ """A command provider to open a Python file in the current working directory."""
+
+ def read_files(self) -> list[Path]:
+ """Get a list of Python files in the current working directory."""
+ return list(Path("./").glob("*.py"))
+
+ async def startup(self) -> None: # (1)!
+ """Called once when the command palette is opened, prior to searching."""
+ worker = self.app.run_worker(self.read_files, thread=True)
+ self.python_paths = await worker.wait()
+
+ async def search(self, query: str) -> Hits: # (2)!
+ """Search for Python files."""
+ matcher = self.matcher(query) # (3)!
+
+ app = self.app
+ assert isinstance(app, ViewerApp)
+
+ for path in self.python_paths:
+ command = f"open {str(path)}"
+ score = matcher.match(command) # (4)!
+ if score > 0:
+ yield Hit(
+ score,
+ matcher.highlight(command), # (5)!
+ partial(app.open_file, path),
+ help="Open this file in the viewer",
+ )
+
+
+class ViewerApp(App):
+ """Demonstrate a command source."""
+
+ COMMANDS = App.COMMANDS | {PythonFileCommands} # (6)!
+
+ def compose(self) -> ComposeResult:
+ with VerticalScroll():
+ yield Static(id="code", expand=True)
+
+ def open_file(self, path: Path) -> None:
+ """Open and display a file with syntax highlighting."""
+ syntax = Syntax.from_path(
+ str(path),
+ line_numbers=True,
+ word_wrap=False,
+ indent_guides=True,
+ theme="github-dark",
+ )
+ self.query_one("#code", Static).update(syntax)
+
+
+if __name__ == "__main__":
+ app = ViewerApp()
+ app.run()
diff --git a/docs/examples/guide/testing/rgb.py b/docs/examples/guide/testing/rgb.py
new file mode 100644
index 0000000000..d8b49cd1c3
--- /dev/null
+++ b/docs/examples/guide/testing/rgb.py
@@ -0,0 +1,42 @@
+from textual import on
+from textual.app import App, ComposeResult
+from textual.containers import Horizontal
+from textual.widgets import Button, Footer
+
+
+class RGBApp(App):
+ CSS = """
+ Screen {
+ align: center middle;
+ }
+ Horizontal {
+ width: auto;
+ height: auto;
+ }
+ """
+
+ BINDINGS = [
+ ("r", "switch_color('red')", "Go Red"),
+ ("g", "switch_color('green')", "Go Green"),
+ ("b", "switch_color('blue')", "Go Blue"),
+ ]
+
+ def compose(self) -> ComposeResult:
+ with Horizontal():
+ yield Button("Red", id="red")
+ yield Button("Green", id="green")
+ yield Button("Blue", id="blue")
+ yield Footer()
+
+ @on(Button.Pressed)
+ def pressed_button(self, event: Button.Pressed) -> None:
+ assert event.button.id is not None
+ self.action_switch_color(event.button.id)
+
+ def action_switch_color(self, color: str) -> None:
+ self.screen.styles.background = color
+
+
+if __name__ == "__main__":
+ app = RGBApp()
+ app.run()
diff --git a/docs/examples/guide/testing/test_rgb.py b/docs/examples/guide/testing/test_rgb.py
new file mode 100644
index 0000000000..030f62b505
--- /dev/null
+++ b/docs/examples/guide/testing/test_rgb.py
@@ -0,0 +1,42 @@
+from rgb import RGBApp
+
+from textual.color import Color
+
+
+async def test_keys(): # (1)!
+ """Test pressing keys has the desired result."""
+ app = RGBApp()
+ async with app.run_test() as pilot: # (2)!
+ # Test pressing the R key
+ await pilot.press("r") # (3)!
+ assert app.screen.styles.background == Color.parse("red") # (4)!
+
+ # Test pressing the G key
+ await pilot.press("g")
+ assert app.screen.styles.background == Color.parse("green")
+
+ # Test pressing the B key
+ await pilot.press("b")
+ assert app.screen.styles.background == Color.parse("blue")
+
+ # Test pressing the X key
+ await pilot.press("x")
+ # No binding (so no change to the color)
+ assert app.screen.styles.background == Color.parse("blue")
+
+
+async def test_buttons():
+ """Test pressing keys has the desired result."""
+ app = RGBApp()
+ async with app.run_test() as pilot:
+ # Test clicking the "red" button
+ await pilot.click("#red") # (5)!
+ assert app.screen.styles.background == Color.parse("red")
+
+ # Test clicking the "green" button
+ await pilot.click("#green")
+ assert app.screen.styles.background == Color.parse("green")
+
+ # Test clicking the "blue" button
+ await pilot.click("#blue")
+ assert app.screen.styles.background == Color.parse("blue")
diff --git a/docs/examples/widgets/collapsible.py b/docs/examples/widgets/collapsible.py
new file mode 100644
index 0000000000..d34ebca403
--- /dev/null
+++ b/docs/examples/widgets/collapsible.py
@@ -0,0 +1,46 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Collapsible, Footer, Label, Markdown
+
+LETO = """\
+# Duke Leto I Atreides
+
+Head of House Atreides."""
+
+JESSICA = """
+# Lady Jessica
+
+Bene Gesserit and concubine of Leto, and mother of Paul and Alia.
+"""
+
+PAUL = """
+# Paul Atreides
+
+Son of Leto and Jessica.
+"""
+
+
+class CollapsibleApp(App[None]):
+ """An example of collapsible container."""
+
+ BINDINGS = [
+ ("c", "collapse_or_expand(True)", "Collapse All"),
+ ("e", "collapse_or_expand(False)", "Expand All"),
+ ]
+
+ def compose(self) -> ComposeResult:
+ """Compose app with collapsible containers."""
+ yield Footer()
+ with Collapsible(collapsed=False, title="Leto"):
+ yield Label(LETO)
+ yield Collapsible(Markdown(JESSICA), collapsed=False, title="Jessica")
+ with Collapsible(collapsed=True, title="Paul"):
+ yield Markdown(PAUL)
+
+ def action_collapse_or_expand(self, collapse: bool) -> None:
+ for child in self.walk_children(Collapsible):
+ child.collapsed = collapse
+
+
+if __name__ == "__main__":
+ app = CollapsibleApp()
+ app.run()
diff --git a/docs/examples/widgets/collapsible_custom_symbol.py b/docs/examples/widgets/collapsible_custom_symbol.py
new file mode 100644
index 0000000000..d2fa266aa6
--- /dev/null
+++ b/docs/examples/widgets/collapsible_custom_symbol.py
@@ -0,0 +1,25 @@
+from textual.app import App, ComposeResult
+from textual.containers import Horizontal
+from textual.widgets import Collapsible, Label
+
+
+class CollapsibleApp(App[None]):
+ def compose(self) -> ComposeResult:
+ with Horizontal():
+ with Collapsible(
+ collapsed_symbol=">>>",
+ expanded_symbol="v",
+ ):
+ yield Label("Hello, world.")
+
+ with Collapsible(
+ collapsed_symbol=">>>",
+ expanded_symbol="v",
+ collapsed=False,
+ ):
+ yield Label("Hello, world.")
+
+
+if __name__ == "__main__":
+ app = CollapsibleApp()
+ app.run()
diff --git a/docs/examples/widgets/collapsible_nested.py b/docs/examples/widgets/collapsible_nested.py
new file mode 100644
index 0000000000..d4b65835f7
--- /dev/null
+++ b/docs/examples/widgets/collapsible_nested.py
@@ -0,0 +1,14 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Collapsible, Label
+
+
+class CollapsibleApp(App[None]):
+ def compose(self) -> ComposeResult:
+ with Collapsible(collapsed=False):
+ with Collapsible():
+ yield Label("Hello, world.")
+
+
+if __name__ == "__main__":
+ app = CollapsibleApp()
+ app.run()
diff --git a/docs/examples/widgets/horizontal_rules.py b/docs/examples/widgets/horizontal_rules.py
index 2327e474ec..643f129bbe 100644
--- a/docs/examples/widgets/horizontal_rules.py
+++ b/docs/examples/widgets/horizontal_rules.py
@@ -1,6 +1,6 @@
from textual.app import App, ComposeResult
-from textual.widgets import Rule, Label
from textual.containers import Vertical
+from textual.widgets import Label, Rule
class HorizontalRulesApp(App):
diff --git a/docs/examples/widgets/java_highlights.scm b/docs/examples/widgets/java_highlights.scm
new file mode 100644
index 0000000000..b6259be125
--- /dev/null
+++ b/docs/examples/widgets/java_highlights.scm
@@ -0,0 +1,140 @@
+; Methods
+
+(method_declaration
+ name: (identifier) @function.method)
+(method_invocation
+ name: (identifier) @function.method)
+(super) @function.builtin
+
+; Annotations
+
+(annotation
+ name: (identifier) @attribute)
+(marker_annotation
+ name: (identifier) @attribute)
+
+"@" @operator
+
+; Types
+
+(type_identifier) @type
+
+(interface_declaration
+ name: (identifier) @type)
+(class_declaration
+ name: (identifier) @type)
+(enum_declaration
+ name: (identifier) @type)
+
+((field_access
+ object: (identifier) @type)
+ (#match? @type "^[A-Z]"))
+((scoped_identifier
+ scope: (identifier) @type)
+ (#match? @type "^[A-Z]"))
+((method_invocation
+ object: (identifier) @type)
+ (#match? @type "^[A-Z]"))
+((method_reference
+ . (identifier) @type)
+ (#match? @type "^[A-Z]"))
+
+(constructor_declaration
+ name: (identifier) @type)
+
+[
+ (boolean_type)
+ (integral_type)
+ (floating_point_type)
+ (floating_point_type)
+ (void_type)
+] @type.builtin
+
+; Variables
+
+((identifier) @constant
+ (#match? @constant "^_*[A-Z][A-Z\\d_]+$"))
+
+(identifier) @variable
+
+(this) @variable.builtin
+
+; Literals
+
+[
+ (hex_integer_literal)
+ (decimal_integer_literal)
+ (octal_integer_literal)
+ (decimal_floating_point_literal)
+ (hex_floating_point_literal)
+] @number
+
+[
+ (character_literal)
+ (string_literal)
+] @string
+
+[
+ (true)
+ (false)
+ (null_literal)
+] @constant.builtin
+
+[
+ (line_comment)
+ (block_comment)
+] @comment
+
+; Keywords
+
+[
+ "abstract"
+ "assert"
+ "break"
+ "case"
+ "catch"
+ "class"
+ "continue"
+ "default"
+ "do"
+ "else"
+ "enum"
+ "exports"
+ "extends"
+ "final"
+ "finally"
+ "for"
+ "if"
+ "implements"
+ "import"
+ "instanceof"
+ "interface"
+ "module"
+ "native"
+ "new"
+ "non-sealed"
+ "open"
+ "opens"
+ "package"
+ "private"
+ "protected"
+ "provides"
+ "public"
+ "requires"
+ "return"
+ "sealed"
+ "static"
+ "strictfp"
+ "switch"
+ "synchronized"
+ "throw"
+ "throws"
+ "to"
+ "transient"
+ "transitive"
+ "try"
+ "uses"
+ "volatile"
+ "while"
+ "with"
+] @keyword
diff --git a/docs/examples/widgets/text_area_custom_language.py b/docs/examples/widgets/text_area_custom_language.py
new file mode 100644
index 0000000000..70ee7e16b9
--- /dev/null
+++ b/docs/examples/widgets/text_area_custom_language.py
@@ -0,0 +1,34 @@
+from pathlib import Path
+
+from tree_sitter_languages import get_language
+
+from textual.app import App, ComposeResult
+from textual.widgets import TextArea
+
+java_language = get_language("java")
+java_highlight_query = (Path(__file__).parent / "java_highlights.scm").read_text()
+java_code = """\
+class HelloWorld {
+ public static void main(String[] args) {
+ System.out.println("Hello, World!");
+ }
+}
+"""
+
+
+class TextAreaCustomLanguage(App):
+ def compose(self) -> ComposeResult:
+ text_area = TextArea(text=java_code)
+ text_area.cursor_blink = False
+
+ # Register the Java language and highlight query
+ text_area.register_language(java_language, java_highlight_query)
+
+ # Switch to Java
+ text_area.language = "java"
+ yield text_area
+
+
+app = TextAreaCustomLanguage()
+if __name__ == "__main__":
+ app.run()
diff --git a/docs/examples/widgets/text_area_custom_theme.py b/docs/examples/widgets/text_area_custom_theme.py
new file mode 100644
index 0000000000..c2c81a115f
--- /dev/null
+++ b/docs/examples/widgets/text_area_custom_theme.py
@@ -0,0 +1,42 @@
+from rich.style import Style
+
+from textual._text_area_theme import TextAreaTheme
+from textual.app import App, ComposeResult
+from textual.widgets import TextArea
+
+TEXT = """\
+# says hello
+def hello(name):
+ print("hello" + name)
+
+# says goodbye
+def goodbye(name):
+ print("goodbye" + name)
+"""
+
+MY_THEME = TextAreaTheme(
+ # This name will be used to refer to the theme...
+ name="my_cool_theme",
+ # Basic styles such as background, cursor, selection, gutter, etc...
+ cursor_style=Style(color="white", bgcolor="blue"),
+ cursor_line_style=Style(bgcolor="yellow"),
+ # `syntax_styles` maps tokens parsed from the document to Rich styles.
+ syntax_styles={
+ "string": Style(color="red"),
+ "comment": Style(color="magenta"),
+ },
+)
+
+
+class TextAreaCustomThemes(App):
+ def compose(self) -> ComposeResult:
+ text_area = TextArea(TEXT, language="python")
+ text_area.cursor_blink = False
+ text_area.register_theme(MY_THEME)
+ text_area.theme = "my_cool_theme"
+ yield text_area
+
+
+app = TextAreaCustomThemes()
+if __name__ == "__main__":
+ app.run()
diff --git a/docs/examples/widgets/text_area_example.py b/docs/examples/widgets/text_area_example.py
new file mode 100644
index 0000000000..2e0e31c060
--- /dev/null
+++ b/docs/examples/widgets/text_area_example.py
@@ -0,0 +1,20 @@
+from textual.app import App, ComposeResult
+from textual.widgets import TextArea
+
+TEXT = """\
+def hello(name):
+ print("hello" + name)
+
+def goodbye(name):
+ print("goodbye" + name)
+"""
+
+
+class TextAreaExample(App):
+ def compose(self) -> ComposeResult:
+ yield TextArea(TEXT, language="python")
+
+
+app = TextAreaExample()
+if __name__ == "__main__":
+ app.run()
diff --git a/docs/examples/widgets/text_area_extended.py b/docs/examples/widgets/text_area_extended.py
new file mode 100644
index 0000000000..8ac237db88
--- /dev/null
+++ b/docs/examples/widgets/text_area_extended.py
@@ -0,0 +1,23 @@
+from textual import events
+from textual.app import App, ComposeResult
+from textual.widgets import TextArea
+
+
+class ExtendedTextArea(TextArea):
+ """A subclass of TextArea with parenthesis-closing functionality."""
+
+ def _on_key(self, event: events.Key) -> None:
+ if event.character == "(":
+ self.insert("()")
+ self.move_cursor_relative(columns=-1)
+ event.prevent_default()
+
+
+class TextAreaKeyPressHook(App):
+ def compose(self) -> ComposeResult:
+ yield ExtendedTextArea(language="python")
+
+
+app = TextAreaKeyPressHook()
+if __name__ == "__main__":
+ app.run()
diff --git a/docs/examples/widgets/text_area_selection.py b/docs/examples/widgets/text_area_selection.py
new file mode 100644
index 0000000000..4165eb2d2d
--- /dev/null
+++ b/docs/examples/widgets/text_area_selection.py
@@ -0,0 +1,23 @@
+from textual.app import App, ComposeResult
+from textual.widgets import TextArea
+from textual.widgets.text_area import Selection
+
+TEXT = """\
+def hello(name):
+ print("hello" + name)
+
+def goodbye(name):
+ print("goodbye" + name)
+"""
+
+
+class TextAreaSelection(App):
+ def compose(self) -> ComposeResult:
+ text_area = TextArea(TEXT, language="python")
+ text_area.selection = Selection(start=(0, 0), end=(2, 0)) # (1)!
+ yield text_area
+
+
+app = TextAreaSelection()
+if __name__ == "__main__":
+ app.run()
diff --git a/docs/examples/widgets/vertical_rules.py b/docs/examples/widgets/vertical_rules.py
index 27592bef8f..5001045305 100644
--- a/docs/examples/widgets/vertical_rules.py
+++ b/docs/examples/widgets/vertical_rules.py
@@ -1,6 +1,6 @@
from textual.app import App, ComposeResult
-from textual.widgets import Rule, Label
from textual.containers import Horizontal
+from textual.widgets import Label, Rule
class VerticalRulesApp(App):
diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md
index 0d38a616cb..63cef3558b 100644
--- a/docs/guide/CSS.md
+++ b/docs/guide/CSS.md
@@ -325,6 +325,8 @@ Here are some other pseudo classes:
- `:enabled` Matches widgets which are in an enabled state.
- `:focus` Matches widgets which have input focus.
- `:focus-within` Matches widgets with a focused a child widget.
+- `:dark` Matches widgets in dark mode (where `App.dark == True`).
+- `:light` Matches widgets in dark mode (where `App.dark == False`).
## Combinators
diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md
new file mode 100644
index 0000000000..0dd15af0f1
--- /dev/null
+++ b/docs/guide/command_palette.md
@@ -0,0 +1,124 @@
+# Command Palette
+
+Textual apps have a built-in *command palette*, which gives users a quick way to access certain functionality within your app.
+
+In this chapter we will explain what a command palette is, how to use it, and how you can add your own commands.
+
+## Launching the command palette
+
+Press ++ctrl++ + `\` (ctrl and backslash) to invoke the command palette screen, which contains of a single input widget.
+Textual will suggest commands as you type in that input.
+Press ++up++ or ++down++ to select a command from the list, and ++enter++ to invoke it.
+
+Commands are looked up via a *fuzzy* search, which means Textual will show commands that match the keys you type in the same order, but not necessarily at the start of the command.
+For instance the "Toggle light/dark mode" command will be shown if you type "to" (for **to**ggle), but you could also type "dm" (to match **d**ark **m**ode).
+This scheme allows the user to quickly get to a particular command with a minimum of key-presses.
+
+
+=== "Command Palette"
+
+ ```{.textual path="docs/examples/guide/command_palette/command01.py" press="ctrl+backslash"}
+ ```
+
+=== "Command Palette after 't'"
+
+ ```{.textual path="docs/examples/guide/command_palette/command01.py" press="ctrl+backslash,t"}
+ ```
+
+=== "Command Palette after 'td'"
+
+ ```{.textual path="docs/examples/guide/command_palette/command01.py" press="ctrl+backslash,t,d"}
+ ```
+
+
+
+## Default commands
+
+Textual apps have the following commands enabled by default:
+
+- `"Toggle light/dark mode"`
+ This will toggle between light and dark mode, by setting `App.dark` to either `True` or `False`.
+- `"Quit the application"`
+ Quits the application. The equivalent of pressing ++ctrl+C++.
+- `"Play the bell"`
+ Plays the terminal bell, by calling [`App.bell`][textual.app.App.bell].
+
+
+## Command providers
+
+To add your own command(s) to the command palette, define a [`command.Provider`][textual.command.Provider] class then add it to the [`COMMANDS`][textual.app.App.COMMANDS] class var on your `App` class.
+
+Let's look at a simple example which adds the ability to open Python files via the command palette.
+
+The following example will display a blank screen initially, but if you bring up the command palette and start typing the name of a Python file, it will show the command to open it.
+
+!!! tip
+
+ If you are running that example from the repository, you may want to add some additional Python files to see how the examples works with multiple files.
+
+
+ ```python title="command01.py" hl_lines="14-42 45"
+ --8<-- "docs/examples/guide/command_palette/command01.py"
+ ```
+
+ 1. This method is called when the command palette is first opened.
+ 2. Called on each key-press.
+ 3. Get a [Matcher][textual.fuzzy.Matcher] instance to compare against hits.
+ 4. Use the matcher to get a score.
+ 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].
+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.
+
+### startup method
+
+The [`startup`][textual.command.Provider.startup] method is called when the command palette is opened.
+You can use this method as way of performing work that needs to be done prior to searching.
+In the example, we use this method to get the Python (.py) files in the current working directory.
+
+### search method
+
+The [`search`][textual.command.Provider.search] method is responsible for finding results (or *hits*) that match the user's input.
+This method should *yield* [`Hit`][textual.command.Hit] objects for any command that matches the `query` argument.
+
+Exactly how the matching is implemented is up to the author of the command provider, but we recommend using the builtin fuzzy matcher object, which you can get by calling [`matcher`][textual.command.Provider.matcher].
+This object has a [`match()`][textual.fuzzy.Matcher.match] method which compares the user's search term against the potential command and returns a *score*.
+A score of zero means *no hit*, and you can discard the potential command.
+A score of above zero indicates the confidence in the result, where 1 is an exact match, and anything lower indicates a less confident match.
+
+The [`Hit`][textual.command.Hit] contains information about the score (used in ordering) and how the hit should be displayed, and an optional help string.
+It also contains a callback, which will be run if the user selects that command.
+
+In the example above, the callback is a lambda which calls the `open_file` method in the example app.
+
+!!! note
+
+ Unlike most other places in Textual, errors in command provider will not *exit* the app.
+ 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
+
+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].
+
+## Screen commands
+
+You can also associate commands with a screen by adding a `COMMANDS` class var to your Screen class.
+
+Commands defined on a screen are only considered when that screen is active.
+You can use this to implement commands that are specific to a particular screen, that wouldn't be applicable everywhere in the app.
+
+## Disabling the command palette
+
+The command palette is enabled by default.
+If you would prefer not to have the command palette, you can set `ENABLE_COMMAND_PALETTE = False` on your app class.
+
+Here's an app class with no command palette:
+
+```python
+class NoPaletteApp(App):
+ ENABLE_COMMAND_PALETTE = False
+```
diff --git a/docs/guide/testing.md b/docs/guide/testing.md
new file mode 100644
index 0000000000..25baba6c40
--- /dev/null
+++ b/docs/guide/testing.md
@@ -0,0 +1,305 @@
+# Testing
+
+Code testing is an important part of software development.
+This chapter will cover how to write tests for your Textual apps.
+
+## What is testing?
+
+It is common to write tests alongside your app.
+A *test* is simply a function that confirms your app is working correctly.
+
+!!! tip "Learn more about testing"
+
+ We recommend [Python Testing with pytest](https://pythontest.com/pytest-book/) for a comprehensive guide to writing tests.
+
+## Do you need to write tests?
+
+The short answer is "no", you don't *need* to write tests.
+
+In practice however, it is almost always a good idea to write tests.
+Writing code that is completely bug free is virtually impossible, even for experienced developers.
+If you want to have confidence that your application will run as you intended it to, then you should write tests.
+Your test code will help you find bugs early, and alert you if you accidentally break something in the future.
+
+## Testing frameworks for Textual
+
+Textual doesn't require any particular test framework.
+You can use any test framework you are familiar with, but we will be using [pytest](https://docs.pytest.org/) in this chapter.
+
+
+## Testing apps
+
+You can often test Textual code in the same way as any other app, and use similar techniques.
+But when testing user interface interactions, you may need to use Textual's dedicated test features.
+
+Let's write a simple Textual app so we can demonstrate how to test it.
+The following app shows three buttons labelled "red", "green", and "blue".
+Clicking one of those buttons or pressing a corresponding ++r++, ++g++, and ++b++ key will change the background color.
+
+=== "rgb.py"
+
+ ```python
+ --8<-- "docs/examples/guide/testing/rgb.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/guide/testing/rgb.py"}
+ ```
+
+Although it is straightforward to test an app like this manually, it is not practical to click every button and hit every key in your app after changing a single line of code.
+Tests allow us to automate such testing so we can quickly simulate user interactions and check the result.
+
+To test our simple app we will use the [`run_test()`][textual.app.App.run_test] method on the `App` class.
+This replaces the usual call to [`run()`][textual.app.App.run] and will run the app in *headless* mode, which prevents Textual from updating the terminal but otherwise behaves as normal.
+
+The `run_test()` method is an *async context manager* which returns a [`Pilot`][textual.pilot.Pilot] object.
+You can use this object to interact with the app as if you were operating it with a keyboard and mouse.
+
+Let's look at the tests for the example above:
+
+```python title="test_rgb.py"
+--8<-- "docs/examples/guide/testing/test_rgb.py"
+```
+
+1. The `run_test()` method requires that it run in a coroutine, so tests must use the `async` keyword.
+2. This runs the app and returns a Pilot instance we can use to interact with it.
+3. Simulates pressing the ++r++ key.
+4. This checks that pressing the ++r++ key has resulted in the background color changing.
+5. Simulates clicking on the widget with an `id` of `red` (the button labelled "Red").
+
+There are two tests defined in `test_rgb.py`.
+The first to test keys and the second to test button clicks.
+Both tests first construct an instance of the app and then call `run_test()` to get a Pilot object.
+The `test_keys` function simulates key presses with [`Pilot.press`][textual.pilot.Pilot.press], and `test_buttons` simulates button clicks with [`Pilot.click`][textual.pilot.Pilot.click].
+
+After simulating a user interaction, Textual tests will typically check the state has been updated with an `assert` statement.
+The `pytest` module will record any failures of these assert statements as a test fail.
+
+If you run the tests with `pytest test_rgb.py` you should get 2 passes, which will confirm that the user will be able to click buttons or press the keys to change the background color.
+
+If you later update this app, and accidentally break this functionality, one or more of your tests will fail.
+Knowing which test has failed will help you quickly track down where your code was broken.
+
+## Simulating key presses
+
+We've seen how the [`press`][textual.pilot.Pilot] method simulates keys.
+You can also supply multiple keys to simulate the user typing in to the app.
+Here's an example of simulating the user typing the word "hello".
+
+```python
+await pilot.press("h", "e", "l", "l", "o")
+```
+
+Each string creates a single keypress.
+You can also use the name for non-printable keys (such as "enter") and the "ctrl+" modifier.
+These are the same identifiers as used for key events, which you can experiment with by running `textual keys`.
+
+## Simulating clicks
+
+You can simulate mouse clicks in a similar way with [`Pilot.click`][textual.pilot.Pilot.click].
+If you supply a CSS selector Textual will simulate clicking on the matching widget.
+
+!!! note
+
+ If there is another widget in front of the widget you want to click, you may end up clicking the topmost widget rather than the widget indicated in the selector.
+ This is generally what you want, because a real user would experience the same thing.
+
+### Clicking the screen
+
+If you don't supply a CSS selector, then the click will be relative to the screen.
+For example, the following simulates a click at (0, 0):
+
+```python
+await pilot.click()
+```
+
+### Click offsets
+
+If you supply an `offset` value, it will be added to the coordinates of the simulated click.
+For example the following line would simulate a click at the coordinates (10, 5).
+
+
+```python
+await pilot.click(offset=(10, 5))
+```
+
+If you combine this with a selector, then the offset will be relative to the widget.
+Here's how you would click the line *above* a button.
+
+```python
+await pilot.click(Button, offset(0, -1))
+```
+
+### Modifier keys
+
+You can simulate clicks in combination with modifier keys, by setting the `shift`, `meta`, or `control` parameters.
+Here's how you could simulate ctrl-clicking a widget with an ID of "slider":
+
+```python
+await pilot.click("#slider", control=True)
+```
+
+## Changing the screen size
+
+The default size of a simulated app is (80, 24).
+You may want to test what happens when the app has a different size.
+To do this, set the `size` parameter of [`run_test`][textual.app.App.run_test] to a different size.
+For example, here is how you would simulate a terminal resized to 100 columns and 50 lines:
+
+```python
+async with app.run_test(size=(100, 50)) as pilot:
+ ...
+```
+
+## Pausing the pilot
+
+Some actions in a Textual app won't change the state immediately.
+For instance, messages may take a moment to bubble from the widget that sent them.
+If you were to post a message and immediately `assert` you may find that it fails because the message hasn't yet been processed.
+
+You can generally solve this by calling [`pause()`][textual.pilot.Pilot.pause] which will wait for all pending messages to be processed.
+You can also supply a `delay` parameter, which will insert a delay prior to waiting for pending messages.
+
+
+## Textual's tests
+
+Textual itself has a large battery of tests.
+If you are interested in how we write tests, see the [tests/](https://github.com/Textualize/textual/tree/main/tests) directory in the Textual repository.
+
+## Snapshot testing
+
+Snapshot testing is the process of recording the output of a test, and comparing it against the output from previous runs.
+
+Textual uses snapshot testing internally to ensure that the builtin widgets look and function correctly in every release.
+We've made the pytest plugin we built available for public use.
+
+The [official Textual pytest plugin](https://github.com/Textualize/pytest-textual-snapshot) can help you catch otherwise difficult to detect visual changes in your app.
+
+It works by generating an SVG _screenshot_ (such as the images in these docs) from your app.
+If the screenshot changes in any test run, you will have the opportunity to visually compare the new output against previous runs.
+
+
+### Installing the plugin
+
+You can install `pytest-textual-snapshot` using your favorite package manager (`pip`, `poetry`, etc.).
+
+```
+pip install pytest-textual-snapshot
+```
+
+### Creating a snapshot test
+
+With the package installed, you now have access to the `snap_compare` pytest fixture.
+
+Let's look at an example of how we'd create a snapshot test for the [calculator app](https://github.com/Textualize/textual/blob/main/examples/calculator.py) below.
+
+```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,wait:400"}
+```
+
+First, we need to create a new test and specify the path to the Python file containing the app.
+This path should be relative to the location of the test.
+
+```python
+def test_calculator(snap_compare):
+ assert snap_compare("path/to/calculator.py")
+```
+
+Let's run the test as normal using `pytest`.
+
+```
+pytest
+```
+
+When this test runs for the first time, an SVG screenshot of the calculator app is generated, and the test will fail.
+Snapshot tests always fail on the first run, since there's no previous version to compare the snapshot to.
+
+![snapshot_report_console_output.png](../images/testing/snapshot_report_console_output.png)
+
+If you open the snapshot report in your browser, you'll see something like this:
+
+![snapshot_report_example.png](../images/testing/snapshot_report_example.png)
+
+!!! tip
+
+ You can usually open the link directly from the terminal, but some terminal emulators may
+ require you to hold ++ctrl++ or ++command++ while clicking for links to work.
+
+The report explains that there's "No history for this test".
+It's our job to validate that the initial snapshot looks correct before proceeding.
+Our calculator is rendering as we expect, so we'll save this snapshot:
+
+```
+pytest --snapshot-update
+```
+
+!!! warning
+
+ Only ever run pytest with `--snapshot-update` if you're happy with how the output looks
+ on the left hand side of the snapshot report. When using `--snapshot-update`, you're saying "I'm happy with all of the
+ screenshots in the snapshot test report, and they will now represent the ground truth which all future runs will be compared
+ against". As such, you should only run `pytest --snapshot-update` _after_ running `pytest` and confirming the output looks good.
+
+Now that our snapshot is saved, if we run `pytest` (with no arguments) again, the test will pass.
+This is because the screenshot taken during this test run matches the one we saved earlier.
+
+### Catching a bug
+
+The real power of snapshot testing comes from its ability to catch visual regressions which could otherwise easily be missed.
+
+Imagine a new developer joins your team, and tries to make a few changes to the calculator.
+While making this change they accidentally break some styling which removes the orange coloring from the buttons on the right of the app.
+When they run `pytest`, they're presented with a report which reveals the damage:
+
+![snapshot_report_diff_before.png](../images/testing/snapshot_report_diff_before.png)
+
+On the right, we can see our "historical" snapshot - this is the one we saved earlier.
+On the left is how our app is currently rendering - clearly not how we intended!
+
+We can click the "Show difference" toggle at the top right of the diff to overlay the two versions:
+
+![snapshot_report_diff_after.png](../images/testing/snapshot_report_diff_after.png)
+
+This reveals another problem, which could easily be missed in a quick visual inspection -
+our new developer has also deleted the number 4!
+
+!!! tip
+
+ Snapshot tests work well in CI on all supported operating systems, and the snapshot
+ report is just an HTML file which can be exported as a build artifact.
+
+
+### Pressing keys
+
+You can simulate pressing keys before the snapshot is captured using the `press` parameter.
+
+```python
+def test_calculator_pressing_numbers(snap_compare):
+ assert snap_compare("path/to/calculator.py", press=["1", "2", "3"])
+```
+
+### Changing the terminal size
+
+To capture the snapshot with a different terminal size, pass a tuple `(width, height)` as the `terminal_size` parameter.
+
+```python
+def test_calculator(snap_compare):
+ assert snap_compare("path/to/calculator.py", terminal_size=(50, 100))
+```
+
+### Running setup code
+
+You can also run arbitrary code before the snapshot is captured using the `run_before` parameter.
+
+In this example, we use `run_before` to hover the mouse cursor over the widget with ID `number-5`
+before taking the snapshot.
+
+```python
+def test_calculator_hover_number(snap_compare):
+ async def run_before(pilot) -> None:
+ await pilot.hover("#number-5")
+
+ assert snap_compare("path/to/calculator.py", run_before=run_before)
+```
+
+For more information, visit the [`pytest-textual-snapshot` repo on GitHub](https://github.com/Textualize/pytest-textual-snapshot).
diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md
index 26d382169a..f568f09b2a 100644
--- a/docs/guide/widgets.md
+++ b/docs/guide/widgets.md
@@ -103,6 +103,13 @@ Here's the Hello example again, this time the widget has embedded default CSS:
```{.textual path="docs/examples/guide/widgets/hello04.py"}
```
+#### Scoped CSS
+
+Default CSS is *scoped* by default.
+All this means is that CSS defined in `DEFAULT_CSS` will affect the widget and potentially its children only.
+This is to prevent you from inadvertently breaking an unrelated widget.
+
+You can disabled scoped CSS by setting the class var `SCOPED_CSS` to `False`.
#### Default specificity
diff --git a/docs/images/testing/snapshot_report_console_output.png b/docs/images/testing/snapshot_report_console_output.png
new file mode 100644
index 0000000000..50389b4102
Binary files /dev/null and b/docs/images/testing/snapshot_report_console_output.png differ
diff --git a/docs/images/testing/snapshot_report_diff_after.png b/docs/images/testing/snapshot_report_diff_after.png
new file mode 100644
index 0000000000..99334082dd
Binary files /dev/null and b/docs/images/testing/snapshot_report_diff_after.png differ
diff --git a/docs/images/testing/snapshot_report_diff_before.png b/docs/images/testing/snapshot_report_diff_before.png
new file mode 100644
index 0000000000..575cafd44b
Binary files /dev/null and b/docs/images/testing/snapshot_report_diff_before.png differ
diff --git a/docs/images/testing/snapshot_report_example.png b/docs/images/testing/snapshot_report_example.png
new file mode 100644
index 0000000000..5cb49828f2
Binary files /dev/null and b/docs/images/testing/snapshot_report_example.png differ
diff --git a/docs/index.md b/docs/index.md
index c8e48c5748..0c03a8cfd9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,11 +1,19 @@
-# Introduction
+---
+hide:
+ - toc
+ - navigation
+---
-Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation.
+!!! tip inline end
+
+ See the navigation links in the header or side-bar.
-!!! tip
+ Click :octicons-three-bars-16: (top left) on mobile.
- See the navigation links in the header or side-bars. Click the :octicons-three-bars-16: button (top left) on mobile.
+# Welcome
+
+Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation.
[Get started](./getting_started.md){ .md-button .md-button--primary } or go straight to the [Tutorial](./tutorial.md)
@@ -16,7 +24,7 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework docume
Textual is a *Rapid Application Development* framework for Python, built by [Textualize.io](https://www.textualize.io).
-Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (*coming soon*) a web browser.
+Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal *or* a web browser (with [textual-web](https://github.com/Textualize/textual-web))!
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 90e05d1e1d..c4b881bceb 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -19,9 +19,8 @@ High-level features we plan on implementing.
* [x] Monochrome mode
* [ ] High contrast theme
* [ ] Color-blind themes
-- [ ] Command interface
- * [ ] Command menu
- * [ ] Fuzzy search
+- [X] Command palette
+ * [X] Fuzzy search
- [ ] Configuration (.toml based extensible configuration format)
- [x] Console
- [ ] Devtools
diff --git a/docs/styles/grid/grid_gutter.md b/docs/styles/grid/grid_gutter.md
index 68ef9dcc53..39e8981c55 100644
--- a/docs/styles/grid/grid_gutter.md
+++ b/docs/styles/grid/grid_gutter.md
@@ -13,11 +13,11 @@ No spacing is added between the edges of the cells and the edges of the containe
## Syntax
--8<-- "docs/snippets/syntax_block_start.md"
-grid-gutter: <scalar> [<scalar>];
+grid-gutter: <integer> [<integer>];
--8<-- "docs/snippets/syntax_block_end.md"
-The `grid-gutter` style takes one or two [``](../../css_types/scalar.md) that set the length of the gutter along the vertical and horizontal axes.
-If only one [``](../../css_types/scalar.md) is supplied, it sets the vertical and horizontal gutters.
+The `grid-gutter` style takes one or two [``](../../css_types/integer.md) that set the length of the gutter along the vertical and horizontal axes.
+If only one [``](../../css_types/integer.md) is supplied, it sets the vertical and horizontal gutters.
If two are supplied, they set the vertical and horizontal gutters, respectively.
## Example
@@ -47,7 +47,7 @@ The example below employs a common trick to apply visually consistent spacing ar
```sass
/* Set vertical and horizontal gutters to be the same */
-grid-gutter: 5%;
+grid-gutter: 5;
/* Set vertical and horizontal gutters separately */
grid-gutter: 1 2;
diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md
index a4a5713462..ca82b5d4e3 100644
--- a/docs/widget_gallery.md
+++ b/docs/widget_gallery.md
@@ -34,6 +34,16 @@ A classic checkbox control.
```{.textual path="docs/examples/widgets/checkbox.py"}
```
+## Collapsible
+
+Content that may be toggled on and off by clicking a title.
+
+[Collapsible reference](./widgets/collapsible.md){ .md-button .md-button--primary }
+
+
+```{.textual path="docs/examples/widgets/collapsible.py"}
+```
+
## ContentSwitcher
@@ -271,7 +281,7 @@ Displays simple static content. Typically used as a base class.
## Switch
-A on / off control, inspired by toggle buttons.
+An on / off control, inspired by toggle buttons.
[Switch reference](./widgets/switch.md){ .md-button .md-button--primary }
@@ -297,6 +307,14 @@ A Combination of Tabs and ContentSwitcher to navigate static content.
```{.textual path="docs/examples/widgets/tabbed_content.py" press="j"}
```
+## TextArea
+
+A multi-line text area which supports syntax highlighting various languages.
+
+[TextArea reference](./widgets/text_area.md){ .md-button .md-button--primary }
+
+```{.textual path="docs/examples/widgets/text_area_example.py" columns="42" lines="8"}
+```
## Tree
diff --git a/docs/widgets/_template.md b/docs/widgets/_template.md
index c4e83c06aa..70c6a43385 100644
--- a/docs/widgets/_template.md
+++ b/docs/widgets/_template.md
@@ -30,8 +30,9 @@ Example app showing the widget:
```
-## Reactive attributes
+## Reactive Attributes
+## Messages
## Bindings
diff --git a/docs/widgets/button.md b/docs/widgets/button.md
index 290895d374..55a4120f9d 100644
--- a/docs/widgets/button.md
+++ b/docs/widgets/button.md
@@ -41,6 +41,14 @@ Clicking any of the non-disabled buttons in the example app below will result in
- [Button.Pressed][textual.widgets.Button.Pressed]
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
## Additional Notes
- The spacing between the text and the edges of a button are _not_ due to padding. The default styling for a `Button` has the `height` set to 3 lines and a `min-width` of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with `border: none;`.
diff --git a/docs/widgets/checkbox.md b/docs/widgets/checkbox.md
index a8d6520c2f..0b227ba311 100644
--- a/docs/widgets/checkbox.md
+++ b/docs/widgets/checkbox.md
@@ -34,6 +34,10 @@ The example below shows check boxes in various states.
| ------- | ------ | ------- | -------------------------- |
| `value` | `bool` | `False` | The value of the checkbox. |
+## Messages
+
+- [Checkbox.Changed][textual.widgets.Checkbox.Changed]
+
## Bindings
The checkbox widget defines the following bindings:
@@ -45,17 +49,13 @@ The checkbox widget defines the following bindings:
## Component Classes
-The checkbox widget provides the following component classes:
+The checkbox widget inherits the following component classes:
::: textual.widgets._toggle_button.ToggleButton.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false
-## Messages
-
-- [Checkbox.Changed][textual.widgets.Checkbox.Changed]
-
---
diff --git a/docs/widgets/collapsible.md b/docs/widgets/collapsible.md
new file mode 100644
index 0000000000..009f7f760b
--- /dev/null
+++ b/docs/widgets/collapsible.md
@@ -0,0 +1,149 @@
+# Collapsible
+
+!!! tip "Added in version 0.37"
+
+A container with a title that can be used to show (expand) or hide (collapse) content, either by clicking or focusing and pressing ++enter++.
+
+- [x] Focusable
+- [x] Container
+
+
+## Composing
+
+You can add content to a Collapsible widget either by passing in children to the constructor, or with a context manager (`with` statement).
+
+Here is an example of using the constructor to add content:
+
+```python
+def compose(self) -> ComposeResult:
+ yield Collapsible(Label("Hello, world."))
+```
+
+Here's how the to use it with the context manager:
+
+```python
+def compose(self) -> ComposeResult:
+ with Collapsible():
+ yield Label("Hello, world.")
+```
+
+The second form is generally preferred, but the end result is the same.
+
+## Title
+
+The default title "Toggle" can be customized by setting the `title` parameter of the constructor:
+
+```python
+def compose(self) -> ComposeResult:
+ with Collapsible(title="An interesting story."):
+ yield Label("Interesting but verbose story.")
+```
+
+## Initial State
+
+The initial state of the `Collapsible` widget can be customized via the `collapsed` parameter of the constructor:
+
+```python
+def compose(self) -> ComposeResult:
+ with Collapsible(title="Contents 1", collapsed=False):
+ yield Label("Hello, world.")
+
+ with Collapsible(title="Contents 2", collapsed=True): # Default.
+ yield Label("Hello, world.")
+```
+
+## Collapse/Expand Symbols
+
+The symbols used to show the collapsed / expanded state can be customized by setting the parameters `collapsed_symbol` and `expanded_symbol`:
+
+```python
+def compose(self) -> ComposeResult:
+ with Collapsible(collapsed_symbol=">>>", expanded_symbol="v"):
+ yield Label("Hello, world.")
+```
+
+## Examples
+
+
+The following example contains three `Collapsible`s in different states.
+
+=== "All expanded"
+
+ ```{.textual path="docs/examples/widgets/collapsible.py" press="e"}
+ ```
+
+=== "All collapsed"
+
+ ```{.textual path="docs/examples/widgets/collapsible.py" press="c"}
+ ```
+
+=== "Mixed"
+
+ ```{.textual path="docs/examples/widgets/collapsible.py"}
+ ```
+
+=== "collapsible.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/collapsible.py"
+ ```
+
+### Setting Initial State
+
+The example below shows nested `Collapsible` widgets and how to set their initial state.
+
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/collapsible_nested.py"}
+ ```
+
+=== "collapsible_nested.py"
+
+ ```python hl_lines="7"
+ --8<-- "docs/examples/widgets/collapsible_nested.py"
+ ```
+
+### Custom Symbols
+
+The following example shows `Collapsible` widgets with custom expand/collapse symbols.
+
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/collapsible_custom_symbol.py"}
+ ```
+
+=== "collapsible_custom_symbol.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/collapsible_custom_symbol.py"
+ ```
+
+## Reactive Attributes
+
+| Name | Type | Default | Description |
+| ----------- | ------ | ------- | ---------------------------------------------------- |
+| `collapsed` | `bool` | `True` | Controls the collapsed/expanded state of the widget. |
+
+## Messages
+
+This widget posts no messages.
+
+## Bindings
+
+The collapsible widget defines the following binding on its title:
+
+::: textual.widgets._collapsible.CollapsibleTitle.BINDINGS
+ options:
+ show_root_heading: false
+ show_root_toc_entry: false
+
+## Component Classes
+
+This widget has no component classes.
+
+
+::: textual.widgets.Collapsible
+ options:
+ heading_level: 2
diff --git a/docs/widgets/content_switcher.md b/docs/widgets/content_switcher.md
index dc8f06bf22..126213c94b 100644
--- a/docs/widgets/content_switcher.md
+++ b/docs/widgets/content_switcher.md
@@ -50,6 +50,18 @@ When the user presses the "Markdown" button the view is switched:
| --------- | --------------- | ------- | ----------------------------------------------------------------------- |
| `current` | `str` \| `None` | `None` | The ID of the currently-visible child. `None` means nothing is visible. |
+## Messages
+
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
---
diff --git a/docs/widgets/digits.md b/docs/widgets/digits.md
index 6dd33044ce..4fb919f762 100644
--- a/docs/widgets/digits.md
+++ b/docs/widgets/digits.md
@@ -44,15 +44,19 @@ Here's another example which uses `Digits` to display the current time:
--8<-- "docs/examples/widgets/clock.py"
```
-## Reactive attributes
+## Reactive Attributes
This widget has no reactive attributes.
+## Messages
+
+This widget posts no messages.
+
## Bindings
This widget has no bindings.
-## Component classes
+## Component Classes
This widget has no component classes.
diff --git a/docs/widgets/directory_tree.md b/docs/widgets/directory_tree.md
index 56f1a00375..992a9fc127 100644
--- a/docs/widgets/directory_tree.md
+++ b/docs/widgets/directory_tree.md
@@ -34,10 +34,6 @@ and directories:
--8<-- "docs/examples/widgets/directory_tree_filtered.py"
~~~
-## Messages
-
-- [DirectoryTree.FileSelected][textual.widgets.DirectoryTree.FileSelected]
-
## Reactive Attributes
| Name | Type | Default | Description |
@@ -46,6 +42,14 @@ and directories:
| `show_guides` | `bool` | `True` | Show guide lines between levels. |
| `guide_depth` | `int` | `4` | Amount of indentation between parent and child. |
+## Messages
+
+- [DirectoryTree.FileSelected][textual.widgets.DirectoryTree.FileSelected]
+
+## Bindings
+
+The directory tree widget inherits [the bindings from the tree widget][textual.widgets.Tree.BINDINGS].
+
## Component Classes
The directory tree widget provides the following component classes:
diff --git a/docs/widgets/footer.md b/docs/widgets/footer.md
index 4affbe2191..fcb25cf836 100644
--- a/docs/widgets/footer.md
+++ b/docs/widgets/footer.md
@@ -30,7 +30,11 @@ widget. Notice how the `Footer` automatically displays the keybinding.
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
## Component Classes
diff --git a/docs/widgets/header.md b/docs/widgets/header.md
index c589ddcf00..1ffdf70dd1 100644
--- a/docs/widgets/header.md
+++ b/docs/widgets/header.md
@@ -45,7 +45,15 @@ This example shows how to set the text in the `Header` using `App.title` and `Ap
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/input.md b/docs/widgets/input.md
index 455861a397..cd861a79b5 100644
--- a/docs/widgets/input.md
+++ b/docs/widgets/input.md
@@ -88,7 +88,7 @@ as seen for `Palindrome` in the example above.
## Bindings
-The Input widget defines the following bindings:
+The input widget defines the following bindings:
::: textual.widgets.Input.BINDINGS
options:
diff --git a/docs/widgets/label.md b/docs/widgets/label.md
index ae1216d0a2..2a0c1819a7 100644
--- a/docs/widgets/label.md
+++ b/docs/widgets/label.md
@@ -28,7 +28,15 @@ This widget has no reactive attributes.
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/list_item.md b/docs/widgets/list_item.md
index 309079ea87..c4d306cb78 100644
--- a/docs/widgets/list_item.md
+++ b/docs/widgets/list_item.md
@@ -29,12 +29,17 @@ of multiple `ListItem`s. The arrow keys can be used to navigate the list.
| ------------- | ------ | ------- | ------------------------------------ |
| `highlighted` | `bool` | `False` | True if this ListItem is highlighted |
+## Messages
-#### Attributes
+This widget posts no messages.
-| attribute | type | purpose |
-| --------- | ---------- | --------------------------- |
-| `item` | `ListItem` | The item that was selected. |
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/list_view.md b/docs/widgets/list_view.md
index cc403f2c8c..d5c85cdbc6 100644
--- a/docs/widgets/list_view.md
+++ b/docs/widgets/list_view.md
@@ -31,9 +31,9 @@ The example below shows an app with a simple `ListView`.
## Reactive Attributes
-| Name | Type | Default | Description |
-| ------- | ----- | ------- | ------------------------------- |
-| `index` | `int` | `0` | The currently highlighted index |
+| Name | Type | Default | Description |
+| ------- | ----- | ------- | -------------------------------- |
+| `index` | `int` | `0` | The currently highlighted index. |
## Messages
@@ -49,6 +49,10 @@ The list view widget defines the following bindings:
show_root_heading: false
show_root_toc_entry: false
+## Component Classes
+
+This widget has no component classes.
+
---
diff --git a/docs/widgets/loading_indicator.md b/docs/widgets/loading_indicator.md
index 1936115522..2a5235d12e 100644
--- a/docs/widgets/loading_indicator.md
+++ b/docs/widgets/loading_indicator.md
@@ -7,6 +7,23 @@ Displays pulsating dots to indicate when data is being loaded.
- [ ] Focusable
- [ ] Container
+## Example
+
+Simple usage example:
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/loading_indicator.py"}
+ ```
+
+=== "loading_indicator.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/loading_indicator.py"
+ ```
+
+## Changing Indicator Color
+
You can set the color of the loading indicator by setting its `color` style.
Here's how you would do that with CSS:
@@ -17,17 +34,22 @@ LoadingIndicator {
}
```
+## Reactive Attributes
-=== "Output"
+This widget has no reactive attributes.
- ```{.textual path="docs/examples/widgets/loading_indicator.py"}
- ```
+## Messages
-=== "loading_indicator.py"
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
- ```python
- --8<-- "docs/examples/widgets/loading_indicator.py"
- ```
---
diff --git a/docs/widgets/log.md b/docs/widgets/log.md
index 04e54f0f00..72509313a6 100644
--- a/docs/widgets/log.md
+++ b/docs/widgets/log.md
@@ -37,10 +37,17 @@ The example below shows how to write text to a `Log` widget:
| `max_lines` | `int` | `None` | Maximum number of lines in the log or `None` for no maximum. |
| `auto_scroll` | `bool` | `False` | Scroll to end of log when new lines are added. |
-
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/markdown.md b/docs/widgets/markdown.md
index 6897c4c713..1382d1a5aa 100644
--- a/docs/widgets/markdown.md
+++ b/docs/widgets/markdown.md
@@ -27,12 +27,29 @@ The following example displays Markdown from a string.
--8<-- "docs/examples/widgets/markdown.py"
~~~
+## Reactive Attributes
+
+This widget has no reactive attributes.
+
## Messages
- [Markdown.TableOfContentsUpdated][textual.widgets.Markdown.TableOfContentsUpdated]
- [Markdown.TableOfContentsSelected][textual.widgets.Markdown.TableOfContentsSelected]
- [Markdown.LinkClicked][textual.widgets.Markdown.LinkClicked]
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+The markdown widget provides the following component classes:
+
+::: textual.widgets.Markdown.COMPONENT_CLASSES
+ options:
+ show_root_heading: false
+ show_root_toc_entry: false
+
## See Also
diff --git a/docs/widgets/markdown_viewer.md b/docs/widgets/markdown_viewer.md
index 6a4e3f47df..d830281fd4 100644
--- a/docs/widgets/markdown_viewer.md
+++ b/docs/widgets/markdown_viewer.md
@@ -33,6 +33,18 @@ The following example displays Markdown from a string and a Table of Contents.
| ------------------------ | ---- | ------- | ----------------------------------------------------------------- |
| `show_table_of_contents` | bool | True | Wether a Table of Contents should be displayed with the Markdown. |
+## Messages
+
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
## See Also
* [Markdown][textual.widgets.Markdown] code reference
diff --git a/docs/widgets/placeholder.md b/docs/widgets/placeholder.md
index c566b871dd..c8006d780a 100644
--- a/docs/widgets/placeholder.md
+++ b/docs/widgets/placeholder.md
@@ -41,7 +41,15 @@ The example below shows each placeholder variant.
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/progress_bar.md b/docs/widgets/progress_bar.md
index ab02516c98..ab927aa763 100644
--- a/docs/widgets/progress_bar.md
+++ b/docs/widgets/progress_bar.md
@@ -104,15 +104,6 @@ Refer to the [section below](#styling-the-progress-bar) for more information.
--8<-- "docs/examples/widgets/progress_bar_styled.tcss"
```
-## Reactive Attributes
-
-| Name | Type | Default | Description |
-| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- |
-| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. |
-| `progress` | `float` | `0` | The number of steps of progress already made. |
-| `total` | `float | None` | The total number of steps that we are keeping track of. |
-
-
## Styling the Progress Bar
The progress bar is composed of three sub-widgets that can be styled independently:
@@ -130,8 +121,27 @@ The progress bar is composed of three sub-widgets that can be styled independent
show_root_heading: false
show_root_toc_entry: false
----
+## Reactive Attributes
+
+| Name | Type | Default | Description |
+| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- |
+| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. |
+| `progress` | `float` | `0` | The number of steps of progress already made. |
+| `total` | `float | None` | The total number of steps that we are keeping track of. |
+
+## Messages
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
+---
::: textual.widgets.ProgressBar
options:
diff --git a/docs/widgets/radiobutton.md b/docs/widgets/radiobutton.md
index 36df3a3c0a..8161ceaf17 100644
--- a/docs/widgets/radiobutton.md
+++ b/docs/widgets/radiobutton.md
@@ -36,6 +36,10 @@ The example below shows radio buttons, used within a [`RadioSet`](./radioset.md)
| ------- | ------ | ------- | ------------------------------ |
| `value` | `bool` | `False` | The value of the radio button. |
+## Messages
+
+- [RadioButton.Changed][textual.widgets.RadioButton.Changed]
+
## Bindings
The radio button widget defines the following bindings:
@@ -47,17 +51,13 @@ The radio button widget defines the following bindings:
## Component Classes
-The radio button widget provides the following component classes:
+The checkbox widget inherits the following component classes:
::: textual.widgets._toggle_button.ToggleButton.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false
-## Messages
-
-- [RadioButton.Changed][textual.widgets.RadioButton.Changed]
-
## See Also
- [RadioSet](./radioset.md)
diff --git a/docs/widgets/radioset.md b/docs/widgets/radioset.md
index e51e56b784..f947dbc2ff 100644
--- a/docs/widgets/radioset.md
+++ b/docs/widgets/radioset.md
@@ -9,6 +9,8 @@ A container widget that groups [`RadioButton`](./radiobutton.md)s together.
## Example
+### Simple example
+
The example below shows two radio sets, one built using a collection of
[radio buttons](./radiobutton.md), the other a collection of simple strings.
@@ -29,11 +31,7 @@ The example below shows two radio sets, one built using a collection of
--8<-- "docs/examples/widgets/radio_set.tcss"
```
-## Messages
-
-- [RadioSet.Changed][textual.widgets.RadioSet.Changed]
-
-#### Example
+### Reacting to Changes in a Radio Set
Here is an example of using the message to react to changes in a `RadioSet`:
@@ -54,6 +52,18 @@ Here is an example of using the message to react to changes in a `RadioSet`:
--8<-- "docs/examples/widgets/radio_set_changed.tcss"
```
+## Messages
+
+- [RadioSet.Changed][textual.widgets.RadioSet.Changed]
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
## See Also
diff --git a/docs/widgets/rich_log.md b/docs/widgets/rich_log.md
index 2778db7ea3..5f373218fd 100644
--- a/docs/widgets/rich_log.md
+++ b/docs/widgets/rich_log.md
@@ -42,6 +42,14 @@ The example below shows an application showing a `RichLog` with different kinds
This widget sends no messages.
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
---
diff --git a/docs/widgets/rule.md b/docs/widgets/rule.md
index bc7a2ec1de..5740b42376 100644
--- a/docs/widgets/rule.md
+++ b/docs/widgets/rule.md
@@ -62,6 +62,14 @@ The example below shows vertical rules with all the available line styles.
This widget sends no messages.
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
---
diff --git a/docs/widgets/select.md b/docs/widgets/select.md
index 7687e2e584..6f9690cb24 100644
--- a/docs/widgets/select.md
+++ b/docs/widgets/select.md
@@ -58,12 +58,8 @@ The following example presents a `Select` with a number of options.
--8<-- "docs/examples/widgets/select.tcss"
```
-## Messages
-
-- [Select.Changed][textual.widgets.Select.Changed]
-
-## Reactive attributes
+## Reactive Attributes
| Name | Type | Default | Description |
@@ -71,6 +67,9 @@ The following example presents a `Select` with a number of options.
| `expanded` | `bool` | `False` | True to expand the options overlay. |
| `value` | `SelectType` \| `None` | `None` | Current value of the Select. |
+## Messages
+
+- [Select.Changed][textual.widgets.Select.Changed]
## Bindings
@@ -81,6 +80,9 @@ The Select widget defines the following bindings:
show_root_heading: false
show_root_toc_entry: false
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/sparkline.md b/docs/widgets/sparkline.md
index 98790f9c65..454e674c42 100644
--- a/docs/widgets/sparkline.md
+++ b/docs/widgets/sparkline.md
@@ -102,7 +102,15 @@ The example below shows how to use component classes to change the colors of the
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/static.md b/docs/widgets/static.md
index 561f053431..9df032994b 100644
--- a/docs/widgets/static.md
+++ b/docs/widgets/static.md
@@ -27,7 +27,15 @@ This widget has no reactive attributes.
## Messages
-This widget sends no messages.
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
## See Also
diff --git a/docs/widgets/switch.md b/docs/widgets/switch.md
index 4cd8b61825..1482c08a8d 100644
--- a/docs/widgets/switch.md
+++ b/docs/widgets/switch.md
@@ -32,6 +32,10 @@ The example below shows switches in various states.
| ------- | ------ | ------- | ------------------------ |
| `value` | `bool` | `False` | The value of the switch. |
+## Messages
+
+- [Switch.Changed][textual.widgets.Switch.Changed]
+
## Bindings
The switch widget defines the following bindings:
@@ -50,10 +54,6 @@ The switch widget provides the following component classes:
show_root_heading: false
show_root_toc_entry: false
-## Messages
-
-- [Switch.Changed][textual.widgets.Switch.Changed]
-
## Additional Notes
- To remove the spacing around a `Switch`, set `border: none;` and `padding: 0;`.
diff --git a/docs/widgets/tabbed_content.md b/docs/widgets/tabbed_content.md
index 7a61318dfc..f121e314e8 100644
--- a/docs/widgets/tabbed_content.md
+++ b/docs/widgets/tabbed_content.md
@@ -94,7 +94,7 @@ The following example contains a `TabbedContent` with three tabs.
--8<-- "docs/examples/widgets/tabbed_content.py"
```
-## Reactive attributes
+## Reactive Attributes
| Name | Type | Default | Description |
| -------- | ----- | ------- | -------------------------------------------------------------- |
@@ -105,6 +105,14 @@ The following example contains a `TabbedContent` with three tabs.
- [TabbedContent.TabActivated][textual.widgets.TabbedContent.TabActivated]
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+This widget has no component classes.
+
## See also
diff --git a/docs/widgets/tabs.md b/docs/widgets/tabs.md
index b7d7130d74..a076fb715b 100644
--- a/docs/widgets/tabs.md
+++ b/docs/widgets/tabs.md
@@ -73,6 +73,9 @@ The Tabs widget defines the following bindings:
show_root_heading: false
show_root_toc_entry: false
+## Component Classes
+
+This widget has no component classes.
---
diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md
new file mode 100644
index 0000000000..2fddae64eb
--- /dev/null
+++ b/docs/widgets/text_area.md
@@ -0,0 +1,467 @@
+
+# TextArea
+
+!!! tip "Added in version 0.38.0"
+
+A widget for editing text which may span multiple lines.
+Supports syntax highlighting for a selection of languages.
+
+- [x] Focusable
+- [ ] Container
+
+
+## Guide
+
+### Loading text
+
+In this example we load some initial text into the `TextArea`, and set the language to `"python"` to enable syntax highlighting.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/text_area_example.py" columns="42" lines="8"}
+ ```
+
+=== "text_area_example.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/text_area_example.py"
+ ```
+
+To load content into the `TextArea` after it has already been created,
+use the [`load_text`][textual.widgets._text_area.TextArea.load_text] method.
+
+To update the parser used for syntax highlighting, set the [`language`][textual.widgets._text_area.TextArea.language] reactive attribute:
+
+```python
+# Set the language to Markdown
+text_area.language = "markdown"
+```
+
+!!! note
+ Syntax highlighting is unavailable on Python 3.7.
+
+!!! 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`:
+
+- The [`TextArea.text`][textual.widgets._text_area.TextArea.text] property returns all content in the text area as a string.
+- The [`TextArea.selected_text`][textual.widgets._text_area.TextArea.selected_text] property returns the text corresponding to the current selection.
+- The [`TextArea.get_text_range`][textual.widgets._text_area.TextArea.get_text_range] method returns the text between two locations.
+
+In all cases, when multiple lines of text are retrieved, the [document line separator](#line-separators) will be used.
+
+### Editing content inside `TextArea`
+
+The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method.
+This method is the programmatic equivalent of selecting some text and then pasting.
+
+Some other convenient methods are available, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear].
+
+### Working with the cursor
+
+#### Moving the cursor
+
+The cursor location is available via the [`cursor_location`][textual.widgets._text_area.TextArea.cursor_location] property, which represents
+the location of the cursor as a tuple `(row_index, column_index)`. These indices are zero-based.
+Writing a new value to `cursor_location` will immediately update the location of the cursor.
+
+```python
+>>> text_area = TextArea()
+>>> text_area.cursor_location
+(0, 0)
+>>> text_area.cursor_location = (0, 4)
+>>> text_area.cursor_location
+(0, 4)
+```
+
+`cursor_location` is a simple way to move the cursor programmatically, but it doesn't let us select text.
+
+#### Selecting text
+
+To select text, we can use the `selection` reactive attribute.
+Let's select the first two lines of text in a document by adding `text_area.selection = Selection(start=(0, 0), end=(2, 0))` to our code:
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/text_area_selection.py" columns="42" lines="8"}
+ ```
+
+=== "text_area_selection.py"
+
+ ```python hl_lines="17"
+ --8<-- "docs/examples/widgets/text_area_selection.py"
+ ```
+
+ 1. Selects the first two lines of text.
+
+Note that selections can happen in both directions, so `Selection((2, 0), (0, 0))` is also valid.
+
+!!! tip
+
+ The `end` attribute of the `selection` is always equal to `TextArea.cursor_location`. In other words,
+ the `cursor_location` attribute is simply a convenience for accessing `text_area.selection.end`.
+
+#### More cursor utilities
+
+There are a number of additional utility methods available for interacting with the cursor.
+
+##### Location information
+
+A number of 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.
+
+We can also check the location the cursor _would_ arrive at if we were to move it.
+For example, [`get_cursor_right_location`][textual.widgets._text_area.TextArea.get_cursor_right_location] returns the location
+the cursor would move to if it were to move right.
+A number of similar methods exist, with names like `get_cursor_*_location`.
+
+##### Cursor movement methods
+
+The [`move_cursor`][textual.widgets._text_area.TextArea.move_cursor] method allows you to move the cursor to a new location while selecting
+text, or move the cursor and scroll to keep it centered.
+
+```python
+# Move the cursor from its current location to row index 4,
+# column index 8, while selecting all the text between.
+text_area.move_cursor((4, 8), select=True)
+```
+
+The [`move_cursor_relative`][textual.widgets._text_area.TextArea.move_cursor_relative] method offers a very similar interface, but moves the cursor relative
+to its current location.
+
+##### Common selections
+
+There are some methods available which make common selections easier:
+
+- [`select_line`][textual.widgets._text_area.TextArea.select_line] selects a line by index. Bound to ++f6++ by default.
+- [`select_all`][textual.widgets._text_area.TextArea.select_all] selects all text. Bound to ++f7++ by default.
+
+### Themes
+
+`TextArea` ships with some builtin themes, and you can easily add your own.
+
+Themes give you control over the look and feel, including syntax highlighting,
+the cursor, selection, gutter, and more.
+
+#### Using builtin themes
+
+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")
+```
+
+You can check which themes are available using the [`available_themes`][textual.widgets._text_area.TextArea.available_themes] property.
+
+```python
+>>> text_area = TextArea()
+>>> print(text_area.available_themes)
+{'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]
+attribute to one of the available themes.
+
+```python
+text_area.theme = "vscode_dark"
+```
+
+On setting this attribute the `TextArea` will immediately refresh to display the updated theme.
+
+#### Custom themes
+
+Using custom (non-builtin) themes is 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].
+
+##### 1. Creating a theme
+
+Let's create a simple theme, `"my_cool_theme"`, which colors the cursor blue, and the cursor line yellow.
+Our theme will also syntax highlight strings as red, and comments as magenta.
+
+```python
+from rich.style import Style
+from textual.widgets.text_area import TextAreaTheme
+# ...
+my_theme = TextAreaTheme(
+ # This name will be used to refer to the theme...
+ name="my_cool_theme",
+ # Basic styles such as background, cursor, selection, gutter, etc...
+ cursor_style=Style(color="white", bgcolor="blue"),
+ cursor_line_style=Style(bgcolor="yellow"),
+ # `syntax_styles` is for syntax highlighting.
+ # It maps tokens parsed from the document to Rich styles.
+ syntax_styles={
+ "string": Style(color="red"),
+ "comment": Style(color="magenta"),
+ }
+)
+```
+
+Attributes like `cursor_style` and `cursor_line_style` apply general language-agnostic
+styling to the widget.
+
+The `syntax_styles` attribute of `TextAreaTheme` is used for syntax highlighting and
+depends on the `language` currently in use.
+For more details, see [syntax highlighting](#syntax-highlighting).
+
+If you wish to build on an existing theme, you can obtain a reference to it using the [`TextAreaTheme.get_builtin_theme`][textual.widgets.text_area.TextAreaTheme.get_builtin_theme] classmethod:
+
+```python
+from textual.widgets.text_area import TextAreaTheme
+
+monokai = TextAreaTheme.get_builtin_theme("monokai")
+```
+
+##### 2. Registering a theme
+
+Our theme can now be registered with the `TextArea` instance.
+
+```python
+text_area.register_theme(my_theme)
+```
+
+After registering a theme, it'll appear in the `available_themes`:
+
+```python
+>>> print(text_area.available_themes)
+{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'}
+```
+
+We can now switch to it:
+
+```python
+text_area.theme = "my_cool_theme"
+```
+
+This immediately updates the appearance of the `TextArea`:
+
+```{.textual path="docs/examples/widgets/text_area_custom_theme.py" columns="42" lines="8"}
+```
+
+### Indentation
+
+The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`.
+
+If `indent_type == "spaces"`, pressing ++tab++ will insert up to `indent_width` spaces in order to align with the next tab stop.
+
+### Line separators
+
+When content is loaded into `TextArea`, the content is scanned from beginning to end
+and the first occurrence of a line separator is recorded.
+
+This separator will then be used when content is later read from the `TextArea` via
+the `text` property. The `TextArea` widget does not support exporting text which
+contains mixed line endings.
+
+Similarly, newline characters pasted into the `TextArea` will be converted.
+
+You can check the line separator of the current document by inspecting `TextArea.document.newline`:
+
+```python
+>>> text_area = TextArea()
+>>> text_area.document.newline
+'\n'
+```
+
+### Line numbers
+
+The gutter (column on the left containing line numbers) can be toggled by setting
+the `show_line_numbers` attribute to `True` or `False`.
+
+Setting this attribute will immediately repaint the `TextArea` to reflect the new value.
+
+### Extending `TextArea`
+
+Sometimes, you may wish to subclass `TextArea` to add some extra functionality.
+In this section, we'll briefly explore how we can extend the widget to achieve common goals.
+
+#### Hooking into key presses
+
+You may wish to hook into certain key presses to inject some functionality.
+This can be done by over-riding `_on_key` and adding the required functionality.
+
+##### Example - closing parentheses automatically
+
+Let's extend `TextArea` to add a feature which automatically closes parentheses and moves the cursor to a sensible location.
+
+```python
+--8<-- "docs/examples/widgets/text_area_extended.py"
+```
+
+This intercepts the key handler when `"("` is pressed, and inserts `"()"` instead.
+It then moves the cursor so that it lands between the open and closing parentheses.
+
+Typing `def hello(` into the `TextArea` results in the bracket automatically being closed:
+
+```{.textual path="docs/examples/widgets/text_area_extended.py" columns="36" lines="4" press="d,e,f,space,h,e,l,l,o,left_parenthesis"}
+```
+
+### Advanced concepts
+
+#### Syntax highlighting
+
+Syntax highlighting inside the `TextArea` is powered by a library called [`tree-sitter`](https://tree-sitter.github.io/tree-sitter/).
+
+Each time you update the document in a `TextArea`, an internal syntax tree is updated.
+This tree is frequently _queried_ to find location ranges relevant to syntax highlighting.
+We give these ranges _names_, and ultimately map them to Rich styles inside `TextAreaTheme.syntax_styles`.
+
+To illustrate how this works, lets look at how the "Monokai" `TextAreaTheme` highlights Markdown files.
+
+When the `language` attribute is set to `"markdown"`, a highlight query similar to the one below is used (trimmed for brevity).
+
+```scheme
+(heading_content) @heading
+(link) @link
+```
+
+This highlight query maps `heading_content` nodes returned by the Markdown parser to the name `@heading`,
+and `link` nodes to the name `@link`.
+
+Inside our `TextAreaTheme.syntax_styles` dict, we can map the name `@heading` to a Rich style.
+Here's a snippet from the "Monokai" theme which does just that:
+
+```python
+TextAreaTheme(
+ name="monokai",
+ base_style=Style(color="#f8f8f2", bgcolor="#272822"),
+ gutter_style=Style(color="#90908a", bgcolor="#272822"),
+ # ...
+ syntax_styles={
+ # Colorise @heading and make them bold
+ "heading": Style(color="#F92672", bold=True),
+ # Colorise and underline @link
+ "link": Style(color="#66D9EF", underline=True),
+ # ...
+ },
+)
+```
+
+To understand which names can be mapped inside `syntax_styles`, we recommend looking at the existing
+themes and highlighting queries (`.scm` files) in the Textual repository.
+
+!!! tip
+
+ You may also wish to take a look at the contents of `TextArea._highlights` on an
+ active `TextArea` instance to see which highlights have been generated for the
+ open document.
+
+#### Adding support for custom languages
+
+To add support for a language to a `TextArea`, use the [`register_language`][textual.widgets._text_area.TextArea.register_language] method.
+
+To register a language, we require two things:
+
+1. A tree-sitter `Language` object which contains the grammar for the language.
+2. A highlight query which is used for [syntax highlighting](#syntax-highlighting).
+
+##### Example - adding Java support
+
+The easiest way to obtain a `Language` object is using the [`py-tree-sitter-languages`](https://github.com/grantjenks/py-tree-sitter-languages) package. Here's how we can use this package to obtain a reference to a `Language` object representing Java:
+
+```python
+from tree_sitter_languages import get_language
+java_language = get_language("java")
+```
+
+!!! note
+
+ `py-tree-sitter-languages` may not be available on some architectures (e.g. Macbooks with Apple Silicon running Python 3.7).
+
+The exact version of the parser used when you call `get_language` can be checked via
+the [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) in
+the version of `py-tree-sitter-languages` you're using. This file contains links to the GitHub
+repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at `queries/highlights.scm`,
+and a file showing all the available node types which can be used in highlight queries at `src/node-types.json`.
+
+Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps:
+
+1. Open [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) from the `py-tree-sitter-languages` repo.
+2. Find the link corresponding to `tree-sitter-java` and go to the repo on GitHub (you may also need to go to the specific commit referenced in `repos.txt`).
+3. Go to [`queries/highlights.scm`](https://github.com/tree-sitter/tree-sitter-java/blob/ac14b4b1884102839455d32543ab6d53ae089ab7/queries/highlights.scm) to see the example highlight query for Java.
+
+Be sure to check the license in the repo to ensure it can be freely copied.
+
+!!! warning
+
+ It's important to use a highlight query which is compatible with the parser in use, so
+ pay attention to the commit hash when visiting the repo via `repos.txt`.
+
+We now have our `Language` and our highlight query, so we can register Java as a language.
+
+```python
+--8<-- "docs/examples/widgets/text_area_custom_language.py"
+```
+
+Running our app, we can see that the Java code is highlighted.
+We can freely edit the text, and the syntax highlighting will update immediately.
+
+```{.textual path="docs/examples/widgets/text_area_custom_language.py" columns="52" lines="8"}
+```
+
+Recall that we map names (like `@heading`) from the tree-sitter highlight query to Rich style objects inside the `TextAreaTheme.syntax_styles` dictionary.
+If you notice some highlights are missing after registering a language, the issue may be:
+
+1. The current `TextAreaTheme` doesn't contain a mapping for the name in the highlight query. Adding a new to `syntax_styles` should resolve the issue.
+2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name.
+
+!!! tip
+
+ 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.
+
+## Reactive attributes
+
+| 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. |
+| `selection` | `Selection` | `Selection()` | The current selection. |
+| `show_line_numbers` | `bool` | `True` | 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. |
+
+## Bindings
+
+The `TextArea` widget defines the following bindings:
+
+::: textual.widgets._text_area.TextArea.BINDINGS
+ options:
+ show_root_heading: false
+ show_root_toc_entry: false
+
+
+## Component classes
+
+The `TextArea` widget defines no component classes.
+
+Styling should be done exclusively via [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme].
+
+## See also
+
+- [`Input`][textual.widgets.Input] - for single-line text input.
+- [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme] - for theming the `TextArea`.
+- 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).
+
+---
+
+::: textual.widgets._text_area.TextArea
+ options:
+ heading_level: 2
+
+---
+
+::: textual.widgets.text_area
+ options:
+ heading_level: 2
diff --git a/docs/widgets/toast.md b/docs/widgets/toast.md
index 647f730369..9f54c0f47b 100644
--- a/docs/widgets/toast.md
+++ b/docs/widgets/toast.md
@@ -71,6 +71,27 @@ Toast.-information .toast--title {
--8<-- "docs/examples/widgets/toast.py"
```
+## Reactive Attributes
+
+This widget has no reactive attributes.
+
+## Messages
+
+This widget posts no messages.
+
+## Bindings
+
+This widget has no bindings.
+
+## Component Classes
+
+The toast widget provides the following component classes:
+
+::: textual.widgets._toast.Toast.COMPONENT_CLASSES
+ options:
+ show_root_heading: false
+ show_root_toc_entry: false
+
---
::: textual.widgets._toast
diff --git a/examples/color_command.py b/examples/color_command.py
new file mode 100644
index 0000000000..bd4148657b
--- /dev/null
+++ b/examples/color_command.py
@@ -0,0 +1,67 @@
+from dataclasses import dataclass
+from functools import partial
+
+from textual import on
+from textual._color_constants import COLOR_NAME_TO_RGB
+from textual.app import App, ComposeResult
+from textual.command import Hit, Hits, Provider
+from textual.message import Message
+from textual.widgets import Header, Static
+
+
+@dataclass
+class SwitchColor(Message, bubble=False):
+ """Message to tell the app to switch color."""
+
+ color: str
+
+
+class ColorCommands(Provider):
+ """A command provider to select colors."""
+
+ async def search(self, query: str) -> Hits:
+ """Called for each key."""
+ matcher = self.matcher(query)
+ for color in COLOR_NAME_TO_RGB.keys():
+ score = matcher.match(color)
+ if score > 0:
+ yield Hit(
+ score,
+ matcher.highlight(color),
+ partial(self.app.post_message, SwitchColor(color)),
+ )
+
+
+class ColorBlock(Static):
+ """Simple block of color."""
+
+ DEFAULT_CSS = """
+ ColorBlock{
+ padding: 3 6;
+ margin: 1 2;
+ color: auto;
+ }
+ """
+
+
+class ColorApp(App):
+ """Experiment with the command palette."""
+
+ COMMANDS = App.COMMANDS | {ColorCommands}
+ TITLE = "Press ctrl + \\ and type a color"
+
+ def compose(self) -> ComposeResult:
+ yield Header()
+
+ @on(SwitchColor)
+ def switch_color(self, event: SwitchColor) -> None:
+ """Adds a color block on demand."""
+ color_block = ColorBlock(event.color)
+ color_block.styles.background = event.color
+ self.mount(color_block)
+ self.screen.scroll_end()
+
+
+if __name__ == "__main__":
+ app = ColorApp()
+ app.run()
diff --git a/mkdocs-common.yml b/mkdocs-common.yml
index aa3584c523..50c574073d 100644
--- a/mkdocs-common.yml
+++ b/mkdocs-common.yml
@@ -46,18 +46,18 @@ theme:
- content.code.annotate
- content.code.copy
palette:
- - media: "(prefers-color-scheme: light)"
- scheme: default
- accent: purple
- toggle:
- icon: material/weather-sunny
- name: Switch to dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: black
- toggle:
- icon: material/weather-night
- name: Switch to light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ accent: purple
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ toggle:
+ icon: material/weather-night
+ name: Switch to light mode
plugins:
search:
@@ -87,6 +87,7 @@ plugins:
- "!^render_lines$"
- "!^get_content_width$"
- "!^get_content_height$"
+ - "!^compose_add_child$"
watch:
- mkdocs-common.yml
- mkdocs-nav.yml
@@ -98,11 +99,9 @@ plugins:
- "**/_template.md"
- "snippets/*"
-
extra_css:
- stylesheets/custom.css
-
extra:
social:
- icon: fontawesome/brands/twitter
diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml
index 9833e9ffa4..2d61063111 100644
--- a/mkdocs-nav.yml
+++ b/mkdocs-nav.yml
@@ -1,210 +1,214 @@
nav:
- - Introduction:
- - "index.md"
- - "getting_started.md"
- - "help.md"
- - "tutorial.md"
- - Guide:
- - "guide/index.md"
- - "guide/devtools.md"
- - "guide/app.md"
- - "guide/styles.md"
- - "guide/CSS.md"
- - "guide/design.md"
- - "guide/queries.md"
- - "guide/layout.md"
- - "guide/events.md"
- - "guide/input.md"
- - "guide/actions.md"
- - "guide/reactivity.md"
- - "guide/widgets.md"
- - "guide/animation.md"
- - "guide/screens.md"
- - "guide/workers.md"
- - "widget_gallery.md"
- - Reference:
- - "reference/index.md"
- - CSS Types:
- - "css_types/index.md"
- - "css_types/border.md"
- - "css_types/color.md"
- - "css_types/horizontal.md"
- - "css_types/integer.md"
- - "css_types/name.md"
- - "css_types/number.md"
- - "css_types/overflow.md"
- - "css_types/percentage.md"
- - "css_types/scalar.md"
- - "css_types/text_align.md"
- - "css_types/text_style.md"
- - "css_types/vertical.md"
- - Events:
- - "events/index.md"
- - "events/blur.md"
- - "events/descendant_blur.md"
- - "events/descendant_focus.md"
- - "events/enter.md"
- - "events/focus.md"
- - "events/hide.md"
- - "events/key.md"
- - "events/leave.md"
- - "events/load.md"
- - "events/mount.md"
- - "events/mouse_capture.md"
- - "events/click.md"
- - "events/mouse_down.md"
- - "events/mouse_move.md"
- - "events/mouse_release.md"
- - "events/mouse_scroll_down.md"
- - "events/mouse_scroll_up.md"
- - "events/mouse_up.md"
- - "events/paste.md"
- - "events/resize.md"
- - "events/screen_resume.md"
- - "events/screen_suspend.md"
- - "events/show.md"
- - Styles:
- - "styles/align.md"
- - "styles/background.md"
- - "styles/border.md"
- - "styles/border_subtitle_align.md"
- - "styles/border_subtitle_background.md"
- - "styles/border_subtitle_color.md"
- - "styles/border_subtitle_style.md"
- - "styles/border_title_align.md"
- - "styles/border_title_background.md"
- - "styles/border_title_color.md"
- - "styles/border_title_style.md"
- - "styles/box_sizing.md"
- - "styles/color.md"
- - "styles/content_align.md"
- - "styles/display.md"
- - "styles/dock.md"
- - "styles/index.md"
- - Grid:
- - "styles/grid/index.md"
- - "styles/grid/column_span.md"
- - "styles/grid/grid_columns.md"
- - "styles/grid/grid_gutter.md"
- - "styles/grid/grid_rows.md"
- - "styles/grid/grid_size.md"
- - "styles/grid/row_span.md"
- - "styles/height.md"
- - "styles/layer.md"
- - "styles/layers.md"
- - "styles/layout.md"
- - Links:
- - "styles/links/index.md"
- - "styles/links/link_background.md"
- - "styles/links/link_color.md"
- - "styles/links/link_hover_background.md"
- - "styles/links/link_hover_color.md"
- - "styles/links/link_hover_style.md"
- - "styles/links/link_style.md"
- - "styles/margin.md"
- - "styles/max_height.md"
- - "styles/max_width.md"
- - "styles/min_height.md"
- - "styles/min_width.md"
- - "styles/offset.md"
- - "styles/opacity.md"
- - "styles/outline.md"
- - "styles/overflow.md"
- - "styles/padding.md"
- - Scrollbar colors:
- - "styles/scrollbar_colors/index.md"
- - "styles/scrollbar_colors/scrollbar_background.md"
- - "styles/scrollbar_colors/scrollbar_background_active.md"
- - "styles/scrollbar_colors/scrollbar_background_hover.md"
- - "styles/scrollbar_colors/scrollbar_color.md"
- - "styles/scrollbar_colors/scrollbar_color_active.md"
- - "styles/scrollbar_colors/scrollbar_color_hover.md"
- - "styles/scrollbar_colors/scrollbar_corner_color.md"
- - "styles/scrollbar_gutter.md"
- - "styles/scrollbar_size.md"
- - "styles/text_align.md"
- - "styles/text_opacity.md"
- - "styles/text_style.md"
- - "styles/tint.md"
- - "styles/visibility.md"
- - "styles/width.md"
- - Widgets:
- - "widgets/button.md"
- - "widgets/checkbox.md"
- - "widgets/content_switcher.md"
- - "widgets/data_table.md"
- - "widgets/digits.md"
- - "widgets/directory_tree.md"
- - "widgets/footer.md"
- - "widgets/header.md"
- - "widgets/index.md"
- - "widgets/input.md"
- - "widgets/label.md"
- - "widgets/list_item.md"
- - "widgets/list_view.md"
- - "widgets/loading_indicator.md"
- - "widgets/log.md"
- - "widgets/markdown_viewer.md"
- - "widgets/markdown.md"
- - "widgets/option_list.md"
- - "widgets/placeholder.md"
- - "widgets/pretty.md"
- - "widgets/progress_bar.md"
- - "widgets/radiobutton.md"
- - "widgets/radioset.md"
- - "widgets/rich_log.md"
- - "widgets/rule.md"
- - "widgets/select.md"
- - "widgets/selection_list.md"
- - "widgets/sparkline.md"
- - "widgets/static.md"
- - "widgets/switch.md"
- - "widgets/tabbed_content.md"
- - "widgets/tabs.md"
- - "widgets/tree.md"
- - API:
- - "api/index.md"
- - "api/app.md"
- - "api/await_remove.md"
- - "api/binding.md"
- - "api/color.md"
- - "api/command_palette.md"
- - "api/containers.md"
- - "api/coordinate.md"
- - "api/dom_node.md"
- - "api/events.md"
- - "api/errors.md"
- - "api/filter.md"
- - "api/fuzzy_matcher.md"
- - "api/geometry.md"
- - "api/logger.md"
- - "api/logging.md"
- - "api/map_geometry.md"
- - "api/message_pump.md"
- - "api/message.md"
- - "api/on.md"
- - "api/pilot.md"
- - "api/query.md"
- - "api/reactive.md"
- - "api/screen.md"
- - "api/scrollbar.md"
- - "api/scroll_view.md"
- - "api/strip.md"
- - "api/suggester.md"
- - "api/system_commands_source.md"
- - "api/timer.md"
- - "api/types.md"
- - "api/validation.md"
- - "api/walk.md"
- - "api/widget.md"
- - "api/work.md"
- - "api/worker.md"
- - "api/worker_manager.md"
- - "How To":
- - "how-to/index.md"
- - "how-to/center-things.md"
- - "how-to/design-a-layout.md"
- - "FAQ.md"
- - "roadmap.md"
- - "Blog":
- - blog/index.md
+ - "index.md"
+ - Introduction:
+ - "getting_started.md"
+ - "help.md"
+ - "tutorial.md"
+ - Guide:
+ - "guide/index.md"
+ - "guide/devtools.md"
+ - "guide/app.md"
+ - "guide/styles.md"
+ - "guide/CSS.md"
+ - "guide/design.md"
+ - "guide/queries.md"
+ - "guide/layout.md"
+ - "guide/events.md"
+ - "guide/input.md"
+ - "guide/actions.md"
+ - "guide/reactivity.md"
+ - "guide/widgets.md"
+ - "guide/animation.md"
+ - "guide/screens.md"
+ - "guide/workers.md"
+ - "guide/command_palette.md"
+ - "guide/testing.md"
+ - "widget_gallery.md"
+ - Reference:
+ - "reference/index.md"
+ - CSS Types:
+ - "css_types/index.md"
+ - "css_types/border.md"
+ - "css_types/color.md"
+ - "css_types/horizontal.md"
+ - "css_types/integer.md"
+ - "css_types/name.md"
+ - "css_types/number.md"
+ - "css_types/overflow.md"
+ - "css_types/percentage.md"
+ - "css_types/scalar.md"
+ - "css_types/text_align.md"
+ - "css_types/text_style.md"
+ - "css_types/vertical.md"
+ - Events:
+ - "events/index.md"
+ - "events/blur.md"
+ - "events/descendant_blur.md"
+ - "events/descendant_focus.md"
+ - "events/enter.md"
+ - "events/focus.md"
+ - "events/hide.md"
+ - "events/key.md"
+ - "events/leave.md"
+ - "events/load.md"
+ - "events/mount.md"
+ - "events/mouse_capture.md"
+ - "events/click.md"
+ - "events/mouse_down.md"
+ - "events/mouse_move.md"
+ - "events/mouse_release.md"
+ - "events/mouse_scroll_down.md"
+ - "events/mouse_scroll_up.md"
+ - "events/mouse_up.md"
+ - "events/paste.md"
+ - "events/resize.md"
+ - "events/screen_resume.md"
+ - "events/screen_suspend.md"
+ - "events/show.md"
+ - Styles:
+ - "styles/align.md"
+ - "styles/background.md"
+ - "styles/border.md"
+ - "styles/border_subtitle_align.md"
+ - "styles/border_subtitle_background.md"
+ - "styles/border_subtitle_color.md"
+ - "styles/border_subtitle_style.md"
+ - "styles/border_title_align.md"
+ - "styles/border_title_background.md"
+ - "styles/border_title_color.md"
+ - "styles/border_title_style.md"
+ - "styles/box_sizing.md"
+ - "styles/color.md"
+ - "styles/content_align.md"
+ - "styles/display.md"
+ - "styles/dock.md"
+ - "styles/index.md"
+ - Grid:
+ - "styles/grid/index.md"
+ - "styles/grid/column_span.md"
+ - "styles/grid/grid_columns.md"
+ - "styles/grid/grid_gutter.md"
+ - "styles/grid/grid_rows.md"
+ - "styles/grid/grid_size.md"
+ - "styles/grid/row_span.md"
+ - "styles/height.md"
+ - "styles/layer.md"
+ - "styles/layers.md"
+ - "styles/layout.md"
+ - Links:
+ - "styles/links/index.md"
+ - "styles/links/link_background.md"
+ - "styles/links/link_color.md"
+ - "styles/links/link_hover_background.md"
+ - "styles/links/link_hover_color.md"
+ - "styles/links/link_hover_style.md"
+ - "styles/links/link_style.md"
+ - "styles/margin.md"
+ - "styles/max_height.md"
+ - "styles/max_width.md"
+ - "styles/min_height.md"
+ - "styles/min_width.md"
+ - "styles/offset.md"
+ - "styles/opacity.md"
+ - "styles/outline.md"
+ - "styles/overflow.md"
+ - "styles/padding.md"
+ - Scrollbar colors:
+ - "styles/scrollbar_colors/index.md"
+ - "styles/scrollbar_colors/scrollbar_background.md"
+ - "styles/scrollbar_colors/scrollbar_background_active.md"
+ - "styles/scrollbar_colors/scrollbar_background_hover.md"
+ - "styles/scrollbar_colors/scrollbar_color.md"
+ - "styles/scrollbar_colors/scrollbar_color_active.md"
+ - "styles/scrollbar_colors/scrollbar_color_hover.md"
+ - "styles/scrollbar_colors/scrollbar_corner_color.md"
+ - "styles/scrollbar_gutter.md"
+ - "styles/scrollbar_size.md"
+ - "styles/text_align.md"
+ - "styles/text_opacity.md"
+ - "styles/text_style.md"
+ - "styles/tint.md"
+ - "styles/visibility.md"
+ - "styles/width.md"
+ - Widgets:
+ - "widgets/button.md"
+ - "widgets/checkbox.md"
+ - "widgets/collapsible.md"
+ - "widgets/content_switcher.md"
+ - "widgets/data_table.md"
+ - "widgets/digits.md"
+ - "widgets/directory_tree.md"
+ - "widgets/footer.md"
+ - "widgets/header.md"
+ - "widgets/index.md"
+ - "widgets/input.md"
+ - "widgets/label.md"
+ - "widgets/list_item.md"
+ - "widgets/list_view.md"
+ - "widgets/loading_indicator.md"
+ - "widgets/log.md"
+ - "widgets/markdown_viewer.md"
+ - "widgets/markdown.md"
+ - "widgets/option_list.md"
+ - "widgets/placeholder.md"
+ - "widgets/pretty.md"
+ - "widgets/progress_bar.md"
+ - "widgets/radiobutton.md"
+ - "widgets/radioset.md"
+ - "widgets/rich_log.md"
+ - "widgets/rule.md"
+ - "widgets/select.md"
+ - "widgets/selection_list.md"
+ - "widgets/sparkline.md"
+ - "widgets/static.md"
+ - "widgets/switch.md"
+ - "widgets/tabbed_content.md"
+ - "widgets/tabs.md"
+ - "widgets/text_area.md"
+ - "widgets/tree.md"
+ - API:
+ - "api/index.md"
+ - "api/app.md"
+ - "api/await_remove.md"
+ - "api/binding.md"
+ - "api/color.md"
+ - "api/command.md"
+ - "api/containers.md"
+ - "api/coordinate.md"
+ - "api/dom_node.md"
+ - "api/events.md"
+ - "api/errors.md"
+ - "api/filter.md"
+ - "api/fuzzy_matcher.md"
+ - "api/geometry.md"
+ - "api/logger.md"
+ - "api/logging.md"
+ - "api/map_geometry.md"
+ - "api/message_pump.md"
+ - "api/message.md"
+ - "api/on.md"
+ - "api/pilot.md"
+ - "api/query.md"
+ - "api/reactive.md"
+ - "api/screen.md"
+ - "api/scrollbar.md"
+ - "api/scroll_view.md"
+ - "api/strip.md"
+ - "api/suggester.md"
+ - "api/system_commands_source.md"
+ - "api/timer.md"
+ - "api/types.md"
+ - "api/validation.md"
+ - "api/walk.md"
+ - "api/widget.md"
+ - "api/work.md"
+ - "api/worker.md"
+ - "api/worker_manager.md"
+ - "How To":
+ - "how-to/index.md"
+ - "how-to/center-things.md"
+ - "how-to/design-a-layout.md"
+ - "FAQ.md"
+ - "roadmap.md"
+ - "Blog":
+ - blog/index.md
diff --git a/poetry.lock b/poetry.lock
index 85f0779436..fcd778a16c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,100 +1,10 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
-
[[package]]
name = "aiohttp"
version = "3.8.5"
description = "Async http client/server framework (asyncio)"
+category = "dev"
optional = false
python-versions = ">=3.6"
-files = [
- {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"},
- {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"},
- {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"},
- {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"},
- {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"},
- {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"},
- {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"},
- {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"},
- {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"},
- {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"},
- {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"},
- {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"},
- {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"},
- {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"},
- {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"},
- {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"},
- {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"},
- {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"},
- {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"},
- {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"},
- {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"},
- {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"},
- {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"},
- {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"},
- {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"},
-]
[package.dependencies]
aiosignal = ">=1.1.2"
@@ -114,12 +24,9 @@ speedups = ["Brotli", "aiodns", "cchardet"]
name = "aiosignal"
version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
- {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
-]
[package.dependencies]
frozenlist = ">=1.1.0"
@@ -128,12 +35,9 @@ frozenlist = ">=1.1.0"
name = "anyio"
version = "3.7.1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
- {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
-]
[package.dependencies]
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
@@ -150,12 +54,9 @@ trio = ["trio (<0.22)"]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
- {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
-]
[package.dependencies]
typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
@@ -164,23 +65,17 @@ typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
name = "asynctest"
version = "0.13.0"
description = "Enhance the standard unittest package with features for testing asyncio libraries"
+category = "dev"
optional = false
python-versions = ">=3.5"
-files = [
- {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
- {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
-]
[[package]]
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
- {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
-]
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
@@ -196,12 +91,9 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "babel"
version = "2.12.1"
description = "Internationalization utilities"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"},
- {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
-]
[package.dependencies]
pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
@@ -210,35 +102,9 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
name = "black"
version = "23.3.0"
description = "The uncompromising code formatter."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
- {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
- {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
- {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
- {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
- {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
- {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
- {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
- {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
- {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
- {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
- {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
- {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
- {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
- {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
- {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
- {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
- {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
- {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
- {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
- {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
- {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
- {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
- {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
- {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
-]
[package.dependencies]
click = ">=8.0.0"
@@ -260,129 +126,41 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "cached-property"
version = "1.5.2"
description = "A decorator for caching properties in classes."
+category = "dev"
optional = false
python-versions = "*"
-files = [
- {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
- {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
-]
[[package]]
name = "certifi"
version = "2023.7.22"
description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
optional = false
python-versions = ">=3.6"
-files = [
- {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
- {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
-]
[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
+category = "dev"
optional = false
python-versions = ">=3.6.1"
-files = [
- {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
- {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
-]
[[package]]
name = "charset-normalizer"
version = "3.2.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "dev"
optional = false
python-versions = ">=3.7.0"
-files = [
- {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
- {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
-]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
- {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
-]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -392,91 +170,25 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
+category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-files = [
- {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
- {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
-]
[[package]]
name = "colored"
version = "1.4.4"
description = "Simple library for color and formatting to terminal"
+category = "dev"
optional = false
python-versions = "*"
-files = [
- {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"},
-]
[[package]]
name = "coverage"
version = "7.2.7"
description = "Code coverage measurement for Python"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
- {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
- {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
- {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
- {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
- {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
- {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
- {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
- {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
- {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
- {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
- {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
- {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
- {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
- {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
- {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
- {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
- {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
- {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
- {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
- {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
- {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
- {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
- {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
- {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
- {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
- {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
- {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
- {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
- {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
- {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
- {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
- {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
- {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
- {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
- {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
- {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
- {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
- {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
- {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
- {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
- {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
- {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
- {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
- {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
- {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
- {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
- {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
- {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
- {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
- {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
- {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
- {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
- {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
- {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
- {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
- {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
- {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
- {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
- {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
-]
[package.extras]
toml = ["tomli"]
@@ -485,23 +197,17 @@ toml = ["tomli"]
name = "distlib"
version = "0.3.7"
description = "Distribution utilities"
+category = "dev"
optional = false
python-versions = "*"
-files = [
- {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
- {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
-]
[[package]]
name = "exceptiongroup"
version = "1.1.3"
description = "Backport of PEP 654 (exception groups)"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
- {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
-]
[package.extras]
test = ["pytest (>=6)"]
@@ -510,12 +216,9 @@ test = ["pytest (>=6)"]
name = "filelock"
version = "3.12.2"
description = "A platform independent file lock."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
- {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
-]
[package.extras]
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
@@ -525,95 +228,17 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p
name = "frozenlist"
version = "1.3.3"
description = "A list-like structure which implements collections.abc.MutableSequence"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"},
- {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"},
- {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"},
- {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"},
- {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"},
- {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"},
- {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"},
- {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"},
- {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"},
- {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"},
- {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"},
- {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"},
- {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"},
- {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"},
- {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"},
- {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"},
- {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"},
- {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"},
- {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"},
- {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"},
- {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"},
- {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"},
- {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"},
- {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"},
- {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"},
- {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"},
- {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"},
- {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"},
- {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"},
- {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"},
- {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"},
- {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"},
- {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"},
- {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"},
- {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"},
- {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"},
- {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"},
- {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"},
- {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"},
- {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"},
- {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"},
- {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"},
- {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"},
- {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"},
- {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"},
- {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"},
- {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"},
- {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"},
- {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"},
- {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"},
- {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"},
- {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"},
- {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"},
- {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"},
- {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"},
- {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"},
- {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"},
- {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"},
- {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"},
- {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"},
- {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"},
- {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"},
- {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"},
- {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"},
- {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"},
- {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"},
- {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"},
- {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"},
- {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"},
- {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"},
- {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"},
- {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"},
- {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"},
- {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"},
-]
[[package]]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
+category = "dev"
optional = false
python-versions = "*"
-files = [
- {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
- {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
-]
[package.dependencies]
python-dateutil = ">=2.8.1"
@@ -625,41 +250,35 @@ dev = ["flake8", "markdown", "twine", "wheel"]
name = "gitdb"
version = "4.0.10"
description = "Git Object Database"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"},
- {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"},
-]
[package.dependencies]
smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
-version = "3.1.34"
+version = "3.1.36"
description = "GitPython is a Python library used to interact with Git repositories"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "GitPython-3.1.34-py3-none-any.whl", hash = "sha256:5d3802b98a3bae1c2b8ae0e1ff2e4aa16bcdf02c145da34d092324f599f01395"},
- {file = "GitPython-3.1.34.tar.gz", hash = "sha256:85f7d365d1f6bf677ae51039c1ef67ca59091c7ebd5a3509aa399d4eda02d6dd"},
-]
[package.dependencies]
gitdb = ">=4.0.1,<5"
typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
+[package.extras]
+test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar", "virtualenv"]
+
[[package]]
name = "griffe"
version = "0.30.1"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"},
- {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"},
-]
[package.dependencies]
cached-property = {version = "*", markers = "python_version < \"3.8\""}
@@ -669,12 +288,9 @@ colorama = ">=0.4"
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
- {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
-]
[package.dependencies]
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
@@ -683,33 +299,27 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
name = "httpcore"
version = "0.16.3"
description = "A minimal low-level HTTP client."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
- {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
-]
[package.dependencies]
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
-sniffio = "==1.*"
+sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
version = "0.23.3"
description = "The next generation HTTP client."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
- {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
-]
[package.dependencies]
certifi = "*"
@@ -719,20 +329,17 @@ sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
-cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.24"
description = "File identification library for Python"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
- {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
-]
[package.extras]
license = ["ukkonen"]
@@ -741,23 +348,17 @@ license = ["ukkonen"]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
optional = false
python-versions = ">=3.5"
-files = [
- {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
- {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
-]
[[package]]
name = "importlib-metadata"
version = "6.7.0"
description = "Read metadata from Python packages"
+category = "main"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
- {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
-]
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
@@ -772,23 +373,17 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
- {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
-]
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
- {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
-]
[package.dependencies]
MarkupSafe = ">=2.0"
@@ -800,12 +395,9 @@ i18n = ["Babel (>=2.7)"]
name = "linkify-it-py"
version = "2.0.2"
description = "Links recognition library with FULL unicode support."
+category = "main"
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"},
-]
[package.dependencies]
uc-micro-py = "*"
@@ -820,12 +412,9 @@ test = ["coverage", "pytest", "pytest-cov"]
name = "markdown"
version = "3.4.4"
description = "Python implementation of John Gruber's Markdown."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"},
- {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"},
-]
[package.dependencies]
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
@@ -838,12 +427,9 @@ testing = ["coverage", "pyyaml"]
name = "markdown-it-py"
version = "2.2.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
+category = "main"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"},
- {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"},
-]
[package.dependencies]
linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
@@ -865,71 +451,17 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
- {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
- {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
- {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
- {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
- {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
- {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
-]
[[package]]
name = "mdit-py-plugins"
version = "0.3.5"
description = "Collection of plugins for markdown-it-py"
+category = "main"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"},
- {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"},
-]
[package.dependencies]
markdown-it-py = ">=1.0.0,<3.0.0"
@@ -943,34 +475,25 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
+category = "main"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
- {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
-]
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
+category = "dev"
optional = false
python-versions = ">=3.6"
-files = [
- {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
- {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
-]
[[package]]
name = "mkdocs"
-version = "1.5.2"
+version = "1.5.3"
description = "Project documentation with Markdown."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mkdocs-1.5.2-py3-none-any.whl", hash = "sha256:60a62538519c2e96fe8426654a67ee177350451616118a41596ae7c876bb7eac"},
- {file = "mkdocs-1.5.2.tar.gz", hash = "sha256:70d0da09c26cff288852471be03c23f0f521fc15cf16ac89c7a3bfb9ae8d24f9"},
-]
[package.dependencies]
click = ">=7.0"
@@ -997,12 +520,9 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
name = "mkdocs-autorefs"
version = "0.4.1"
description = "Automatically link across pages in MkDocs."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
- {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
-]
[package.dependencies]
Markdown = ">=3.3"
@@ -1012,11 +532,9 @@ mkdocs = ">=1.1"
name = "mkdocs-exclude"
version = "1.0.2"
description = "A mkdocs plugin that lets you exclude files or trees."
+category = "dev"
optional = false
python-versions = "*"
-files = [
- {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"},
-]
[package.dependencies]
mkdocs = "*"
@@ -1025,12 +543,9 @@ mkdocs = "*"
name = "mkdocs-material"
version = "9.2.7"
description = "Documentation that simply works"
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mkdocs_material-9.2.7-py3-none-any.whl", hash = "sha256:92e4160d191cc76121fed14ab9f14638e43a6da0f2e9d7a9194d377f0a4e7f18"},
- {file = "mkdocs_material-9.2.7.tar.gz", hash = "sha256:b44da35b0d98cd762d09ef74f1ddce5b6d6e35c13f13beb0c9d82a629e5f229e"},
-]
[package.dependencies]
babel = ">=2.10,<3.0"
@@ -1049,44 +564,35 @@ requests = ">=2.26,<3.0"
name = "mkdocs-material-extensions"
version = "1.1.1"
description = "Extension pack for Python Markdown and MkDocs Material."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"},
- {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"},
-]
[[package]]
name = "mkdocs-rss-plugin"
version = "1.5.0"
description = "MkDocs plugin which generates a static RSS feed using git log and page.meta."
+category = "dev"
optional = false
python-versions = ">=3.7, <4"
-files = [
- {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"},
- {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"},
-]
[package.dependencies]
GitPython = ">=3.1,<3.2"
mkdocs = ">=1.1,<2"
-pytz = {version = "==2022.*", markers = "python_version < \"3.9\""}
-tzdata = {version = "==2022.*", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
+pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""}
+tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
[package.extras]
-dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (==4.0.*)", "validator-collection (>=1.5,<1.6)"]
-doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (==0.5.*)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"]
+dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"]
+doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"]
[[package]]
name = "mkdocstrings"
version = "0.20.0"
description = "Automatic documentation from sources, for MkDocs."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"},
- {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"},
-]
[package.dependencies]
Jinja2 = ">=2.11.1"
@@ -1106,24 +612,1134 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
name = "mkdocstrings-python"
version = "0.10.1"
description = "A Python handler for mkdocstrings."
+category = "dev"
optional = false
python-versions = ">=3.7"
-files = [
+
+[package.dependencies]
+griffe = ">=0.24"
+mkdocstrings = ">=0.20"
+
+[[package]]
+name = "msgpack"
+version = "1.0.5"
+description = "MessagePack serializer"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "multidict"
+version = "6.0.4"
+description = "multidict implementation"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "mypy"
+version = "1.4.1"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "packaging"
+version = "23.1"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "paginate"
+version = "0.5.6"
+description = "Divides large result sets into pages for easier browsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pathspec"
+version = "0.11.2"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "platformdirs"
+version = "3.10.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""}
+
+[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)"]
+
+[[package]]
+name = "pluggy"
+version = "1.2.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pre-commit"
+version = "2.21.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pygments"
+version = "2.16.1"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+plugins = ["importlib-metadata"]
+
+[[package]]
+name = "pymdown-extensions"
+version = "10.2.1"
+description = "Extension pack for Python Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+markdown = ">=3.2"
+pyyaml = "*"
+
+[package.extras]
+extra = ["pygments (>=2.12)"]
+
+[[package]]
+name = "pytest"
+version = "7.4.2"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-aiohttp"
+version = "1.0.5"
+description = "Pytest plugin for aiohttp support"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+aiohttp = ">=3.8.1"
+pytest = ">=6.1.0"
+pytest-asyncio = ">=0.17.2"
+
+[package.extras]
+testing = ["coverage (==6.2)", "mypy (==0.931)"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.21.1"
+description = "Pytest support for asyncio"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pytest = ">=7.0.0"
+typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+
+[[package]]
+name = "pytest-cov"
+version = "2.12.1"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+coverage = ">=5.2.1"
+pytest = ">=4.6"
+toml = "*"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytest-textual-snapshot"
+version = "0.4.0"
+description = "Snapshot testing for Textual apps"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+jinja2 = ">=3.0.0"
+pytest = ">=7.0.0"
+rich = ">=12.0.0"
+syrupy = ">=3.0.0"
+textual = ">=0.28.0"
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2022.7.1"
+description = "World timezone definitions, modern and historical"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+description = "A custom YAML tag for referencing environment variables in YAML files. "
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "regex"
+version = "2022.10.31"
+description = "Alternative regular expression module, to replace re."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "requests"
+version = "2.31.0"
+description = "Python HTTP for Humans."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "rich"
+version = "13.5.3"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "setuptools"
+version = "68.0.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "smmap"
+version = "5.0.1"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "syrupy"
+version = "3.0.6"
+description = "Pytest Snapshot Test Utility"
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4"
+
+[package.dependencies]
+colored = ">=1.3.92,<2.0.0"
+pytest = ">=5.1.0,<8.0.0"
+
+[[package]]
+name = "textual-dev"
+version = "1.1.0"
+description = "Development tools for working with Textual"
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+aiohttp = ">=3.8.1"
+click = ">=8.1.2"
+msgpack = ">=1.0.3"
+textual = ">=0.32.0"
+typing-extensions = ">=4.4.0,<5.0.0"
+
+[[package]]
+name = "time-machine"
+version = "2.10.0"
+description = "Travel through time in your tests."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+python-dateutil = "*"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tree-sitter"
+version = "0.20.2"
+description = "Python bindings for the Tree-Sitter parsing library"
+category = "main"
+optional = false
+python-versions = ">=3.3"
+
+[[package]]
+name = "tree-sitter-languages"
+version = "1.7.0"
+description = "Binary Python wheels for all tree sitter languages."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+tree-sitter = "*"
+
+[[package]]
+name = "typed-ast"
+version = "1.5.5"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "types-setuptools"
+version = "67.8.0.0"
+description = "Typing stubs for setuptools"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-tree-sitter"
+version = "0.20.1.5"
+description = "Typing stubs for tree-sitter"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-tree-sitter-languages"
+version = "1.7.0.1"
+description = "Typing stubs for tree-sitter-languages"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+types-tree-sitter = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "4.7.1"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tzdata"
+version = "2022.7"
+description = "Provider of IANA time zone data"
+category = "dev"
+optional = false
+python-versions = ">=2"
+
+[[package]]
+name = "uc-micro-py"
+version = "1.0.2"
+description = "Micro subset of unicode data files for linkify-it-py projects."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["coverage", "pytest", "pytest-cov"]
+
+[[package]]
+name = "urllib3"
+version = "2.0.5"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.24.5"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
+platformdirs = ">=3.9.1,<4"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
+[[package]]
+name = "watchdog"
+version = "3.0.0"
+description = "Filesystem events monitoring"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[[package]]
+name = "yarl"
+version = "1.9.2"
+description = "Yet another URL library"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "zipp"
+version = "3.15.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.7"
+content-hash = "c53cf8b109a11121625f7fb1037b22ff677dea740b70a4318edbd2829ea6080b"
+
+[metadata.files]
+aiohttp = [
+ {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"},
+ {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"},
+ {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"},
+ {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"},
+ {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"},
+ {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"},
+ {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"},
+ {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"},
+ {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"},
+ {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"},
+ {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"},
+ {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"},
+ {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"},
+ {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"},
+ {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"},
+ {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"},
+ {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"},
+ {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"},
+ {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"},
+ {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"},
+ {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"},
+ {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"},
+ {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"},
+ {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"},
+ {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"},
+ {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"},
+ {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"},
+ {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"},
+ {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"},
+ {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"},
+ {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"},
+ {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"},
+ {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"},
+ {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"},
+ {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"},
+ {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"},
+ {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"},
+ {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"},
+ {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"},
+ {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"},
+ {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"},
+ {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"},
+ {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"},
+ {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"},
+ {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"},
+ {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"},
+ {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"},
+ {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"},
+ {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"},
+ {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"},
+ {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"},
+ {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"},
+ {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"},
+ {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"},
+ {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"},
+ {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"},
+ {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"},
+ {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"},
+ {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"},
+ {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"},
+ {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"},
+ {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"},
+ {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"},
+]
+aiosignal = [
+ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
+ {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
+]
+anyio = [
+ {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
+ {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
+]
+async-timeout = [
+ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
+ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
+]
+asynctest = [
+ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
+ {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
+]
+attrs = [
+ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+ {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
+]
+babel = [
+ {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"},
+ {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
+]
+black = [
+ {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
+ {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
+ {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
+ {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
+ {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
+ {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
+ {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
+ {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
+ {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
+ {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
+ {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
+ {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
+ {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
+ {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
+ {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
+ {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
+ {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
+ {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
+ {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
+ {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
+ {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
+ {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
+ {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
+ {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
+ {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
+]
+cached-property = [
+ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
+ {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
+]
+certifi = [
+ {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
+ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
+]
+cfgv = [
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+charset-normalizer = [
+ {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
+ {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
+]
+click = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+colorama = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+colored = [
+ {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"},
+]
+coverage = [
+ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+ {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+ {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+ {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+ {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+ {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+ {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+ {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+ {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+ {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+ {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+ {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+ {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+ {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+ {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+ {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+ {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
+]
+distlib = [
+ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
+ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
+]
+exceptiongroup = [
+ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
+ {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
+]
+filelock = [
+ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
+ {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
+]
+frozenlist = [
+ {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"},
+ {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"},
+ {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"},
+ {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"},
+ {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"},
+ {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"},
+ {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"},
+ {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"},
+ {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"},
+ {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"},
+ {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"},
+ {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"},
+ {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"},
+ {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"},
+ {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"},
+ {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"},
+ {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"},
+ {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"},
+ {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"},
+ {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"},
+ {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"},
+ {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"},
+ {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"},
+ {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"},
+ {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"},
+ {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"},
+ {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"},
+ {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"},
+ {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"},
+ {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"},
+ {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"},
+ {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"},
+ {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"},
+ {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"},
+ {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"},
+ {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"},
+ {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"},
+ {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"},
+ {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"},
+ {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"},
+ {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"},
+ {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"},
+ {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"},
+ {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"},
+ {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"},
+ {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"},
+ {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"},
+ {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"},
+ {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"},
+ {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"},
+ {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"},
+ {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"},
+ {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"},
+ {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"},
+ {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"},
+ {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"},
+ {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"},
+ {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"},
+ {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"},
+ {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"},
+ {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"},
+ {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"},
+]
+ghp-import = [
+ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
+ {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
+]
+gitdb = [
+ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"},
+ {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"},
+]
+gitpython = [
+ {file = "GitPython-3.1.36-py3-none-any.whl", hash = "sha256:8d22b5cfefd17c79914226982bb7851d6ade47545b1735a9d010a2a4c26d8388"},
+ {file = "GitPython-3.1.36.tar.gz", hash = "sha256:4bb0c2a6995e85064140d31a33289aa5dce80133a23d36fcd372d716c54d3ebf"},
+]
+griffe = [
+ {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"},
+ {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"},
+]
+h11 = [
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+httpcore = [
+ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
+ {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
+]
+httpx = [
+ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
+ {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
+]
+identify = [
+ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
+ {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
+]
+idna = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
+ {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
+]
+iniconfig = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+jinja2 = [
+ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+linkify-it-py = [
+ {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"},
+]
+markdown = [
+ {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"},
+ {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"},
+]
+markdown-it-py = [
+ {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"},
+ {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"},
+]
+markupsafe = [
+ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
+ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
+]
+mdit-py-plugins = [
+ {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"},
+ {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"},
+]
+mdurl = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+mergedeep = [
+ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
+ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
+]
+mkdocs = [
+ {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"},
+ {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"},
+]
+mkdocs-autorefs = [
+ {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
+ {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
+]
+mkdocs-exclude = [
+ {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"},
+]
+mkdocs-material = [
+ {file = "mkdocs_material-9.2.7-py3-none-any.whl", hash = "sha256:92e4160d191cc76121fed14ab9f14638e43a6da0f2e9d7a9194d377f0a4e7f18"},
+ {file = "mkdocs_material-9.2.7.tar.gz", hash = "sha256:b44da35b0d98cd762d09ef74f1ddce5b6d6e35c13f13beb0c9d82a629e5f229e"},
+]
+mkdocs-material-extensions = [
+ {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"},
+ {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"},
+]
+mkdocs-rss-plugin = [
+ {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"},
+ {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"},
+]
+mkdocstrings = [
+ {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"},
+ {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"},
+]
+mkdocstrings-python = [
{file = "mkdocstrings_python-0.10.1-py3-none-any.whl", hash = "sha256:ef239cee2c688e2b949a0a47e42a141d744dd12b7007311b3309dc70e3bafc5c"},
{file = "mkdocstrings_python-0.10.1.tar.gz", hash = "sha256:b72301fff739070ec517b5b36bf2f7c49d1360a275896a64efb97fc17d3f3968"},
]
-
-[package.dependencies]
-griffe = ">=0.24"
-mkdocstrings = ">=0.20"
-
-[[package]]
-name = "msgpack"
-version = "1.0.5"
-description = "MessagePack serializer"
-optional = false
-python-versions = "*"
-files = [
+msgpack = [
{file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"},
{file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"},
{file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"},
@@ -1188,14 +1804,7 @@ files = [
{file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"},
{file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"},
]
-
-[[package]]
-name = "multidict"
-version = "6.0.4"
-description = "multidict implementation"
-optional = false
-python-versions = ">=3.7"
-files = [
+multidict = [
{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"},
@@ -1271,14 +1880,7 @@ files = [
{file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
{file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
]
-
-[[package]]
-name = "mypy"
-version = "1.4.1"
-description = "Optional static typing for Python"
-optional = false
-python-versions = ">=3.7"
-files = [
+mypy = [
{file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"},
{file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"},
{file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"},
@@ -1306,293 +1908,74 @@ files = [
{file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"},
{file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"},
]
-
-[package.dependencies]
-mypy-extensions = ">=1.0.0"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
-typing-extensions = ">=4.1.0"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-install-types = ["pip"]
-python2 = ["typed-ast (>=1.4.0,<2)"]
-reports = ["lxml"]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.0.0"
-description = "Type system extensions for programs checked with the mypy type checker."
-optional = false
-python-versions = ">=3.5"
-files = [
+mypy-extensions = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
-
-[[package]]
-name = "nodeenv"
-version = "1.8.0"
-description = "Node.js virtual environment builder"
-optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
-files = [
+nodeenv = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
]
-
-[package.dependencies]
-setuptools = "*"
-
-[[package]]
-name = "packaging"
-version = "23.1"
-description = "Core utilities for Python packages"
-optional = false
-python-versions = ">=3.7"
-files = [
+packaging = [
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
-
-[[package]]
-name = "paginate"
-version = "0.5.6"
-description = "Divides large result sets into pages for easier browsing"
-optional = false
-python-versions = "*"
-files = [
+paginate = [
{file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
]
-
-[[package]]
-name = "pathspec"
-version = "0.11.2"
-description = "Utility library for gitignore style pattern matching of file paths."
-optional = false
-python-versions = ">=3.7"
-files = [
+pathspec = [
{file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
{file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
]
-
-[[package]]
-name = "platformdirs"
-version = "3.10.0"
-description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-optional = false
-python-versions = ">=3.7"
-files = [
+platformdirs = [
{file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
{file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
]
-
-[package.dependencies]
-typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""}
-
-[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)"]
-
-[[package]]
-name = "pluggy"
-version = "1.2.0"
-description = "plugin and hook calling mechanisms for python"
-optional = false
-python-versions = ">=3.7"
-files = [
+pluggy = [
{file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
{file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
]
-
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
-
-[[package]]
-name = "pre-commit"
-version = "2.21.0"
-description = "A framework for managing and maintaining multi-language pre-commit hooks."
-optional = false
-python-versions = ">=3.7"
-files = [
+pre-commit = [
{file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
{file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
]
-
-[package.dependencies]
-cfgv = ">=2.0.0"
-identify = ">=1.0.0"
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-nodeenv = ">=0.11.1"
-pyyaml = ">=5.1"
-virtualenv = ">=20.10.0"
-
-[[package]]
-name = "pygments"
-version = "2.16.1"
-description = "Pygments is a syntax highlighting package written in Python."
-optional = false
-python-versions = ">=3.7"
-files = [
+pygments = [
{file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
]
-
-[package.extras]
-plugins = ["importlib-metadata"]
-
-[[package]]
-name = "pymdown-extensions"
-version = "10.2.1"
-description = "Extension pack for Python Markdown."
-optional = false
-python-versions = ">=3.7"
-files = [
+pymdown-extensions = [
{file = "pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4"},
{file = "pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591"},
]
-
-[package.dependencies]
-markdown = ">=3.2"
-pyyaml = "*"
-
-[package.extras]
-extra = ["pygments (>=2.12)"]
-
-[[package]]
-name = "pytest"
-version = "7.4.1"
-description = "pytest: simple powerful testing with Python"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"},
- {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"},
+pytest = [
+ {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
+ {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
]
-
-[package.dependencies]
-colorama = {version = "*", markers = "sys_platform == \"win32\""}
-exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-iniconfig = "*"
-packaging = "*"
-pluggy = ">=0.12,<2.0"
-tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
-
-[package.extras]
-testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
-
-[[package]]
-name = "pytest-aiohttp"
-version = "1.0.4"
-description = "Pytest plugin for aiohttp support"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"},
- {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"},
+pytest-aiohttp = [
+ {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"},
+ {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"},
]
-
-[package.dependencies]
-aiohttp = ">=3.8.1"
-pytest = ">=6.1.0"
-pytest-asyncio = ">=0.17.2"
-
-[package.extras]
-testing = ["coverage (==6.2)", "mypy (==0.931)"]
-
-[[package]]
-name = "pytest-asyncio"
-version = "0.21.1"
-description = "Pytest support for asyncio"
-optional = false
-python-versions = ">=3.7"
-files = [
+pytest-asyncio = [
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
]
-
-[package.dependencies]
-pytest = ">=7.0.0"
-typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
-
-[package.extras]
-docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
-testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
-
-[[package]]
-name = "pytest-cov"
-version = "2.12.1"
-description = "Pytest plugin for measuring coverage."
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-files = [
+pytest-cov = [
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
{file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
]
-
-[package.dependencies]
-coverage = ">=5.2.1"
-pytest = ">=4.6"
-toml = "*"
-
-[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
-
-[[package]]
-name = "pytest-textual-snapshot"
-version = "0.4.0"
-description = "Snapshot testing for Textual apps"
-optional = false
-python-versions = ">=3.6,<4.0"
-files = [
+pytest-textual-snapshot = [
{file = "pytest_textual_snapshot-0.4.0-py3-none-any.whl", hash = "sha256:879cc5de29cdd31cfe1b6daeb1dc5e42682abebcf4f88e7e3375bd5200683fc0"},
{file = "pytest_textual_snapshot-0.4.0.tar.gz", hash = "sha256:63782e053928a925d88ff7359dd640f2900e23bc708b3007f8b388e65f2527cb"},
]
-
-[package.dependencies]
-jinja2 = ">=3.0.0"
-pytest = ">=7.0.0"
-rich = ">=12.0.0"
-syrupy = ">=3.0.0"
-textual = ">=0.28.0"
-
-[[package]]
-name = "python-dateutil"
-version = "2.8.2"
-description = "Extensions to the standard Python datetime module"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-files = [
- {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
- {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
-]
-
-[package.dependencies]
-six = ">=1.5"
-
-[[package]]
-name = "pytz"
-version = "2022.7.1"
-description = "World timezone definitions, modern and historical"
-optional = false
-python-versions = "*"
-files = [
+python-dateutil = [
+ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+pytz = [
{file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
{file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
]
-
-[[package]]
-name = "pyyaml"
-version = "6.0.1"
-description = "YAML parser and emitter for Python"
-optional = false
-python-versions = ">=3.6"
-files = [
+pyyaml = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
@@ -1634,28 +2017,11 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
-
-[[package]]
-name = "pyyaml-env-tag"
-version = "0.1"
-description = "A custom YAML tag for referencing environment variables in YAML files. "
-optional = false
-python-versions = ">=3.6"
-files = [
+pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
-
-[package.dependencies]
-pyyaml = "*"
-
-[[package]]
-name = "regex"
-version = "2022.10.31"
-description = "Alternative regular expression module, to replace re."
-optional = false
-python-versions = ">=3.6"
-files = [
+regex = [
{file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"},
{file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"},
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"},
@@ -1745,153 +2111,43 @@ files = [
{file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"},
{file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"},
]
-
-[[package]]
-name = "requests"
-version = "2.31.0"
-description = "Python HTTP for Humans."
-optional = false
-python-versions = ">=3.7"
-files = [
+requests = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
]
-
-[package.dependencies]
-certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
-idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<3"
-
-[package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
-
-[[package]]
-name = "rfc3986"
-version = "1.5.0"
-description = "Validating URI References per RFC 3986"
-optional = false
-python-versions = "*"
-files = [
+rfc3986 = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
-
-[package.dependencies]
-idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
-
-[package.extras]
-idna2008 = ["idna"]
-
-[[package]]
-name = "rich"
-version = "13.5.2"
-description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-optional = false
-python-versions = ">=3.7.0"
-files = [
- {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"},
- {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"},
+rich = [
+ {file = "rich-13.5.3-py3-none-any.whl", hash = "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9"},
+ {file = "rich-13.5.3.tar.gz", hash = "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6"},
]
-
-[package.dependencies]
-markdown-it-py = ">=2.2.0"
-pygments = ">=2.13.0,<3.0.0"
-typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
-
-[package.extras]
-jupyter = ["ipywidgets (>=7.5.1,<9)"]
-
-[[package]]
-name = "setuptools"
-version = "68.0.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-optional = false
-python-versions = ">=3.7"
-files = [
+setuptools = [
{file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
{file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
-
-[[package]]
-name = "six"
-version = "1.16.0"
-description = "Python 2 and 3 compatibility utilities"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-files = [
+six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
-
-[[package]]
-name = "smmap"
-version = "5.0.0"
-description = "A pure Python implementation of a sliding window memory map manager"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
- {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+smmap = [
+ {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"},
+ {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"},
]
-
-[[package]]
-name = "sniffio"
-version = "1.3.0"
-description = "Sniff out which async library your code is running under"
-optional = false
-python-versions = ">=3.7"
-files = [
+sniffio = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
-
-[[package]]
-name = "syrupy"
-version = "3.0.6"
-description = "Pytest Snapshot Test Utility"
-optional = false
-python-versions = ">=3.7,<4"
-files = [
+syrupy = [
{file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"},
{file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"},
]
-
-[package.dependencies]
-colored = ">=1.3.92,<2.0.0"
-pytest = ">=5.1.0,<8.0.0"
-
-[[package]]
-name = "textual-dev"
-version = "1.1.0"
-description = "Development tools for working with Textual"
-optional = false
-python-versions = ">=3.7,<4.0"
-files = [
+textual-dev = [
{file = "textual_dev-1.1.0-py3-none-any.whl", hash = "sha256:c57320636098e31fa5d5c29fc3bc60829bb420da3c76bfed24db6eacf178dbc6"},
{file = "textual_dev-1.1.0.tar.gz", hash = "sha256:e2f8ce4e1c18a16b80282f3257cd2feb49a7ede289a78908c9063ce071bb77ce"},
]
-
-[package.dependencies]
-aiohttp = ">=3.8.1"
-click = ">=8.1.2"
-msgpack = ">=1.0.3"
-textual = ">=0.32.0"
-typing-extensions = ">=4.4.0,<5.0.0"
-
-[[package]]
-name = "time-machine"
-version = "2.10.0"
-description = "Travel through time in your tests."
-optional = false
-python-versions = ">=3.7"
-files = [
+time-machine = [
{file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d5e93c14b935d802a310c1d4694a9fe894b48a733ebd641c9a570d6f9e1f667"},
{file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c0dda6b132c0180941944ede357109016d161d840384c2fb1096a3a2ef619f4"},
{file = "time_machine-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:900517e4a4121bf88527343d6aea2b5c99df134815bb8271ef589ec792502a71"},
@@ -1947,39 +2203,124 @@ files = [
{file = "time_machine-2.10.0-cp39-cp39-win_arm64.whl", hash = "sha256:c1775a949dd830579d1af5a271ec53d920dc01657035ad305f55c5a1ac9b9f1e"},
{file = "time_machine-2.10.0.tar.gz", hash = "sha256:64fd89678cf589fc5554c311417128b2782222dd65f703bf248ef41541761da0"},
]
-
-[package.dependencies]
-python-dateutil = "*"
-
-[[package]]
-name = "toml"
-version = "0.10.2"
-description = "Python Library for Tom's Obvious, Minimal Language"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-files = [
+toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
-
-[[package]]
-name = "tomli"
-version = "2.0.1"
-description = "A lil' TOML parser"
-optional = false
-python-versions = ">=3.7"
-files = [
+tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
-
-[[package]]
-name = "typed-ast"
-version = "1.5.5"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-optional = false
-python-versions = ">=3.6"
-files = [
+tree-sitter = [
+ {file = "tree_sitter-0.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a151ccf9233b0b84850422654247f68a4d78f548425c76520402ea6fb6cdb24"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52ca2738c3c4c660c83054ac3e44a49cbecb9f89dc26bb8e154d6ca288aa06b0"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d51478ea078da7cc6f626e9e36f131bbc5fac036cf38ea4b5b81632cbac37d"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0b2b59e1633efbf19cd2ed1ceb8d51b2c44a278153b1113998c70bc1570b750"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7f691c57d2a65d6e53e2f3574153c9cd0c157ff938b8d6f252edd5e619811403"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba72a363387eebaff9a0b788f864fe47da425136cbd4cac6cd125051f043c296"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-win32.whl", hash = "sha256:55e33eb206446d5046d3b5fe36ab300840f5a8a844246adb0ccc68c55c30b722"},
+ {file = "tree_sitter-0.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ce9d14daba0a71a778417d9d61dd4038ca96981ddec19e1e8990881469321c"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:942dbfb8bc380f09b0e323d3884de07d19022930516f33b7503a6eb5f6e18979"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee5651c11924d426f8d6858a40fd5090ae31574f81ef180bef2055282f43bf62"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fb6982b480031628dad7f229c4c8d90b17d4c281ba97848d3b100666d7fa45f"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:067609c6c7cb6e5a6c4be50076a380fe52b6e8f0641ee9d0da33b24a5b972e82"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:849d7e6b66fe7ded08a633943b30e0ed807eee76104288e6c6841433f4a9651b"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e85689573797e49f86e2d7cf48b9dd23bc044c477df074a78546e666d6990a29"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-win32.whl", hash = "sha256:098906148e44ea391a91b019d584dd8d0ea1437af62a9744e280e93163fd35ca"},
+ {file = "tree_sitter-0.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:2753a87094b72fe7f02276b3948155618f53aa14e1ca20588f0eeed510f68512"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5de192cb9e7b1c882d45418decb7899f1547f7056df756bcae186bbf4966d96e"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3a77e663293a73a97edbf2a2e05001de08933eb5d311a16bdc25b9b2fac54f3"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415da4a70c56a003758537517fe9e60b8b0c5f70becde54cc8b8f3ba810adc70"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:707fb4d7a6123b8f9f2b005d61194077c3168c0372556e7418802280eddd4892"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:75fcbfb0a61ad64e7f787eb3f8fbf29b8e2b858dc011897ad039d838a06cee02"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-win32.whl", hash = "sha256:622926530895d939fa6e1e2487e71a311c71d3b09f4c4f19301695ea866304a4"},
+ {file = "tree_sitter-0.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:5c0712f031271d9bc462f1db7623d23703ed9fbcbaa6dc19ba535f58d6110774"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dfdf680ecf5619447243c4c20e4040a7b5e7afca4e1569f03c814e86bfda248"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79650ee23a15559b69542c71ed9eb3297dce21932a7c5c148be384dd0f2cd49d"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63059746b4b2f2f87dd19c208141c69452694aae32459b7a4ebca8539d13bf4"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9398d1e214d4915032cf68a678de7eb803f64d25ef04724d70b88db7bb7746e9"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b506fb2e2bd7a5a1603c644bbb90401fe488f86bbca39706addaa8d2bfc80815"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-win32.whl", hash = "sha256:405e83804ba60ca1c3dbd258adbe0d7b0f1bdce948e5eec5587a2ebedcf930ba"},
+ {file = "tree_sitter-0.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a1e66d211c04144484e223922ac094a2367476e6f57000f986c5560dc5a83c6e"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f8adc325c74c042204ed47d095e0ec86f83de3c7ec4979645f86b58514f60297"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb49c861e1d111e0df119ecbfaa409e6413b8d91e8f56bcdb15f07fbc35594e"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e17ee83409b01fdd09021997b0c747be2f773bb2bb140ba6fb48b7e12fdd039a"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475ab841647a0d1bc1266c8978279f8e4f7b9520b9a7336d532e5dfc8910214d"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:222350189675d9814966a5c88c6c1378a2ee2f3041c439a6f1d1ff2006f403aa"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:31ea52f0deee70f2cb00aff01e40aae325a34ebe1661de274c9107322fb95f54"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-win32.whl", hash = "sha256:cceaf7287137cbca707006624a4a8d4b5ccbfec025793fde84d90524c2bb0946"},
+ {file = "tree_sitter-0.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:25b9669911f21ec2b3727bb2f4dfeff6ddb6f81898c3e968d378a660e0d7f90e"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce30a17f46a6b39a04a599dea88c127a19e3e1f43a2ad0ced71b5c032d585077"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9576e8b2e663639527e01ab251b87f0bd370bfdd40515588689ebc424aec786"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d03731a498f624ce3536c821ef23b03d1ad569b3845b326a5b7149ef189d732c"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0116ecb163573ebaa0fc04cc99c90bd94c0be5cc4d0a1ebeb102de9cc9a054"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0943b00d3700f253c3ee6a53a71b9a6ca46defd9c0a33edb07a9388e70dc3a9e"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cb566b6f0b5457148cb8310a1ca3d764edf28e47fcccfe0b167861ecaa50c12"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-win32.whl", hash = "sha256:4544204a24c2b4d25d1731b0df83f7c819ce87c4f2538a19724b8753815ef388"},
+ {file = "tree_sitter-0.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:9517b204e471d6aa59ee2232f6220f315ed5336079034d5c861a24660d6511d6"},
+ {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:84343678f58cb354d22ed14b627056ffb33c540cf16c35a83db4eeee8827b935"},
+ {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:611a80171d8fa6833dd0c8b022714d2ea789de15a955ec42ec4fd5fcc1032edb"},
+ {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bacecfb61694c95ccee462742b3fcea50ba1baf115c42e60adf52b549ef642ce"},
+ {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f344ae94a268479456f19712736cc7398de5822dc74cca7d39538c28085721d0"},
+ {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:221784d7f326fe81ce7174ac5972800f58b9a7c5c48a03719cad9830c22e5a76"},
+ {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64210ed8d2a1b7e2951f6576aa0cb7be31ad06d87da26c52961318fc54c7fe77"},
+ {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2634ac73b39ceacfa431d6d95692eae7465977fa0b9e9f7ae6cb445991e829a5"},
+ {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:71663a0e8230dae99d9c55e6895bd2c9e42534ec861b255775f704ae2db70c1d"},
+ {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:32c3e0f30b45a58d36bf6a0ec982ca3eaa23c7f924628da499b7ad22a8abad71"},
+ {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b02e4ab2158c25f6f520c93318d562da58fa4ba53e1dbd434be008f48104980"},
+ {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10e567eb6961a1e86aebbe26a9ca07d324f8529bca90937a924f8aa0ea4dc127"},
+ {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63f8e8e69f5f25c2b565449e1b8a2aa7b6338b4f37c8658c5fbdec04858c30be"},
+ {file = "tree_sitter-0.20.2.tar.gz", hash = "sha256:0a6c06abaa55de174241a476b536173bba28241d2ea85d198d33aa8bf009f028"},
+]
+tree-sitter-languages = [
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd8b856c224a74c395ed9495761c3ef8ba86014dbf6037d73634436ae683c808"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:277d1bec6e101a26a4445cd7cb1eb8f8cf5a9bbad1ca80692bfae1af63568272"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0473bd896799ccc87f428766813ddedd3506cad8430dbe863b663c81d7387680"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6799419bc7e3029112f2a3f8b77b6c299f94f03bb70e5c31a437b3180486be"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e5b705c8ce6ef47fc461484878956ecd42a67cbeb0a17e323b86a4439a8fdc3d"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:28a732be6fced2f70184c1b34f64961e3b6259fe6d5f7540c91028c2a43a7109"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-win32.whl", hash = "sha256:f5cdb1ec88f0b8c617330c953555a20cc7e96ca6b1f5c68ab6db347e869cfeeb"},
+ {file = "tree_sitter_languages-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:26cb344a75798fce1a73b690504d8e7789f6ba25a178efcd203444d7868caf38"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:433b56cb3dca02b30f21c596f431a2cff90905326be1f8913c3515acb984b21e"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96686390e1a01af44aedef7b33d6be82de3cf674a98a5c7b417e540e6afa62cc"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25a4b6d559fbd76c6ec1b73cf03d09f53aaa5a1b61078a3f518b162866d9d97e"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e504f199c7a4c8b1b1efb05a063450aa23234feea6fa6c06f4077f7248ea9c98"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6b29856e9314b5f68f05dfa45e6674f47535229dda32294ba6d129077a97759c"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:786fdaf3d2120eef9384b0f22d7e2e42a561073ba753c7b438e90a1e7b351650"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-win32.whl", hash = "sha256:a55a7007056d0927b78481b437d79ea0487cc991c7f9c19d67adcceac3d47f53"},
+ {file = "tree_sitter_languages-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:4b01d3bdf7ce2aeee4d0df62071a0ca91e618a29845686a5bd714d93c5ef3b36"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b603f1ad01bfb9d178f965125e2528cb7da9666d180f4a9a1acfaedbf5862ea"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70610aa26dd985d2fb9eb07ea8eacc3ceb0cc9c2e91416f51305120cfd919e28"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0444ebc8bdb7dc0d66a816050cfd52376c4e62a94a9c54fde90b29acf3e4bab1"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7eeb5a3307ff1c0994ffff5ea37ec656a716a728b8c9359374104da521a76ded"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6c319cef16f2df667f1c165fe4eee160f2b51a0c4b61db1e70de2ab86420ca9a"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-win32.whl", hash = "sha256:b216650126d95d494f927393903e836a7ef5f0c4db0834f3a0b576f97c13abaf"},
+ {file = "tree_sitter_languages-1.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6c96e5785d164a205962a10256808b3d12dccee9827ec88a46899063a2a2d28"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adafeabbd8d47b80122fad18bb61c25ed3da04f5347b7d774b53826accb27b7a"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50e2bc5d2da770ecd5af94f9d716faa4764f890fd61bc0a488e9269653d9fb71"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac773097cff7de6cf265c5be9990b4c6690161452da1d9fc41021d4bf7e8c73a"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b233bfc48cf0f16436200afc7d7643cd87101c321de25b919b61f21f1693aa52"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:eab3caedf50467045ed5cab776a57b494332616376d387c6600fd7ea4f5483cf"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:d533f743a22f5696494d3a5a60adb4cfbef63d58b8b5622993d93d6d0a602444"},
+ {file = "tree_sitter_languages-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:aab96f64be30c9f73d6dc958ec22bb1a9fe70e90b2d2a3d233d537b347cea729"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1bf89d771621e28847036b377f865f947e555a6654356d21beab738bb2531a69"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b2f171089ec3c4f1de275edc8f0722e1e3dc7a54e83107098315ea2f0952cfcd"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091577d3a8454c40f813ee2834314c73cc504522f70f9e33d7c2268d33973f9"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8287efa87d080b340b583a6e81266cc3d8266deb61b8f3312649a9d1562e665a"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9c5080c06a2df7a59c69d2422a6ae83a5e37e92d57c4bd5e572d0eb5226ab3b0"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca8f629cfb406a2f9b9f8a3a5c804d4d1ba4cdca41cccba63f51fc1bab13e5de"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-win32.whl", hash = "sha256:fd3561b37a99c9d501719819a8736529ae3a6d597128c15be432d1855f3cb0d9"},
+ {file = "tree_sitter_languages-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:377ad60f7a7bf27315676c4fa84cc766aa0019c1e556083763136ed951e934c0"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1dc71b68e48f58cd5b6a9ab7a541714201815629a6554a969cfc579a6ee6e53"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb1521367b14c275bef70997ea90526e7049f840ba1bbd3ef56c72f5b15596e9"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f73651f7e78371dc3d455e8aba510cc6fb9e1ac1d648c3334157950781eb295"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:049b0dd63be721fe3f9642a2b5a044bea2852de2b35818467996242ae4b7f01f"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c428a8e1f5ecc4eb5c79abff3eb2881123446cde16fd1d8866d527470a6fdd2f"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:40fb3fc11ff90caf65b4713feeb6c4852e5d2a04ef8ae6a2ac734a702a6a6c7e"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-win32.whl", hash = "sha256:f28e9904833b7a909f8227c4560401049bd3310cebe3e0a884d9461f783b9af2"},
+ {file = "tree_sitter_languages-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ea47ee390ec2e1c9bf96d7b418775263766021a834910c9f2d578f95a3e27d0f"},
+]
+typed-ast = [
{file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"},
{file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"},
{file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"},
@@ -2022,99 +2363,39 @@ files = [
{file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"},
{file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"},
]
-
-[[package]]
-name = "types-setuptools"
-version = "67.8.0.0"
-description = "Typing stubs for setuptools"
-optional = false
-python-versions = "*"
-files = [
+types-setuptools = [
{file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"},
{file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"},
]
-
-[[package]]
-name = "typing-extensions"
-version = "4.7.1"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-optional = false
-python-versions = ">=3.7"
-files = [
+types-tree-sitter = [
+ {file = "types-tree-sitter-0.20.1.5.tar.gz", hash = "sha256:94f971599548b90b9bbb6af651d235ad795a094a07651bc565a4b8856caebab1"},
+ {file = "types_tree_sitter-0.20.1.5-py3-none-any.whl", hash = "sha256:8d7f9961febbad29789ce5c65f79b95b0702f3d34a7c12fabcd69c36c2bbe184"},
+]
+types-tree-sitter-languages = [
+ {file = "types-tree-sitter-languages-1.7.0.1.tar.gz", hash = "sha256:eadbbfa13f3fcad0711ac8f866cf87692f3c0cfeee72e979a5202b797588d57d"},
+ {file = "types_tree_sitter_languages-1.7.0.1-py3-none-any.whl", hash = "sha256:818ec7824ed1bb5bcdbe21022340e0df3930199eb969ea1e08eb03a92440bce2"},
+]
+typing-extensions = [
{file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
{file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
]
-
-[[package]]
-name = "tzdata"
-version = "2022.7"
-description = "Provider of IANA time zone data"
-optional = false
-python-versions = ">=2"
-files = [
+tzdata = [
{file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"},
{file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"},
]
-
-[[package]]
-name = "uc-micro-py"
-version = "1.0.2"
-description = "Micro subset of unicode data files for linkify-it-py projects."
-optional = false
-python-versions = ">=3.7"
-files = [
+uc-micro-py = [
{file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"},
{file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"},
]
-
-[package.extras]
-test = ["coverage", "pytest", "pytest-cov"]
-
-[[package]]
-name = "urllib3"
-version = "2.0.4"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
- {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
+urllib3 = [
+ {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"},
+ {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"},
]
-
-[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
-secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
-socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
-
-[[package]]
-name = "virtualenv"
-version = "20.24.4"
-description = "Virtual Python Environment builder"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "virtualenv-20.24.4-py3-none-any.whl", hash = "sha256:29c70bb9b88510f6414ac3e55c8b413a1f96239b6b789ca123437d5e892190cb"},
- {file = "virtualenv-20.24.4.tar.gz", hash = "sha256:772b05bfda7ed3b8ecd16021ca9716273ad9f4467c801f27e83ac73430246dca"},
+virtualenv = [
+ {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
+ {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
]
-
-[package.dependencies]
-distlib = ">=0.3.7,<1"
-filelock = ">=3.12.2,<4"
-importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
-platformdirs = ">=3.9.1,<4"
-
-[package.extras]
-docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
-test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
-
-[[package]]
-name = "watchdog"
-version = "3.0.0"
-description = "Filesystem events monitoring"
-optional = false
-python-versions = ">=3.7"
-files = [
+watchdog = [
{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"},
@@ -2143,17 +2424,7 @@ files = [
{file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
{file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
]
-
-[package.extras]
-watchmedo = ["PyYAML (>=3.10)"]
-
-[[package]]
-name = "yarl"
-version = "1.9.2"
-description = "Yet another URL library"
-optional = false
-python-versions = ">=3.7"
-files = [
+yarl = [
{file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"},
{file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"},
{file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"},
@@ -2229,28 +2500,7 @@ files = [
{file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
{file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
]
-
-[package.dependencies]
-idna = ">=2.0"
-multidict = ">=4.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "zipp"
-version = "3.15.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-optional = false
-python-versions = ">=3.7"
-files = [
+zipp = [
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
-
-[metadata]
-lock-version = "2.0"
-python-versions = "^3.7"
-content-hash = "3817b3d8b678845abb17cddd49d5a6ea5fb9d0083faa356ef232184a94312ba6"
diff --git a/pyproject.toml b/pyproject.toml
index 0b6ea079cf..78040063bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
-version = "0.36.0"
+version = "0.38.1"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
@@ -46,6 +46,8 @@ markdown-it-py = { extras = ["plugins", "linkify"], version = ">=2.1.0" }
#rich = {path="../rich", develop=true}
importlib-metadata = ">=4.11.3"
typing-extensions = "^4.4.0"
+tree-sitter = "^0.20.1"
+tree_sitter_languages = {version = ">=1.7.0", python = "^3.8"}
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
@@ -65,7 +67,9 @@ httpx = "^0.23.1"
types-setuptools = "^67.2.0.1"
textual-dev = "^1.1.0"
pytest-asyncio = "*"
-pytest-textual-snapshot = "*"
+pytest-textual-snapshot = ">=0.4.0"
+types-tree-sitter = "^0.20.1.4"
+types-tree-sitter-languages = "^1.7.0.1"
[tool.black]
includes = "src"
diff --git a/src/textual/_ansi_sequences.py b/src/textual/_ansi_sequences.py
index fc3b8de624..9d0c4a601b 100644
--- a/src/textual/_ansi_sequences.py
+++ b/src/textual/_ansi_sequences.py
@@ -221,6 +221,8 @@
"\x1b[1;5B": (Keys.ControlDown,), # Cursor Mode
"\x1b[1;5C": (Keys.ControlRight,), # Cursor Mode
"\x1b[1;5D": (Keys.ControlLeft,), # Cursor Mode
+ "\x1bf": (Keys.ControlRight,), # iTerm natural editing keys
+ "\x1bb": (Keys.ControlLeft,), # iTerm natural editing keys
"\x1b[1;5F": (Keys.ControlEnd,),
"\x1b[1;5H": (Keys.ControlHome,),
# Tmux sends following keystrokes when control+arrow is pressed, but for
diff --git a/src/textual/_system_commands_source.py b/src/textual/_system_commands.py
similarity index 61%
rename from src/textual/_system_commands_source.py
rename to src/textual/_system_commands.py
index deacd7788f..c8b499c395 100644
--- a/src/textual/_system_commands_source.py
+++ b/src/textual/_system_commands.py
@@ -1,26 +1,26 @@
-"""A command palette command source for Textual system commands.
+"""A command palette command provider for Textual system commands.
-This is a simple command source that makes the most obvious application
-actions available via the [command palette][textual.command_palette.CommandPalette].
+This is a simple command provider that makes the most obvious application
+actions available via the [command palette][textual.command.CommandPalette].
"""
-from .command_palette import CommandMatches, CommandSource, CommandSourceHit
+from .command import Hit, Hits, Provider
-class SystemCommandSource(CommandSource):
- """A [source][textual.command_palette.CommandSource] of command palette commands that run app-wide tasks.
+class SystemCommands(Provider):
+ """A [source][textual.command.Provider] of command palette commands that run app-wide tasks.
- Used by default in [`App.COMMAND_SOURCES`][textual.app.App.COMMAND_SOURCES].
+ Used by default in [`App.COMMANDS`][textual.app.App.COMMANDS].
"""
- async def search(self, query: str) -> CommandMatches:
+ async def search(self, query: str) -> Hits:
"""Handle a request to search for system commands that match the query.
Args:
user_input: The user input to be matched.
Yields:
- Command source hits for use in the command palette.
+ Command hits for use in the command palette.
"""
# We're going to use Textual's builtin fuzzy matcher to find
# matching commands.
@@ -47,10 +47,9 @@ async def search(self, query: str) -> CommandMatches:
):
match = matcher.match(name)
if match > 0:
- yield CommandSourceHit(
+ yield Hit(
match,
matcher.highlight(name),
runnable,
- name,
- help_text,
+ help=help_text,
)
diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py
new file mode 100644
index 0000000000..93bad81c85
--- /dev/null
+++ b/src/textual/_text_area_theme.py
@@ -0,0 +1,353 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from rich.style import Style
+
+from textual.app import DEFAULT_COLORS
+from textual.color import Color
+from textual.design import DEFAULT_DARK_SURFACE
+
+
+@dataclass
+class TextAreaTheme:
+ """A theme for the `TextArea` widget.
+
+ Allows theming the general widget (gutter, selections, cursor, and so on) and
+ mapping of tree-sitter tokens to Rich styles.
+
+ For example, consider the following snippet from the `markdown.scm` highlight
+ query file. We've assigned the `heading_content` token type to the name `heading`.
+
+ ```
+ (heading_content) @heading
+ ```
+
+ Now, we can map this `heading` name to a Rich style, and it will be styled as
+ such in the `TextArea`, assuming a parser which returns a `heading_content`
+ node is used (as will be the case when language="markdown").
+
+ ```
+ TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})
+ ```
+
+ We can register this theme with our `TextArea` using the [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] method,
+ and headings in our markdown files will be styled bold cyan.
+ """
+
+ name: str
+ """The name of the theme."""
+
+ base_style: Style | None = None
+ """The background style of the text area. If `None` the parent style will be used."""
+
+ gutter_style: Style | None = None
+ """The style of the gutter. If `None`, a legible Style will be generated."""
+
+ cursor_style: Style | None = None
+ """The style of the cursor. If `None`, a legible Style will be generated."""
+
+ cursor_line_style: Style | None = None
+ """The style to apply to the line the cursor is on."""
+
+ cursor_line_gutter_style: Style | None = None
+ """The style to apply to the gutter of the line the cursor is on. If `None`, a legible Style will be
+ generated."""
+
+ bracket_matching_style: Style | None = None
+ """The style to apply to matching brackets. If `None`, a legible Style will be generated."""
+
+ selection_style: Style | None = None
+ """The style of the selection. If `None` a default selection Style will be generated."""
+
+ syntax_styles: dict[str, Style] = field(default_factory=dict)
+ """The mapping of tree-sitter names from the `highlight_query` to Rich styles."""
+
+ def __post_init__(self) -> None:
+ """Generate some styles if they haven't been supplied."""
+ if self.base_style is None:
+ self.base_style = Style()
+
+ if self.base_style.color is None:
+ self.base_style = Style(color="#f3f3f3", bgcolor=self.base_style.bgcolor)
+
+ if self.base_style.bgcolor is None:
+ self.base_style = Style(
+ color=self.base_style.color, bgcolor=DEFAULT_DARK_SURFACE
+ )
+
+ assert self.base_style is not None
+ assert self.base_style.color is not None
+ assert self.base_style.bgcolor is not None
+
+ if self.gutter_style is None:
+ self.gutter_style = self.base_style.copy()
+
+ background_color = Color.from_rich_color(self.base_style.bgcolor)
+ if self.cursor_style is None:
+ self.cursor_style = Style(
+ color=background_color.rich_color,
+ bgcolor=background_color.inverse.rich_color,
+ )
+
+ 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:
+ bracket_matching_background = background_color.blend(
+ background_color.inverse, factor=0.05
+ )
+ self.bracket_matching_style = Style(
+ bgcolor=bracket_matching_background.rich_color
+ )
+
+ if self.selection_style is None:
+ selection_background_color = background_color.blend(
+ DEFAULT_COLORS["dark"].primary, factor=0.75
+ )
+ self.selection_style = Style.from_color(
+ bgcolor=selection_background_color.rich_color
+ )
+
+ @classmethod
+ def get_builtin_theme(cls, theme_name: str) -> "TextAreaTheme" | None:
+ """Get a `TextAreaTheme` by name.
+
+ Given a `theme_name`, return the corresponding `TextAreaTheme` object.
+
+ Args:
+ theme_name: The name of the theme.
+
+ Returns:
+ The `TextAreaTheme` corresponding to the name or `None` if the theme isn't
+ found.
+ """
+ return _BUILTIN_THEMES.get(theme_name)
+
+ def get_highlight(self, name: str) -> Style | None:
+ """Return the Rich style corresponding to the name defined in the tree-sitter
+ highlight query for the current theme.
+
+ Args:
+ name: The name of the highlight.
+
+ Returns:
+ The `Style` to use for this highlight, or `None` if no style.
+ """
+ return self.syntax_styles.get(name)
+
+ @classmethod
+ def builtin_themes(cls) -> list[TextAreaTheme]:
+ """Get a list of all builtin TextAreaThemes.
+
+ Returns:
+ A list of all builtin TextAreaThemes.
+ """
+ return list(_BUILTIN_THEMES.values())
+
+ @classmethod
+ def default(cls) -> TextAreaTheme:
+ """Get the default syntax theme.
+
+ Returns:
+ The default TextAreaTheme (probably "monokai").
+ """
+ return _MONOKAI
+
+
+_MONOKAI = TextAreaTheme(
+ name="monokai",
+ base_style=Style(color="#f8f8f2", bgcolor="#272822"),
+ gutter_style=Style(color="#90908a", bgcolor="#272822"),
+ cursor_style=Style(color="#272822", bgcolor="#f8f8f0"),
+ cursor_line_style=Style(bgcolor="#3e3d32"),
+ cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#3e3d32"),
+ bracket_matching_style=Style(bgcolor="#838889", bold=True),
+ selection_style=Style(bgcolor="#65686a"),
+ syntax_styles={
+ "string": Style(color="#E6DB74"),
+ "string.documentation": Style(color="#E6DB74"),
+ "comment": Style(color="#75715E"),
+ "keyword": Style(color="#F92672"),
+ "operator": Style(color="#F92672"),
+ "repeat": Style(color="#F92672"),
+ "exception": Style(color="#F92672"),
+ "include": Style(color="#F92672"),
+ "keyword.function": Style(color="#F92672"),
+ "keyword.return": Style(color="#F92672"),
+ "keyword.operator": Style(color="#F92672"),
+ "conditional": Style(color="#F92672"),
+ "number": Style(color="#AE81FF"),
+ "float": Style(color="#AE81FF"),
+ "class": Style(color="#A6E22E"),
+ "type.class": Style(color="#A6E22E"),
+ "function": Style(color="#A6E22E"),
+ "function.call": Style(color="#A6E22E"),
+ "method": Style(color="#A6E22E"),
+ "method.call": Style(color="#A6E22E"),
+ "boolean": Style(color="#66D9EF", italic=True),
+ "json.null": Style(color="#66D9EF", italic=True),
+ "regex.punctuation.bracket": Style(color="#F92672"),
+ "regex.operator": Style(color="#F92672"),
+ "html.end_tag_error": Style(color="red", underline=True),
+ "tag": Style(color="#F92672"),
+ "yaml.field": Style(color="#F92672", bold=True),
+ "json.label": Style(color="#F92672", bold=True),
+ "toml.type": Style(color="#F92672"),
+ "toml.datetime": Style(color="#AE81FF"),
+ "heading": Style(color="#F92672", bold=True),
+ "bold": Style(bold=True),
+ "italic": Style(italic=True),
+ "strikethrough": Style(strike=True),
+ "link": Style(color="#66D9EF", underline=True),
+ "inline_code": Style(color="#E6DB74"),
+ },
+)
+
+_DRACULA = TextAreaTheme(
+ name="dracula",
+ base_style=Style(color="#f8f8f2", bgcolor="#1E1F35"),
+ gutter_style=Style(color="#6272a4"),
+ cursor_style=Style(color="#282a36", bgcolor="#f8f8f0"),
+ cursor_line_style=Style(bgcolor="#282b45"),
+ cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#282b45", bold=True),
+ bracket_matching_style=Style(bgcolor="#99999d", bold=True, underline=True),
+ selection_style=Style(bgcolor="#44475A"),
+ syntax_styles={
+ "string": Style(color="#f1fa8c"),
+ "string.documentation": Style(color="#f1fa8c"),
+ "comment": Style(color="#6272a4"),
+ "keyword": Style(color="#ff79c6"),
+ "operator": Style(color="#ff79c6"),
+ "repeat": Style(color="#ff79c6"),
+ "exception": Style(color="#ff79c6"),
+ "include": Style(color="#ff79c6"),
+ "keyword.function": Style(color="#ff79c6"),
+ "keyword.return": Style(color="#ff79c6"),
+ "keyword.operator": Style(color="#ff79c6"),
+ "conditional": Style(color="#ff79c6"),
+ "number": Style(color="#bd93f9"),
+ "float": Style(color="#bd93f9"),
+ "class": Style(color="#50fa7b"),
+ "type.class": Style(color="#50fa7b"),
+ "function": Style(color="#50fa7b"),
+ "function.call": Style(color="#50fa7b"),
+ "method": Style(color="#50fa7b"),
+ "method.call": Style(color="#50fa7b"),
+ "boolean": Style(color="#bd93f9"),
+ "json.null": Style(color="#bd93f9"),
+ "regex.punctuation.bracket": Style(color="#ff79c6"),
+ "regex.operator": Style(color="#ff79c6"),
+ "html.end_tag_error": Style(color="#F83333", underline=True),
+ "tag": Style(color="#ff79c6"),
+ "yaml.field": Style(color="#ff79c6", bold=True),
+ "json.label": Style(color="#ff79c6", bold=True),
+ "toml.type": Style(color="#ff79c6"),
+ "toml.datetime": Style(color="#bd93f9"),
+ "heading": Style(color="#ff79c6", bold=True),
+ "bold": Style(bold=True),
+ "italic": Style(italic=True),
+ "strikethrough": Style(strike=True),
+ "link": Style(color="#bd93f9", underline=True),
+ "inline_code": Style(color="#f1fa8c"),
+ },
+)
+
+_DARK_VS = TextAreaTheme(
+ name="vscode_dark",
+ base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"),
+ gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"),
+ cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"),
+ cursor_line_style=Style(bgcolor="#2b2b2b"),
+ bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True),
+ cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"),
+ selection_style=Style(bgcolor="#264F78"),
+ syntax_styles={
+ "string": Style(color="#ce9178"),
+ "string.documentation": Style(color="#ce9178"),
+ "comment": Style(color="#6A9955"),
+ "keyword": Style(color="#569cd6"),
+ "operator": Style(color="#569cd6"),
+ "conditional": Style(color="#569cd6"),
+ "keyword.function": Style(color="#569cd6"),
+ "keyword.return": Style(color="#569cd6"),
+ "keyword.operator": Style(color="#569cd6"),
+ "repeat": Style(color="#569cd6"),
+ "exception": Style(color="#569cd6"),
+ "include": Style(color="#569cd6"),
+ "number": Style(color="#b5cea8"),
+ "float": Style(color="#b5cea8"),
+ "class": Style(color="#4EC9B0"),
+ "type.class": Style(color="#4EC9B0"),
+ "function": Style(color="#4EC9B0"),
+ "function.call": Style(color="#4EC9B0"),
+ "method": Style(color="#4EC9B0"),
+ "method.call": Style(color="#4EC9B0"),
+ "boolean": Style(color="#7DAF9C"),
+ "json.null": Style(color="#7DAF9C"),
+ "tag": Style(color="#EFCB43"),
+ "yaml.field": Style(color="#569cd6", bold=True),
+ "json.label": Style(color="#569cd6", bold=True),
+ "toml.type": Style(color="#569cd6"),
+ "heading": Style(color="#569cd6", bold=True),
+ "bold": Style(bold=True),
+ "italic": Style(italic=True),
+ "strikethrough": Style(strike=True),
+ "link": Style(color="#40A6FF", underline=True),
+ "inline_code": Style(color="#ce9178"),
+ "info_string": Style(color="#ce9178", bold=True, italic=True),
+ },
+)
+
+_GITHUB_LIGHT = TextAreaTheme(
+ name="github_light",
+ base_style=Style(color="#24292e", bgcolor="#f0f0f0"),
+ gutter_style=Style(color="#BBBBBB", bgcolor="#f0f0f0"),
+ cursor_style=Style(color="#fafbfc", bgcolor="#24292e"),
+ cursor_line_style=Style(bgcolor="#ebebeb"),
+ bracket_matching_style=Style(color="#24292e", underline=True),
+ cursor_line_gutter_style=Style(color="#A4A4A4", bgcolor="#ebebeb"),
+ selection_style=Style(bgcolor="#c8c8fa"),
+ syntax_styles={
+ "string": Style(color="#093069"),
+ "string.documentation": Style(color="#093069"),
+ "comment": Style(color="#6a737d"),
+ "keyword": Style(color="#d73a49"),
+ "operator": Style(color="#0450AE"),
+ "conditional": Style(color="#CF222E"),
+ "keyword.function": Style(color="#CF222E"),
+ "keyword.return": Style(color="#CF222E"),
+ "keyword.operator": Style(color="#CF222E"),
+ "repeat": Style(color="#CF222E"),
+ "exception": Style(color="#CF222E"),
+ "include": Style(color="#CF222E"),
+ "number": Style(color="#d73a49"),
+ "float": Style(color="#d73a49"),
+ "parameter": Style(color="#24292e"),
+ "class": Style(color="#963800"),
+ "variable": Style(color="#e36209"),
+ "function": Style(color="#6639BB"),
+ "method": Style(color="#6639BB"),
+ "boolean": Style(color="#7DAF9C"),
+ "tag": Style(color="#6639BB"),
+ "yaml.field": Style(color="#6639BB"),
+ "json.label": Style(color="#6639BB"),
+ "toml.type": Style(color="#6639BB"),
+ "heading": Style(color="#24292e", bold=True),
+ "bold": Style(bold=True),
+ "italic": Style(italic=True),
+ "strikethrough": Style(strike=True),
+ "link": Style(color="#40A6FF", underline=True),
+ "inline_code": Style(color="#093069"),
+ },
+)
+
+_BUILTIN_THEMES = {
+ "monokai": _MONOKAI,
+ "dracula": _DRACULA,
+ "vscode_dark": _DARK_VS,
+ "github_light": _GITHUB_LIGHT,
+}
+
+DEFAULT_THEME = TextAreaTheme.get_builtin_theme("monokai")
+"""The default TextAreaTheme used by Textual."""
diff --git a/src/textual/_tree_sitter.py b/src/textual/_tree_sitter.py
new file mode 100644
index 0000000000..01e300115c
--- /dev/null
+++ b/src/textual/_tree_sitter.py
@@ -0,0 +1,10 @@
+from __future__ import annotations
+
+try:
+ from tree_sitter import Language, Parser, Tree
+ from tree_sitter.binding import Query
+ from tree_sitter_languages import get_language, get_parser
+
+ TREE_SITTER = True
+except ImportError:
+ TREE_SITTER = False
diff --git a/src/textual/_types.py b/src/textual/_types.py
index 85eb27c421..669950c5a2 100644
--- a/src/textual/_types.py
+++ b/src/textual/_types.py
@@ -1,6 +1,12 @@
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union
-from typing_extensions import Protocol
+from typing_extensions import (
+ Literal,
+ Protocol,
+ SupportsIndex,
+ get_args,
+ runtime_checkable,
+)
if TYPE_CHECKING:
from rich.segment import Segment
@@ -26,9 +32,15 @@ def post_message(self, message: "Message") -> bool:
...
+class UnusedParameter:
+ """Helper type for a parameter that isn't specified in a method call."""
+
+
SegmentLines = List[List["Segment"]]
CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]]
"""Type used for arbitrary callables used in callbacks."""
+IgnoreReturnCallbackType = Union[Callable[[], Awaitable[Any]], Callable[[], Any]]
+"""A callback which ignores the return type."""
WatchCallbackType = Union[
Callable[[], Awaitable[None]],
Callable[[Any], Awaitable[None]],
diff --git a/src/textual/app.py b/src/textual/app.py
index 2991f51e28..48fd36188a 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -68,13 +68,13 @@
from ._context import message_hook as message_hook_context_var
from ._event_broker import NoHandler, extract_handler_actions
from ._path import CSSPathType, _css_path_type_as_list, _make_path_object_relative
-from ._system_commands_source import SystemCommandSource
+from ._system_commands import SystemCommands
from ._wait import wait_for_idle
from ._worker_manager import WorkerManager
from .actions import ActionParseResult, SkipAction
from .await_remove import AwaitRemove
from .binding import Binding, BindingType, _Bindings
-from .command_palette import CommandPalette, CommandPaletteCallable, CommandSource
+from .command import CommandPalette, Provider
from .css.query import NoMatches
from .css.stylesheet import Stylesheet
from .design import ColorSystem
@@ -326,22 +326,17 @@ class MyApp(App[None]):
"""
ENABLE_COMMAND_PALETTE: ClassVar[bool] = True
- """Should the [command palette][textual.command_palette.CommandPalette] be enabled for the application?"""
+ """Should the [command palette][textual.command.CommandPalette] be enabled for the application?"""
- COMMAND_SOURCES: ClassVar[set[type[CommandSource]]] = {SystemCommandSource}
- """The [command sources](/api/command_palette/) for the application.
+ COMMANDS: ClassVar[set[type[Provider]]] = {SystemCommands}
+ """Command providers used by the [command palette](/guide/command_palette).
- This is the collection of [command sources][textual.command_palette.CommandSource]
- that provide matched
- commands to the [command palette][textual.command_palette.CommandPalette].
-
- The default Textual command palette source is
- [the Textual system-wide command source][textual._system_commands_source.SystemCommandSource].
+ Should be a set of [command.Provider][textual.command.Provider] classes.
"""
BINDINGS: ClassVar[list[BindingType]] = [
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
- Binding("ctrl+@", "command_palette", show=False, priority=True),
+ Binding("ctrl+backslash", "command_palette", show=False, priority=True),
]
title: Reactive[str] = Reactive("", compute=False)
@@ -1201,9 +1196,13 @@ async def run_test(
notifications: bool = False,
message_hook: Callable[[Message], None] | None = None,
) -> AsyncGenerator[Pilot, None]:
- """An asynchronous context manager for testing app.
+ """An asynchronous context manager for testing apps.
+
+ !!! tip
- Use this to run your app in "headless" (no output) mode and driver the app via a [Pilot][textual.pilot.Pilot] object.
+ See the guide for [testing](/guide/testing) Textual apps.
+
+ Use this to run your app in "headless" mode (no output) and drive the app via a [Pilot][textual.pilot.Pilot] object.
Example:
@@ -1738,7 +1737,10 @@ def _load_screen_css(self, screen: Screen):
screen_css_path = f"{screen.__class__.__name__}"
if not self.stylesheet.has_source(screen_css_path):
self.stylesheet.add_source(
- screen.CSS, path=screen_css_path, is_default_css=False
+ screen.CSS,
+ path=screen_css_path,
+ is_default_css=False,
+ scope=screen._css_type_name if screen.SCOPED_CSS else "",
)
update = True
if update:
@@ -1788,9 +1790,11 @@ def push_screen(
self.screen.post_message(events.ScreenSuspend())
self.screen.refresh()
next_screen, await_mount = self._get_screen(screen)
- next_screen._push_result_callback(
- self.screen if self._screen_stack else None, callback
- )
+ try:
+ message_pump = active_message_pump.get()
+ except LookupError:
+ message_pump = self.app
+ next_screen._push_result_callback(message_pump, callback)
self._load_screen_css(next_screen)
self._screen_stack.append(next_screen)
self.stylesheet.update(next_screen)
@@ -2052,9 +2056,13 @@ async def _process_messages(
try:
if self.css_path:
self.stylesheet.read_all(self.css_path)
- for path, css, tie_breaker in self._get_default_css():
+ for path, css, tie_breaker, scope in self._get_default_css():
self.stylesheet.add_source(
- css, path=path, is_default_css=True, tie_breaker=tie_breaker
+ css,
+ path=path,
+ is_default_css=True,
+ tie_breaker=tie_breaker,
+ scope=scope,
)
if self.CSS:
try:
diff --git a/src/textual/command_palette.py b/src/textual/command.py
similarity index 79%
rename from src/textual/command_palette.py
rename to src/textual/command.py
index e1201a7c4f..495c328cee 100644
--- a/src/textual/command_palette.py
+++ b/src/textual/command.py
@@ -1,21 +1,19 @@
-"""The Textual command palette."""
+"""The Textual command palette.
+
+See the guide on the [Command Palette](../guide/command_palette.md) for full details.
+
+"""
from __future__ import annotations
from abc import ABC, abstractmethod
-from asyncio import CancelledError, Queue, TimeoutError, wait_for
+from asyncio import CancelledError, Queue, Task, TimeoutError, wait, wait_for
+from dataclasses import dataclass
from functools import total_ordering
from time import monotonic
-from typing import (
- TYPE_CHECKING,
- Any,
- AsyncGenerator,
- AsyncIterator,
- Callable,
- ClassVar,
- NamedTuple,
-)
+from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar
+import rich.repr
from rich.align import Align
from rich.console import Group, RenderableType
from rich.emoji import Emoji
@@ -26,13 +24,14 @@
from . import on, work
from ._asyncio import create_task
-from ._fuzzy import Matcher
from .binding import Binding, BindingType
from .containers import Horizontal, Vertical
from .events import Click, Mount
+from .fuzzy import Matcher
from .reactive import var
from .screen import ModalScreen, Screen
from .timer import Timer
+from .types import CallbackType, IgnoreReturnCallbackType
from .widget import Widget
from .widgets import Button, Input, LoadingIndicator, OptionList, Static
from .widgets.option_list import Option
@@ -42,79 +41,84 @@
from .app import App, ComposeResult
__all__ = [
- "CommandMatches",
"CommandPalette",
- "CommandPaletteCallable",
- "CommandSource",
- "CommandSourceHit",
+ "Hit",
+ "Hits",
"Matcher",
+ "Provider",
]
-CommandPaletteCallable: TypeAlias = Callable[[], Any]
-"""The type of a function that will be called when a command is selected from the command palette."""
-
-
-@total_ordering
-class CommandSourceHit(NamedTuple):
+@dataclass
+class Hit:
"""Holds the details of a single command search hit."""
- match_value: float
- """The match value of the command hit.
+ score: float
+ """The score of the command hit.
The value should be between 0 (no match) and 1 (complete match).
"""
match_display: RenderableType
- """The Rich renderable representation of the hit.
-
- Ideally a [rich Text object][rich.text.Text] object or similar.
- """
+ """A string or Rich renderable representation of the hit."""
- command: CommandPaletteCallable
+ command: IgnoreReturnCallbackType
"""The function to call when the command is chosen."""
- command_text: str
+ text: str | None = None
"""The command text associated with the hit, as plain text.
- This is the text that will be placed into the `Input` field of the
- [command palette][textual.command_palette.CommandPalette] when a
- selection is made.
+ If `match_display` is not simple text, this attribute should be provided by the
+ [Provider][textual.command.Provider] object.
"""
- command_help: str | None = None
+ help: str | None = None
"""Optional help text for the command."""
def __lt__(self, other: object) -> bool:
- if isinstance(other, CommandSourceHit):
- return self.match_value < other.match_value
+ if isinstance(other, Hit):
+ return self.score < other.score
return NotImplemented
def __eq__(self, other: object) -> bool:
- if isinstance(other, CommandSourceHit):
- return self.match_value == other.match_value
+ if isinstance(other, Hit):
+ return self.score == other.score
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"
+ )
+
-CommandMatches: TypeAlias = AsyncIterator[CommandSourceHit]
-"""Return type for the command source match searching method."""
+Hits: TypeAlias = AsyncIterator[Hit]
+"""Return type for the command provider's `search` method."""
-class CommandSource(ABC):
- """Base class for command palette command sources.
+class Provider(ABC):
+ """Base class for command palette command providers.
- To create a source of commands inherit from this class and implement
- [`search`][textual.command_palette.CommandSource.search].
+ To create new command provider, inherit from this class and implement
+ [`search`][textual.command.Provider.search].
"""
def __init__(self, screen: Screen[Any], match_style: Style | None = None) -> None:
- """Initialise the command source.
+ """Initialise the command provider.
Args:
screen: A reference to the active screen.
"""
self.__screen = screen
self.__match_style = match_style
+ self._init_task: Task | None = None
+ self._init_success = False
@property
def focused(self) -> Widget | None:
@@ -136,44 +140,96 @@ def app(self) -> App[object]:
@property
def match_style(self) -> Style | None:
- """The preferred style to use when highlighting matching portions of the [`match_display`][textual.command_palette.CommandSourceHit.match_display]."""
+ """The preferred style to use when highlighting matching portions of the [`match_display`][textual.command.Hit.match_display]."""
return self.__match_style
def matcher(self, user_input: str, case_sensitive: bool = False) -> Matcher:
- """Create a [fuzzy matcher][textual._fuzzy.Matcher] for the given user input.
+ """Create a [fuzzy matcher][textual.fuzzy.Matcher] for the given user input.
Args:
user_input: The text that the user has input.
- case_sensitive: Should match be case sensitive?
+ case_sensitive: Should matching be case sensitive?
Returns:
- A [fuzzy matcher][textual._fuzzy.Matcher] object for matching against candidate hits.
+ A [fuzzy matcher][textual.fuzzy.Matcher] object for matching against candidate hits.
"""
return Matcher(
user_input, match_style=self.match_style, case_sensitive=case_sensitive
)
+ def _post_init(self) -> None:
+ """Internal method to run post init task."""
+
+ async def post_init_task() -> None:
+ """Wrapper to post init that runs in a task."""
+ try:
+ await self.startup()
+ except Exception:
+ self.app.log.error(Traceback())
+ else:
+ self._init_success = True
+
+ self._init_task = create_task(post_init_task())
+
+ async def _wait_init(self) -> None:
+ """Wait for initialization."""
+ if self._init_task is not None:
+ await self._init_task
+
+ async def startup(self) -> None:
+ """Called after the Provider is initialized, but before any calls to `search`."""
+
+ async def _search(self, query: str) -> Hits:
+ """Internal method to perform search.
+
+ Args:
+ query: The user input to be matched.
+
+ Yields:
+ Instances of [`Hit`][textual.command.Hit].
+ """
+ await self._wait_init()
+ if self._init_success:
+ hits = self.search(query)
+ async for hit in hits:
+ yield hit
+
@abstractmethod
- async def search(self, query: str) -> CommandMatches:
+ async def search(self, query: str) -> Hits:
"""A request to search for commands relevant to the given query.
Args:
query: The user input to be matched.
Yields:
- Instances of [`CommandSourceHit`][textual.command_palette.CommandSourceHit].
+ Instances of [`Hit`][textual.command.Hit].
"""
yield NotImplemented
+ async def _shutdown(self) -> None:
+ """Internal method to call shutdown and log errors."""
+ try:
+ await self.shutdown()
+ except Exception:
+ self.app.log.error(Traceback())
+
+ async def shutdown(self) -> None:
+ """Called when the Provider is shutdown.
+
+ Use this method to perform an cleanup, if required.
+
+ """
+
+@rich.repr.auto
@total_ordering
class Command(Option):
- """Class that holds a command in the [`CommandList`][textual.command_palette.CommandList]."""
+ """Class that holds a command in the [`CommandList`][textual.command.CommandList]."""
def __init__(
self,
prompt: RenderableType,
- command: CommandSourceHit,
+ command: Hit,
id: str | None = None,
disabled: bool = False,
) -> None:
@@ -207,7 +263,7 @@ class CommandList(OptionList, can_focus=False):
CommandList {
visibility: hidden;
border-top: blank;
- border-bottom: hkey $accent;
+ border-bottom: hkey $primary;
border-left: none;
border-right: none;
height: auto;
@@ -273,7 +329,7 @@ class CommandInput(Input):
"""
-class CommandPalette(ModalScreen[CommandPaletteCallable], inherit_css=False):
+class CommandPalette(ModalScreen[CallbackType], inherit_css=False):
"""The Textual command palette."""
COMPONENT_CLASSES: ClassVar[set[str]] = {
@@ -294,12 +350,17 @@ class CommandPalette(ModalScreen[CommandPaletteCallable], inherit_css=False):
}
CommandPalette > .command-palette--help-text {
- text-style: dim;
background: transparent;
+ color: $text-muted;
}
+ CommandPalette:dark > .command-palette--highlight {
+ text-style: bold;
+ color: $warning;
+ }
CommandPalette > .command-palette--highlight {
- text-style: bold reverse;
+ text-style: bold;
+ color: $warning-darken-2;
}
CommandPalette > Vertical {
@@ -312,7 +373,7 @@ class CommandPalette(ModalScreen[CommandPaletteCallable], inherit_css=False):
CommandPalette #--input {
height: auto;
visibility: visible;
- border: hkey $accent;
+ border: hkey $primary;
background: $panel;
}
@@ -339,7 +400,7 @@ class CommandPalette(ModalScreen[CommandPaletteCallable], inherit_css=False):
height: auto;
visibility: hidden;
background: $panel;
- border-bottom: hkey $accent;
+ border-bottom: hkey $primary;
}
CommandPalette LoadingIndicator.--visible {
@@ -393,10 +454,12 @@ class CommandPalette(ModalScreen[CommandPaletteCallable], inherit_css=False):
def __init__(self) -> None:
"""Initialise the command palette."""
super().__init__(id=self._PALETTE_ID)
- self._selected_command: CommandSourceHit | None = None
+ self._selected_command: 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."""
+ self._providers: list[Provider] = []
+ """List of Provider instances involved in searches."""
@staticmethod
def is_open(app: App) -> bool:
@@ -411,17 +474,17 @@ def is_open(app: App) -> bool:
return app.screen.id == CommandPalette._PALETTE_ID
@property
- def _sources(self) -> set[type[CommandSource]]:
- """The currently available command sources.
+ def _provider_classes(self) -> set[type[Provider]]:
+ """The currently available command providers.
- This is a combination of the command sources defined [in the
- application][textual.app.App.COMMAND_SOURCES] and those [defined in
- the current screen][textual.screen.Screen.COMMAND_SOURCES].
+ This is a combination of the command providers defined [in the
+ application][textual.app.App.COMMANDS] and those [defined in
+ the current screen][textual.screen.Screen.COMMANDS].
"""
return (
set()
if self._calling_screen is None
- else self.app.COMMAND_SOURCES | self._calling_screen.COMMAND_SOURCES
+ else self.app.COMMANDS | self._calling_screen.COMMANDS
)
def compose(self) -> ComposeResult:
@@ -433,7 +496,7 @@ def compose(self) -> ComposeResult:
with Vertical():
with Horizontal(id="--input"):
yield SearchIcon()
- yield CommandInput(placeholder="Search...")
+ yield CommandInput(placeholder="Command Palette Search...")
if not self.run_on_select:
yield Button("\u25b6")
with Vertical(id="--results"):
@@ -453,10 +516,30 @@ def _on_click(self, event: Click) -> None:
self.workers.cancel_all()
self.dismiss()
- def _on_mount(self, _: Mount) -> None:
+ def on_mount(self, _: Mount) -> None:
"""Capture the calling screen."""
self._calling_screen = self.app.screen_stack[-2]
+ match_style = self.get_component_rich_style(
+ "command-palette--highlight", partial=True
+ )
+
+ assert self._calling_screen is not None
+ self._providers = [
+ provider_class(self._calling_screen, match_style)
+ for provider_class in self._provider_classes
+ ]
+ for provider in self._providers:
+ provider._post_init()
+
+ async def on_unmount(self) -> None:
+ """Shutdown providers when command palette is closed."""
+ if self._providers:
+ await wait(
+ [create_task(provider._shutdown()) for provider in self._providers],
+ )
+ self._providers.clear()
+
def _stop_busy_countdown(self) -> None:
"""Stop any busy countdown that's in effect."""
if self._busy_timer is not None:
@@ -474,9 +557,7 @@ def _become_busy() -> None:
if self._list_visible:
self._show_busy = True
- self._busy_timer = self._busy_timer = self.set_timer(
- self._BUSY_COUNTDOWN, _become_busy
- )
+ self._busy_timer = self.set_timer(self._BUSY_COUNTDOWN, _become_busy)
def _watch__list_visible(self) -> None:
"""React to the list visible flag being toggled."""
@@ -497,49 +578,39 @@ async def _watch__show_busy(self) -> None:
self.query_one(CommandList).set_class(self._show_busy, "--populating")
@staticmethod
- async def _consume(
- source: CommandMatches, commands: Queue[CommandSourceHit]
- ) -> None:
+ async def _consume(hits: Hits, commands: Queue[Hit]) -> None:
"""Consume a source of matching commands, feeding the given command queue.
Args:
- source: The source to consume.
+ hits: The hits to consume.
commands: The command queue to feed.
"""
- async for hit in source:
+ async for hit in hits:
await commands.put(hit)
- async def _search_for(
- self, search_value: str
- ) -> AsyncGenerator[CommandSourceHit, bool]:
- """Search for a given search value amongst all of the command sources.
+ async def _search_for(self, search_value: str) -> AsyncGenerator[Hit, bool]:
+ """Search for a given search value amongst all of the command providers.
Args:
search_value: The value to search for.
Yields:
- The hits made amongst the registered command sources.
+ The hits made amongst the registered command providers.
"""
- # Get the style for highlighted parts of a hit match.
- match_style = self._sans_background(
- self.get_component_rich_style("command-palette--highlight")
- )
+ # Set up a queue to stream in the command hits from all the providers.
+ commands: Queue[Hit] = Queue()
- # Set up a queue to stream in the command hits from all the sources.
- commands: Queue[CommandSourceHit] = Queue()
-
- # Fire up an instance of each command source, inside a task, and
+ # Fire up an instance of each command provider, inside a task, and
# have them go start looking for matches.
- assert self._calling_screen is not None
searches = [
create_task(
self._consume(
- source(self._calling_screen, match_style).search(search_value),
+ provider._search(search_value),
commands,
)
)
- for source in self._sources
+ for provider in self._providers
]
# Set up a delay for showing that we're busy.
@@ -569,7 +640,7 @@ async def _search_for(
# Check through all the finished searches, see if any have
# exceptions, and log them. In most other circumstances we'd
# re-raise the exception and quit the application, but the decision
- # has been made to find and log exceptions with command sources.
+ # has been made to find and log exceptions with command providers.
#
# https://github.com/Textualize/textual/pull/3058#discussion_r1310051855
for search in searches:
@@ -584,10 +655,13 @@ async def _search_for(
# Having finished the main processing loop, we're not busy any more.
# Anything left in the queue (see next) will fall out more or less
- # instantly.
- self._stop_busy_countdown()
+ # instantly. If we're aborted, that means a fresh search is incoming
+ # and it'll have cleaned up the countdown anyway, so don't do that
+ # here as they'll be a clash.
+ if not aborted:
+ self._stop_busy_countdown()
- # If all the sources are pretty fast it could be that we've reached
+ # If all the providers are pretty fast it could be that we've reached
# this point but the queue isn't empty yet. So here we flush the
# queue of anything left.
while not aborted and not commands.empty():
@@ -680,7 +754,7 @@ async def _gather_commands(self, search_value: str) -> None:
)
# The list to hold on to the commands we've gathered from the
- # command sources.
+ # command providers.
gathered_commands: list[Command] = []
# Get a reference to the widget that we're going to drop the
@@ -739,8 +813,8 @@ async def _gather_commands(self, search_value: str) -> None:
# 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
- if hit.command_help:
- prompt = Group(prompt, Text(hit.command_help, style=help_style))
+ if hit.help:
+ prompt = Group(prompt, Text(hit.help, style=help_style))
gathered_commands.append(Command(prompt, hit, id=str(command_id)))
# Before we go making any changes to the UI, we do a quick
@@ -802,6 +876,7 @@ def _input(self, event: Input.Changed) -> None:
Args:
event: The input event.
"""
+ event.stop()
self.workers.cancel_all()
search_value = event.value.strip()
if search_value:
@@ -822,7 +897,7 @@ def _select_command(self, event: OptionList.OptionSelected) -> None:
input = self.query_one(CommandInput)
with self.prevent(Input.Changed):
assert isinstance(event.option, Command)
- input.value = str(event.option.command.command_text)
+ input.value = str(event.option.command.text)
self._selected_command = event.option.command
input.action_end()
self._list_visible = False
@@ -832,10 +907,14 @@ def _select_command(self, event: OptionList.OptionSelected) -> None:
@on(Input.Submitted)
@on(Button.Pressed)
- def _select_or_command(self) -> None:
+ def _select_or_command(
+ self, event: Input.Submitted | Button.Pressed | None = None
+ ) -> None:
"""Depending on context, select or execute a command."""
# If the list is visible, that means we're in "pick a command"
# mode...
+ if event is not None:
+ event.stop()
if self._list_visible:
# ...so if nothing in the list is highlighted yet...
if self.query_one(CommandList).highlighted is None:
@@ -863,10 +942,10 @@ def _action_escape(self) -> None:
self.dismiss()
def _action_command_list(self, action: str) -> None:
- """Pass an action on to the [`CommandList`][textual.command_palette.CommandList].
+ """Pass an action on to the [`CommandList`][textual.command.CommandList].
Args:
- action: The action to pass on to the [`CommandList`][textual.command_palette.CommandList].
+ action: The action to pass on to the [`CommandList`][textual.command.CommandList].
"""
try:
command_action = getattr(self.query_one(CommandList), f"action_{action}")
diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py
index 95487e8704..729e8f9010 100644
--- a/src/textual/css/constants.py
+++ b/src/textual/css/constants.py
@@ -64,11 +64,13 @@
VALID_PSEUDO_CLASSES: Final = {
"blur",
"can-focus",
+ "dark",
"disabled",
"enabled",
"focus-within",
"focus",
"hover",
+ "light",
}
VALID_OVERLAY: Final = {"none", "screen"}
VALID_CONSTRAIN: Final = {"x", "y", "both", "inflect", "none"}
diff --git a/src/textual/css/model.py b/src/textual/css/model.py
index 3766606de1..cf67bd9ddd 100644
--- a/src/textual/css/model.py
+++ b/src/textual/css/model.py
@@ -174,6 +174,8 @@ def _selector_to_css(cls, selectors: list[Selector]) -> str:
elif selector.combinator == CombinatorType.CHILD:
tokens.append(" > ")
tokens.append(selector.css)
+ for pseudo_class in selector.pseudo_classes:
+ tokens.append(f":{pseudo_class}")
return "".join(tokens).strip()
@property
diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py
index 1b31e8b66b..40305df1cf 100644
--- a/src/textual/css/parse.py
+++ b/src/textual/css/parse.py
@@ -85,6 +85,7 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
def parse_rule_set(
+ scope: str,
tokens: Iterator[Token],
token: Token,
is_default_rules: bool = False,
@@ -127,6 +128,19 @@ def parse_rule_set(
token = next(tokens)
if selectors:
+ if scope and selectors[0].name != scope:
+ scope_selector, scope_specificity = get_selector(
+ scope, (SelectorType.TYPE, (0, 0, 0))
+ )
+ selectors.insert(
+ 0,
+ Selector(
+ name=scope,
+ combinator=CombinatorType.DESCENDENT,
+ type=scope_selector,
+ specificity=scope_specificity,
+ ),
+ )
rule_selectors.append(selectors[:])
declaration = Declaration(token, "")
@@ -328,6 +342,7 @@ def substitute_references(
def parse(
+ scope: str,
css: str,
path: str | PurePath,
variables: dict[str, str] | None = None,
@@ -339,6 +354,7 @@ def parse(
and generating rule sets from it.
Args:
+ scope: CSS type name
css: The input CSS
path: Path to the CSS
variables: Substitution variables to substitute tokens for.
@@ -357,6 +373,7 @@ def parse(
break
if token.name.startswith("selector_start"):
yield from parse_rule_set(
+ scope,
tokens,
token,
is_default_rules=is_default_rules,
diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py
index 22bfccaa3f..3cafd7fa25 100644
--- a/src/textual/css/stylesheet.py
+++ b/src/textual/css/stylesheet.py
@@ -111,11 +111,14 @@ class CssSource(NamedTuple):
content: The CSS as a string.
is_defaults: True if the CSS is default (i.e. that defined at the widget level).
False if it's user CSS (which will override the defaults).
+ tie_breaker: Specificity tie breaker.
+ scope: Scope of CSS.
"""
content: str
is_defaults: bool
tie_breaker: int = 0
+ scope: str = ""
@rich.repr.auto(angular=True)
@@ -196,15 +199,16 @@ def _parse_rules(
path: str | PurePath,
is_default_rules: bool = False,
tie_breaker: int = 0,
+ scope: str = "",
) -> list[RuleSet]:
"""Parse CSS and return rules.
Args:
- is_default_rules:
css: String containing Textual CSS.
path: Path to CSS or unique identifier
is_default_rules: True if the rules we're extracting are
default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.
+ scope: Scope of rules, or empty string for global scope.
Raises:
StylesheetError: If the CSS is invalid.
@@ -215,6 +219,7 @@ def _parse_rules(
try:
rules = list(
parse(
+ scope,
css,
path,
variable_tokens=self._variable_tokens,
@@ -276,6 +281,7 @@ def add_source(
path: str | PurePath | None = None,
is_default_css: bool = False,
tie_breaker: int = 0,
+ scope: str = "",
) -> None:
"""Parse CSS from a string.
@@ -285,6 +291,7 @@ def add_source(
is_default_css: True if the CSS is defined in the Widget, False if the CSS is defined
in a user stylesheet.
tie_breaker: Integer representing the priority of this source.
+ scope: CSS type name to limit scope or empty string for no scope.
Raises:
StylesheetError: If the CSS could not be read.
@@ -297,11 +304,11 @@ def add_source(
path = str(css)
if path in self.source and self.source[path].content == css:
# Path already in source, and CSS is identical
- content, is_defaults, source_tie_breaker = self.source[path]
+ content, is_defaults, source_tie_breaker, scope = self.source[path]
if source_tie_breaker > tie_breaker:
- self.source[path] = CssSource(content, is_defaults, tie_breaker)
+ self.source[path] = CssSource(content, is_defaults, tie_breaker, scope)
return
- self.source[path] = CssSource(css, is_default_css, tie_breaker)
+ self.source[path] = CssSource(css, is_default_css, tie_breaker, scope)
self._require_parse = True
def parse(self) -> None:
@@ -313,7 +320,7 @@ def parse(self) -> None:
rules: list[RuleSet] = []
add_rules = rules.extend
- for path, (css, is_default_rules, tie_breaker) in self.source.items():
+ for path, (css, is_default_rules, tie_breaker, scope) in self.source.items():
if css in self._invalid_css:
continue
try:
@@ -322,6 +329,7 @@ def parse(self) -> None:
path,
is_default_rules=is_default_rules,
tie_breaker=tie_breaker,
+ scope=scope,
)
except Exception:
self._invalid_css.add(css)
@@ -343,9 +351,13 @@ def reparse(self) -> None:
"""
# Do this in a fresh Stylesheet so if there are errors we don't break self.
stylesheet = Stylesheet(variables=self._variables)
- for path, (css, is_defaults, tie_breaker) in self.source.items():
+ for path, (css, is_defaults, tie_breaker, scope) in self.source.items():
stylesheet.add_source(
- css, path, is_default_css=is_defaults, tie_breaker=tie_breaker
+ css,
+ path,
+ is_default_css=is_defaults,
+ tie_breaker=tie_breaker,
+ scope=scope,
)
stylesheet.parse()
self._rules = stylesheet.rules
diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py
new file mode 100644
index 0000000000..5e8e37d8d0
--- /dev/null
+++ b/src/textual/document/_document.py
@@ -0,0 +1,389 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from functools import lru_cache
+from typing import TYPE_CHECKING, NamedTuple, Tuple, overload
+
+if TYPE_CHECKING:
+ from tree_sitter import Node
+ from tree_sitter.binding import Query
+
+from textual._cells import cell_len
+from textual._types import Literal, get_args
+from textual.geometry import Size
+
+Newline = Literal["\r\n", "\n", "\r"]
+"""The type representing valid line separators."""
+VALID_NEWLINES = set(get_args(Newline))
+"""The set of valid line separator strings."""
+
+
+@dataclass
+class EditResult:
+ """Contains information about an edit that has occurred."""
+
+ end_location: Location
+ """The new end Location after the edit is complete."""
+ replaced_text: str
+ """The text that was replaced."""
+
+
+@lru_cache(maxsize=1024)
+def _utf8_encode(text: str) -> bytes:
+ """Encode the input text as utf-8 bytes.
+
+ The returned encoded bytes may be retrieved from a cache.
+
+ Args:
+ text: The text to encode.
+
+ Returns:
+ The utf-8 bytes representing the input string.
+ """
+ return text.encode("utf-8")
+
+
+def _detect_newline_style(text: str) -> Newline:
+ """Return the newline type used in this document.
+
+ Args:
+ text: The text to inspect.
+
+ Returns:
+ The NewlineStyle used in the file.
+ """
+ if "\r\n" in text: # Windows newline
+ return "\r\n"
+ elif "\n" in text: # Unix/Linux/MacOS newline
+ return "\n"
+ elif "\r" in text: # Old MacOS newline
+ return "\r"
+ else:
+ return "\n" # Default to Unix style newline
+
+
+class DocumentBase(ABC):
+ """Describes the minimum functionality a Document implementation must
+ provide in order to be used by the TextArea widget."""
+
+ @abstractmethod
+ def replace_range(self, start: Location, end: Location, text: str) -> EditResult:
+ """Replace the text at the given range.
+
+ Args:
+ start: A tuple (row, column) where the edit starts.
+ end: A tuple (row, column) where the edit ends.
+ text: The text to insert between start and end.
+
+ Returns:
+ The new end location after the edit is complete.
+ """
+
+ @property
+ @abstractmethod
+ def text(self) -> str:
+ """The text from the document as a string."""
+
+ @property
+ @abstractmethod
+ def newline(self) -> Newline:
+ """Return the line separator used in the document."""
+
+ @abstractmethod
+ def get_line(self, index: int) -> str:
+ """Returns the line with the given index from the document.
+
+ This is used in rendering lines, and will be called by the
+ TextArea for each line that is rendered.
+
+ Args:
+ index: The index of the line in the document.
+
+ Returns:
+ The str instance representing the line.
+ """
+
+ @abstractmethod
+ def get_text_range(self, start: Location, end: Location) -> str:
+ """Get the text that falls between the start and end locations.
+
+ Args:
+ start: The start location of the selection.
+ end: The end location of the selection.
+
+ Returns:
+ The text between start (inclusive) and end (exclusive).
+ """
+
+ @abstractmethod
+ def get_size(self, indent_width: int) -> Size:
+ """Get the size of the document.
+
+ The height is generally the number of lines, and the width
+ is generally the maximum cell length of all the lines.
+
+ Args:
+ indent_width: The width to use for tab characters.
+
+ Returns:
+ The Size of the document bounding box.
+ """
+
+ def query_syntax_tree(
+ self,
+ query: "Query",
+ start_point: tuple[int, int] | None = None,
+ end_point: tuple[int, int] | None = None,
+ ) -> list[tuple["Node", str]]:
+ """Query the tree-sitter syntax tree.
+
+ The default implementation always returns an empty list.
+
+ To support querying in a subclass, this must be implemented.
+
+ Args:
+ query: The tree-sitter Query to perform.
+ start_point: The (row, column byte) to start the query at.
+ end_point: The (row, column byte) to end the query at.
+
+ Returns:
+ A tuple containing the nodes and text captured by the query.
+ """
+ return []
+
+ def prepare_query(self, query: str) -> "Query" | None:
+ return None
+
+ @property
+ @abstractmethod
+ def line_count(self) -> int:
+ """Returns the number of lines in the document."""
+
+ @overload
+ def __getitem__(self, line_index: int) -> str:
+ ...
+
+ @overload
+ def __getitem__(self, line_index: slice) -> list[str]:
+ ...
+
+ @abstractmethod
+ def __getitem__(self, line_index: int | slice) -> str | list[str]:
+ """Return the content of a line as a string, excluding newline characters.
+
+ Args:
+ line_index: The index or slice of the line(s) to retrieve.
+
+ Returns:
+ The line or list of lines requested.
+ """
+
+
+class Document(DocumentBase):
+ """A document which can be opened in a TextArea."""
+
+ def __init__(self, text: str) -> None:
+ self._newline = _detect_newline_style(text)
+ """The type of newline used in the text."""
+ self._lines: list[str] = text.splitlines(keepends=False)
+ """The lines of the document, excluding newline characters.
+
+ If there's a newline at the end of the file, the final line is an empty string.
+ """
+ if text.endswith(tuple(VALID_NEWLINES)) or not text:
+ self._lines.append("")
+
+ @property
+ def lines(self) -> list[str]:
+ """Get the document as a list of strings, where each string represents a line.
+
+ Newline characters are not included in at the end of the strings.
+
+ The newline character used in this document can be found via the `Document.newline` property.
+ """
+ return self._lines
+
+ @property
+ def text(self) -> str:
+ """Get the text from the document."""
+ return self._newline.join(self._lines)
+
+ @property
+ def newline(self) -> Newline:
+ """Get the Newline used in this document (e.g. '\r\n', '\n'. etc.)"""
+ return self._newline
+
+ def get_size(self, tab_width: int) -> Size:
+ """The Size of the document, taking into account the tab rendering width.
+
+ Args:
+ tab_width: The width to use for tab indents.
+
+ Returns:
+ The size (width, height) of the document.
+ """
+ lines = self._lines
+ cell_lengths = [cell_len(line.expandtabs(tab_width)) for line in lines]
+ max_cell_length = max(cell_lengths, default=0)
+ height = len(lines)
+ return Size(max_cell_length, height)
+
+ def replace_range(self, start: Location, end: Location, text: str) -> EditResult:
+ """Replace text at the given range.
+
+ Args:
+ start: A tuple (row, column) where the edit starts.
+ end: A tuple (row, column) where the edit ends.
+ text: The text to insert between start and end.
+
+ Returns:
+ The EditResult containing information about the completed
+ replace operation.
+ """
+ top, bottom = sorted((start, end))
+ top_row, top_column = top
+ bottom_row, bottom_column = bottom
+
+ insert_lines = text.splitlines()
+ if text.endswith(tuple(VALID_NEWLINES)):
+ # Special case where a single newline character is inserted.
+ insert_lines.append("")
+
+ lines = self._lines
+
+ replaced_text = self.get_text_range(top, bottom)
+ if bottom_row >= len(lines):
+ after_selection = ""
+ else:
+ after_selection = lines[bottom_row][bottom_column:]
+
+ if top_row >= len(lines):
+ before_selection = ""
+ else:
+ before_selection = lines[top_row][:top_column]
+
+ if insert_lines:
+ insert_lines[0] = before_selection + insert_lines[0]
+ destination_column = len(insert_lines[-1])
+ insert_lines[-1] = insert_lines[-1] + after_selection
+ else:
+ destination_column = len(before_selection)
+ insert_lines = [before_selection + after_selection]
+
+ lines[top_row : bottom_row + 1] = insert_lines
+ destination_row = top_row + len(insert_lines) - 1
+
+ end_location = (destination_row, destination_column)
+ return EditResult(end_location, replaced_text)
+
+ def get_text_range(self, start: Location, end: Location) -> str:
+ """Get the text that falls between the start and end locations.
+
+ Returns the text between `start` and `end`, including the appropriate
+ line separator character as specified by `Document._newline`. Note that
+ `_newline` is set automatically to the first line separator character
+ found in the document.
+
+ Args:
+ start: The start location of the selection.
+ end: The end location of the selection.
+
+ Returns:
+ The text between start (inclusive) and end (exclusive).
+ """
+ if start == end:
+ return ""
+
+ top, bottom = sorted((start, end))
+ top_row, top_column = top
+ bottom_row, bottom_column = bottom
+ lines = self._lines
+ if top_row == bottom_row:
+ line = lines[top_row]
+ selected_text = line[top_column:bottom_column]
+ else:
+ start_line = lines[top_row]
+ end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else ""
+ selected_text = start_line[top_column:]
+ for row in range(top_row + 1, bottom_row):
+ selected_text += self._newline + lines[row]
+
+ if bottom_row < self.line_count:
+ selected_text += self._newline
+ selected_text += end_line[:bottom_column]
+
+ return selected_text
+
+ @property
+ def line_count(self) -> int:
+ """Returns the number of lines in the document."""
+ return len(self._lines)
+
+ def get_line(self, index: int) -> str:
+ """Returns the line with the given index from the document.
+
+ Args:
+ index: The index of the line in the document.
+
+ Returns:
+ The string representing the line.
+ """
+ line_string = self[index]
+ return line_string
+
+ @overload
+ def __getitem__(self, line_index: int) -> str:
+ ...
+
+ @overload
+ def __getitem__(self, line_index: slice) -> list[str]:
+ ...
+
+ def __getitem__(self, line_index: int | slice) -> str | list[str]:
+ """Return the content of a line as a string, excluding newline characters.
+
+ Args:
+ line_index: The index or slice of the line(s) to retrieve.
+
+ Returns:
+ The line or list of lines requested.
+ """
+ return self._lines[line_index]
+
+
+Location = Tuple[int, int]
+"""A location (row, column) within the document. Indexing starts at 0."""
+
+
+class Selection(NamedTuple):
+ """A range of characters within a document from a start point to the end point.
+ The location of the cursor is always considered to be the `end` point of the selection.
+ The selection is inclusive of the minimum point and exclusive of the maximum point.
+ """
+
+ start: Location = (0, 0)
+ """The start location of the selection.
+
+ If you were to click and drag a selection inside a text-editor, this is where you *started* dragging.
+ """
+ end: Location = (0, 0)
+ """The end location of the selection.
+
+ If you were to click and drag a selection inside a text-editor, this is where you *finished* dragging.
+ """
+
+ @classmethod
+ def cursor(cls, location: Location) -> "Selection":
+ """Create a Selection with the same start and end point - a "cursor".
+
+ Args:
+ location: The location to create the zero-width Selection.
+ """
+ return cls(location, location)
+
+ @property
+ def is_empty(self) -> bool:
+ """Return True if the selection has 0 width, i.e. it's just a cursor."""
+ start, end = self
+ return start == end
diff --git a/src/textual/document/_languages.py b/src/textual/document/_languages.py
new file mode 100644
index 0000000000..a33f7544e8
--- /dev/null
+++ b/src/textual/document/_languages.py
@@ -0,0 +1,13 @@
+BUILTIN_LANGUAGES = sorted(
+ [
+ "markdown",
+ "yaml",
+ "sql",
+ "css",
+ "html",
+ "json",
+ "python",
+ "regex",
+ "toml",
+ ]
+)
diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py
new file mode 100644
index 0000000000..3fd828ae48
--- /dev/null
+++ b/src/textual/document/_syntax_aware_document.py
@@ -0,0 +1,268 @@
+from __future__ import annotations
+
+try:
+ from tree_sitter import Language, Node, Parser, Tree
+ from tree_sitter.binding import Query
+ from tree_sitter_languages import get_language, get_parser
+
+ TREE_SITTER = True
+except ImportError:
+ TREE_SITTER = False
+
+from textual.document._document import Document, EditResult, Location, _utf8_encode
+from textual.document._languages import BUILTIN_LANGUAGES
+
+
+class SyntaxAwareDocumentError(Exception):
+ """General error raised when SyntaxAwareDocument is used incorrectly."""
+
+
+class SyntaxAwareDocument(Document):
+ """A wrapper around a Document which also maintains a tree-sitter syntax
+ tree when the document is edited.
+
+ The primary reason for this split is actually to keep tree-sitter stuff separate,
+ since it isn't supported in Python 3.7. By having the tree-sitter code
+ isolated in this subclass, it makes it easier to conditionally import. However,
+ it does come with other design flaws (e.g. Document is required to have methods
+ which only really make sense on SyntaxAwareDocument).
+
+ If you're reading this and Python 3.7 is no longer supported by Textual,
+ consider merging this subclass into the `Document` superclass.
+ """
+
+ def __init__(
+ self,
+ text: str,
+ language: str | Language,
+ ):
+ """Construct a SyntaxAwareDocument.
+
+ Args:
+ text: The initial text contained in the document.
+ language: The language to use. You can pass a string to use a supported
+ language, or pass in your own tree-sitter `Language` object.
+ """
+
+ if not TREE_SITTER:
+ raise RuntimeError("SyntaxAwareDocument unavailable.")
+
+ super().__init__(text)
+ self.language: Language | None = None
+ """The tree-sitter Language or None if tree-sitter is unavailable."""
+
+ self._parser: Parser | None = None
+ """The tree-sitter Parser or None if tree-sitter is unavailable."""
+
+ # If the language is `None`, then avoid doing any parsing related stuff.
+ if isinstance(language, str):
+ if language not in BUILTIN_LANGUAGES:
+ raise SyntaxAwareDocumentError(f"Invalid language {language!r}")
+ self.language = get_language(language)
+ self._parser = get_parser(language)
+ else:
+ self.language = language
+ self._parser = Parser()
+ self._parser.set_language(language)
+
+ self._syntax_tree: Tree = self._parser.parse(self._read_callable) # type: ignore
+ """The tree-sitter Tree (syntax tree) built from the document."""
+
+ @property
+ def language_name(self) -> str | None:
+ return self.language.name if self.language else None
+
+ def prepare_query(self, query: str) -> Query | None:
+ """Prepare a tree-sitter tree query.
+
+ Queries should be prepared once, then reused.
+
+ To execute a query, call `query_syntax_tree`.
+
+ Args:
+ The string query to prepare.
+
+ Returns:
+ The prepared query.
+ """
+ if not TREE_SITTER:
+ raise SyntaxAwareDocumentError(
+ "Couldn't prepare query - tree-sitter is not available on this architecture."
+ )
+
+ if self.language is None:
+ raise SyntaxAwareDocumentError(
+ "Couldn't prepare query - no language assigned."
+ )
+
+ return self.language.query(query)
+
+ def query_syntax_tree(
+ self,
+ query: Query,
+ start_point: tuple[int, int] | None = None,
+ end_point: tuple[int, int] | None = None,
+ ) -> list[tuple["Node", str]]:
+ """Query the tree-sitter syntax tree.
+
+ The default implementation always returns an empty list.
+
+ To support querying in a subclass, this must be implemented.
+
+ Args:
+ query: The tree-sitter Query to perform.
+ start_point: The (row, column byte) to start the query at.
+ end_point: The (row, column byte) to end the query at.
+
+ Returns:
+ A tuple containing the nodes and text captured by the query.
+ """
+
+ if not TREE_SITTER:
+ raise SyntaxAwareDocumentError(
+ "tree-sitter is not available on this architecture."
+ )
+
+ captures_kwargs = {}
+ if start_point is not None:
+ captures_kwargs["start_point"] = start_point
+ if end_point is not None:
+ captures_kwargs["end_point"] = end_point
+
+ captures = query.captures(self._syntax_tree.root_node, **captures_kwargs)
+ return captures
+
+ def replace_range(self, start: Location, end: Location, text: str) -> EditResult:
+ """Replace text at the given range.
+
+ Args:
+ start: A tuple (row, column) where the edit starts.
+ end: A tuple (row, column) where the edit ends.
+ text: The text to insert between start and end.
+
+ Returns:
+ The new end location after the edit is complete.
+ """
+ top, bottom = sorted((start, end))
+
+ # An optimisation would be finding the byte offsets as a single operation rather
+ # than doing two passes over the document content.
+ start_byte = self._location_to_byte_offset(top)
+ start_point = self._location_to_point(top)
+ old_end_byte = self._location_to_byte_offset(bottom)
+ old_end_point = self._location_to_point(bottom)
+
+ replace_result = super().replace_range(start, end, text)
+
+ text_byte_length = len(_utf8_encode(text))
+ end_location = replace_result.end_location
+ assert self._syntax_tree is not None
+ assert self._parser is not None
+ self._syntax_tree.edit(
+ start_byte=start_byte,
+ old_end_byte=old_end_byte,
+ new_end_byte=start_byte + text_byte_length,
+ start_point=start_point,
+ old_end_point=old_end_point,
+ new_end_point=self._location_to_point(end_location),
+ )
+ # Incrementally parse the document.
+ self._syntax_tree = self._parser.parse(
+ self._read_callable, self._syntax_tree # type: ignore[arg-type]
+ )
+
+ return replace_result
+
+ def get_line(self, line_index: int) -> str:
+ """Return the string representing the line, not including new line characters.
+
+ Args:
+ line_index: The index of the line.
+
+ Returns:
+ The string representing the line.
+ """
+ line_string = self[line_index]
+ return line_string
+
+ def _location_to_byte_offset(self, location: Location) -> int:
+ """Given a document coordinate, return the byte offset of that coordinate.
+ This method only does work if tree-sitter was imported, otherwise it returns 0.
+
+ Args:
+ location: The location to convert.
+
+ Returns:
+ An integer byte offset for the given location.
+ """
+ lines = self._lines
+ row, column = location
+ lines_above = lines[:row]
+ end_of_line_width = len(self.newline)
+ bytes_lines_above = sum(
+ len(_utf8_encode(line)) + end_of_line_width for line in lines_above
+ )
+ if row < len(lines):
+ bytes_on_left = len(_utf8_encode(lines[row][:column]))
+ else:
+ bytes_on_left = 0
+ byte_offset = bytes_lines_above + bytes_on_left
+ return byte_offset
+
+ def _location_to_point(self, location: Location) -> tuple[int, int]:
+ """Convert a document location (row_index, column_index) to a tree-sitter
+ point (row_index, byte_offset_from_start_of_row). If tree-sitter isn't available
+ returns (0, 0).
+
+ Args:
+ location: A location (row index, column codepoint offset)
+
+ Returns:
+ The point corresponding to that location (row index, column byte offset).
+ """
+ lines = self._lines
+ row, column = location
+ if row < len(lines):
+ bytes_on_left = len(_utf8_encode(lines[row][:column]))
+ else:
+ bytes_on_left = 0
+ return row, bytes_on_left
+
+ def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes:
+ """A callable which informs tree-sitter about the document content.
+
+ This is passed to tree-sitter which will call it frequently to retrieve
+ the bytes from the document.
+
+ Args:
+ byte_offset: The number of (utf-8) bytes from the start of the document.
+ point: A tuple (row index, column *byte* offset). Note that this differs
+ from our Location tuple which is (row_index, column codepoint offset).
+
+ Returns:
+ All the utf-8 bytes between the byte_offset/point and the end of the current
+ line _including_ the line separator character(s). Returns None if the
+ offset/point requested by tree-sitter doesn't correspond to a byte.
+ """
+ row, column = point
+ lines = self._lines
+ newline = self.newline
+
+ row_out_of_bounds = row >= len(lines)
+ if row_out_of_bounds:
+ return b""
+ else:
+ row_text = lines[row]
+
+ encoded_row = _utf8_encode(row_text)
+ encoded_row_length = len(encoded_row)
+
+ if column < encoded_row_length:
+ return encoded_row[column:] + _utf8_encode(newline)
+ elif column == encoded_row_length:
+ return _utf8_encode(newline[0])
+ elif column == encoded_row_length + 1:
+ if newline == "\r\n":
+ return b"\n"
+
+ return b""
diff --git a/src/textual/dom.py b/src/textual/dom.py
index a65b8beeea..d63308ad5c 100644
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -132,6 +132,10 @@ class DOMNode(MessagePump):
# Mapping of key bindings
BINDINGS: ClassVar[list[BindingType]] = []
+ # Indicates if the CSS should be automatically scoped
+ SCOPED_CSS: ClassVar[bool] = True
+ """Should default css be limited to the widget type?"""
+
# True if this node inherits the CSS from the base class.
_inherit_css: ClassVar[bool] = True
@@ -144,6 +148,9 @@ class DOMNode(MessagePump):
# List of names of base classes that inherit CSS
_css_type_names: ClassVar[frozenset[str]] = frozenset()
+ # Name of the widget in CSS
+ _css_type_name: str = ""
+
# Generated list of bindings
_merged_bindings: ClassVar[_Bindings | None] = None
@@ -304,7 +311,9 @@ def __init_subclass__(
cls._inherit_bindings = inherit_bindings
cls._inherit_component_classes = inherit_component_classes
css_type_names: set[str] = set()
- for base in cls._css_bases(cls):
+ bases = cls._css_bases(cls)
+ cls._css_type_name = bases[0].__name__
+ for base in bases:
css_type_names.add(base.__name__)
cls._merged_bindings = cls._merge_bindings()
cls._css_type_names = frozenset(css_type_names)
@@ -399,23 +408,26 @@ def _post_register(self, app: App) -> None:
"""
def __rich_repr__(self) -> rich.repr.Result:
- yield "name", self._name, None
- yield "id", self._id, None
- if self._classes:
+ # Being a bit defensive here to guard against errors when calling repr before initialization
+ if hasattr(self, "_name"):
+ yield "name", self._name, None
+ if hasattr(self, "_id"):
+ yield "id", self._id, None
+ if hasattr(self, "_classes") and self._classes:
yield "classes", " ".join(self._classes)
- def _get_default_css(self) -> list[tuple[str, str, int]]:
+ def _get_default_css(self) -> list[tuple[str, str, int, str]]:
"""Gets the CSS for this class and inherited from bases.
Default CSS is inherited from base classes, unless `inherit_css` is set to
`False` when subclassing.
Returns:
- A list of tuples containing (PATH, SOURCE) for this
+ A list of tuples containing (PATH, SOURCE, SPECIFICITY, SCOPE) for this
and inherited from base classes.
"""
- css_stack: list[tuple[str, str, int]] = []
+ css_stack: list[tuple[str, str, int, str]] = []
def get_path(base: Type[DOMNode]) -> str:
"""Get a path to the DOM Node"""
@@ -425,10 +437,17 @@ def get_path(base: Type[DOMNode]) -> str:
return f"{base.__name__}"
for tie_breaker, base in enumerate(self._node_bases):
- css = base.__dict__.get("DEFAULT_CSS", "").strip()
+ css: str = base.__dict__.get("DEFAULT_CSS", "").strip()
if css:
- css_stack.append((get_path(base), css, -tie_breaker))
-
+ scoped: bool = base.__dict__.get("SCOPED_CSS", True)
+ css_stack.append(
+ (
+ get_path(base),
+ css,
+ -tie_breaker,
+ base._css_type_name if scoped else "",
+ )
+ )
return css_stack
@classmethod
diff --git a/src/textual/errors.py b/src/textual/errors.py
index 021bcff0fa..034139e204 100644
--- a/src/textual/errors.py
+++ b/src/textual/errors.py
@@ -1,3 +1,8 @@
+"""
+General exception classes.
+
+"""
+
from __future__ import annotations
diff --git a/src/textual/events.py b/src/textual/events.py
index af8aaf0533..7cff7d01d0 100644
--- a/src/textual/events.py
+++ b/src/textual/events.py
@@ -417,6 +417,19 @@ def get_content_offset(self, widget: Widget) -> Offset | None:
"""
if self.screen_offset not in widget.content_region:
return None
+ return self.get_content_offset_capture(widget)
+
+ def get_content_offset_capture(self, widget: Widget) -> Offset:
+ """Get offset from a widget's content area.
+
+ This method works even if the offset is outside the widget content region.
+
+ Args:
+ widget: Widget receiving the event.
+
+ Returns:
+ An offset where the origin is at the top left of the content area.
+ """
return self.offset - widget.gutter.top_left
def _apply_offset(self, x: int, y: int) -> MouseEvent:
diff --git a/src/textual/expand_tabs.py b/src/textual/expand_tabs.py
new file mode 100644
index 0000000000..9227f796c2
--- /dev/null
+++ b/src/textual/expand_tabs.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import re
+
+from rich.cells import cell_len
+
+_TABS_SPLITTER_RE = re.compile(r"(.*?\t|.+?$)")
+
+
+def expand_tabs_inline(line: str, tab_size: int = 4) -> str:
+ """Expands tabs, taking into account double cell characters.
+
+ Args:
+ line: The text to expand tabs in.
+ tab_size: Number of cells in a tab.
+ Returns:
+ New string with tabs replaced with spaces.
+ """
+ if "\t" not in line:
+ return line
+ new_line_parts: list[str] = []
+ add_part = new_line_parts.append
+ cell_position = 0
+ parts = _TABS_SPLITTER_RE.findall(line)
+
+ for part in parts:
+ if part.endswith("\t"):
+ part = f"{part[:-1]} "
+ cell_position += cell_len(part)
+ tab_remainder = cell_position % tab_size
+ if tab_remainder:
+ spaces = tab_size - tab_remainder
+ part += spaces * " "
+ add_part(part)
+
+ return "".join(new_line_parts)
+
+
+if __name__ == "__main__":
+ print(expand_tabs_inline("\tbar"))
+ print(expand_tabs_inline("1\tbar"))
+ print(expand_tabs_inline("12\tbar"))
+ print(expand_tabs_inline("123\tbar"))
+ print(expand_tabs_inline("1234\tbar"))
+ print(expand_tabs_inline("💩\tbar"))
+ print(expand_tabs_inline("💩💩\tbar"))
+ print(expand_tabs_inline("💩💩💩\tbar"))
+ print(expand_tabs_inline("F💩\tbar"))
+ print(expand_tabs_inline("F💩O\tbar"))
diff --git a/src/textual/filter.py b/src/textual/filter.py
index 65378818eb..7494d9a52a 100644
--- a/src/textual/filter.py
+++ b/src/textual/filter.py
@@ -1,3 +1,16 @@
+"""Filter classes.
+
+!!! note
+
+ Filters are used internally, and not recommended for use by Textual app developers.
+
+Filters are used internally to process terminal output after it has been rendered.
+Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set.
+
+In the future, this system will be used to implement accessibility features.
+
+"""
+
from __future__ import annotations
from abc import ABC, abstractmethod
diff --git a/src/textual/_fuzzy.py b/src/textual/fuzzy.py
similarity index 97%
rename from src/textual/_fuzzy.py
rename to src/textual/fuzzy.py
index 3fa4b0094f..f2c46259d2 100644
--- a/src/textual/_fuzzy.py
+++ b/src/textual/fuzzy.py
@@ -1,3 +1,10 @@
+"""
+Fuzzy matcher.
+
+This class is used by the [command palette](guide/command_palette) to match search terms.
+
+"""
+
from __future__ import annotations
from re import IGNORECASE, compile, escape
diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py
index 7ed468dca2..3d49080a6a 100644
--- a/src/textual/message_pump.py
+++ b/src/textual/message_pump.py
@@ -1,6 +1,11 @@
"""
-A message pump is a base class for any object which processes messages, which includes Widget, Screen, and App.
+A `MessagePump` is a base class for any object which processes messages, which includes Widget, Screen, and App.
+
+!!! tip
+
+ Most of the method here are useful in general app development.
+
"""
from __future__ import annotations
diff --git a/src/textual/pilot.py b/src/textual/pilot.py
index a94b41a908..c3c64d2e9a 100644
--- a/src/textual/pilot.py
+++ b/src/textual/pilot.py
@@ -1,6 +1,9 @@
"""
The pilot object is used by [App.run_test][textual.app.App.run_test] to programmatically operate an app.
+
+See the guide on how to [test Textual apps](/guide/testing).
+
"""
from __future__ import annotations
@@ -13,13 +16,12 @@
from ._wait import wait_for_idle
from .app import App, ReturnType
from .events import Click, MouseDown, MouseMove, MouseUp
-from .geometry import Offset
from .widget import Widget
def _get_mouse_message_arguments(
target: Widget,
- offset: Offset = Offset(),
+ offset: tuple[int, int] = (0, 0),
button: int = 0,
shift: bool = False,
meta: bool = False,
@@ -43,7 +45,10 @@ def _get_mouse_message_arguments(
class WaitForScreenTimeout(Exception):
- pass
+ """Exception raised if messages aren't being processed quickly enough.
+
+ If this occurs, the most likely explanation is some kind of deadlock in the app code.
+ """
@rich.repr.auto(angular=True)
@@ -74,7 +79,7 @@ async def press(self, *keys: str) -> None:
async def click(
self,
selector: type[Widget] | str | None = None,
- offset: Offset = Offset(),
+ offset: tuple[int, int] = (0, 0),
shift: bool = False,
meta: bool = False,
control: bool = False,
@@ -112,7 +117,7 @@ async def click(
async def hover(
self,
selector: type[Widget] | str | None | None = None,
- offset: Offset = Offset(),
+ offset: tuple[int, int] = (0, 0),
) -> None:
"""Simulate hovering with the mouse cursor.
diff --git a/src/textual/screen.py b/src/textual/screen.py
index 88df054aa6..631dadb4ba 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -49,7 +49,7 @@
if TYPE_CHECKING:
from typing_extensions import Final
- from .command_palette import CommandSource
+ from .command import Provider
# Unused & ignored imports are needed for the docs to link to these objects:
from .errors import NoWidget # type: ignore # noqa: F401
@@ -72,7 +72,7 @@ class ResultCallback(Generic[ScreenResultType]):
def __init__(
self,
- requester: Widget | None,
+ requester: MessagePump,
callback: ScreenResultCallbackType[ScreenResultType] | None,
) -> None:
"""Initialise the result callback object.
@@ -81,7 +81,7 @@ def __init__(
requester: The object making a request for the callback.
callback: The callback function.
"""
- self.requester: Widget | None = requester
+ self.requester = requester
"""The object in the DOM that requested the callback."""
self.callback: ScreenResultCallbackType | None = callback
"""The callback function."""
@@ -157,8 +157,11 @@ class Screen(Generic[ScreenResultType], Widget):
title: Reactive[str | None] = Reactive(None, compute=False)
"""Screen title to override [the app title][textual.app.App.title]."""
- COMMAND_SOURCES: ClassVar[set[type[CommandSource]]] = set()
- """The [command sources](/api/command_palette/) for the screen."""
+ COMMANDS: ClassVar[set[type[Provider]]] = set()
+ """Command providers used by the [command palette](/guide/command_palette), associated with the screen.
+
+ Should be a set of [`command.Provider`][textual.command.Provider] classes.
+ """
BINDINGS = [
Binding("tab", "focus_next", "Focus Next", show=False),
@@ -682,7 +685,7 @@ def _invoke_later(self, callback: CallbackType, sender: MessagePump) -> None:
def _push_result_callback(
self,
- requester: Widget | None,
+ requester: MessagePump,
callback: ScreenResultCallbackType[ScreenResultType] | None,
) -> None:
"""Add a result callback to the screen.
@@ -950,7 +953,9 @@ def _forward_event(self, event: events.Event) -> None:
except errors.NoWidget:
self.set_focus(None)
else:
- if isinstance(event, events.MouseUp) and widget.focusable:
+ if isinstance(event, events.MouseDown) and widget.focusable:
+ self.set_focus(widget)
+ elif isinstance(event, events.MouseUp) and widget.focusable:
if self.focused is not widget:
self.set_focus(widget)
event.stop()
diff --git a/src/textual/suggester.py b/src/textual/suggester.py
index 362fe89f6d..505993b43a 100644
--- a/src/textual/suggester.py
+++ b/src/textual/suggester.py
@@ -1,3 +1,9 @@
+"""
+
+The `Suggester` class is used by the [Input](/widgets/input) widget.
+
+"""
+
from __future__ import annotations
from abc import ABC, abstractmethod
diff --git a/src/textual/tree-sitter/highlights/bash.scm b/src/textual/tree-sitter/highlights/bash.scm
new file mode 100644
index 0000000000..23bf03e697
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/bash.scm
@@ -0,0 +1,145 @@
+(simple_expansion) @none
+(expansion
+ "${" @punctuation.special
+ "}" @punctuation.special) @none
+[
+ "("
+ ")"
+ "(("
+ "))"
+ "{"
+ "}"
+ "["
+ "]"
+ "[["
+ "]]"
+ ] @punctuation.bracket
+
+[
+ ";"
+ ";;"
+ (heredoc_start)
+ ] @punctuation.delimiter
+
+[
+ "$"
+] @punctuation.special
+
+[
+ ">"
+ ">>"
+ "<"
+ "<<"
+ "&"
+ "&&"
+ "|"
+ "||"
+ "="
+ "=~"
+ "=="
+ "!="
+ ] @operator
+
+[
+ (string)
+ (raw_string)
+ (ansi_c_string)
+ (heredoc_body)
+] @string @spell
+
+(variable_assignment (word) @string)
+
+[
+ "if"
+ "then"
+ "else"
+ "elif"
+ "fi"
+ "case"
+ "in"
+ "esac"
+ ] @conditional
+
+[
+ "for"
+ "do"
+ "done"
+ "select"
+ "until"
+ "while"
+ ] @repeat
+
+[
+ "declare"
+ "export"
+ "local"
+ "readonly"
+ "unset"
+ ] @keyword
+
+"function" @keyword.function
+
+(special_variable_name) @constant
+
+; trap -l
+((word) @constant.builtin
+ (#match? @constant.builtin "^SIG(HUP|INT|QUIT|ILL|TRAP|ABRT|BUS|FPE|KILL|USR[12]|SEGV|PIPE|ALRM|TERM|STKFLT|CHLD|CONT|STOP|TSTP|TT(IN|OU)|URG|XCPU|XFSZ|VTALRM|PROF|WINCH|IO|PWR|SYS|RTMIN([+]([1-9]|1[0-5]))?|RTMAX(-([1-9]|1[0-4]))?)$"))
+
+((word) @boolean
+ (#any-of? @boolean "true" "false"))
+
+(comment) @comment @spell
+(test_operator) @string
+
+(command_substitution
+ [ "$(" ")" ] @punctuation.bracket)
+
+(process_substitution
+ [ "<(" ")" ] @punctuation.bracket)
+
+
+(function_definition
+ name: (word) @function)
+
+(command_name (word) @function.call)
+
+((command_name (word) @function.builtin)
+ (#any-of? @function.builtin
+ "alias" "bg" "bind" "break" "builtin" "caller" "cd"
+ "command" "compgen" "complete" "compopt" "continue"
+ "coproc" "dirs" "disown" "echo" "enable" "eval"
+ "exec" "exit" "fc" "fg" "getopts" "hash" "help"
+ "history" "jobs" "kill" "let" "logout" "mapfile"
+ "popd" "printf" "pushd" "pwd" "read" "readarray"
+ "return" "set" "shift" "shopt" "source" "suspend"
+ "test" "time" "times" "trap" "type" "typeset"
+ "ulimit" "umask" "unalias" "wait"))
+
+(command
+ argument: [
+ (word) @parameter
+ (concatenation (word) @parameter)
+ ])
+
+((word) @number
+ (#lua-match? @number "^[0-9]+$"))
+
+(file_redirect
+ descriptor: (file_descriptor) @operator
+ destination: (word) @parameter)
+
+(expansion
+ [ "${" "}" ] @punctuation.bracket)
+
+(variable_name) @variable
+
+((variable_name) @constant
+ (#lua-match? @constant "^[A-Z][A-Z_0-9]*$"))
+
+(case_item
+ value: (word) @parameter)
+
+(regex) @string.regex
+
+((program . (comment) @preproc)
+ (#lua-match? @preproc "^#!/"))
diff --git a/src/textual/tree-sitter/highlights/css.scm b/src/textual/tree-sitter/highlights/css.scm
new file mode 100644
index 0000000000..b26f0ec96c
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/css.scm
@@ -0,0 +1,91 @@
+[
+ "@media"
+ "@charset"
+ "@namespace"
+ "@supports"
+ "@keyframes"
+ (at_keyword)
+ (to)
+ (from)
+ ] @keyword
+
+"@import" @include
+
+(comment) @comment @spell
+
+[
+ (tag_name)
+ (nesting_selector)
+ (universal_selector)
+ ] @type
+
+(function_name) @function
+
+[
+ "~"
+ ">"
+ "+"
+ "-"
+ "*"
+ "/"
+ "="
+ "^="
+ "|="
+ "~="
+ "$="
+ "*="
+ "and"
+ "or"
+ "not"
+ "only"
+ ] @operator
+
+(important) @type.qualifier
+
+(attribute_selector (plain_value) @string)
+(pseudo_element_selector "::" (tag_name) @property)
+(pseudo_class_selector (class_name) @property)
+
+[
+ (class_name)
+ (id_name)
+ (property_name)
+ (feature_name)
+ (attribute_name)
+ ] @property
+
+(namespace_name) @namespace
+
+((property_name) @type.definition
+ (#lua-match? @type.definition "^[-][-]"))
+((plain_value) @type
+ (#lua-match? @type "^[-][-]"))
+
+[
+ (string_value)
+ (color_value)
+ (unit)
+ ] @string
+
+[
+ (integer_value)
+ (float_value)
+ ] @number
+
+[
+ "#"
+ ","
+ "."
+ ":"
+ "::"
+ ";"
+ ] @punctuation.delimiter
+
+[
+ "{"
+ ")"
+ "("
+ "}"
+ ] @punctuation.bracket
+
+(ERROR) @error
diff --git a/src/textual/tree-sitter/highlights/html.scm b/src/textual/tree-sitter/highlights/html.scm
new file mode 100644
index 0000000000..15f2adb436
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/html.scm
@@ -0,0 +1,64 @@
+(tag_name) @tag
+(erroneous_end_tag_name) @html.end_tag_error
+(comment) @comment
+(attribute_name) @tag.attribute
+(attribute
+ (quoted_attribute_value) @string)
+(text) @text @spell
+
+((element (start_tag (tag_name) @_tag) (text) @text.title)
+ (#eq? @_tag "title"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.title.1)
+ (#eq? @_tag "h1"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.title.2)
+ (#eq? @_tag "h2"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.title.3)
+ (#eq? @_tag "h3"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.title.4)
+ (#eq? @_tag "h4"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.title.5)
+ (#eq? @_tag "h5"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.title.6)
+ (#eq? @_tag "h6"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.strong)
+ (#any-of? @_tag "strong" "b"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.emphasis)
+ (#any-of? @_tag "em" "i"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.strike)
+ (#any-of? @_tag "s" "del"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.underline)
+ (#eq? @_tag "u"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.literal)
+ (#any-of? @_tag "code" "kbd"))
+
+((element (start_tag (tag_name) @_tag) (text) @text.uri)
+ (#eq? @_tag "a"))
+
+((attribute
+ (attribute_name) @_attr
+ (quoted_attribute_value (attribute_value) @text.uri))
+ (#any-of? @_attr "href" "src"))
+
+[
+ "<"
+ ">"
+ ""
+ "/>"
+] @tag.delimiter
+
+"=" @operator
+
+(doctype) @constant
+
+""
+ "="
+ "=="
+ ">"
+ ">="
+ ">>"
+ ">>="
+ "@"
+ "@="
+ "|"
+ "|="
+ "~"
+ "->"
+] @operator
+
+; Keywords
+[
+ "and"
+ "in"
+ "is"
+ "not"
+ "or"
+ "del"
+] @keyword.operator
+
+[
+ "def"
+ "lambda"
+] @keyword.function
+
+[
+ "assert"
+ "async"
+ "await"
+ "class"
+ "exec"
+ "global"
+ "nonlocal"
+ "pass"
+ "print"
+ "with"
+ "as"
+] @keyword
+
+[
+ "return"
+ "yield"
+] @keyword.return
+(yield "from" @keyword.return)
+
+(future_import_statement
+ "from" @include
+ "__future__" @constant.builtin)
+(import_from_statement "from" @include)
+"import" @include
+
+(aliased_import "as" @include)
+
+["if" "elif" "else" "match" "case"] @conditional
+
+["for" "while" "break" "continue"] @repeat
+
+[
+ "try"
+ "except"
+ "raise"
+ "finally"
+] @exception
+
+(raise_statement "from" @exception)
+
+(try_statement
+ (else_clause
+ "else" @exception))
+
+["(" ")" "[" "]" "{" "}"] @punctuation.bracket
+
+(interpolation
+ "{" @punctuation.special
+ "}" @punctuation.special)
+
+["," "." ":" ";" (ellipsis)] @punctuation.delimiter
+
+;; Class definitions
+
+(class_definition name: (identifier) @type.class)
+
+(class_definition
+ body: (block
+ (function_definition
+ name: (identifier) @method)))
+
+(class_definition
+ superclasses: (argument_list
+ (identifier) @type))
+
+((class_definition
+ body: (block
+ (expression_statement
+ (assignment
+ left: (identifier) @field))))
+ (#match? @field "^([A-Z])@!.*$"))
+((class_definition
+ body: (block
+ (expression_statement
+ (assignment
+ left: (_
+ (identifier) @field)))))
+ (#match? @field "^([A-Z])@!.*$"))
+
+((class_definition
+ (block
+ (function_definition
+ name: (identifier) @constructor)))
+ (#any-of? @constructor "__new__" "__init__"))
+
+;; Error
+(ERROR) @error
diff --git a/src/textual/tree-sitter/highlights/regex.scm b/src/textual/tree-sitter/highlights/regex.scm
new file mode 100644
index 0000000000..7c671c2c04
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/regex.scm
@@ -0,0 +1,34 @@
+;; Forked from tree-sitter-regex
+;; The MIT License (MIT) Copyright (c) 2014 Max Brunsfeld
+[
+ "("
+ ")"
+ "(?"
+ "(?:"
+ "(?<"
+ ">"
+ "["
+ "]"
+ "{"
+ "}"
+] @regex.punctuation.bracket
+
+(group_name) @property
+
+;; These are escaped special characters that lost their special meaning
+;; -> no special highlighting
+(identity_escape) @string.regex
+
+(class_character) @constant
+
+[
+ (control_letter_escape)
+ (character_class_escape)
+ (control_escape)
+ (start_assertion)
+ (end_assertion)
+ (boundary_assertion)
+ (non_boundary_assertion)
+] @string.escape
+
+[ "*" "+" "?" "|" "=" "!" ] @regex.operator
diff --git a/src/textual/tree-sitter/highlights/sql.scm b/src/textual/tree-sitter/highlights/sql.scm
new file mode 100644
index 0000000000..03a15fe381
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/sql.scm
@@ -0,0 +1,114 @@
+(string) @string
+(number) @number
+(comment) @comment
+
+(function_call
+ function: (identifier) @function)
+
+[
+ (NULL)
+ (TRUE)
+ (FALSE)
+] @constant.builtin
+
+([
+ (type_cast
+ (type (identifier) @type.builtin))
+ (create_function_statement
+ (type (identifier) @type.builtin))
+ (create_function_statement
+ (create_function_parameters
+ (create_function_parameter (type (identifier) @type.builtin))))
+ (create_type_statement
+ (type_spec_composite (type (identifier) @type.builtin)))
+ (create_table_statement
+ (table_parameters
+ (table_column (type (identifier) @type.builtin))))
+ ]
+ (#match?
+ @type.builtin
+ "^(bigint|BIGINT|int8|INT8|bigserial|BIGSERIAL|serial8|SERIAL8|bit|BIT|varbit|VARBIT|boolean|BOOLEAN|bool|BOOL|box|BOX|bytea|BYTEA|character|CHARACTER|char|CHAR|varchar|VARCHAR|cidr|CIDR|circle|CIRCLE|date|DATE|float8|FLOAT8|inet|INET|integer|INTEGER|int|INT|int4|INT4|interval|INTERVAL|json|JSON|jsonb|JSONB|line|LINE|lseg|LSEG|macaddr|MACADDR|money|MONEY|numeric|NUMERIC|decimal|DECIMAL|path|PATH|pg_lsn|PG_LSN|point|POINT|polygon|POLYGON|real|REAL|float4|FLOAT4|smallint|SMALLINT|int2|INT2|smallserial|SMALLSERIAL|serial2|SERIAL2|serial|SERIAL|serial4|SERIAL4|text|TEXT|time|TIME|time|TIME|timestamp|TIMESTAMP|tsquery|TSQUERY|tsvector|TSVECTOR|txid_snapshot|TXID_SNAPSHOT|enum|ENUM|range|RANGE)$"))
+
+(identifier) @variable
+
+[
+ "::"
+ "<"
+ "<="
+ "<>"
+ "="
+ ">"
+ ">="
+] @operator
+
+[
+ "("
+ ")"
+ "["
+ "]"
+] @punctuation.bracket
+
+[
+ ";"
+ "."
+] @punctuation.delimiter
+
+[
+ (type)
+ (array_type)
+] @type
+
+[
+ (primary_key_constraint)
+ (unique_constraint)
+ (null_constraint)
+] @keyword
+
+[
+ "AND"
+ "AS"
+ "AUTO_INCREMENT"
+ "CREATE"
+ "CREATE_DOMAIN"
+ "CREATE_OR_REPLACE_FUNCTION"
+ "CREATE_SCHEMA"
+ "TABLE"
+ "TEMPORARY"
+ "CREATE_TYPE"
+ "DATABASE"
+ "FROM"
+ "GRANT"
+ "GROUP_BY"
+ "IF_NOT_EXISTS"
+ "INDEX"
+ "INNER"
+ "INSERT"
+ "INTO"
+ "IN"
+ "JOIN"
+ "LANGUAGE"
+ "LEFT"
+ "LOCAL"
+ "NOT"
+ "ON"
+ "OR"
+ "ORDER_BY"
+ "OUTER"
+ "PRIMARY_KEY"
+ "PUBLIC"
+ "RETURNS"
+ "SCHEMA"
+ "SELECT"
+ "SESSION"
+ "SET"
+ "TABLE"
+ "TIME_ZONE"
+ "TO"
+ "UNIQUE"
+ "UPDATE"
+ "USAGE"
+ "VALUES"
+ "WHERE"
+ "WITH"
+ "WITHOUT"
+] @keyword
diff --git a/src/textual/tree-sitter/highlights/toml.scm b/src/textual/tree-sitter/highlights/toml.scm
new file mode 100644
index 0000000000..9228d28072
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/toml.scm
@@ -0,0 +1,36 @@
+; Properties
+;-----------
+
+(bare_key) @toml.type
+(quoted_key) @string
+(pair (bare_key)) @property
+
+; Literals
+;---------
+
+(boolean) @boolean
+(comment) @comment @spell
+(string) @string
+(integer) @number
+(float) @float
+(offset_date_time) @toml.datetime
+(local_date_time) @toml.datetime
+(local_date) @toml.datetime
+(local_time) @toml.datetime
+
+; Punctuation
+;------------
+
+"." @punctuation.delimiter
+"," @punctuation.delimiter
+
+"=" @toml.operator
+
+"[" @punctuation.bracket
+"]" @punctuation.bracket
+"[[" @punctuation.bracket
+"]]" @punctuation.bracket
+"{" @punctuation.bracket
+"}" @punctuation.bracket
+
+(ERROR) @toml.error
diff --git a/src/textual/tree-sitter/highlights/yaml.scm b/src/textual/tree-sitter/highlights/yaml.scm
new file mode 100644
index 0000000000..a57f464dfc
--- /dev/null
+++ b/src/textual/tree-sitter/highlights/yaml.scm
@@ -0,0 +1,53 @@
+(boolean_scalar) @boolean
+(null_scalar) @constant.builtin
+(double_quote_scalar) @string
+(single_quote_scalar) @string
+((block_scalar) @string (#set! "priority" 99))
+(string_scalar) @string
+(escape_sequence) @string.escape
+(integer_scalar) @number
+(float_scalar) @number
+(comment) @comment
+(anchor_name) @type
+(alias_name) @type
+(tag) @type
+(ERROR) @error
+
+[
+ (yaml_directive)
+ (tag_directive)
+ (reserved_directive)
+] @preproc
+
+(block_mapping_pair
+ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @yaml.field))
+(block_mapping_pair
+ key: (flow_node (plain_scalar (string_scalar) @yaml.field)))
+
+(flow_mapping
+ (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @yaml.field)))
+(flow_mapping
+ (_ key: (flow_node (plain_scalar (string_scalar) @yaml.field))))
+
+[
+ ","
+ "-"
+ ":"
+ ">"
+ "?"
+ "|"
+] @punctuation.delimiter
+
+[
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "*"
+ "&"
+ "---"
+ "..."
+] @punctuation.special
diff --git a/src/textual/types.py b/src/textual/types.py
index 0bb237f943..024d388f24 100644
--- a/src/textual/types.py
+++ b/src/textual/types.py
@@ -5,7 +5,13 @@
from ._animator import Animatable, EasingFunction
from ._context import NoActiveAppError
from ._path import CSSPathError, CSSPathType
-from ._types import CallbackType, MessageTarget, WatchCallbackType
+from ._types import (
+ CallbackType,
+ IgnoreReturnCallbackType,
+ MessageTarget,
+ UnusedParameter,
+ WatchCallbackType,
+)
from .actions import ActionParseResult
from .css.styles import RenderStyles
from .widgets._data_table import CursorType
@@ -19,9 +25,11 @@
"CSSPathType",
"CursorType",
"EasingFunction",
+ "IgnoreReturnCallbackType",
"InputValidationOn",
"MessageTarget",
"NoActiveAppError",
"RenderStyles",
+ "UnusedParameter",
"WatchCallbackType",
]
diff --git a/src/textual/widget.py b/src/textual/widget.py
index 2f79d4c20b..b229c70735 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -915,9 +915,13 @@ def _post_register(self, app: App) -> None:
app: App instance.
"""
# Parse the Widget's CSS
- for path, css, tie_breaker in self._get_default_css():
+ for path, css, tie_breaker, scope in self._get_default_css():
self.app.stylesheet.add_source(
- css, path=path, is_default_css=True, tie_breaker=tie_breaker
+ css,
+ path=path,
+ is_default_css=True,
+ tie_breaker=tie_breaker,
+ scope=scope,
)
def _get_box_model(
@@ -2757,6 +2761,7 @@ def get_pseudo_classes(self) -> Iterable[str]:
except NoScreen:
pass
else:
+ yield "dark" if self.app.dark else "light"
if focused:
node = focused
while node is not None:
@@ -3186,7 +3191,7 @@ def release_mouse(self) -> None:
def begin_capture_print(self, stdout: bool = True, stderr: bool = True) -> None:
"""Capture text from print statements (or writes to stdout / stderr).
- If printing is captured, the widget will be send an [events.Print][textual.events.Print] message.
+ If printing is captured, the widget will be sent an [events.Print][textual.events.Print] message.
Call [end_capture_print][textual.widget.Widget.end_capture_print] to disable print capture.
diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py
index 8b03bfb5e5..cd6e21f13b 100644
--- a/src/textual/widgets/__init__.py
+++ b/src/textual/widgets/__init__.py
@@ -12,6 +12,7 @@
from ..widget import Widget
from ._button import Button
from ._checkbox import Checkbox
+ from ._collapsible import Collapsible
from ._content_switcher import ContentSwitcher
from ._data_table import DataTable
from ._digits import Digits
@@ -40,14 +41,15 @@
from ._switch import Switch
from ._tabbed_content import TabbedContent, TabPane
from ._tabs import Tab, Tabs
+ from ._text_area import TextArea
from ._tooltip import Tooltip
from ._tree import Tree
from ._welcome import Welcome
-
__all__ = [
"Button",
"Checkbox",
+ "Collapsible",
"ContentSwitcher",
"DataTable",
"Digits",
@@ -78,6 +80,7 @@
"TabbedContent",
"TabPane",
"Tabs",
+ "TextArea",
"RichLog",
"Tooltip",
"Tree",
diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi
index de3d049357..d4db2f8f52 100644
--- a/src/textual/widgets/__init__.pyi
+++ b/src/textual/widgets/__init__.pyi
@@ -1,6 +1,7 @@
# This stub file must re-export every classes exposed in the __init__.py's `__all__` list:
from ._button import Button as Button
from ._checkbox import Checkbox as Checkbox
+from ._collapsible import Collapsible as Collapsible
from ._content_switcher import ContentSwitcher as ContentSwitcher
from ._data_table import DataTable as DataTable
from ._digits import Digits as Digits
@@ -32,6 +33,7 @@ from ._tabbed_content import TabbedContent as TabbedContent
from ._tabbed_content import TabPane as TabPane
from ._tabs import Tab as Tab
from ._tabs import Tabs as Tabs
+from ._text_area import TextArea as TextArea
from ._tooltip import Tooltip as Tooltip
from ._tree import Tree as Tree
from ._welcome import Welcome as Welcome
diff --git a/src/textual/widgets/_collapsible.py b/src/textual/widgets/_collapsible.py
new file mode 100644
index 0000000000..5901cbc9de
--- /dev/null
+++ b/src/textual/widgets/_collapsible.py
@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+from rich.console import RenderableType
+from rich.text import Text
+
+from .. import events
+from ..app import ComposeResult
+from ..binding import Binding
+from ..containers import Container
+from ..css.query import NoMatches
+from ..message import Message
+from ..reactive import reactive
+from ..widget import Widget
+
+__all__ = ["Collapsible", "CollapsibleTitle"]
+
+
+class CollapsibleTitle(Widget, can_focus=True):
+ """Title and symbol for the Collapsible."""
+
+ DEFAULT_CSS = """
+ CollapsibleTitle {
+ width: auto;
+ height: auto;
+ padding: 0 1 0 1;
+ }
+
+ CollapsibleTitle:hover {
+ background: $foreground 10%;
+ color: $text;
+ }
+
+ CollapsibleTitle:focus {
+ background: $accent;
+ color: $text;
+ }
+ """
+
+ BINDINGS = [Binding("enter", "toggle", "Toggle collapsible", show=False)]
+ """
+ | Key(s) | Description |
+ | :- | :- |
+ | enter | Toggle the collapsible. |
+ """
+
+ collapsed = reactive(True)
+
+ def __init__(
+ self,
+ *,
+ label: str,
+ collapsed_symbol: str,
+ expanded_symbol: str,
+ collapsed: bool,
+ ) -> None:
+ super().__init__()
+ self.collapsed_symbol = collapsed_symbol
+ self.expanded_symbol = expanded_symbol
+ self.label = label
+ self.collapse = collapsed
+
+ class Toggle(Message):
+ """Request toggle."""
+
+ async def _on_click(self, event: events.Click) -> None:
+ """Inform ancestor we want to toggle."""
+ event.stop()
+ self.post_message(self.Toggle())
+
+ def action_toggle(self) -> None:
+ """Toggle the state of the parent collapsible."""
+ self.post_message(self.Toggle())
+
+ def render(self) -> RenderableType:
+ """Compose right/down arrow and label."""
+ if self.collapsed:
+ return Text(f"{self.collapsed_symbol} {self.label}")
+ else:
+ return Text(f"{self.expanded_symbol} {self.label}")
+
+
+class Collapsible(Widget):
+ """A collapsible container."""
+
+ collapsed = reactive(True)
+
+ DEFAULT_CSS = """
+ Collapsible {
+ width: 1fr;
+ height: auto;
+ background: $boost;
+ border-top: hkey $background;
+ padding-bottom: 1;
+ padding-left: 1;
+ }
+
+ Collapsible.-collapsed > Contents {
+ display: none;
+ }
+ """
+
+ class Contents(Container):
+ DEFAULT_CSS = """
+ Contents {
+ width: 100%;
+ height: auto;
+ padding: 1 0 0 3;
+ }
+ """
+
+ def __init__(
+ self,
+ *children: Widget,
+ title: str = "Toggle",
+ collapsed: bool = True,
+ collapsed_symbol: str = "▶",
+ expanded_symbol: str = "▼",
+ name: str | None = None,
+ id: str | None = None,
+ classes: str | None = None,
+ disabled: bool = False,
+ ) -> None:
+ """Initialize a Collapsible widget.
+
+ Args:
+ *children: Contents that will be collapsed/expanded.
+ title: Title of the collapsed/expanded contents.
+ collapsed: Default status of the contents.
+ collapsed_symbol: Collapsed symbol before the title.
+ expanded_symbol: Expanded symbol before the title.
+ name: The name of the collapsible.
+ id: The ID of the collapsible in the DOM.
+ classes: The CSS classes of the collapsible.
+ disabled: Whether the collapsible is disabled or not.
+ """
+ self._title = CollapsibleTitle(
+ label=title,
+ collapsed_symbol=collapsed_symbol,
+ expanded_symbol=expanded_symbol,
+ collapsed=collapsed,
+ )
+ self._contents_list: list[Widget] = list(children)
+ super().__init__(name=name, id=id, classes=classes, disabled=disabled)
+ self.collapsed = collapsed
+
+ def on_collapsible_title_toggle(self, event: CollapsibleTitle.Toggle) -> None:
+ event.stop()
+ self.collapsed = not self.collapsed
+
+ def _watch_collapsed(self, collapsed: bool) -> None:
+ """Update collapsed state when reactive is changed."""
+ self._update_collapsed(collapsed)
+
+ def _update_collapsed(self, collapsed: bool) -> None:
+ """Update children to match collapsed state."""
+ try:
+ self._title.collapsed = collapsed
+ self.set_class(collapsed, "-collapsed")
+ except NoMatches:
+ pass
+
+ def _on_mount(self) -> None:
+ """Initialise collapsed state."""
+ self._update_collapsed(self.collapsed)
+
+ def compose(self) -> ComposeResult:
+ yield self._title
+ yield self.Contents(*self._contents_list)
+
+ def compose_add_child(self, widget: Widget) -> None:
+ """When using the context manager compose syntax, we want to attach nodes to the contents.
+
+ Args:
+ widget: A Widget to add.
+ """
+ self._contents_list.append(widget)
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
index 62aa1e5464..ee4fdae3ec 100644
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -33,7 +33,7 @@
from ..widget import PseudoClasses
CellCacheKey: TypeAlias = (
- "tuple[RowKey, ColumnKey, Style, bool, bool, int, PseudoClasses]"
+ "tuple[RowKey, ColumnKey, Style, bool, bool, bool, int, PseudoClasses]"
)
LineCacheKey: TypeAlias = "tuple[int, int, int, int, Coordinate, Coordinate, Style, CursorType, bool, int, PseudoClasses]"
RowCacheKey: TypeAlias = "tuple[RowKey, int, Style, Coordinate, Coordinate, CursorType, bool, bool, int, PseudoClasses]"
@@ -187,6 +187,7 @@ class Row:
key: RowKey
height: int
label: Text | None = None
+ auto_height: bool = False
class RowRenderables(NamedTuple):
@@ -244,7 +245,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
"""
DEFAULT_CSS = """
- App.-dark DataTable {
+ DataTable:dark {
background:;
}
DataTable {
@@ -290,7 +291,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
background: $secondary 30%;
}
- .-dark-mode DataTable > .datatable--even-row {
+ DataTable:dark > .datatable--even-row {
background: $primary 15%;
}
@@ -951,6 +952,7 @@ def _clear_caches(self) -> None:
self._styles_cache.clear()
self._offset_cache.clear()
self._ordered_row_cache.clear()
+ self._get_styles_to_render_cell.cache_clear()
def get_row_height(self, row_key: RowKey) -> int:
"""Given a row key, return the height of that row in terminal cells.
@@ -965,7 +967,7 @@ def get_row_height(self, row_key: RowKey) -> int:
return self.header_height
return self.rows[row_key].height
- async def _on_styles_updated(self) -> None:
+ def notify_style_update(self) -> None:
self._clear_caches()
self.refresh()
@@ -1190,8 +1192,16 @@ def _update_column_widths(self, updated_cells: set[CellKey]) -> None:
self._require_update_dimensions = True
def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
- """Called to recalculate the virtual (scrollable) size."""
+ """Called to recalculate the virtual (scrollable) size.
+
+ This recomputes column widths and then checks if any of the new rows need
+ to have their height computed.
+
+ Args:
+ new_rows: The new rows that will affect the `DataTable` dimensions.
+ """
console = self.app.console
+ auto_height_rows: list[tuple[int, Row, list[RenderableType]]] = []
for row_key in new_rows:
row_index = self._row_locations.get(row_key)
@@ -1201,6 +1211,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
continue
row = self.rows.get(row_key)
+ assert row is not None
if row.label is not None:
self._labelled_row_exists = True
@@ -1215,7 +1226,65 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
content_width = measure(console, renderable, 1)
column.content_width = max(column.content_width, content_width)
- self._clear_caches()
+ if row.auto_height:
+ auto_height_rows.append((row_index, row, cells_in_row))
+
+ # If there are rows that need to have their height computed, render them correctly
+ # so that we can cache this rendering for later.
+ if auto_height_rows:
+ render_cell = self._render_cell # This method renders & caches.
+ should_highlight = self._should_highlight
+ cursor_type = self.cursor_type
+ cursor_location = self.cursor_coordinate
+ hover_location = self.hover_coordinate
+ base_style = self.rich_style
+ fixed_style = self.get_component_styles(
+ "datatable--fixed"
+ ).rich_style + Style.from_meta({"fixed": True})
+ ordered_columns = self.ordered_columns
+ fixed_columns = self.fixed_columns
+
+ for row_index, row, cells_in_row in auto_height_rows:
+ height = 0
+ row_style = self._get_row_style(row_index, base_style)
+
+ # As we go through the cells, save their rendering, height, and
+ # column width. After we compute the height of the row, go over the cells
+ # that were rendered with the wrong height and append the missing padding.
+ rendered_cells: list[tuple[SegmentLines, int, int]] = []
+ for column_index, column in enumerate(ordered_columns):
+ style = fixed_style if column_index < fixed_columns else row_style
+ cell_location = Coordinate(row_index, column_index)
+ rendered_cell = render_cell(
+ row_index,
+ column_index,
+ style,
+ column.render_width,
+ cursor=should_highlight(
+ cursor_location, cell_location, cursor_type
+ ),
+ hover=should_highlight(
+ hover_location, cell_location, cursor_type
+ ),
+ )
+ cell_height = len(rendered_cell)
+ rendered_cells.append(
+ (rendered_cell, cell_height, column.render_width)
+ )
+ height = max(height, cell_height)
+
+ row.height = height
+ # Do surgery on the cache for cells that were rendered with the incorrect
+ # height during the first pass.
+ for cell_renderable, cell_height, column_width in rendered_cells:
+ if cell_height < height:
+ first_line_space_style = cell_renderable[0][0].style
+ cell_renderable.extend(
+ [
+ [Segment(" " * column_width, first_line_space_style)]
+ for _ in range(height - cell_height)
+ ]
+ )
data_cells_width = sum(column.render_width for column in self.columns.values())
total_width = data_cells_width + self._row_label_column_width
@@ -1373,7 +1442,7 @@ def add_column(
def add_row(
self,
*cells: CellType,
- height: int = 1,
+ height: int | None = 1,
key: str | None = None,
label: TextType | None = None,
) -> RowKey:
@@ -1381,13 +1450,14 @@ def add_row(
Args:
*cells: Positional arguments should contain cell data.
- height: The height of a row (in lines).
+ height: The height of a row (in lines). Use `None` to auto-detect the optimal
+ height.
key: A key which uniquely identifies this row. If None, it will be generated
for you and returned.
label: The label for the row. Will be displayed to the left if supplied.
Returns:
- Uniquely identifies this row. Can be used to retrieve this row regardless
+ Unique identifier for this row. Can be used to retrieve this row regardless
of its current location in the DataTable (it could have moved after
being added due to sorting or insertion/deletion of other rows).
"""
@@ -1407,7 +1477,15 @@ def add_row(
for column, cell in zip_longest(self.ordered_columns, cells)
}
label = Text.from_markup(label) if isinstance(label, str) else label
- self.rows[row_key] = Row(row_key, height, label)
+ # Rows with auto-height get a height of 0 because 1) we need an integer height
+ # to do some intermediate computations and 2) because 0 doesn't impact the data
+ # table while we don't figure out how tall this row is.
+ self.rows[row_key] = Row(
+ row_key,
+ height or 0,
+ label,
+ height is None,
+ )
self._new_rows.add(row_key)
self._require_update_dimensions = True
self.cursor_coordinate = self.cursor_coordinate
@@ -1546,7 +1624,8 @@ async def _on_idle(self, _: events.Idle) -> None:
if self._require_update_dimensions:
# Add the new rows *before* updating the column widths, since
- # cells in a new row may influence the final width of a column
+ # cells in a new row may influence the final width of a column.
+ # Only then can we compute optimal height of rows with "auto" height.
self._require_update_dimensions = False
new_rows = self._new_rows.copy()
self._new_rows.clear()
@@ -1754,7 +1833,7 @@ def _render_cell(
row_key = self._row_locations.get_key(row_index)
column_key = self._column_locations.get_key(column_index)
- cell_cache_key = (
+ cell_cache_key: CellCacheKey = (
row_key,
column_key,
base_style,
@@ -1767,7 +1846,6 @@ def _render_cell(
if cell_cache_key not in self._cell_render_cache:
base_style += Style.from_meta({"row": row_index, "column": column_index})
- height = self.header_height if is_header_cell else self.rows[row_key].height
row_label, row_cells = self._get_row_renderables(row_index)
if is_row_label_cell:
@@ -1775,50 +1853,104 @@ def _render_cell(
else:
cell = row_cells[column_index]
- get_component = self.get_component_rich_style
- show_cursor = self.show_cursor
- component_style = Style()
-
- if hover and show_cursor and self._show_hover_cursor:
- component_style += get_component("datatable--hover")
- if is_header_cell or is_row_label_cell:
- # Apply subtle variation in style for the header/label (blue background by
- # default) rows and columns affected by the cursor, to ensure we can
- # still differentiate between the labels and the data.
- component_style += get_component("datatable--header-hover")
-
- if cursor and show_cursor:
- cursor_style = get_component("datatable--cursor")
- component_style += cursor_style
- if is_header_cell or is_row_label_cell:
- component_style += get_component("datatable--header-cursor")
- elif is_fixed_style_cell:
- component_style += get_component("datatable--fixed-cursor")
-
- post_foreground = (
- Style.from_color(color=component_style.color)
- if self.cursor_foreground_priority == "css"
- else Style.null()
- )
- post_background = (
- Style.from_color(bgcolor=component_style.bgcolor)
- if self.cursor_background_priority == "css"
- else Style.null()
+ component_style, post_style = self._get_styles_to_render_cell(
+ is_header_cell,
+ is_row_label_cell,
+ is_fixed_style_cell,
+ hover,
+ cursor,
+ self.show_cursor,
+ self._show_hover_cursor,
+ self.cursor_foreground_priority == "css",
+ self.cursor_background_priority == "css",
)
+ if is_header_cell:
+ options = self.app.console.options.update_dimensions(
+ width, self.header_height
+ )
+ else:
+ row = self.rows[row_key]
+ # If an auto-height row hasn't had its height calculated, we don't fix
+ # the value for `height` so that we can measure the height of the cell.
+ if row.auto_height and row.height == 0:
+ options = self.app.console.options.update_width(width)
+ else:
+ options = self.app.console.options.update_dimensions(
+ width, row.height
+ )
lines = self.app.console.render_lines(
Styled(
Padding(cell, (0, 1)),
pre_style=base_style + component_style,
- post_style=post_foreground + post_background,
+ post_style=post_style,
),
- self.app.console.options.update_dimensions(width, height),
+ options,
)
self._cell_render_cache[cell_cache_key] = lines
return self._cell_render_cache[cell_cache_key]
+ @functools.lru_cache(maxsize=32)
+ def _get_styles_to_render_cell(
+ self,
+ is_header_cell: bool,
+ is_row_label_cell: bool,
+ is_fixed_style_cell: bool,
+ hover: bool,
+ cursor: bool,
+ show_cursor: bool,
+ show_hover_cursor: bool,
+ has_css_foreground_priority: bool,
+ has_css_background_priority: bool,
+ ) -> tuple[Style, Style]:
+ """Auxiliary method to compute styles used to render a given cell.
+
+ Args:
+ is_header_cell: Is this a cell from a header?
+ is_row_label_cell: Is this the label of any given row?
+ is_fixed_style_cell: Should this cell be styled like a fixed cell?
+ hover: Does this cell have the hover pseudo class?
+ cursor: Is this cell covered by the cursor?
+ show_cursor: Do we want to show the cursor in the data table?
+ show_hover_cursor: Do we want to show the mouse hover when using the keyboard
+ to move the cursor?
+ has_css_foreground_priority: `self.cursor_foreground_priority == "css"`?
+ has_css_background_priority: `self.cursor_background_priority == "css"`?
+ """
+ get_component = self.get_component_rich_style
+ component_style = Style()
+
+ if hover and show_cursor and show_hover_cursor:
+ component_style += get_component("datatable--hover")
+ if is_header_cell or is_row_label_cell:
+ # Apply subtle variation in style for the header/label (blue background by
+ # default) rows and columns affected by the cursor, to ensure we can
+ # still differentiate between the labels and the data.
+ component_style += get_component("datatable--header-hover")
+
+ if cursor and show_cursor:
+ cursor_style = get_component("datatable--cursor")
+ component_style += cursor_style
+ if is_header_cell or is_row_label_cell:
+ component_style += get_component("datatable--header-cursor")
+ elif is_fixed_style_cell:
+ component_style += get_component("datatable--fixed-cursor")
+
+ post_foreground = (
+ Style.from_color(color=component_style.color)
+ if has_css_foreground_priority
+ else Style.null()
+ )
+ post_background = (
+ Style.from_color(bgcolor=component_style.bgcolor)
+ if has_css_background_priority
+ else Style.null()
+ )
+
+ return component_style, post_foreground + post_background
+
def _render_line_in_row(
self,
row_key: RowKey,
@@ -1859,29 +1991,9 @@ def _render_line_in_row(
if cache_key in self._row_render_cache:
return self._row_render_cache[cache_key]
- def _should_highlight(
- cursor: Coordinate,
- target_cell: Coordinate,
- type_of_cursor: CursorType,
- ) -> bool:
- """Determine whether we should highlight a cell given the location
- of the cursor, the location of the cell, and the type of cursor that
- is currently active."""
- if type_of_cursor == "cell":
- return cursor == target_cell
- elif type_of_cursor == "row":
- cursor_row, _ = cursor
- cell_row, _ = target_cell
- return cursor_row == cell_row
- elif type_of_cursor == "column":
- _, cursor_column = cursor
- _, cell_column = target_cell
- return cursor_column == cell_column
- else:
- return False
-
- is_header_row = row_key is self._header_row_key
+ should_highlight = self._should_highlight
render_cell = self._render_cell
+ header_style = self.get_component_styles("datatable--header").rich_style
if row_key in self._row_locations:
row_index = self._row_locations.get(row_key)
@@ -1890,7 +2002,6 @@ def _should_highlight(
# If the row has a label, add it to fixed_row here with correct style.
fixed_row = []
- header_style = self.get_component_styles("datatable--header").rich_style
if self._labelled_row_exists and self.show_row_labels:
# The width of the row label is updated again on idle
@@ -1900,14 +2011,17 @@ def _should_highlight(
-1,
header_style,
width=self._row_label_column_width,
- cursor=_should_highlight(cursor_location, cell_location, cursor_type),
- hover=_should_highlight(hover_location, cell_location, cursor_type),
+ cursor=should_highlight(cursor_location, cell_location, cursor_type),
+ hover=should_highlight(hover_location, cell_location, cursor_type),
)[line_no]
fixed_row.append(label_cell_lines)
if self.fixed_columns:
- fixed_style = self.get_component_styles("datatable--fixed").rich_style
- fixed_style += Style.from_meta({"fixed": True})
+ if row_key is self._header_row_key:
+ fixed_style = header_style # We use the header style either way.
+ else:
+ fixed_style = self.get_component_styles("datatable--fixed").rich_style
+ fixed_style += Style.from_meta({"fixed": True})
for column_index, column in enumerate(
self.ordered_columns[: self.fixed_columns]
):
@@ -1915,28 +2029,16 @@ def _should_highlight(
fixed_cell_lines = render_cell(
row_index,
column_index,
- header_style if is_header_row else fixed_style,
+ fixed_style,
column.render_width,
- cursor=_should_highlight(
+ cursor=should_highlight(
cursor_location, cell_location, cursor_type
),
- hover=_should_highlight(hover_location, cell_location, cursor_type),
+ hover=should_highlight(hover_location, cell_location, cursor_type),
)[line_no]
fixed_row.append(fixed_cell_lines)
- is_header_row = row_key is self._header_row_key
- if is_header_row:
- row_style = self.get_component_styles("datatable--header").rich_style
- elif row_index < self.fixed_rows:
- row_style = self.get_component_styles("datatable--fixed").rich_style
- else:
- if self.zebra_stripes:
- component_row_style = (
- "datatable--odd-row" if row_index % 2 else "datatable--even-row"
- )
- row_style = self.get_component_styles(component_row_style).rich_style
- else:
- row_style = base_style
+ row_style = self._get_row_style(row_index, base_style)
scrollable_row = []
for column_index, column in enumerate(self.ordered_columns):
@@ -1946,8 +2048,8 @@ def _should_highlight(
column_index,
row_style,
column.render_width,
- cursor=_should_highlight(cursor_location, cell_location, cursor_type),
- hover=_should_highlight(hover_location, cell_location, cursor_type),
+ cursor=should_highlight(cursor_location, cell_location, cursor_type),
+ hover=should_highlight(hover_location, cell_location, cursor_type),
)[line_no]
scrollable_row.append(cell_lines)
@@ -2075,6 +2177,63 @@ def render_line(self, y: int) -> Strip:
return self._render_line(y, scroll_x, scroll_x + width, self.rich_style)
+ def _should_highlight(
+ self,
+ cursor: Coordinate,
+ target_cell: Coordinate,
+ type_of_cursor: CursorType,
+ ) -> bool:
+ """Determine if the given cell should be highlighted because of the cursor.
+
+ This auxiliary method takes the cursor position and type into account when
+ determining whether the cell should be highlighted.
+
+ Args:
+ cursor: The current position of the cursor.
+ target_cell: The cell we're checking for the need to highlight.
+ type_of_cursor: The type of cursor that is currently active.
+
+ Returns:
+ Whether or not the given cell should be highlighted.
+ """
+ if type_of_cursor == "cell":
+ return cursor == target_cell
+ elif type_of_cursor == "row":
+ cursor_row, _ = cursor
+ cell_row, _ = target_cell
+ return cursor_row == cell_row
+ elif type_of_cursor == "column":
+ _, cursor_column = cursor
+ _, cell_column = target_cell
+ return cursor_column == cell_column
+ else:
+ return False
+
+ def _get_row_style(self, row_index: int, base_style: Style) -> Style:
+ """Gets the Style that should be applied to the row at the given index.
+
+ Args:
+ row_index: The index of the row to style.
+ base_style: The base style to use by default.
+
+ Returns:
+ The appropriate style.
+ """
+
+ if row_index == -1:
+ row_style = self.get_component_styles("datatable--header").rich_style
+ elif row_index < self.fixed_rows:
+ row_style = self.get_component_styles("datatable--fixed").rich_style
+ else:
+ if self.zebra_stripes:
+ component_row_style = (
+ "datatable--odd-row" if row_index % 2 else "datatable--even-row"
+ )
+ row_style = self.get_component_styles(component_row_style).rich_style
+ else:
+ row_style = base_style
+ return row_style
+
def _on_mouse_move(self, event: events.MouseMove):
"""If the hover cursor is visible, display it by extracting the row
and column metadata from the segments present in the cells."""
diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py
index 2c1bcaf326..ac9aeef649 100644
--- a/src/textual/widgets/_header.py
+++ b/src/textual/widgets/_header.py
@@ -7,7 +7,7 @@
from rich.text import Text
from ..app import RenderResult
-from ..events import Mount
+from ..events import Click, Mount
from ..reactive import Reactive
from ..widget import Widget
@@ -22,11 +22,20 @@ class HeaderIcon(Widget):
width: 8;
content-align: left middle;
}
+
+ HeaderIcon:hover {
+ background: $foreground 10%;
+ }
"""
icon = Reactive("⭘")
"""The character to use as the icon within the header."""
+ async def on_click(self, event: Click) -> None:
+ """Launch the command palette when icon is clicked."""
+ event.stop()
+ await self.run_action("command_palette")
+
def render(self) -> RenderResult:
"""Render the header icon.
diff --git a/src/textual/widgets/_list_item.py b/src/textual/widgets/_list_item.py
index bf3a43e28a..e87b8cf4fc 100644
--- a/src/textual/widgets/_list_item.py
+++ b/src/textual/widgets/_list_item.py
@@ -16,6 +16,8 @@ class ListItem(Widget, can_focus=False):
documentation for more details on use.
"""
+ SCOPED_CSS = False
+
DEFAULT_CSS = """
ListItem {
color: $text;
diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py
index 7c15be6534..ac6de4f4bc 100644
--- a/src/textual/widgets/_markdown.py
+++ b/src/textual/widgets/_markdown.py
@@ -223,6 +223,7 @@ class MarkdownHorizontalRule(MarkdownBlock):
class MarkdownParagraph(MarkdownBlock):
"""A paragraph Markdown block."""
+ SCOPED_CSS = False
DEFAULT_CSS = """
Markdown > MarkdownParagraph {
margin: 0 0 1 0;
@@ -544,7 +545,19 @@ class Markdown(Widget):
text-style: bold dim;
}
"""
+
COMPONENT_CLASSES = {"em", "strong", "s", "code_inline"}
+ """
+ These component classes target standard inline markdown styles.
+ Changing these will potentially break the standard markdown formatting.
+
+ | Class | Description |
+ | :- | :- |
+ | `code_inline` | Target text that is styled as inline code. |
+ | `em` | Target text that is emphasized inline. |
+ | `s` | Target text that is styled inline with strykethrough. |
+ | `strong` | Target text that is styled inline with strong. |
+ """
BULLETS = ["\u25CF ", "▪ ", "‣ ", "• ", "⭑ "]
@@ -648,7 +661,7 @@ def sanitize_location(location: str) -> tuple[Path, str]:
location, _, anchor = location.partition("#")
return Path(location), anchor
- def goto_anchor(self, anchor: str) -> None:
+ def goto_anchor(self, anchor: str) -> bool:
"""Try and find the given anchor in the current document.
Args:
@@ -661,14 +674,18 @@ def goto_anchor(self, anchor: str) -> None:
Note that the slugging method used is similar to that found on
GitHub.
+
+ Returns:
+ True when the anchor was found in the current document, False otherwise.
"""
if not self._table_of_contents or not isinstance(self.parent, Widget):
- return
+ return False
unique = TrackedSlugs()
for _, title, header_id in self._table_of_contents:
if unique.slug(title) == anchor:
self.parent.scroll_to_widget(self.query_one(f"#{header_id}"), top=True)
- return
+ return True
+ return False
async def load(self, path: Path) -> None:
"""Load a new Markdown document.
@@ -934,6 +951,8 @@ async def _on_tree_node_selected(self, message: Tree.NodeSelected) -> None:
class MarkdownViewer(VerticalScroll, can_focus=True, can_focus_children=True):
"""A Markdown viewer widget."""
+ SCOPED_CSS = False
+
DEFAULT_CSS = """
MarkdownViewer {
height: 1fr;
diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py
index 74c89fb202..60a804287a 100644
--- a/src/textual/widgets/_option_list.py
+++ b/src/textual/widgets/_option_list.py
@@ -245,7 +245,6 @@ class OptionList(ScrollView, can_focus=True):
background: $accent 60%;
}
"""
- """The default styling for an `OptionList`."""
highlighted: reactive[int | None] = reactive["int | None"](None)
"""The index of the currently-highlighted option, or `None` if no option is highlighted."""
diff --git a/src/textual/widgets/_progress_bar.py b/src/textual/widgets/_progress_bar.py
index 617d390892..ec8c1b22cb 100644
--- a/src/textual/widgets/_progress_bar.py
+++ b/src/textual/widgets/_progress_bar.py
@@ -8,16 +8,19 @@
from rich.style import Style
-from textual.geometry import clamp
-
+from .._types import UnusedParameter
from ..app import ComposeResult, RenderResult
from ..containers import Horizontal
+from ..geometry import clamp
from ..reactive import reactive
from ..renderables.bar import Bar as BarRenderable
from ..timer import Timer
from ..widget import Widget
from ..widgets import Label
+UNUSED = UnusedParameter()
+"""Sentinel for method signatures."""
+
class Bar(Widget, can_focus=False):
"""The bar portion of the progress bar."""
@@ -276,7 +279,6 @@ class ProgressBar(Widget, can_focus=False):
"""The total number of steps associated with this progress bar, when known.
The value `None` will render an indeterminate progress bar.
- Once `total` is set to a numerical value, it cannot be set back to `None`.
"""
percentage: reactive[float | None] = reactive[Optional[float]](None)
"""The percentage of progress that has been completed.
@@ -398,6 +400,7 @@ def advance(self, advance: float = 1) -> None:
```py
progress_bar.advance(10) # Advance 10 steps.
```
+
Args:
advance: Number of steps to advance progress by.
"""
@@ -406,30 +409,28 @@ def advance(self, advance: float = 1) -> None:
def update(
self,
*,
- total: float | None = None,
- progress: float | None = None,
- advance: float | None = None,
+ total: None | float | UnusedParameter = UNUSED,
+ progress: float | UnusedParameter = UNUSED,
+ advance: float | UnusedParameter = UNUSED,
) -> None:
"""Update the progress bar with the given options.
- Options only affect the progress bar if they are not `None`.
-
Example:
```py
progress_bar.update(
total=200, # Set new total to 200 steps.
- progress=None, # This has no effect.
+ progress=50, # Set the progress to 50 (out of 200).
)
```
Args:
- total: New total number of steps (if not `None`).
- progress: Set the progress to the given number of steps (if not `None`).
- advance: Advance the progress by this number of steps (if not `None`).
+ total: New total number of steps.
+ progress: Set the progress to the given number of steps.
+ advance: Advance the progress by this number of steps.
"""
- if total is not None:
+ if not isinstance(total, UnusedParameter):
self.total = total
- if progress is not None:
+ if not isinstance(progress, UnusedParameter):
self.progress = progress
- if advance is not None:
+ if not isinstance(advance, UnusedParameter):
self.progress += advance
diff --git a/src/textual/widgets/_selection_list.py b/src/textual/widgets/_selection_list.py
index a448c5e412..f8dece7142 100644
--- a/src/textual/widgets/_selection_list.py
+++ b/src/textual/widgets/_selection_list.py
@@ -97,15 +97,15 @@ class SelectionList(Generic[SelectionType], OptionList):
height: auto;
}
- .-light-mode SelectionList:focus > .selection-list--button-selected {
+ SelectionList:light:focus > .selection-list--button-selected {
color: $primary;
}
- .-light-mode SelectionList > .selection-list--button-selected-highlighted {
+ SelectionList:light > .selection-list--button-selected-highlighted {
color: $primary;
}
- .-light-mode SelectionList:focus > .selection-list--button-selected-highlighted {
+ SelectionList:light:focus > .selection-list--button-selected-highlighted {
color: $primary;
}
diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py
new file mode 100644
index 0000000000..e76c67b70c
--- /dev/null
+++ b/src/textual/widgets/_text_area.py
@@ -0,0 +1,1864 @@
+from __future__ import annotations
+
+import re
+from collections import defaultdict
+from dataclasses import dataclass, field
+from functools import lru_cache
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
+
+from rich.style import Style
+from rich.text import Text
+
+from textual._text_area_theme import TextAreaTheme
+from textual._tree_sitter import TREE_SITTER
+from textual.color import Color
+from textual.document._document import (
+ Document,
+ DocumentBase,
+ EditResult,
+ Location,
+ Selection,
+ _utf8_encode,
+)
+from textual.document._languages import BUILTIN_LANGUAGES
+from textual.document._syntax_aware_document import (
+ SyntaxAwareDocument,
+ SyntaxAwareDocumentError,
+)
+from textual.expand_tabs import expand_tabs_inline
+
+if TYPE_CHECKING:
+ from tree_sitter import Language
+ from tree_sitter.binding import Query
+
+from textual import events, log
+from textual._cells import cell_len
+from textual._types import Literal, Protocol, runtime_checkable
+from textual.binding import Binding
+from textual.events import MouseEvent
+from textual.geometry import Offset, Region, Size, Spacing, clamp
+from textual.reactive import Reactive, reactive
+from textual.scroll_view import ScrollView
+from textual.strip import Strip
+
+_OPENING_BRACKETS = {"{": "}", "[": "]", "(": ")"}
+_CLOSING_BRACKETS = {v: k for k, v in _OPENING_BRACKETS.items()}
+_TREE_SITTER_PATH = Path(__file__).parent / "../tree-sitter/"
+_HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/"
+
+StartColumn = int
+EndColumn = Optional[int]
+HighlightName = str
+Highlight = Tuple[StartColumn, EndColumn, HighlightName]
+"""A tuple representing a syntax highlight within one line."""
+
+
+class ThemeDoesNotExist(Exception):
+ """Raised when the user tries to use a theme which does not exist.
+ This means a theme which is not builtin, or has not been registered.
+ """
+
+ pass
+
+
+class LanguageDoesNotExist(Exception):
+ """Raised when the user tries to use a language which does not exist.
+ This means a language which is not builtin, or has not been registered.
+ """
+
+ pass
+
+
+@dataclass
+class TextAreaLanguage:
+ """A container for a language which has been registered with the TextArea.
+
+ Attributes:
+ name: The name of the language.
+ language: The tree-sitter Language.
+ highlight_query: The tree-sitter highlight query corresponding to the language, as a string.
+ """
+
+ name: str
+ language: "Language"
+ highlight_query: str
+
+
+class TextArea(ScrollView, can_focus=True):
+ DEFAULT_CSS = """\
+TextArea {
+ width: 1fr;
+ height: 1fr;
+}
+"""
+
+ BINDINGS = [
+ Binding("escape", "screen.focus_next", "Shift Focus", show=False),
+ # Cursor movement
+ Binding("up", "cursor_up", "cursor up", show=False),
+ Binding("down", "cursor_down", "cursor down", show=False),
+ Binding("left", "cursor_left", "cursor left", show=False),
+ Binding("right", "cursor_right", "cursor right", show=False),
+ Binding("ctrl+left", "cursor_word_left", "cursor word left", show=False),
+ Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False),
+ Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False),
+ Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False),
+ Binding("pageup", "cursor_page_up", "cursor page up", show=False),
+ Binding("pagedown", "cursor_page_down", "cursor page down", show=False),
+ # Making selections (generally holding the shift key and moving cursor)
+ Binding(
+ "ctrl+shift+left",
+ "cursor_word_left(True)",
+ "cursor left word select",
+ show=False,
+ ),
+ Binding(
+ "ctrl+shift+right",
+ "cursor_word_right(True)",
+ "cursor right word select",
+ show=False,
+ ),
+ Binding(
+ "shift+home",
+ "cursor_line_start(True)",
+ "cursor line start select",
+ show=False,
+ ),
+ Binding(
+ "shift+end", "cursor_line_end(True)", "cursor line end select", show=False
+ ),
+ Binding("shift+up", "cursor_up(True)", "cursor up select", show=False),
+ Binding("shift+down", "cursor_down(True)", "cursor down select", show=False),
+ Binding("shift+left", "cursor_left(True)", "cursor left select", show=False),
+ Binding("shift+right", "cursor_right(True)", "cursor right select", show=False),
+ # Shortcut ways of making selections
+ # Binding("f5", "select_word", "select word", show=False),
+ Binding("f6", "select_line", "select line", show=False),
+ Binding("f7", "select_all", "select all", show=False),
+ # Deletion
+ Binding("backspace", "delete_left", "delete left", show=False),
+ Binding(
+ "ctrl+w", "delete_word_left", "delete left to start of word", show=False
+ ),
+ Binding("delete,ctrl+d", "delete_right", "delete right", show=False),
+ Binding(
+ "ctrl+f", "delete_word_right", "delete right to start of word", show=False
+ ),
+ Binding("ctrl+x", "delete_line", "delete line", show=False),
+ Binding(
+ "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),
+ ]
+ """
+ | Key(s) | Description |
+ | :- | :- |
+ | escape | Focus on the next item. |
+ | up | Move the cursor up. |
+ | down | Move the cursor down. |
+ | left | Move the cursor left. |
+ | ctrl+left | Move the cursor to the start of the word. |
+ | ctrl+shift+left | Move the cursor to the start of the word and select. |
+ | right | Move the cursor right. |
+ | ctrl+right | Move the cursor to the end of the word. |
+ | ctrl+shift+right | Move the cursor to the end of the word and select. |
+ | home,ctrl+a | Move the cursor to the start of the line. |
+ | end,ctrl+e | Move the cursor to the end of the line. |
+ | shift+home | Move the cursor to the start of the line and select. |
+ | shift+end | Move the cursor to the end of the line and select. |
+ | pageup | Move the cursor one page up. |
+ | pagedown | Move the cursor one page down. |
+ | shift+up | Select while moving the cursor up. |
+ | shift+down | Select while moving the cursor down. |
+ | shift+left | Select while moving the cursor left. |
+ | shift+right | Select while moving the cursor right. |
+ | backspace | Delete character to the left of cursor. |
+ | ctrl+w | Delete from cursor to start of the word. |
+ | delete,ctrl+d | Delete character to the right of cursor. |
+ | ctrl+f | Delete from cursor to end of the word. |
+ | ctrl+x | Delete the current line. |
+ | ctrl+u | Delete from cursor to the start of the line. |
+ | ctrl+k | Delete from cursor to the end of the line. |
+ | f6 | Select the current line. |
+ | f7 | Select all text in the document. |
+ """
+
+ language: Reactive[str | None] = reactive(None, always_update=True, init=False)
+ """The language to use.
+
+ This must be set to a valid, non-None value for syntax highlighting to work.
+
+ If the value is a string, a built-in language parser will be used if available.
+
+ If you wish to use an unsupported language, you'll have to register
+ it first using [`TextArea.register_language`][textual.widgets._text_area.TextArea.register_language].
+ """
+
+ theme: Reactive[str | None] = reactive(None, 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.
+
+ Syntax highlighting is only possible when the `language` attribute is set.
+ """
+
+ selection: Reactive[Selection] = reactive(Selection(), always_update=True)
+ """The selection start and end locations (zero-based line_index, offset).
+
+ This represents the cursor location and the current selection.
+
+ The `Selection.end` always refers to the cursor location.
+
+ If no text is selected, then `Selection.end == Selection.start` is True.
+
+ The text selected in the document is available via the `TextArea.selected_text` property.
+ """
+
+ show_line_numbers: Reactive[bool] = reactive(True)
+ """True to show the line number column on the left edge, otherwise False.
+
+ Changing this value will immediately re-render the `TextArea`."""
+
+ indent_width: Reactive[int] = reactive(4)
+ """The width of tabs or the multiple of spaces to align to on pressing the `tab` key.
+
+ If the document currently open contains tabs that are currently visible on screen,
+ altering this value will immediately change the display width of the visible tabs.
+ """
+
+ match_cursor_bracket: Reactive[bool] = reactive(True)
+ """If the cursor is at a bracket, highlight the matching bracket (if found)."""
+
+ cursor_blink: Reactive[bool] = reactive(True)
+ """True if the cursor should blink."""
+
+ _cursor_blink_visible: Reactive[bool] = reactive(True, repaint=False)
+ """Indicates where the cursor is in the blink cycle. If it's currently
+ not visible due to blinking, this is False."""
+
+ def __init__(
+ self,
+ text: str = "",
+ *,
+ language: str | None = None,
+ theme: str | None = None,
+ name: str | None = None,
+ id: str | None = None,
+ classes: str | None = None,
+ disabled: bool = False,
+ ) -> None:
+ """Construct a new `TextArea`.
+
+ Args:
+ text: The initial text to load into the TextArea.
+ language: The language to use.
+ theme: The theme to use.
+ 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.
+ 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."""
+
+ self._themes: dict[str, TextAreaTheme] = {}
+ """Maps theme names to TextAreaTheme."""
+
+ self.indent_type: Literal["tabs", "spaces"] = "spaces"
+ """Whether to indent using tabs or spaces."""
+
+ self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)")
+ """Compiled regular expression for what we consider to be a 'word'."""
+
+ self._last_intentional_cell_width: int = 0
+ """Tracks the last column (measured in terms of cell length, since we care here about where the cursor
+ visually moves rather than logical characters) the user explicitly navigated to so that we can reset to it
+ whenever possible."""
+
+ self._undo_stack: list[Undoable] = []
+ """A stack (the end of the list is the top of the stack) for tracking edits."""
+
+ self._selecting = False
+ """True if we're currently selecting text using the mouse, otherwise False."""
+
+ self._matching_bracket_location: Location | None = None
+ """The location (row, column) of the bracket which matches the bracket the
+ cursor is currently at. If the cursor is at a bracket, or there's no matching
+ bracket, this will be `None`."""
+
+ 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
+ """The query that's currently being used for highlighting."""
+
+ self.document: DocumentBase = Document(text)
+ """The document this widget is currently editing."""
+
+ self._theme: TextAreaTheme | None = None
+ """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
+
+ @staticmethod
+ def _get_builtin_highlight_query(language_name: str) -> str:
+ """Get the highlight query for a builtin language.
+
+ Args:
+ language_name: The name of the builtin language.
+
+ Returns:
+ The highlight query.
+ """
+ try:
+ highlight_query_path = (
+ Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm"
+ )
+ highlight_query = highlight_query_path.read_text()
+ except OSError as e:
+ log.warning(f"Unable to load highlight query. {e}")
+ highlight_query = ""
+
+ return highlight_query
+
+ def _build_highlight_map(self) -> None:
+ """Query the tree for ranges to highlights, and update the internal highlights mapping."""
+ highlights = self._highlights
+ highlights.clear()
+ if not self._highlight_query:
+ return
+
+ captures = self.document.query_syntax_tree(self._highlight_query)
+ for capture in captures:
+ node, highlight_name = capture
+ node_start_row, node_start_column = node.start_point
+ node_end_row, node_end_column = node.end_point
+
+ if node_start_row == node_end_row:
+ highlight = (node_start_column, node_end_column, highlight_name)
+ highlights[node_start_row].append(highlight)
+ else:
+ # Add the first line of the node range
+ highlights[node_start_row].append(
+ (node_start_column, None, highlight_name)
+ )
+
+ # Add the middle lines - entire row of this node is highlighted
+ for node_row in range(node_start_row + 1, node_end_row):
+ highlights[node_row].append((0, None, highlight_name))
+
+ # Add the last line of the node range
+ highlights[node_end_row].append((0, node_end_column, highlight_name))
+
+ def _watch_selection(self, selection: Selection) -> None:
+ """When the cursor moves, scroll it into view."""
+ self.scroll_cursor_visible()
+ cursor_location = selection.end
+ cursor_row, cursor_column = cursor_location
+
+ try:
+ character = self.document[cursor_row][cursor_column]
+ except IndexError:
+ character = ""
+
+ # Record the location of a matching closing/opening bracket.
+ match_location = self.find_matching_bracket(character, cursor_location)
+ self._matching_bracket_location = match_location
+ if match_location is not None:
+ match_row, match_column = match_location
+ if match_row in range(*self._visible_line_indices):
+ self.refresh_lines(match_row)
+
+ def find_matching_bracket(
+ self, bracket: str, search_from: Location
+ ) -> Location | None:
+ """If the character is a bracket, find the matching bracket.
+
+ Args:
+ bracket: The character we're searching for the matching bracket of.
+ search_from: The location to start the search.
+
+ Returns:
+ The `Location` of the matching bracket, or `None` if it's not found.
+ If the character is not available for bracket matching, `None` is returned.
+ """
+ match_location = None
+ bracket_stack = []
+ if bracket in _OPENING_BRACKETS:
+ for candidate, candidate_location in self._yield_character_locations(
+ search_from
+ ):
+ if candidate in _OPENING_BRACKETS:
+ bracket_stack.append(candidate)
+ elif candidate in _CLOSING_BRACKETS:
+ if (
+ bracket_stack
+ and bracket_stack[-1] == _CLOSING_BRACKETS[candidate]
+ ):
+ bracket_stack.pop()
+ if not bracket_stack:
+ match_location = candidate_location
+ break
+ elif bracket in _CLOSING_BRACKETS:
+ for (
+ candidate,
+ candidate_location,
+ ) in self._yield_character_locations_reverse(search_from):
+ if candidate in _CLOSING_BRACKETS:
+ bracket_stack.append(candidate)
+ elif candidate in _OPENING_BRACKETS:
+ if (
+ bracket_stack
+ and bracket_stack[-1] == _OPENING_BRACKETS[candidate]
+ ):
+ bracket_stack.pop()
+ if not bracket_stack:
+ match_location = candidate_location
+ break
+
+ return match_location
+
+ def _validate_selection(self, selection: Selection) -> Selection:
+ """Clamp the selection to valid locations."""
+ start, end = selection
+ clamp_visitable = self.clamp_visitable
+ return Selection(clamp_visitable(start), clamp_visitable(end))
+
+ def _watch_language(self, language: str | None) -> None:
+ """When the language is updated, update the type of document."""
+ if language is not None and language not in self.available_languages:
+ raise LanguageDoesNotExist(
+ f"{language!r} is not a builtin language, or it has not been registered. "
+ f"To use a custom language, register it first using `register_language`, "
+ 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 = ""
+
+ def _watch_show_line_numbers(self) -> None:
+ """The line number gutter contributes to virtual size, so recalculate."""
+ self._refresh_size()
+
+ def _watch_indent_width(self) -> None:
+ """Changing width of tabs will change document display width."""
+ self._refresh_size()
+
+ def _watch_theme(self, theme: str | None) -> None:
+ """We set the styles on this widget when the theme changes, to ensure that
+ if padding is applied, the colours match."""
+
+ 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 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."
+ )
+
+ self._theme = theme_object
+ if theme_object:
+ base_style = theme_object.base_style
+ if base_style:
+ color = base_style.color
+ background = base_style.bgcolor
+ if color:
+ self.styles.color = Color.from_rich_color(color)
+ if background:
+ self.styles.background = Color.from_rich_color(background)
+
+ @property
+ def available_themes(self) -> set[str]:
+ """A list of the names of the themes available to the `TextArea`.
+
+ The values in this list can be assigned `theme` reactive attribute of
+ `TextArea`.
+
+ You can retrieve the full specification for a theme by passing one of
+ the strings from this list into `TextAreaTheme.get_by_name(theme_name: str)`.
+
+ Alternatively, you can directly retrieve a list of `TextAreaTheme` objects
+ (which contain the full theme specification) by calling
+ `TextAreaTheme.builtin_themes()`.
+ """
+ return {
+ theme.name for theme in TextAreaTheme.builtin_themes()
+ } | self._themes.keys()
+
+ def register_theme(self, theme: TextAreaTheme) -> None:
+ """Register a theme for use by the `TextArea`.
+
+ After registering a theme, you can set themes by assigning the theme
+ name to the `TextArea.theme` reactive attribute. For example
+ `text_area.theme = "my_custom_theme"` where `"my_custom_theme"` is the
+ name of the theme you registered.
+
+ If you supply a theme with a name that already exists that theme
+ will be overwritten.
+ """
+ self._themes[theme.name] = theme
+
+ @property
+ def available_languages(self) -> set[str]:
+ """A list of the names of languages available to the `TextArea`.
+
+ The values in this list can be assigned to the `language` reactive attribute
+ of `TextArea`.
+
+ The returned list contains the builtin languages plus those registered via the
+ `register_language` method. Builtin languages will be listed before
+ user-registered languages, but there are no other ordering guarantees.
+ """
+ return set(BUILTIN_LANGUAGES) | self._languages.keys()
+
+ def register_language(
+ self,
+ language: str | "Language",
+ highlight_query: str,
+ ) -> None:
+ """Register a language and corresponding highlight query.
+
+ Calling this method does not change the language of the `TextArea`.
+ On switching to this language (via the `language` reactive attribute),
+ syntax highlighting will be performed using the given highlight query.
+
+ If a string `name` is supplied for a builtin supported language, then
+ this method will update the default highlight query for that language.
+
+ Registering a language only registers it to this instance of `TextArea`.
+
+ Args:
+ language: A string referring to a builtin language or a tree-sitter `Language` object.
+ highlight_query: The highlight query to use for syntax highlighting this language.
+ """
+
+ # If tree-sitter is unavailable, do nothing.
+ if not TREE_SITTER:
+ return
+
+ from tree_sitter_languages import get_language
+
+ if isinstance(language, str):
+ language_name = language
+ language = get_language(language_name)
+ else:
+ language_name = language.name
+
+ # Update the custom languages. When changing the document,
+ # we should first look in here for a language specification.
+ # If nothing is found, then we can go to the builtin languages.
+ self._languages[language_name] = TextAreaLanguage(
+ name=language_name,
+ language=language,
+ highlight_query=highlight_query,
+ )
+ # If we updated the currently set language, rebuild the highlights
+ # using the newly updated highlights query.
+ if language_name == self.language:
+ self._set_document(self.text, language_name)
+
+ def _set_document(self, text: str, language: str | None) -> None:
+ """Construct and return an appropriate document.
+
+ Args:
+ text: The text of the document.
+ language: The name of the language to use. This must either be a
+ built-in supported language, or a language previously registered
+ via the `register_language` method.
+ """
+ self._highlight_query = None
+ if TREE_SITTER and language:
+ # Attempt to get the override language.
+ text_area_language = self._languages.get(language, None)
+ document_language: str | "Language"
+ if text_area_language:
+ document_language = text_area_language.language
+ highlight_query = text_area_language.highlight_query
+ else:
+ document_language = language
+ highlight_query = self._get_builtin_highlight_query(language)
+ document: DocumentBase
+ try:
+ document = SyntaxAwareDocument(text, document_language)
+ except SyntaxAwareDocumentError:
+ document = Document(text)
+ log.warning(
+ f"Parser not found for language {document_language!r}. Parsing disabled."
+ )
+ else:
+ self._highlight_query = document.prepare_query(highlight_query)
+ elif language and not TREE_SITTER:
+ log.warning(
+ "tree-sitter not available in this environment. Parsing disabled."
+ )
+ document = Document(text)
+ else:
+ document = Document(text)
+
+ self.document = document
+ self._build_highlight_map()
+
+ @property
+ def _visible_line_indices(self) -> tuple[int, int]:
+ """Return the visible line indices as a tuple (top, bottom).
+
+ Returns:
+ A tuple (top, bottom) indicating the top and bottom visible line indices.
+ """
+ return self.scroll_offset.y, self.scroll_offset.y + self.size.height
+
+ def load_text(self, text: str) -> None:
+ """Load text into the TextArea.
+
+ This will replace the text currently in the TextArea.
+
+ Args:
+ text: The text to load into the TextArea.
+ """
+ self._set_document(text, self.language)
+ self.move_cursor((0, 0))
+ self._refresh_size()
+
+ def load_document(self, document: DocumentBase) -> None:
+ """Load a document into the TextArea.
+
+ Args:
+ document: The document to load into the TextArea.
+ """
+ self.document = document
+ self.move_cursor((0, 0))
+ self._refresh_size()
+
+ @property
+ def is_syntax_aware(self) -> bool:
+ """True if the TextArea is currently syntax aware - i.e. it's parsing document content."""
+ return isinstance(self.document, SyntaxAwareDocument)
+
+ def _yield_character_locations(
+ self, start: Location
+ ) -> Iterable[tuple[str, Location]]:
+ """Yields character locations starting from the given location.
+
+ Does not yield location of line separator characters like `\\n`.
+
+ Args:
+ start: The location to start yielding from.
+
+ Returns:
+ Yields tuples of (character, (row, column)).
+ """
+ row, column = start
+ document = self.document
+ line_count = document.line_count
+
+ while 0 <= row < line_count:
+ line = document[row]
+ while column < len(line):
+ yield line[column], (row, column)
+ column += 1
+ column = 0
+ row += 1
+
+ def _yield_character_locations_reverse(
+ self, start: Location
+ ) -> Iterable[tuple[str, Location]]:
+ row, column = start
+ document = self.document
+ line_count = document.line_count
+
+ while line_count > row >= 0:
+ line = document[row]
+ if column == -1:
+ column = len(line) - 1
+ while column >= 0:
+ yield line[column], (row, column)
+ column -= 1
+ row -= 1
+
+ def _refresh_size(self) -> None:
+ """Update the virtual size of the TextArea."""
+ width, height = self.document.get_size(self.indent_width)
+ # +1 width to make space for the cursor resting at the end of the line
+ self.virtual_size = Size(width + self.gutter_width + 1, height)
+
+ def render_line(self, widget_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.
+
+ Returns:
+ A rendered line.
+ """
+ document = self.document
+ scroll_x, scroll_y = self.scroll_offset
+
+ # Account for how much the TextArea is scrolled.
+ line_index = widget_y + scroll_y
+
+ # Render the lines beyond the valid line numbers
+ out_of_bounds = line_index >= document.line_count
+ if out_of_bounds:
+ return Strip.blank(self.size.width)
+
+ theme = self._theme
+
+ # Get the line from the Document.
+ line_string = document.get_line(line_index)
+ line = Text(line_string, end="")
+
+ line_character_count = len(line)
+ line.tab_size = self.indent_width
+ virtual_width, virtual_height = self.virtual_size
+ expanded_length = max(virtual_width, self.size.width)
+ line.set_length(expanded_length)
+
+ selection = self.selection
+ start, end = selection
+ selection_top, selection_bottom = sorted(selection)
+ 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_row, cursor_column = end
+ 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)
+
+ # Selection styling
+ if start != end and selection_top_row <= line_index <= selection_bottom_row:
+ # If this row intersects with the selection range
+ selection_style = theme.selection_style if theme else None
+ cursor_row, _ = end
+ if selection_style:
+ if line_character_count == 0 and line_index != cursor_row:
+ # A simple highlight to show empty lines are included in the selection
+ line = Text("▌", end="", style=Style(color=selection_style.bgcolor))
+ line.set_length(self.virtual_size.width)
+ else:
+ if line_index == selection_top_row == selection_bottom_row:
+ # Selection within a single line
+ line.stylize(
+ selection_style,
+ start=selection_top_column,
+ end=selection_bottom_column,
+ )
+ else:
+ # Selection spanning multiple lines
+ if line_index == selection_top_row:
+ line.stylize(
+ selection_style,
+ start=selection_top_column,
+ end=line_character_count,
+ )
+ elif line_index == selection_bottom_row:
+ line.stylize(selection_style, end=selection_bottom_column)
+ else:
+ line.stylize(selection_style, end=line_character_count)
+
+ # Highlight the cursor
+ matching_bracket = self._matching_bracket_location
+ match_cursor_bracket = self.match_cursor_bracket
+ draw_matched_brackets = (
+ match_cursor_bracket and matching_bracket is not None and start == end
+ )
+
+ if cursor_row == line_index:
+ draw_cursor = not self.cursor_blink or (
+ self.cursor_blink and self._cursor_blink_visible
+ )
+ if draw_matched_brackets:
+ matching_bracket_style = theme.bracket_matching_style if theme else None
+ if matching_bracket_style:
+ line.stylize(
+ matching_bracket_style,
+ cursor_column,
+ cursor_column + 1,
+ )
+
+ if draw_cursor:
+ cursor_style = theme.cursor_style if theme else None
+ if cursor_style:
+ line.stylize(cursor_style, cursor_column, cursor_column + 1)
+
+ # Highlight the partner opening/closing bracket.
+ if draw_matched_brackets:
+ # mypy doesn't know matching bracket is guaranteed to be non-None
+ assert matching_bracket is not None
+ bracket_match_row, bracket_match_column = matching_bracket
+ if theme and bracket_match_row == line_index:
+ matching_bracket_style = theme.bracket_matching_style
+ if matching_bracket_style:
+ line.stylize(
+ matching_bracket_style,
+ bracket_match_column,
+ bracket_match_column + 1,
+ )
+
+ # Build the gutter text for this line
+ gutter_width = self.gutter_width
+ if self.show_line_numbers:
+ if cursor_row == line_index:
+ gutter_style = theme.cursor_line_gutter_style if theme else None
+ else:
+ gutter_style = theme.gutter_style if theme else None
+
+ gutter_width_no_margin = gutter_width - 2
+ gutter = Text(
+ f"{line_index + 1:>{gutter_width_no_margin}} ",
+ style=gutter_style or "",
+ end="",
+ )
+ else:
+ gutter = Text("", end="")
+
+ # Render the gutter and the text of this line
+ console = self.app.console
+ gutter_segments = console.render(gutter)
+ text_segments = console.render(
+ line,
+ console.options.update_width(expanded_length),
+ )
+
+ # Crop the line to show only the visible part (some may be scrolled out of view)
+ gutter_strip = Strip(gutter_segments, cell_length=gutter_width)
+ text_strip = Strip(text_segments).crop(
+ scroll_x, scroll_x + virtual_width - gutter_width
+ )
+
+ # Stylize the line the cursor is currently on.
+ if cursor_row == line_index:
+ text_strip = text_strip.extend_cell_length(
+ expanded_length, cursor_line_style
+ )
+ else:
+ text_strip = text_strip.extend_cell_length(
+ expanded_length, theme.base_style if theme else None
+ )
+
+ # Join and return the gutter and the visible portion of this line
+ strip = Strip.join([gutter_strip, text_strip]).simplify()
+
+ return strip.apply_style(
+ theme.base_style
+ if theme and theme.base_style is not None
+ else self.rich_style
+ )
+
+ @property
+ def text(self) -> str:
+ """The entire text content of the document."""
+ return self.document.text
+
+ @property
+ def selected_text(self) -> str:
+ """The text between the start and end points of the current selection."""
+ start, end = self.selection
+ return self.get_text_range(start, end)
+
+ def get_text_range(self, start: Location, end: Location) -> str:
+ """Get the text between a start and end location.
+
+ Args:
+ start: The start location.
+ end: The end location.
+
+ Returns:
+ The text between start and end.
+ """
+ start, end = sorted((start, end))
+ return self.document.get_text_range(start, end)
+
+ def edit(self, edit: Edit) -> Any:
+ """Perform an Edit.
+
+ Args:
+ edit: The Edit to perform.
+
+ Returns:
+ Data relating to the edit that may be useful. The data returned
+ may be different depending on the edit performed.
+ """
+ result = edit.do(self)
+ self._refresh_size()
+ edit.after(self)
+ self._build_highlight_map()
+ return result
+
+ async def _on_key(self, event: events.Key) -> None:
+ """Handle key presses which correspond to document inserts."""
+ key = event.key
+ insert_values = {
+ "tab": " " * self._find_columns_to_next_tab_stop(),
+ "enter": "\n",
+ }
+ self._restart_blink()
+ if event.is_printable or key in insert_values:
+ event.stop()
+ event.prevent_default()
+ insert = insert_values.get(key, event.character)
+ # `insert` is not None because event.character cannot be
+ # 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)
+
+ 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.
+
+ If the cursor is already at a tab stop, this returns the *next* tab stop location.
+
+ Returns:
+ The number of cells to the next tab stop from the current cursor column.
+ """
+ cursor_row, cursor_column = self.cursor_location
+ line_text = self.document[cursor_row]
+ indent_width = self.indent_width
+ if not line_text:
+ return indent_width
+
+ width_before_cursor = self.get_column_width(cursor_row, cursor_column)
+ spaces_to_insert = indent_width - (
+ (indent_width + width_before_cursor) % indent_width
+ )
+
+ return spaces_to_insert
+
+ def get_target_document_location(self, event: MouseEvent) -> Location:
+ """Given a MouseEvent, return the row and column offset of the event in document-space.
+
+ Args:
+ event: The MouseEvent.
+
+ Returns:
+ The location of the mouse event within the document.
+ """
+ scroll_x, scroll_y = self.scroll_offset
+ target_x = event.x - self.gutter_width + scroll_x - self.gutter.left
+ target_x = max(target_x, 0)
+ target_row = clamp(
+ event.y + scroll_y - self.gutter.top,
+ 0,
+ self.document.line_count - 1,
+ )
+ target_column = self.cell_width_to_column_index(target_x, target_row)
+ return target_row, target_column
+
+ # --- Lower level event/key handling
+ @property
+ def gutter_width(self) -> int:
+ """The width of the gutter (the left column containing line numbers).
+
+ Returns:
+ The cell-width of the line number column. If `show_line_numbers` is `False` returns 0.
+ """
+ # The longest number in the gutter plus two extra characters: `│ `.
+ gutter_margin = 2
+ gutter_width = (
+ len(str(self.document.line_count + 1)) + gutter_margin
+ if self.show_line_numbers
+ else 0
+ )
+ return gutter_width
+
+ def _on_mount(self, _: events.Mount) -> None:
+ self.blink_timer = self.set_interval(
+ 0.5,
+ self._toggle_cursor_blink_visible,
+ pause=not (self.cursor_blink and self.has_focus),
+ )
+
+ def _on_blur(self, _: events.Blur) -> None:
+ self._pause_blink(visible=True)
+
+ def _on_focus(self, _: events.Focus) -> None:
+ self._restart_blink()
+
+ def _toggle_cursor_blink_visible(self) -> None:
+ """Toggle visibility of the cursor for the purposes of 'cursor blink'."""
+ self._cursor_blink_visible = not self._cursor_blink_visible
+ cursor_row, _ = self.cursor_location
+ self.refresh_lines(cursor_row)
+
+ def _restart_blink(self) -> None:
+ """Reset the cursor blink timer."""
+ if self.cursor_blink:
+ self._cursor_blink_visible = True
+ self.blink_timer.reset()
+
+ def _pause_blink(self, visible: bool = True) -> None:
+ """Pause the cursor blinking but ensure it stays visible."""
+ self._cursor_blink_visible = visible
+ self.blink_timer.pause()
+
+ async def _on_mouse_down(self, event: events.MouseDown) -> None:
+ """Update the cursor position, and begin a selection using the mouse."""
+ target = self.get_target_document_location(event)
+ self.selection = Selection.cursor(target)
+ self._selecting = True
+ # Capture the mouse so that if the cursor moves outside the
+ # TextArea widget while selecting, the widget still scrolls.
+ self.capture_mouse()
+ self._pause_blink(visible=True)
+
+ async def _on_mouse_move(self, event: events.MouseMove) -> None:
+ """Handles click and drag to expand and contract the selection."""
+ if self._selecting:
+ target = self.get_target_document_location(event)
+ selection_start, _ = self.selection
+ 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."""
+ self._selecting = False
+ self.release_mouse()
+ self.record_cursor_width()
+ self._restart_blink()
+
+ async def _on_paste(self, event: events.Paste) -> None:
+ """When a paste occurs, insert the text from the paste event into the document."""
+ self.replace(event.text, *self.selection)
+
+ 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.
+
+ Args:
+ cell_width: The cell width to convert.
+ row_index: The index of the row to examine.
+
+ Returns:
+ The column corresponding to the cell width on that row.
+ """
+ tab_width = self.indent_width
+ total_cell_offset = 0
+ line = self.document[row_index]
+ for column_index, character in enumerate(line):
+ total_cell_offset += cell_len(expand_tabs_inline(character, tab_width))
+ if total_cell_offset >= cell_width + 1:
+ return column_index
+ return len(line)
+
+ def clamp_visitable(self, location: Location) -> Location:
+ """Clamp the given location to the nearest visitable location.
+
+ Args:
+ location: The location to clamp.
+
+ Returns:
+ The nearest location that we could conceivably navigate to using the cursor.
+ """
+ document = self.document
+
+ row, column = location
+ try:
+ line_text = document[row]
+ except IndexError:
+ line_text = ""
+
+ row = clamp(row, 0, document.line_count - 1)
+ column = clamp(column, 0, len(line_text))
+
+ return row, column
+
+ # --- Cursor/selection utilities
+ def scroll_cursor_visible(
+ self, center: bool = False, animate: bool = False
+ ) -> Offset:
+ """Scroll the `TextArea` such that the cursor is visible on screen.
+
+ Args:
+ center: True if the cursor should be scrolled to the center.
+ animate: True if we should animate while scrolling.
+
+ Returns:
+ The offset that was scrolled to bring the cursor into view.
+ """
+ row, column = self.selection.end
+ text = self.document[row][:column]
+ column_offset = cell_len(expand_tabs_inline(text, self.indent_width))
+ scroll_offset = self.scroll_to_region(
+ Region(x=column_offset, y=row, width=3, height=1),
+ spacing=Spacing(right=self.gutter_width),
+ animate=animate,
+ force=True,
+ center=center,
+ )
+ return scroll_offset
+
+ def move_cursor(
+ self,
+ location: Location,
+ select: bool = False,
+ center: bool = False,
+ record_width: bool = True,
+ ) -> None:
+ """Move the cursor to a location.
+
+ Args:
+ location: The location to move the cursor to.
+ select: If True, select text between the old and new location.
+ center: If True, scroll such that the cursor is centered.
+ record_width: If True, record the cursor column cell width after navigating
+ so that we jump back to the same width the next time we move to a row
+ that is wide enough.
+ """
+ if select:
+ start, end = self.selection
+ self.selection = Selection(start, location)
+ else:
+ self.selection = Selection.cursor(location)
+
+ if record_width:
+ self.record_cursor_width()
+
+ if center:
+ self.scroll_cursor_visible(center)
+
+ def move_cursor_relative(
+ self,
+ rows: int = 0,
+ columns: int = 0,
+ select: bool = False,
+ center: bool = False,
+ record_width: bool = True,
+ ) -> None:
+ """Move the cursor relative to its current location.
+
+ Args:
+ rows: The number of rows to move down by (negative to move up)
+ columns: The number of columns to move right by (negative to move left)
+ select: If True, select text between the old and new location.
+ center: If True, scroll such that the cursor is centered.
+ record_width: If True, record the cursor column cell width after navigating
+ so that we jump back to the same width the next time we move to a row
+ that is wide enough.
+ """
+ clamp_visitable = self.clamp_visitable
+ 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)
+
+ def select_line(self, index: int) -> None:
+ """Select all the text in the specified line.
+
+ Args:
+ index: The index of the line to select (starting from 0).
+ """
+ try:
+ line = self.document[index]
+ except IndexError:
+ return
+ else:
+ self.selection = Selection((index, 0), (index, len(line)))
+ self.record_cursor_width()
+
+ def action_select_line(self) -> None:
+ """Select all the text on the current line."""
+ cursor_row, _ = self.cursor_location
+ self.select_line(cursor_row)
+
+ def select_all(self) -> None:
+ """Select all of the text in the `TextArea`."""
+ last_line = self.document.line_count - 1
+ length_of_last_line = len(self.document[last_line])
+ selection_start = (0, 0)
+ selection_end = (last_line, length_of_last_line)
+ self.selection = Selection(selection_start, selection_end)
+ self.record_cursor_width()
+
+ def action_select_all(self) -> None:
+ """Select all the text in the document."""
+ self.select_all()
+
+ @property
+ def cursor_location(self) -> Location:
+ """The current location of the cursor in the document.
+
+ This is a utility for accessing the `end` of `TextArea.selection`.
+ """
+ return self.selection.end
+
+ @cursor_location.setter
+ def cursor_location(self, location: Location) -> None:
+ """Set the cursor_location to a new location.
+
+ If a selection is in progress, the anchor point will remain.
+ """
+ self.move_cursor(location, select=not self.selection.is_empty)
+
+ @property
+ def cursor_at_first_line(self) -> bool:
+ """True if and only if the cursor is on the first line."""
+ return self.selection.end[0] == 0
+
+ @property
+ def cursor_at_last_line(self) -> bool:
+ """True if and only if the cursor is on the last line."""
+ return self.selection.end[0] == self.document.line_count - 1
+
+ @property
+ def cursor_at_start_of_line(self) -> bool:
+ """True if and only if the cursor is at column 0."""
+ return self.selection.end[1] == 0
+
+ @property
+ def cursor_at_end_of_line(self) -> bool:
+ """True if and only if the cursor is at the end of a row."""
+ cursor_row, cursor_column = self.selection.end
+ row_length = len(self.document[cursor_row])
+ cursor_at_end = cursor_column == row_length
+ return cursor_at_end
+
+ @property
+ def cursor_at_start_of_text(self) -> bool:
+ """True if and only if the cursor is at location (0, 0)"""
+ return self.selection.end == (0, 0)
+
+ @property
+ def cursor_at_end_of_text(self) -> bool:
+ """True if and only if the cursor is at the very end of the document."""
+ return self.cursor_at_last_line and self.cursor_at_end_of_line
+
+ # ------ Cursor movement actions
+ def action_cursor_left(self, select: bool = False) -> None:
+ """Move the cursor one location to the left.
+
+ If the cursor is at the left edge of the document, try to move it to
+ the end of the previous line.
+
+ Args:
+ select: If True, select the text while moving.
+ """
+ new_cursor_location = self.get_cursor_left_location()
+ self.move_cursor(new_cursor_location, select=select)
+
+ def get_cursor_left_location(self) -> Location:
+ """Get the location the cursor will move to if it moves left.
+
+ Returns:
+ The location of the cursor if it moves left.
+ """
+ if self.cursor_at_start_of_text:
+ return 0, 0
+ cursor_row, cursor_column = self.selection.end
+ length_of_row_above = len(self.document[cursor_row - 1])
+ target_row = cursor_row if cursor_column != 0 else cursor_row - 1
+ target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above
+ return target_row, target_column
+
+ def action_cursor_right(self, select: bool = False) -> None:
+ """Move the cursor one location to the right.
+
+ If the cursor is at the end of a line, attempt to go to the start of the next line.
+
+ Args:
+ select: If True, select the text while moving.
+ """
+ target = self.get_cursor_right_location()
+ self.move_cursor(target, select=select)
+
+ def get_cursor_right_location(self) -> Location:
+ """Get the location the cursor will move to if it moves right.
+
+ Returns:
+ the location the cursor will move to if it moves right.
+ """
+ if self.cursor_at_end_of_text:
+ return self.selection.end
+ cursor_row, cursor_column = self.selection.end
+ target_row = cursor_row + 1 if self.cursor_at_end_of_line else cursor_row
+ target_column = 0 if self.cursor_at_end_of_line else cursor_column + 1
+ return target_row, target_column
+
+ def action_cursor_down(self, select: bool = False) -> None:
+ """Move the cursor down one cell.
+
+ Args:
+ select: If True, select the text while moving.
+ """
+ target = self.get_cursor_down_location()
+ self.move_cursor(target, record_width=False, select=select)
+
+ def get_cursor_down_location(self) -> Location:
+ """Get the location the cursor will move to if it moves down.
+
+ Returns:
+ The location the cursor will move to if it moves down.
+ """
+ cursor_row, cursor_column = self.selection.end
+ if self.cursor_at_last_line:
+ return cursor_row, len(self.document[cursor_row])
+
+ target_row = min(self.document.line_count - 1, cursor_row + 1)
+ # Attempt to snap last intentional cell length
+ target_column = self.cell_width_to_column_index(
+ self._last_intentional_cell_width, target_row
+ )
+ target_column = clamp(target_column, 0, len(self.document[target_row]))
+ return target_row, target_column
+
+ def action_cursor_up(self, select: bool = False) -> None:
+ """Move the cursor up one cell.
+
+ Args:
+ select: If True, select the text while moving.
+ """
+ target = self.get_cursor_up_location()
+ self.move_cursor(target, record_width=False, select=select)
+
+ def get_cursor_up_location(self) -> Location:
+ """Get the location the cursor will move to if it moves up.
+
+ Returns:
+ The location the cursor will move to if it moves up.
+ """
+ if self.cursor_at_first_line:
+ return 0, 0
+ cursor_row, cursor_column = self.selection.end
+ target_row = max(0, cursor_row - 1)
+ # Attempt to snap last intentional cell length
+ target_column = self.cell_width_to_column_index(
+ self._last_intentional_cell_width, target_row
+ )
+ target_column = clamp(target_column, 0, len(self.document[target_row]))
+ return target_row, target_column
+
+ def action_cursor_line_end(self, select: bool = False) -> None:
+ """Move the cursor to the end of the line."""
+ location = self.get_cursor_line_end_location()
+ self.move_cursor(location, select=select)
+
+ def get_cursor_line_end_location(self) -> Location:
+ """Get the location of the end of the current line.
+
+ Returns:
+ The (row, column) location of the end of the cursors current line.
+ """
+ start, end = self.selection
+ cursor_row, cursor_column = end
+ target_column = len(self.document[cursor_row])
+ return cursor_row, target_column
+
+ def action_cursor_line_start(self, select: bool = False) -> None:
+ """Move the cursor to the start of the line."""
+
+ cursor_row, cursor_column = self.cursor_location
+ line = self.document[cursor_row]
+
+ first_non_whitespace = 0
+ for index, code_point in enumerate(line):
+ if not code_point.isspace():
+ first_non_whitespace = index
+ break
+
+ if cursor_column <= first_non_whitespace and cursor_column != 0:
+ target = self.get_cursor_line_start_location()
+ self.move_cursor(target, select=select)
+ else:
+ target = cursor_row, first_non_whitespace
+ self.move_cursor(target, select=select)
+
+ def get_cursor_line_start_location(self) -> Location:
+ """Get the location of the start of the current line.
+
+ Returns:
+ The (row, column) location of the start of the cursors current line.
+ """
+ _start, end = self.selection
+ cursor_row, _cursor_column = end
+ return cursor_row, 0
+
+ def action_cursor_word_left(self, select: bool = False) -> None:
+ """Move the cursor left by a single word, skipping trailing whitespace.
+
+ Args:
+ select: Whether to select while moving the cursor.
+ """
+ if self.cursor_at_start_of_text:
+ return
+ target = self.get_cursor_word_left_location()
+ self.move_cursor(target, select=select)
+
+ def get_cursor_word_left_location(self) -> Location:
+ """Get the location the cursor will jump to if it goes 1 word left.
+
+ Returns:
+ The location the cursor will jump on "jump word left".
+ """
+ cursor_row, cursor_column = self.cursor_location
+ if cursor_row > 0 and cursor_column == 0:
+ # Going to the previous row
+ return cursor_row - 1, len(self.document[cursor_row - 1])
+
+ # Staying on the same row
+ line = self.document[cursor_row][:cursor_column]
+ search_string = line.rstrip()
+ matches = list(re.finditer(self._word_pattern, search_string))
+ cursor_column = matches[-1].start() if matches else 0
+ return cursor_row, cursor_column
+
+ def action_cursor_word_right(self, select: bool = False) -> None:
+ """Move the cursor right by a single word, skipping leading whitespace."""
+
+ if self.cursor_at_end_of_text:
+ return
+
+ target = self.get_cursor_word_right_location()
+ self.move_cursor(target, select=select)
+
+ def get_cursor_word_right_location(self) -> Location:
+ """Get the location the cursor will jump to if it goes 1 word right.
+
+ Returns:
+ The location the cursor will jump on "jump word right".
+ """
+ cursor_row, cursor_column = self.selection.end
+ line = self.document[cursor_row]
+ if cursor_row < self.document.line_count - 1 and cursor_column == len(line):
+ # Moving to the line below
+ return cursor_row + 1, 0
+
+ # Staying on the same line
+ search_string = line[cursor_column:]
+ pre_strip_length = len(search_string)
+ search_string = search_string.lstrip()
+ strip_offset = pre_strip_length - len(search_string)
+
+ matches = list(re.finditer(self._word_pattern, search_string))
+ if matches:
+ cursor_column += matches[0].start() + strip_offset
+ else:
+ cursor_column = len(line)
+
+ return cursor_row, cursor_column
+
+ def action_cursor_page_up(self) -> None:
+ """Move the cursor and scroll up one page."""
+ height = self.content_size.height
+ _, cursor_location = self.selection
+ row, column = cursor_location
+ target = (row - height, column)
+ self.scroll_relative(y=-height, animate=False)
+ self.move_cursor(target)
+
+ def action_cursor_page_down(self) -> None:
+ """Move the cursor and scroll down one page."""
+ height = self.content_size.height
+ _, cursor_location = self.selection
+ row, column = cursor_location
+ target = (row + height, column)
+ self.scroll_relative(y=height, animate=False)
+ self.move_cursor(target)
+
+ def get_column_width(self, row: int, column: int) -> int:
+ """Get the cell offset of the column from the start of the row.
+
+ Args:
+ row: The row index.
+ column: The column index (codepoint offset from start of row).
+
+ Returns:
+ The cell width of the column relative to the start of the row.
+ """
+ line = self.document[row]
+ return cell_len(expand_tabs_inline(line[:column], self.indent_width))
+
+ def record_cursor_width(self) -> None:
+ """Record the current cell width of the cursor.
+
+ This is used where we navigate up and down through rows.
+ If we're in the middle of a row, and go down to a row with no
+ content, then we go down to another row, we want our cursor to
+ jump back to the same offset that we were originally at.
+ """
+ row, column = self.selection.end
+ column_cell_length = self.get_column_width(row, column)
+ self._last_intentional_cell_width = column_cell_length
+
+ # --- Editor operations
+ def insert(
+ self,
+ text: str,
+ location: Location | None = None,
+ *,
+ maintain_selection_offset: bool = True,
+ ) -> EditResult:
+ """Insert text into the document.
+
+ Args:
+ text: The text to insert.
+ location: The location to insert text, or None to use the cursor location.
+ maintain_selection_offset: If True, the active Selection will be updated
+ such that the same text is selected before and after the selection,
+ if possible. Otherwise, the cursor will jump to the end point of the
+ edit.
+
+ Returns:
+ An `EditResult` containing information about the edit.
+ """
+ if location is None:
+ location = self.cursor_location
+ return self.edit(Edit(text, location, location, maintain_selection_offset))
+
+ def delete(
+ self,
+ start: Location,
+ end: Location,
+ *,
+ maintain_selection_offset: bool = True,
+ ) -> EditResult:
+ """Delete the text between two locations in the document.
+
+ Args:
+ start: The start location.
+ end: The end location.
+ maintain_selection_offset: If True, the active Selection will be updated
+ such that the same text is selected before and after the selection,
+ if possible. Otherwise, the cursor will jump to the end point of the
+ edit.
+
+ Returns:
+ An `EditResult` containing information about the edit.
+ """
+ top, bottom = sorted((start, end))
+ return self.edit(Edit("", top, bottom, maintain_selection_offset))
+
+ def replace(
+ self,
+ insert: str,
+ start: Location,
+ end: Location,
+ *,
+ maintain_selection_offset: bool = True,
+ ) -> EditResult:
+ """Replace text in the document with new text.
+
+ Args:
+ insert: The text to insert.
+ start: The start location
+ end: The end location.
+ maintain_selection_offset: If True, the active Selection will be updated
+ such that the same text is selected before and after the selection,
+ if possible. Otherwise, the cursor will jump to the end point of the
+ edit.
+
+ Returns:
+ An `EditResult` containing information about the edit.
+ """
+ return self.edit(Edit(insert, start, end, maintain_selection_offset))
+
+ def clear(self) -> None:
+ """Delete all text from the document."""
+ 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)
+
+ def action_delete_left(self) -> None:
+ """Deletes the character to the left of the cursor and updates the cursor location.
+
+ If there's a selection, then the selected range is deleted."""
+
+ selection = self.selection
+ start, end = selection
+
+ if selection.is_empty:
+ end = self.get_cursor_left_location()
+
+ self.delete(start, end, maintain_selection_offset=False)
+
+ def action_delete_right(self) -> None:
+ """Deletes the character to the right of the cursor and keeps the cursor at the same location.
+
+ If there's a selection, then the selected range is deleted."""
+
+ selection = self.selection
+ start, end = selection
+
+ if selection.is_empty:
+ end = self.get_cursor_right_location()
+
+ self.delete(start, end, maintain_selection_offset=False)
+
+ 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
+ end_row, end_column = end
+
+ # Generally editors will only delete line the end line of the
+ # selection if the cursor is not at column 0 of that line.
+ if start_row != end_row and end_column == 0 and end_row >= 0:
+ end_row -= 1
+
+ from_location = (start_row, 0)
+ to_location = (end_row + 1, 0)
+
+ self.delete(from_location, to_location, maintain_selection_offset=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
+ cursor_row, cursor_column = from_location
+ to_location = (cursor_row, 0)
+ self.delete(from_location, to_location, maintain_selection_offset=False)
+
+ 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
+ cursor_row, cursor_column = from_location
+ to_location = (cursor_row, len(self.document[cursor_row]))
+ self.delete(from_location, to_location, maintain_selection_offset=False)
+
+ def action_delete_word_left(self) -> None:
+ """Deletes the word to the left of the cursor and updates the cursor location."""
+ if self.cursor_at_start_of_text:
+ return
+
+ # If there's a non-zero selection, then "delete word left" typically only
+ # 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)
+ return
+
+ to_location = self.get_cursor_word_left_location()
+ self.delete(self.selection.end, to_location, maintain_selection_offset=False)
+
+ def action_delete_word_right(self) -> None:
+ """Deletes the word to the right of the cursor and keeps the cursor at the same location.
+
+ Note that the location that we delete to using this action is not the same
+ as the location we move to when we move the cursor one word to the right.
+ This action does not skip leading whitespace, whereas cursor movement does.
+ """
+ if self.cursor_at_end_of_text:
+ return
+
+ start, end = self.selection
+ if start != end:
+ self.delete(start, end, maintain_selection_offset=False)
+ return
+
+ cursor_row, cursor_column = end
+
+ # Check the current line for a word boundary
+ line = self.document[cursor_row][cursor_column:]
+ matches = list(re.finditer(self._word_pattern, line))
+
+ current_row_length = len(self.document[cursor_row])
+ if matches:
+ to_location = (cursor_row, cursor_column + matches[0].end())
+ elif (
+ cursor_row < self.document.line_count - 1
+ and cursor_column == current_row_length
+ ):
+ to_location = (cursor_row + 1, 0)
+ else:
+ to_location = (cursor_row, current_row_length)
+
+ self.delete(end, to_location, maintain_selection_offset=False)
+
+
+@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_top, edit_bottom = sorted((edit_from, edit_to))
+ edit_bottom_row, edit_bottom_column = edit_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(edit_from, edit_to, 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()
+
+
+@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.
+
+ Args:
+ data: utf-8 bytes.
+
+ Returns:
+ A `dict[int, int]` mapping byte indices to codepoint indices within `data`.
+ """
+ byte_to_codepoint = {}
+ current_byte_offset = 0
+ code_point_offset = 0
+
+ while current_byte_offset < len(data):
+ byte_to_codepoint[current_byte_offset] = code_point_offset
+ first_byte = data[current_byte_offset]
+
+ # Single-byte character
+ if (first_byte & 0b10000000) == 0:
+ current_byte_offset += 1
+ # 2-byte character
+ elif (first_byte & 0b11100000) == 0b11000000:
+ current_byte_offset += 2
+ # 3-byte character
+ elif (first_byte & 0b11110000) == 0b11100000:
+ current_byte_offset += 3
+ # 4-byte character
+ elif (first_byte & 0b11111000) == 0b11110000:
+ current_byte_offset += 4
+ else:
+ raise ValueError(f"Invalid UTF-8 byte: {first_byte}")
+
+ code_point_offset += 1
+
+ # Mapping for the end of the string
+ byte_to_codepoint[current_byte_offset] = code_point_offset
+ return byte_to_codepoint
diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py
index 4c29c236ae..90828c7ac1 100644
--- a/src/textual/widgets/_toggle_button.py
+++ b/src/textual/widgets/_toggle_button.py
@@ -94,16 +94,16 @@ class ToggleButton(Static, can_focus=True):
/* Light mode overrides. */
- App.-light-mode ToggleButton > .toggle--button {
+ ToggleButton:light > .toggle--button {
color: $background;
background: $foreground 10%;
}
- App.-light-mode ToggleButton:focus > .toggle--button {
+ ToggleButton:light:focus > .toggle--button {
background: $foreground 25%;
}
- App.-light-mode ToggleButton.-on > .toggle--button {
+ ToggleButton:light.-on > .toggle--button {
color: $primary;
}
""" # TODO: https://github.com/Textualize/textual/issues/1780
diff --git a/src/textual/widgets/rule.py b/src/textual/widgets/rule.py
index ef4f57d56d..a9ab5d23e9 100644
--- a/src/textual/widgets/rule.py
+++ b/src/textual/widgets/rule.py
@@ -1,9 +1,4 @@
-from ._rule import (
- InvalidLineStyle,
- InvalidRuleOrientation,
- LineStyle,
- RuleOrientation,
-)
+from ._rule import InvalidLineStyle, InvalidRuleOrientation, LineStyle, RuleOrientation
__all__ = [
"InvalidLineStyle",
diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py
new file mode 100644
index 0000000000..82a69e38b3
--- /dev/null
+++ b/src/textual/widgets/text_area.py
@@ -0,0 +1,37 @@
+from textual._text_area_theme import TextAreaTheme
+from textual.document._document import (
+ Document,
+ DocumentBase,
+ EditResult,
+ Location,
+ Selection,
+)
+from textual.document._languages import BUILTIN_LANGUAGES
+from textual.document._syntax_aware_document import SyntaxAwareDocument
+from textual.widgets._text_area import (
+ Edit,
+ EndColumn,
+ Highlight,
+ HighlightName,
+ LanguageDoesNotExist,
+ StartColumn,
+ ThemeDoesNotExist,
+)
+
+__all__ = [
+ "BUILTIN_LANGUAGES",
+ "Document",
+ "DocumentBase",
+ "Edit",
+ "EditResult",
+ "EndColumn",
+ "Highlight",
+ "HighlightName",
+ "LanguageDoesNotExist",
+ "Location",
+ "Selection",
+ "StartColumn",
+ "SyntaxAwareDocument",
+ "TextAreaTheme",
+ "ThemeDoesNotExist",
+]
diff --git a/tests/command_palette/test_click_away.py b/tests/command_palette/test_click_away.py
index e2fe6915c0..383f39cdb2 100644
--- a/tests/command_palette/test_click_away.py
+++ b/tests/command_palette/test_click_away.py
@@ -1,22 +1,17 @@
from textual.app import App
-from textual.command_palette import (
- CommandMatches,
- CommandPalette,
- CommandSource,
- CommandSourceHit,
-)
+from textual.command import CommandPalette, Hit, Hits, Provider
-class SimpleSource(CommandSource):
- async def search(self, query: str) -> CommandMatches:
+class SimpleSource(Provider):
+ async def search(self, query: str) -> Hits:
def goes_nowhere_does_nothing() -> None:
pass
- yield CommandSourceHit(1, query, goes_nowhere_does_nothing, query)
+ yield Hit(1, query, goes_nowhere_does_nothing, query)
class CommandPaletteApp(App[None]):
- COMMAND_SOURCES = {SimpleSource}
+ COMMANDS = {SimpleSource}
def on_mount(self) -> None:
self.action_command_palette()
diff --git a/tests/command_palette/test_command_source_environment.py b/tests/command_palette/test_command_source_environment.py
index 11e632bb7c..af9b691d70 100644
--- a/tests/command_palette/test_command_source_environment.py
+++ b/tests/command_palette/test_command_source_environment.py
@@ -1,30 +1,25 @@
from __future__ import annotations
from textual.app import App, ComposeResult
-from textual.command_palette import (
- CommandMatches,
- CommandPalette,
- CommandSource,
- CommandSourceHit,
-)
+from textual.command import CommandPalette, Hit, Hits, Provider
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Input
-class SimpleSource(CommandSource):
+class SimpleSource(Provider):
environment: set[tuple[App, Screen, Widget | None]] = set()
- async def search(self, _: str) -> CommandMatches:
+ async def search(self, _: str) -> Hits:
def goes_nowhere_does_nothing() -> None:
pass
SimpleSource.environment.add((self.app, self.screen, self.focused))
- yield CommandSourceHit(1, "Hit", goes_nowhere_does_nothing, "Hit")
+ yield Hit(1, "Hit", goes_nowhere_does_nothing, "Hit")
class CommandPaletteApp(App[None]):
- COMMAND_SOURCES = {SimpleSource}
+ COMMANDS = {SimpleSource}
def compose(self) -> ComposeResult:
yield Input()
diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py
index cac9f5205d..c5bae17904 100644
--- a/tests/command_palette/test_declare_sources.py
+++ b/tests/command_palette/test_declare_sources.py
@@ -1,24 +1,19 @@
from textual.app import App
-from textual.command_palette import (
- CommandMatches,
- CommandPalette,
- CommandSource,
- CommandSourceHit,
-)
+from textual.command import CommandPalette, Hit, Hits, Provider
from textual.screen import Screen
async def test_sources_with_no_known_screen() -> None:
"""A command palette with no known screen should have an empty source set."""
- assert CommandPalette()._sources == set()
+ assert CommandPalette()._provider_classes == set()
-class ExampleCommandSource(CommandSource):
- async def search(self, _: str) -> CommandMatches:
+class ExampleCommandSource(Provider):
+ async def search(self, _: str) -> Hits:
def goes_nowhere_does_nothing() -> None:
pass
- yield CommandSourceHit(1, "Hit", goes_nowhere_does_nothing, "Hit")
+ yield Hit(1, "Hit", goes_nowhere_does_nothing, "Hit")
class AppWithActiveCommandPalette(App[None]):
@@ -33,19 +28,19 @@ class AppWithNoSources(AppWithActiveCommandPalette):
async def test_no_app_command_sources() -> None:
"""An app with no sources declared should work fine."""
async with AppWithNoSources().run_test() as pilot:
- assert pilot.app.query_one(CommandPalette)._sources == App.COMMAND_SOURCES
+ assert pilot.app.query_one(CommandPalette)._provider_classes == App.COMMANDS
class AppWithSources(AppWithActiveCommandPalette):
- COMMAND_SOURCES = {ExampleCommandSource}
+ COMMANDS = {ExampleCommandSource}
async def test_app_command_sources() -> None:
"""Command sources declared on an app should be in the command palette."""
async with AppWithSources().run_test() as pilot:
assert (
- pilot.app.query_one(CommandPalette)._sources
- == AppWithSources.COMMAND_SOURCES
+ pilot.app.query_one(CommandPalette)._provider_classes
+ == AppWithSources.COMMANDS
)
@@ -66,19 +61,19 @@ def on_mount(self) -> None:
async def test_no_screen_command_sources() -> None:
"""An app with a screen with no sources declared should work fine."""
async with AppWithInitialScreen(ScreenWithNoSources()).run_test() as pilot:
- assert pilot.app.query_one(CommandPalette)._sources == App.COMMAND_SOURCES
+ assert pilot.app.query_one(CommandPalette)._provider_classes == App.COMMANDS
class ScreenWithSources(ScreenWithNoSources):
- COMMAND_SOURCES = {ExampleCommandSource}
+ COMMANDS = {ExampleCommandSource}
async def test_screen_command_sources() -> None:
"""Command sources declared on a screen should be in the command palette."""
async with AppWithInitialScreen(ScreenWithSources()).run_test() as pilot:
assert (
- pilot.app.query_one(CommandPalette)._sources
- == App.COMMAND_SOURCES | ScreenWithSources.COMMAND_SOURCES
+ pilot.app.query_one(CommandPalette)._provider_classes
+ == App.COMMANDS | ScreenWithSources.COMMANDS
)
@@ -87,7 +82,7 @@ class AnotherCommandSource(ExampleCommandSource):
class CombinedSourceApp(App[None]):
- COMMAND_SOURCES = {AnotherCommandSource}
+ COMMANDS = {AnotherCommandSource}
def on_mount(self) -> None:
self.push_screen(ScreenWithSources())
@@ -97,6 +92,6 @@ async def test_app_and_screen_command_sources_combine() -> None:
"""If an app and the screen have command sources they should combine."""
async with CombinedSourceApp().run_test() as pilot:
assert (
- pilot.app.query_one(CommandPalette)._sources
- == CombinedSourceApp.COMMAND_SOURCES | ScreenWithSources.COMMAND_SOURCES
+ pilot.app.query_one(CommandPalette)._provider_classes
+ == CombinedSourceApp.COMMANDS | ScreenWithSources.COMMANDS
)
diff --git a/tests/command_palette/test_escaping.py b/tests/command_palette/test_escaping.py
index 1dbf48337f..2ac2013b6c 100644
--- a/tests/command_palette/test_escaping.py
+++ b/tests/command_palette/test_escaping.py
@@ -1,22 +1,17 @@
from textual.app import App
-from textual.command_palette import (
- CommandMatches,
- CommandPalette,
- CommandSource,
- CommandSourceHit,
-)
+from textual.command import CommandPalette, Hit, Hits, Provider
-class SimpleSource(CommandSource):
- async def search(self, query: str) -> CommandMatches:
+class SimpleSource(Provider):
+ async def search(self, query: str) -> Hits:
def goes_nowhere_does_nothing() -> None:
pass
- yield CommandSourceHit(1, query, goes_nowhere_does_nothing, query)
+ yield Hit(1, query, goes_nowhere_does_nothing, query)
class CommandPaletteApp(App[None]):
- COMMAND_SOURCES = {SimpleSource}
+ COMMANDS = {SimpleSource}
def on_mount(self) -> None:
self.action_command_palette()
diff --git a/tests/command_palette/test_interaction.py b/tests/command_palette/test_interaction.py
index 9dcbb90bb7..d243a35565 100644
--- a/tests/command_palette/test_interaction.py
+++ b/tests/command_palette/test_interaction.py
@@ -1,24 +1,18 @@
from textual.app import App
-from textual.command_palette import (
- CommandList,
- CommandMatches,
- CommandPalette,
- CommandSource,
- CommandSourceHit,
-)
+from textual.command import CommandList, CommandPalette, Hit, Hits, Provider
-class SimpleSource(CommandSource):
- async def search(self, query: str) -> CommandMatches:
+class SimpleSource(Provider):
+ async def search(self, query: str) -> Hits:
def goes_nowhere_does_nothing() -> None:
pass
for _ in range(100):
- yield CommandSourceHit(1, query, goes_nowhere_does_nothing, query)
+ yield Hit(1, query, goes_nowhere_does_nothing, query)
class CommandPaletteApp(App[None]):
- COMMAND_SOURCES = {SimpleSource}
+ COMMANDS = {SimpleSource}
def on_mount(self) -> None:
self.action_command_palette()
diff --git a/tests/command_palette/test_no_results.py b/tests/command_palette/test_no_results.py
index 9ea99185dd..427892cc93 100644
--- a/tests/command_palette/test_no_results.py
+++ b/tests/command_palette/test_no_results.py
@@ -1,10 +1,10 @@
from textual.app import App
-from textual.command_palette import CommandPalette
+from textual.command import CommandPalette
from textual.widgets import OptionList
class CommandPaletteApp(App[None]):
- COMMAND_SOURCES = set()
+ COMMANDS = set()
def on_mount(self) -> None:
self.action_command_palette()
diff --git a/tests/command_palette/test_run_on_select.py b/tests/command_palette/test_run_on_select.py
index 9b010bb3f7..a652096b56 100644
--- a/tests/command_palette/test_run_on_select.py
+++ b/tests/command_palette/test_run_on_select.py
@@ -1,23 +1,18 @@
from functools import partial
from textual.app import App
-from textual.command_palette import (
- CommandMatches,
- CommandPalette,
- CommandSource,
- CommandSourceHit,
-)
+from textual.command import CommandPalette, Hit, Hits, Provider
from textual.widgets import Input
-class SimpleSource(CommandSource):
- async def search(self, _: str) -> CommandMatches:
+class SimpleSource(Provider):
+ async def search(self, _: str) -> Hits:
def goes_nowhere_does_nothing(selection: int) -> None:
assert isinstance(self.app, CommandPaletteRunOnSelectApp)
self.app.selection = selection
for n in range(100):
- yield CommandSourceHit(
+ yield Hit(
n + 1 / 100,
str(n),
partial(goes_nowhere_does_nothing, n),
@@ -27,7 +22,7 @@ def goes_nowhere_does_nothing(selection: int) -> None:
class CommandPaletteRunOnSelectApp(App[None]):
- COMMAND_SOURCES = {SimpleSource}
+ COMMANDS = {SimpleSource}
def __init__(self) -> None:
super().__init__()
diff --git a/tests/css/test_screen_css.py b/tests/css/test_screen_css.py
index 42821a62ee..54138fb8a5 100644
--- a/tests/css/test_screen_css.py
+++ b/tests/css/test_screen_css.py
@@ -16,6 +16,7 @@ def compose(self):
class ScreenWithCSS(Screen):
+ SCOPED_CSS = False
CSS = """
#screen-css {
background: #ff0000;
diff --git a/tests/document/test_document.py b/tests/document/test_document.py
new file mode 100644
index 0000000000..b6e9952782
--- /dev/null
+++ b/tests/document/test_document.py
@@ -0,0 +1,100 @@
+import pytest
+
+from textual.widgets.text_area import Document
+
+TEXT = """I must not fear.
+Fear is the mind-killer."""
+
+TEXT_NEWLINE = TEXT + "\n"
+TEXT_WINDOWS = TEXT.replace("\n", "\r\n")
+TEXT_WINDOWS_NEWLINE = TEXT_NEWLINE.replace("\n", "\r\n")
+
+
+@pytest.mark.parametrize(
+ "text", [TEXT, TEXT_NEWLINE, TEXT_WINDOWS, TEXT_WINDOWS_NEWLINE]
+)
+def test_text(text):
+ """The text we put in is the text we get out."""
+ document = Document(text)
+ assert document.text == text
+
+
+def test_lines_newline_eof():
+ document = Document(TEXT_NEWLINE)
+ assert document.lines == ["I must not fear.", "Fear is the mind-killer.", ""]
+
+
+def test_lines_no_newline_eof():
+ document = Document(TEXT)
+ assert document.lines == [
+ "I must not fear.",
+ "Fear is the mind-killer.",
+ ]
+
+
+def test_lines_windows():
+ document = Document(TEXT_WINDOWS)
+ assert document.lines == ["I must not fear.", "Fear is the mind-killer."]
+
+
+def test_lines_windows_newline():
+ document = Document(TEXT_WINDOWS_NEWLINE)
+ assert document.lines == ["I must not fear.", "Fear is the mind-killer.", ""]
+
+
+def test_newline_unix():
+ document = Document(TEXT)
+ assert document.newline == "\n"
+
+
+def test_newline_windows():
+ document = Document(TEXT_WINDOWS)
+ assert document.newline == "\r\n"
+
+
+def test_get_selected_text_no_selection():
+ document = Document(TEXT)
+ selection = document.get_text_range((0, 0), (0, 0))
+ assert selection == ""
+
+
+def test_get_selected_text_single_line():
+ document = Document(TEXT_WINDOWS)
+ selection = document.get_text_range((0, 2), (0, 6))
+ assert selection == "must"
+
+
+def test_get_selected_text_multiple_lines_unix():
+ document = Document(TEXT)
+ selection = document.get_text_range((0, 2), (1, 2))
+ assert selection == "must not fear.\nFe"
+
+
+def test_get_selected_text_multiple_lines_windows():
+ document = Document(TEXT_WINDOWS)
+ selection = document.get_text_range((0, 2), (1, 2))
+ assert selection == "must not fear.\r\nFe"
+
+
+def test_get_selected_text_including_final_newline_unix():
+ document = Document(TEXT_NEWLINE)
+ selection = document.get_text_range((0, 0), (2, 0))
+ assert selection == TEXT_NEWLINE
+
+
+def test_get_selected_text_including_final_newline_windows():
+ document = Document(TEXT_WINDOWS_NEWLINE)
+ selection = document.get_text_range((0, 0), (2, 0))
+ assert selection == TEXT_WINDOWS_NEWLINE
+
+
+def test_get_selected_text_no_newline_at_end_of_file():
+ document = Document(TEXT)
+ selection = document.get_text_range((0, 0), (2, 0))
+ assert selection == TEXT
+
+
+def test_get_selected_text_no_newline_at_end_of_file_windows():
+ document = Document(TEXT_WINDOWS)
+ selection = document.get_text_range((0, 0), (2, 0))
+ assert selection == TEXT_WINDOWS
diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py
new file mode 100644
index 0000000000..d00fa686c9
--- /dev/null
+++ b/tests/document/test_document_delete.py
@@ -0,0 +1,146 @@
+import pytest
+
+from textual.widgets.text_area import Document, EditResult
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+I forgot the rest of the quote.
+Sorry Will."""
+
+
+@pytest.fixture
+def document():
+ document = Document(TEXT)
+ return document
+
+
+def test_delete_single_character(document):
+ replace_result = document.replace_range((0, 0), (0, 1), "")
+ assert replace_result == EditResult(end_location=(0, 0), replaced_text="I")
+ assert document.lines == [
+ " must not fear.",
+ "Fear is the mind-killer.",
+ "I forgot the rest of the quote.",
+ "Sorry Will.",
+ ]
+
+
+def test_delete_single_newline(document):
+ """Testing deleting newline from right to left"""
+ replace_result = document.replace_range((1, 0), (0, 16), "")
+ assert replace_result == EditResult(end_location=(0, 16), replaced_text="\n")
+ assert document.lines == [
+ "I must not fear.Fear is the mind-killer.",
+ "I forgot the rest of the quote.",
+ "Sorry Will.",
+ ]
+
+
+def test_delete_near_end_of_document(document):
+ """Test deleting a range near the end of a document."""
+ replace_result = document.replace_range((1, 0), (3, 11), "")
+ assert replace_result == EditResult(
+ end_location=(1, 0),
+ replaced_text="Fear is the mind-killer.\n"
+ "I forgot the rest of the quote.\n"
+ "Sorry Will.",
+ )
+ assert document.lines == [
+ "I must not fear.",
+ "",
+ ]
+
+
+def test_delete_clearing_the_document(document):
+ replace_result = document.replace_range((0, 0), (4, 0), "")
+ assert replace_result == EditResult(
+ end_location=(0, 0),
+ replaced_text=TEXT,
+ )
+ assert document.lines == [""]
+
+
+def test_delete_multiple_characters_on_one_line(document):
+ replace_result = document.replace_range((0, 2), (0, 7), "")
+ assert replace_result == EditResult(
+ end_location=(0, 2),
+ replaced_text="must ",
+ )
+ assert document.lines == [
+ "I not fear.",
+ "Fear is the mind-killer.",
+ "I forgot the rest of the quote.",
+ "Sorry Will.",
+ ]
+
+
+def test_delete_multiple_lines_partially_spanned(document):
+ """Deleting a selection that partially spans the first and final lines of the selection."""
+ replace_result = document.replace_range((0, 2), (2, 2), "")
+ assert replace_result == EditResult(
+ end_location=(0, 2),
+ replaced_text="must not fear.\nFear is the mind-killer.\nI ",
+ )
+ assert document.lines == [
+ "I forgot the rest of the quote.",
+ "Sorry Will.",
+ ]
+
+
+def test_delete_end_of_line(document):
+ """Testing deleting newline from left to right"""
+ replace_result = document.replace_range((0, 16), (1, 0), "")
+ assert replace_result == EditResult(
+ end_location=(0, 16),
+ replaced_text="\n",
+ )
+ assert document.lines == [
+ "I must not fear.Fear is the mind-killer.",
+ "I forgot the rest of the quote.",
+ "Sorry Will.",
+ ]
+
+
+def test_delete_single_line_excluding_newline(document):
+ """Delete from the start to the end of the line."""
+ replace_result = document.replace_range((2, 0), (2, 31), "")
+ assert replace_result == EditResult(
+ end_location=(2, 0),
+ replaced_text="I forgot the rest of the quote.",
+ )
+ assert document.lines == [
+ "I must not fear.",
+ "Fear is the mind-killer.",
+ "",
+ "Sorry Will.",
+ ]
+
+
+def test_delete_single_line_including_newline(document):
+ """Delete from the start of a line to the start of the line below."""
+ replace_result = document.replace_range((2, 0), (3, 0), "")
+ assert replace_result == EditResult(
+ end_location=(2, 0),
+ replaced_text="I forgot the rest of the quote.\n",
+ )
+ assert document.lines == [
+ "I must not fear.",
+ "Fear is the mind-killer.",
+ "Sorry Will.",
+ ]
+
+
+TEXT_NEWLINE_EOF = """\
+I must not fear.
+Fear is the mind-killer.
+"""
+
+
+def test_delete_end_of_file_newline():
+ document = Document(TEXT_NEWLINE_EOF)
+ replace_result = document.replace_range((2, 0), (1, 24), "")
+ assert replace_result == EditResult(end_location=(1, 24), replaced_text="\n")
+ assert document.lines == [
+ "I must not fear.",
+ "Fear is the mind-killer.",
+ ]
diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py
new file mode 100644
index 0000000000..ea706c9abf
--- /dev/null
+++ b/tests/document/test_document_insert.py
@@ -0,0 +1,107 @@
+from textual.widgets.text_area import Document
+
+TEXT = """I must not fear.
+Fear is the mind-killer."""
+
+
+def test_insert_no_newlines():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 1), " really")
+ assert document.lines == [
+ "I really must not fear.",
+ "Fear is the mind-killer.",
+ ]
+
+
+def test_insert_empty_string():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 1), "")
+ assert document.lines == ["I must not fear.", "Fear is the mind-killer."]
+
+
+def test_insert_invalid_column():
+ document = Document(TEXT)
+ document.replace_range((0, 999), (0, 999), " really")
+ assert document.lines == ["I must not fear. really", "Fear is the mind-killer."]
+
+
+def test_insert_invalid_row_and_column():
+ document = Document(TEXT)
+ document.replace_range((999, 0), (999, 0), " really")
+ assert document.lines == ["I must not fear.", "Fear is the mind-killer.", " really"]
+
+
+def test_insert_range_newline_file_start():
+ document = Document(TEXT)
+ document.replace_range((0, 0), (0, 0), "\n")
+ assert document.lines == ["", "I must not fear.", "Fear is the mind-killer."]
+
+
+def test_insert_newline_splits_line():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 1), "\n")
+ assert document.lines == ["I", " must not fear.", "Fear is the mind-killer."]
+
+
+def test_insert_newline_splits_line_selection():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 6), "\n")
+ assert document.lines == ["I", " not fear.", "Fear is the mind-killer."]
+
+
+def test_insert_multiple_lines_ends_with_newline():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 1), "Hello,\nworld!\n")
+ assert document.lines == [
+ "IHello,",
+ "world!",
+ " must not fear.",
+ "Fear is the mind-killer.",
+ ]
+
+
+def test_insert_multiple_lines_ends_with_no_newline():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 1), "Hello,\nworld!")
+ assert document.lines == [
+ "IHello,",
+ "world! must not fear.",
+ "Fear is the mind-killer.",
+ ]
+
+
+def test_insert_multiple_lines_starts_with_newline():
+ document = Document(TEXT)
+ document.replace_range((0, 1), (0, 1), "\nHello,\nworld!\n")
+ assert document.lines == [
+ "I",
+ "Hello,",
+ "world!",
+ " must not fear.",
+ "Fear is the mind-killer.",
+ ]
+
+
+def test_insert_range_text_no_newlines():
+ """Ensuring we can do a simple replacement of text."""
+ document = Document(TEXT)
+ document.replace_range((0, 2), (0, 6), "MUST")
+ assert document.lines == [
+ "I MUST not fear.",
+ "Fear is the mind-killer.",
+ ]
+
+
+TEXT_NEWLINE_EOF = """\
+I must not fear.
+Fear is the mind-killer.
+"""
+
+
+def test_newline_eof():
+ document = Document(TEXT_NEWLINE_EOF)
+ assert document.lines == [
+ "I must not fear.",
+ "Fear is the mind-killer.",
+ "",
+ ]
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 11e7c0d646..a1b1206dbe 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -1698,7 +1698,7 @@
'''
# ---
-# name: test_columns_height
+# name: test_collapsible_collapsed
'''
'''
# ---
-# name: test_command_palette
+# name: test_collapsible_custom_symbol
'''