diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index ebf5a048a2..7bbae74e0b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -6,6 +6,13 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'main' ]
+ paths:
+ - '.github/workflows/codeql.yml'
+ - '**.py'
+ - '**.pyi'
+ - '**.lock'
+ - '**.js'
+ - '**.ts'
schedule:
- cron: '10 20 * * 4'
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 01a5639af0..02e367aa44 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -3,56 +3,50 @@ name: Test Textual module
on:
pull_request:
paths:
- - '.github/workflows/pythonpackage.yml'
- - '**.py'
- - '**.pyi'
- - '**.css'
- - '**.ambr'
- - '**.lock'
- - 'Makefile'
+ - ".github/workflows/pythonpackage.yml"
+ - "**.py"
+ - "**.pyi"
+ - "**.css"
+ - "**.ambr"
+ - "**.lock"
+ - "Makefile"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
defaults:
run:
shell: bash
steps:
- - uses: actions/checkout@v3.5.2
- - name: Install and configure Poetry # This could be cached, too...
- uses: snok/install-poetry@v1.3.3
- with:
- version: 1.4.2
- virtualenvs-in-project: true
+ - uses: actions/checkout@v4.1.1
+ - name: Install Poetry
+ run: pipx install poetry==1.7.1
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4.6.0
+ uses: actions/setup-python@v4.7.1
with:
python-version: ${{ matrix.python-version }}
- architecture: x64
- allow-prereleases: true
- - name: Load cached venv
- id: cached-poetry-dependencies
- uses: actions/cache@v3
- with:
- path: .venv
- key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
+ cache: 'poetry'
- name: Install dependencies
- run: poetry install
- if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
-# - name: Typecheck with mypy
-# run: |
-# make typecheck
+ run: poetry install --no-interaction --extras syntax
+ if: ${{ matrix.python-version != '3.12' }}
+ - name: Install dependencies for 3.12 # https://github.com/Textualize/textual/issues/3491#issuecomment-1854156476
+ run: poetry install --no-interaction
+ if: ${{ matrix.python-version == '3.12' }}
- name: Test with pytest
run: |
- source $VENV
- pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
+ poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
+ if: ${{ matrix.python-version != '3.12' }}
+ - name: Test with pytest for 3.12 # https://github.com/Textualize/textual/issues/3491#issuecomment-1854156476
+ run: |
+ poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing -m 'not syntax'
+ if: ${{ matrix.python-version == '3.12' }}
- name: Upload snapshot report
if: always()
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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b68fd51211..39994d188c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,10 +4,18 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: check-yaml
- args: [ '--unsafe' ]
+ - id: check-ast # simply checks whether the files parse as valid python
+ - id: check-builtin-literals # requires literal syntax when initializing empty or zero python builtin types
+ - id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems
+ - id: check-merge-conflict # checks for files that contain merge conflict strings
+ - id: check-json # checks json files for parseable syntax
+ - id: check-toml # checks toml files for parseable syntax
+ - id: check-yaml # checks yaml files for parseable syntax
+ args: [ '--unsafe' ] # Instead of loading the files, simply parse them for syntax.
+ - id: check-shebang-scripts-are-executable # ensures that (non-binary) files with a shebang are executable
+ - id: check-vcs-permalinks # ensures that links to vcs websites are permalinks
+ - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline
+ - id: mixed-line-ending # replaces or checks mixed line ending
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
@@ -19,4 +27,10 @@ repos:
rev: 23.1.0
hooks:
- id: black
+ - repo: https://github.com/hadialqattan/pycln # removes unused imports
+ rev: v2.3.0
+ hooks:
+ - id: pycln
+ language_version: "3.11"
+ args: [--all]
exclude: ^tests/snapshot_tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f56879e54..36b22172bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,290 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [0.47.0] - 2024-01-04
+
+### Fixed
+
+- `Widget.move_child` would break if `before`/`after` is set to the index of the widget in `child` https://github.com/Textualize/textual/issues/1743
+- Fixed auto width text not processing markup https://github.com/Textualize/textual/issues/3918
+- Fixed `Tree.clear` not retaining the root's expanded state https://github.com/Textualize/textual/issues/3557
+
+### Changed
+
+- Breaking change: `Widget.move_child` parameters `before` and `after` are now keyword-only https://github.com/Textualize/textual/pull/3896
+- Style tweak to toasts https://github.com/Textualize/textual/pull/3955
+
+### Added
+
+- Added textual.lazy https://github.com/Textualize/textual/pull/3936
+- Added App.push_screen_wait https://github.com/Textualize/textual/pull/3955
+- Added nesting of CSS https://github.com/Textualize/textual/pull/3946
+
+## [0.46.0] - 2023-12-17
+
+### Fixed
+
+- Disabled radio buttons could be selected with the keyboard https://github.com/Textualize/textual/issues/3839
+- Fixed zero width scrollbars causing content to disappear https://github.com/Textualize/textual/issues/3886
+
+### Changed
+
+- The tabs within a `TabbedContent` now prefix their IDs to stop any clash with their associated `TabPane` https://github.com/Textualize/textual/pull/3815
+- Breaking change: `tab` is no longer a `@on` decorator selector for `TabbedContent.TabActivated` -- use `pane` instead https://github.com/Textualize/textual/pull/3815
+
+### Added
+
+- Added `Collapsible.title` reactive attribute https://github.com/Textualize/textual/pull/3830
+- Added a `pane` attribute to `TabbedContent.TabActivated` https://github.com/Textualize/textual/pull/3815
+- Added caching of rules attributes and `cache` parameter to Stylesheet.apply https://github.com/Textualize/textual/pull/3880
+
+## [0.45.1] - 2023-12-12
+
+### Fixed
+
+- Fixed issues where styles wouldn't update if changed in mount. https://github.com/Textualize/textual/pull/3860
+
+## [0.45.0] - 2023-12-12
+
+### Fixed
+
+- Fixed `DataTable.update_cell` not raising an error with an invalid column key https://github.com/Textualize/textual/issues/3335
+- Fixed `Input` showing suggestions when not focused https://github.com/Textualize/textual/pull/3808
+- Fixed loading indicator not covering scrollbars https://github.com/Textualize/textual/pull/3816
+
+### Removed
+
+- Removed renderables/align.py which was no longer used.
+
+### Changed
+
+- Dropped ALLOW_CHILDREN flag introduced in 0.43.0 https://github.com/Textualize/textual/pull/3814
+- Widgets with an auto height in an auto height container will now expand if they have no siblings https://github.com/Textualize/textual/pull/3814
+- Breaking change: Removed `limit_rules` from Stylesheet.apply https://github.com/Textualize/textual/pull/3844
+
+### Added
+
+- Added `get_loading_widget` to Widget and App customize the loading widget. https://github.com/Textualize/textual/pull/3816
+- Added messages `Collapsible.Expanded` and `Collapsible.Collapsed` that inherit from `Collapsible.Toggled`. https://github.com/Textualize/textual/issues/3824
+
+## [0.44.1] - 2023-12-4
+
+### Fixed
+
+- Fixed slow scrolling when there are many widgets https://github.com/Textualize/textual/pull/3801
+
+## [0.44.0] - 2023-12-1
+
+### Changed
+
+- Breaking change: Dropped 3.7 support https://github.com/Textualize/textual/pull/3766
+- Breaking changes https://github.com/Textualize/textual/issues/1530
+ - `link-hover-background` renamed to `link-background-hover`
+ - `link-hover-color` renamed to `link-color-hover`
+ - `link-hover-style` renamed to `link-style-hover`
+- `Tree` now forces a scroll when `scroll_to_node` is called https://github.com/Textualize/textual/pull/3786
+- Brought rxvt's use of shift-numpad keys in line with most other terminals https://github.com/Textualize/textual/pull/3769
+
+### Added
+
+- Added support for Ctrl+Fn and Ctrl+Shift+Fn keys in urxvt https://github.com/Textualize/textual/pull/3737
+- Friendly error messages when trying to mount non-widgets https://github.com/Textualize/textual/pull/3780
+- Added `Select.from_values` class method that can be used to initialize a Select control with an iterator of values https://github.com/Textualize/textual/pull/3743
+
+### Fixed
+
+- Fixed NoWidget when mouse goes outside window https://github.com/Textualize/textual/pull/3790
+- Removed spurious print statements from press_keys https://github.com/Textualize/textual/issues/3785
+
+## [0.43.2] - 2023-11-29
+
+### Fixed
+
+- Fixed NoWidget error https://github.com/Textualize/textual/pull/3779
+
+## [0.43.1] - 2023-11-29
+
+### Fixed
+
+- Fixed clicking on scrollbar moves TextArea cursor https://github.com/Textualize/textual/issues/3763
+
+## [0.43.0] - 2023-11-28
+
+### Fixed
+
+- Fixed mouse targeting issue in `TextArea` when tabs were not fully expanded https://github.com/Textualize/textual/pull/3725
+- Fixed `Select` not updating after changing the `prompt` reactive https://github.com/Textualize/textual/issues/2983
+- Fixed flicker when updating Markdown https://github.com/Textualize/textual/pull/3757
+
+### Added
+
+- Added experimental Canvas class https://github.com/Textualize/textual/pull/3669/
+- Added `keyline` rule https://github.com/Textualize/textual/pull/3669/
+- Widgets can now have an ALLOW_CHILDREN (bool) classvar to disallow adding children to a widget https://github.com/Textualize/textual/pull/3758
+- Added the ability to set the `label` property of a `Checkbox` https://github.com/Textualize/textual/pull/3765
+- Added the ability to set the `label` property of a `RadioButton` https://github.com/Textualize/textual/pull/3765
+- Added support for various modified edit and navigation keys in urxvt https://github.com/Textualize/textual/pull/3739
+- Added app focus/blur for textual-web https://github.com/Textualize/textual/pull/3767
+
+### Changed
+
+- Method `MarkdownTableOfContents.set_table_of_contents` renamed to `MarkdownTableOfContents.rebuild_table_of_contents` https://github.com/Textualize/textual/pull/3730
+- Exception `Tree.UnknownNodeID` moved out of `Tree`, import from `textual.widgets.tree` https://github.com/Textualize/textual/pull/3730
+- Exception `TreeNode.RemoveRootError` moved out of `TreeNode`, import from `textual.widgets.tree` https://github.com/Textualize/textual/pull/3730
+- Optimized startup time https://github.com/Textualize/textual/pull/3753
+- App.COMMANDS or Screen.COMMANDS can now accept a callable which returns a command palette provider https://github.com/Textualize/textual/pull/3756
+
+## [0.42.0] - 2023-11-22
+
+### Fixed
+
+- Duplicate CSS errors when parsing CSS from a screen https://github.com/Textualize/textual/issues/3581
+- Added missing `blur` pseudo class https://github.com/Textualize/textual/issues/3439
+- Fixed visual glitched characters on Windows due to Python limitation https://github.com/Textualize/textual/issues/2548
+- Fixed `ScrollableContainer` to receive focus https://github.com/Textualize/textual/pull/3632
+- Fixed app-level queries causing a crash when the command palette is active https://github.com/Textualize/textual/issues/3633
+- Fixed outline not rendering correctly in some scenarios (e.g. on Button widgets) https://github.com/Textualize/textual/issues/3628
+- Fixed live-reloading of screen CSS https://github.com/Textualize/textual/issues/3454
+- `Select.value` could be in an invalid state https://github.com/Textualize/textual/issues/3612
+- Off-by-one in CSS error reporting https://github.com/Textualize/textual/issues/3625
+- Loading indicators and app notifications overlapped in the wrong order https://github.com/Textualize/textual/issues/3677
+- Widgets being loaded are disabled and have their scrolling explicitly disabled too https://github.com/Textualize/textual/issues/3677
+- Method render on a widget could be called before mounting said widget https://github.com/Textualize/textual/issues/2914
+
+### Added
+
+- Exceptions to `textual.widgets.select` https://github.com/Textualize/textual/pull/3614
+ - `InvalidSelectValueError` for when setting a `Select` to an invalid value
+ - `EmptySelectError` when creating/setting a `Select` to have no options when `allow_blank` is `False`
+- `Select` methods https://github.com/Textualize/textual/pull/3614
+ - `clear`
+ - `is_blank`
+- Constant `Select.BLANK` to flag an empty selection https://github.com/Textualize/textual/pull/3614
+- Added `restrict`, `type`, `max_length`, and `valid_empty` to Input https://github.com/Textualize/textual/pull/3657
+- Added `Pilot.mouse_down` to simulate `MouseDown` events https://github.com/Textualize/textual/pull/3495
+- Added `Pilot.mouse_up` to simulate `MouseUp` events https://github.com/Textualize/textual/pull/3495
+- Added `Widget.is_mounted` property https://github.com/Textualize/textual/pull/3709
+- Added `TreeNode.refresh` https://github.com/Textualize/textual/pull/3639
+
+### Changed
+
+- CSS error reporting will no longer provide links to the files in question https://github.com/Textualize/textual/pull/3582
+- inline CSS error reporting will report widget/class variable where the CSS was read from https://github.com/Textualize/textual/pull/3582
+- Breaking change: `Tree.refresh_line` has now become an internal https://github.com/Textualize/textual/pull/3639
+- Breaking change: Setting `Select.value` to `None` no longer clears the selection (See `Select.BLANK` and `Select.clear`) https://github.com/Textualize/textual/pull/3614
+- Breaking change: `Button` no longer inherits from `Static`, now it inherits directly from `Widget` https://github.com/Textualize/textual/issues/3603
+- Rich markup in markdown headings is now escaped when building the TOC https://github.com/Textualize/textual/issues/3689
+- Mechanics behind mouse clicks. See [this](https://github.com/Textualize/textual/pull/3495#issue-1934915047) for more details. https://github.com/Textualize/textual/pull/3495
+- Breaking change: max/min-width/height now includes padding and border. https://github.com/Textualize/textual/pull/3712
+
+## [0.41.0] - 2023-10-31
+
+### Fixed
+
+- Fixed `Input.cursor_blink` reactive not changing blink state after `Input` was mounted https://github.com/Textualize/textual/pull/3498
+- Fixed `Tabs.active` attribute value not being re-assigned after removing a tab or clearing https://github.com/Textualize/textual/pull/3498
+- Fixed `DirectoryTree` race-condition crash when changing path https://github.com/Textualize/textual/pull/3498
+- Fixed issue with `LRUCache.discard` https://github.com/Textualize/textual/issues/3537
+- Fixed `DataTable` not scrolling to rows that were just added https://github.com/Textualize/textual/pull/3552
+- Fixed cache bug with `DataTable.update_cell` https://github.com/Textualize/textual/pull/3551
+- Fixed CSS errors being repeated https://github.com/Textualize/textual/pull/3566
+- Fix issue with chunky highlights on buttons https://github.com/Textualize/textual/pull/3571
+- Fixed `OptionList` event leakage from `CommandPalette` to `App`.
+- Fixed crash in `LoadingIndicator` https://github.com/Textualize/textual/pull/3498
+- Fixed crash when `Tabs` appeared as a descendant of `TabbedContent` in the DOM https://github.com/Textualize/textual/pull/3602
+- Fixed the command palette cancelling other workers https://github.com/Textualize/textual/issues/3615
+
+### Added
+
+- Add Document `get_index_from_location` / `get_location_from_index` https://github.com/Textualize/textual/pull/3410
+- Add setter for `TextArea.text` https://github.com/Textualize/textual/discussions/3525
+- Added `key` argument to the `DataTable.sort()` method, allowing the table to be sorted using a custom function (or other callable) https://github.com/Textualize/textual/pull/3090
+- Added `initial` to all css rules, which restores default (i.e. value from DEFAULT_CSS) https://github.com/Textualize/textual/pull/3566
+- Added HorizontalPad to pad.py https://github.com/Textualize/textual/pull/3571
+- Added `AwaitComplete` class, to be used for optionally awaitable return values https://github.com/Textualize/textual/pull/3498
+
+### Changed
+
+- Breaking change: `Button.ACTIVE_EFFECT_DURATION` classvar converted to `Button.active_effect_duration` attribute https://github.com/Textualize/textual/pull/3498
+- Breaking change: `Input.blink_timer` made private (renamed to `Input._blink_timer`) https://github.com/Textualize/textual/pull/3498
+- Breaking change: `Input.cursor_blink` reactive updated to not run on mount (now `init=False`) https://github.com/Textualize/textual/pull/3498
+- Breaking change: `AwaitTabbedContent` class removed https://github.com/Textualize/textual/pull/3498
+- Breaking change: `Tabs.remove_tab` now returns an `AwaitComplete` instead of an `AwaitRemove` https://github.com/Textualize/textual/pull/3498
+- Breaking change: `Tabs.clear` now returns an `AwaitComplete` instead of an `AwaitRemove` https://github.com/Textualize/textual/pull/3498
+- `TabbedContent.add_pane` now returns an `AwaitComplete` instead of an `AwaitTabbedContent` https://github.com/Textualize/textual/pull/3498
+- `TabbedContent.remove_pane` now returns an `AwaitComplete` instead of an `AwaitTabbedContent` https://github.com/Textualize/textual/pull/3498
+- `TabbedContent.clear_pane` now returns an `AwaitComplete` instead of an `AwaitTabbedContent` https://github.com/Textualize/textual/pull/3498
+- `Tabs.add_tab` now returns an `AwaitComplete` instead of an `AwaitMount` https://github.com/Textualize/textual/pull/3498
+- `DirectoryTree.reload` now returns an `AwaitComplete`, which may be awaited to ensure the node has finished being processed by the internal queue https://github.com/Textualize/textual/pull/3498
+- `Tabs.remove_tab` now returns an `AwaitComplete`, which may be awaited to ensure the tab is unmounted and internal state is updated https://github.com/Textualize/textual/pull/3498
+- `App.switch_mode` now returns an `AwaitMount`, which may be awaited to ensure the screen is mounted https://github.com/Textualize/textual/pull/3498
+- Buttons will now display multiple lines, and have auto height https://github.com/Textualize/textual/pull/3539
+- DataTable now has a max-height of 100vh rather than 100%, which doesn't work with auto
+- Breaking change: empty rules now result in an error https://github.com/Textualize/textual/pull/3566
+- Improved startup time by caching CSS parsing https://github.com/Textualize/textual/pull/3575
+- Workers are now created/run in a thread-safe way https://github.com/Textualize/textual/pull/3586
+
+## [0.40.0] - 2023-10-11
+
+### Added
+
+- Added `loading` reactive property to widgets https://github.com/Textualize/textual/pull/3509
+
+## [0.39.0] - 2023-10-10
+
+### Fixed
+
+- `Pilot.click`/`Pilot.hover` can't use `Screen` as a selector https://github.com/Textualize/textual/issues/3395
+- App exception when a `Tree` is initialized/mounted with `disabled=True` https://github.com/Textualize/textual/issues/3407
+- Fixed `print` locations not being correctly reported in `textual console` https://github.com/Textualize/textual/issues/3237
+- Fix location of IME and emoji popups https://github.com/Textualize/textual/pull/3408
+- Fixed application freeze when pasting an emoji into an application on Windows https://github.com/Textualize/textual/issues/3178
+- Fixed duplicate option ID handling in the `OptionList` https://github.com/Textualize/textual/issues/3455
+- Fix crash when removing and updating DataTable cell at same time https://github.com/Textualize/textual/pull/3487
+- Fixed fractional styles to allow integer values https://github.com/Textualize/textual/issues/3414
+- Stop eating stdout/stderr in headless mode - print works again in tests https://github.com/Textualize/textual/pull/3486
+
+### Added
+
+- `OutOfBounds` exception to be raised by `Pilot` https://github.com/Textualize/textual/pull/3360
+- `TextArea.cursor_screen_offset` property for getting the screen-relative position of the cursor https://github.com/Textualize/textual/pull/3408
+- `Input.cursor_screen_offset` property for getting the screen-relative position of the cursor https://github.com/Textualize/textual/pull/3408
+- Reactive `cell_padding` (and respective parameter) to define horizontal cell padding in data table columns https://github.com/Textualize/textual/issues/3435
+- Added `Input.clear` method https://github.com/Textualize/textual/pull/3430
+- Added `TextArea.SelectionChanged` and `TextArea.Changed` messages https://github.com/Textualize/textual/pull/3442
+- Added `wait_for_dismiss` parameter to `App.push_screen` https://github.com/Textualize/textual/pull/3477
+- Allow scrollbar-size to be set to 0 to achieve scrollable containers with no visible scrollbars https://github.com/Textualize/textual/pull/3488
+
+### Changed
+
+- Breaking change: tree-sitter and tree-sitter-languages dependencies moved to `syntax` extra https://github.com/Textualize/textual/pull/3398
+- `Pilot.click`/`Pilot.hover` now raises `OutOfBounds` when clicking outside visible screen https://github.com/Textualize/textual/pull/3360
+- `Pilot.click`/`Pilot.hover` now return a Boolean indicating whether the click/hover landed on the widget that matches the selector https://github.com/Textualize/textual/pull/3360
+- Added a delay to when the `No Matches` message appears in the command palette, thus removing a flicker https://github.com/Textualize/textual/pull/3399
+- Timer callbacks are now typed more loosely https://github.com/Textualize/textual/issues/3434
+
+## [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
+- 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.1] - 2023-09-16
### Fixed
@@ -12,7 +296,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- 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`.
-## [0.36.0] - 2023-09-15
+## [0.37.0] - 2023-09-15
### Added
@@ -40,6 +324,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
@@ -57,6 +342,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
@@ -135,7 +421,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- DescendantBlur and DescendantFocus can now be used with @on decorator
-
## [0.32.0] - 2023-08-03
### Added
@@ -1283,6 +1568,21 @@ 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.47.0]: https://github.com/Textualize/textual/compare/v0.46.0...v0.47.0
+[0.46.0]: https://github.com/Textualize/textual/compare/v0.45.1...v0.46.0
+[0.45.1]: https://github.com/Textualize/textual/compare/v0.45.0...v0.45.1
+[0.45.0]: https://github.com/Textualize/textual/compare/v0.44.1...v0.45.0
+[0.44.1]: https://github.com/Textualize/textual/compare/v0.44.0...v0.44.1
+[0.44.0]: https://github.com/Textualize/textual/compare/v0.43.2...v0.44.0
+[0.43.2]: https://github.com/Textualize/textual/compare/v0.43.1...v0.43.2
+[0.43.1]: https://github.com/Textualize/textual/compare/v0.43.0...v0.43.1
+[0.43.0]: https://github.com/Textualize/textual/compare/v0.42.0...v0.43.0
+[0.42.0]: https://github.com/Textualize/textual/compare/v0.41.0...v0.42.0
+[0.41.0]: https://github.com/Textualize/textual/compare/v0.40.0...v0.41.0
+[0.40.0]: https://github.com/Textualize/textual/compare/v0.39.0...v0.40.0
+[0.39.0]: https://github.com/Textualize/textual/compare/v0.38.1...v0.39.0
+[0.38.1]: https://github.com/Textualize/textual/compare/v0.38.0...v0.38.1
+[0.38.0]: https://github.com/Textualize/textual/compare/v0.37.1...v0.38.0
[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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 41b440244b..a28f2155ee 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,120 +1,112 @@
-# 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 `make setup` 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
+([Read this](#makefile-commands) if the command `make` doesn't work for you.)
-Once you have Textual installed, run the following to get an impression of what it can do:
+## Demo
+
+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.
-**2.** Clone the forked repository.
+## Guidelines
-```bash
-git clone https://github.com//textual.git
-```
+- Read any issue instructions carefully. Feel free to ask for clarification if any details are missing.
-**3.** Navigate to the project directory.
+- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from.
-```bash
-cd textual
-```
+- 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.
-**4.** Create a new [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)
+## 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:
-### 📣 Pull Requests(PRs)
+ - [ ] 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`)
-The process described here should check off these goals:
+([Read this](#makefile-commands) if the command `make` doesn't work for you.)
-- [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.
+## Updating and building the documentation
-### After the PR 🥳
-When you open a PR, your code will be reviewed by one of the Textual maintainers.
-In that review process,
+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.
-- 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.
+([Read this](#makefile-commands) if the command `make` doesn't work for you.)
-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.
+We strive to write our documentation in a clear and accessible way so, if you find any issues with the documentation, we encourage you to open an issue where you can enumerate the things you think should be changed or added.
-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.
+Opening an issue or a discussion is typically better than opening a PR directly.
+That's because there are many subjective considerations that go into writing documentation and we cannot expect you, a well-intentioned external contributor, to be aware of those subjective considerations that we take into account when writing our documentation.
+Of course, this does not apply to objective/technical issues with the documentation like bugs or broken links.
+## After opening a PR
-## 🛑 Important
+When you open a PR, your code will be reviewed by one of the Textual maintainers.
+In that review process,
-- 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!
+- 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
-- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from.
+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.
-- Write tests for your code.
+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.
-- 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 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.
+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.
-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/Enf6Z3qhVr) 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/Makefile b/Makefile
index 9fe2b53427..8b5f8d2191 100644
--- a/Makefile
+++ b/Makefile
@@ -85,6 +85,7 @@ clean: clean-screenshot-cache clean-offline-docs
.PHONY: setup
setup:
poetry install
+ poetry install --extras syntax
.PHONY: update
update:
@@ -97,3 +98,7 @@ install-pre-commit:
.PHONY: demo
demo:
$(run) python -m textual
+
+.PHONY: repl
+repl:
+ $(run) python
diff --git a/README.md b/README.md
index b2b518b0a0..e000b74c6c 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
Textual is a *Rapid Application Development* framework for Python.
-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 and a [web browser](https://github.com/Textualize/textual-web)!
@@ -36,7 +36,7 @@ On modern terminal software (installed by default on most systems), Textual apps
## Compatibility
-Textual runs on Linux, macOS, and Windows. Textual requires Python 3.7 or above.
+Textual runs on Linux, macOS, and Windows. Textual requires Python 3.8 or above.
## Installing
diff --git a/docs/_templates/python/material/_base/docstring/parameters.html b/docs/_templates/python/material/_base/docstring/parameters.html
index afff067059..10176a23ae 100644
--- a/docs/_templates/python/material/_base/docstring/parameters.html
+++ b/docs/_templates/python/material/_base/docstring/parameters.html
@@ -5,33 +5,33 @@
{{ section.title or "Parameters" }}
-
Name
-
Type
-
Description
+
Parameter
Default
+
Description
{% for parameter in section.value %}
-
{{ parameter.name }}
+ {{ parameter.name }}
+
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
{% include "expression.html" with context %}
{% endwith %}
{% endif %}
{% endfor %}
diff --git a/docs/api/app.md b/docs/api/app.md
index 3a797ce06f..3a83ec5f02 100644
--- a/docs/api/app.md
+++ b/docs/api/app.md
@@ -1 +1,5 @@
::: textual.app
+ options:
+ filters:
+ - "!^_"
+ - "^__init__$"
diff --git a/docs/api/await_complete.md b/docs/api/await_complete.md
new file mode 100644
index 0000000000..523cb8a289
--- /dev/null
+++ b/docs/api/await_complete.md
@@ -0,0 +1 @@
+::: textual.await_complete
diff --git a/docs/api/containers.md b/docs/api/containers.md
index f65b508681..d44d570d01 100644
--- a/docs/api/containers.md
+++ b/docs/api/containers.md
@@ -1 +1,3 @@
::: textual.containers
+
+::: textual.widgets.ContentSwitcher
diff --git a/docs/api/lazy.md b/docs/api/lazy.md
new file mode 100644
index 0000000000..1b5039f136
--- /dev/null
+++ b/docs/api/lazy.md
@@ -0,0 +1 @@
+::: textual.lazy
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/renderables.md b/docs/api/renderables.md
new file mode 100644
index 0000000000..08f3861e71
--- /dev/null
+++ b/docs/api/renderables.md
@@ -0,0 +1,7 @@
+A collection of Rich renderables which may be returned from a widget's `render()` method.
+
+::: textual.renderables.bar
+::: textual.renderables.blank
+::: textual.renderables.digits
+::: textual.renderables.gradient
+::: textual.renderables.sparkline
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/images/textual-plotext/demo1.png b/docs/blog/images/textual-plotext/demo1.png
new file mode 100644
index 0000000000..359ace1e92
Binary files /dev/null and b/docs/blog/images/textual-plotext/demo1.png differ
diff --git a/docs/blog/images/textual-plotext/demo2.png b/docs/blog/images/textual-plotext/demo2.png
new file mode 100644
index 0000000000..50d47c090a
Binary files /dev/null and b/docs/blog/images/textual-plotext/demo2.png differ
diff --git a/docs/blog/images/textual-plotext/demo3.png b/docs/blog/images/textual-plotext/demo3.png
new file mode 100644
index 0000000000..25866ded17
Binary files /dev/null and b/docs/blog/images/textual-plotext/demo3.png differ
diff --git a/docs/blog/images/textual-plotext/demo4.png b/docs/blog/images/textual-plotext/demo4.png
new file mode 100644
index 0000000000..84050cd849
Binary files /dev/null and b/docs/blog/images/textual-plotext/demo4.png differ
diff --git a/docs/blog/images/textual-plotext/scatter.png b/docs/blog/images/textual-plotext/scatter.png
new file mode 100644
index 0000000000..fdfba71393
Binary files /dev/null and b/docs/blog/images/textual-plotext/scatter.png differ
diff --git a/docs/blog/images/textual-plotext/weather.png b/docs/blog/images/textual-plotext/weather.png
new file mode 100644
index 0000000000..9a7063acc4
Binary files /dev/null and b/docs/blog/images/textual-plotext/weather.png 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/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!
+
+
+ ![cursor_position_updating_via_api.png](../images/text-area-learnings/cursor_position_updating_via_api.png){ loading=lazy }
+ A TetrisArea white-boarding session.
+
+
+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!
+
+
+
+ ![text-area-pyinstrument.png](../images/text-area-learnings/text-area-pyinstrument.png){ loading=lazy }
+ "pyinstrument -r html" produces this beautiful output.
+
+
+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.
+
+
+ ![text-area-theme-cycle.gif](../images/text-area-learnings/text-area-theme-cycle.gif){ loading=lazy }
+ Cycling through a few of the builtin themes.
+
+
+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:
+
+
+ ![text-area-syntax-error.gif](../images/text-area-learnings/text-area-syntax-error.gif){ loading=lazy }
+ Highlighting mismatched closing HTML tags in red.
+
+
+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/blog/posts/textual-plotext.md b/docs/blog/posts/textual-plotext.md
new file mode 100644
index 0000000000..491e0d7621
--- /dev/null
+++ b/docs/blog/posts/textual-plotext.md
@@ -0,0 +1,118 @@
+---
+draft: false
+date: 2023-10-04
+categories:
+ - DevLog
+title: "Announcing textual-plotext"
+authors:
+ - davep
+---
+
+# Announcing textual-plotext
+
+It's no surprise that a common question on the [Textual Discord
+server](https://discord.gg/Enf6Z3qhVr) is how to go about producing plots in
+the terminal. A popular solution that has been suggested is
+[Plotext](https://github.com/piccolomo/plotext). While Plotext doesn't
+directly support Textual, it is [easy to use with
+Rich](https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich)
+and, because of this, we wanted to make it just as easy to use in your
+Textual applications.
+
+
+
+With this in mind we've created
+[`textual-plotext`](https://github.com/Textualize/textual-plotext): a library
+that provides a widget for using Plotext plots in your app. In doing this
+we've tried our best to make it as similar as possible to using Plotext in a
+conventional Python script.
+
+Take this code from the [Plotext README](https://github.com/piccolomo/plotext#readme):
+
+```python
+import plotext as plt
+y = plt.sin() # sinusoidal test signal
+plt.scatter(y)
+plt.title("Scatter Plot") # to apply a title
+plt.show() # to finally plot
+```
+
+The Textual equivalent of this (including everything needed to make this a
+fully-working Textual application) is:
+
+```python
+from textual.app import App, ComposeResult
+
+from textual_plotext import PlotextPlot
+
+class ScatterApp(App[None]):
+
+ def compose(self) -> ComposeResult:
+ yield PlotextPlot()
+
+ def on_mount(self) -> None:
+ plt = self.query_one(PlotextPlot).plt
+ y = plt.sin() # sinusoidal test signal
+ plt.scatter(y)
+ plt.title("Scatter Plot") # to apply a title
+
+if __name__ == "__main__":
+ ScatterApp().run()
+```
+
+When run the result will look like this:
+
+![Scatter plot in a Textual application](/blog/images/textual-plotext/scatter.png)
+
+Aside from a couple of the more far-out plot types[^1] you should find that
+everything you can do with Plotext in a conventional script can also be done
+in a Textual application.
+
+Here's a small selection of screenshots from a demo built into the library,
+each of the plots taken from the Plotext README:
+
+![Sample from the library demo application](/blog/images/textual-plotext/demo1.png)
+
+![Sample from the library demo application](/blog/images/textual-plotext/demo2.png)
+
+![Sample from the library demo application](/blog/images/textual-plotext/demo3.png)
+
+![Sample from the library demo application](/blog/images/textual-plotext/demo4.png)
+
+A key design goal of this widget is that you can develop your plots so that
+the resulting code looks very similar to that in the Plotext documentation.
+The core difference is that, where you'd normally import the `plotext`
+module `as plt` and then call functions via `plt`, you instead use the `plt`
+property made available by the widget.
+
+You don't even need to call the `build` or `show` functions as
+`textual-plotext` takes care of this for you. You can see this in action in
+the scatter code shown earlier.
+
+Of course, moving any existing plotting code into your Textual app means you
+will need to think about how you get the data and when and where you build
+your plot. This might be where the [Textual worker
+API](https://textual.textualize.io/guide/workers/) becomes useful.
+
+We've included a longer-form example application that shows off the glorious
+Scottish weather we enjoy here at Textual Towers, with [an application that
+uses workers to pull down weather data from a year ago and plot
+it](https://github.com/Textualize/textual-plotext/blob/main/examples/textual_towers_weather.py).
+
+![The Textual Towers weather history app](/blog/images/textual-plotext/weather.png)
+
+If you are an existing Plotext user who wants to turn your plots into full
+terminal applications, we think this will be very familiar and accessible.
+If you're a Textual user who wants to add plots to your application, we
+think Plotext is a great library for this.
+
+If you have any questions about this, or anything else to do with Textual,
+feel free to come and join us on our [Discord
+server](https://discord.gg/Enf6Z3qhVr) or in our [GitHub
+discussions](https://github.com/Textualize/textual/discussions).
+
+[^1]: Right now there's no [animated
+ gif](https://github.com/piccolomo/plotext/blob/master/readme/image.md#gif-plot)
+ or
+ [video](https://github.com/piccolomo/plotext/blob/master/readme/video.md)
+ support.
diff --git a/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py b/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py
index 20f2daba87..21e1760aaf 100644
--- a/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py
+++ b/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py
@@ -1,5 +1,4 @@
import asyncio
-import time
from random import randint
from textual.app import App, ComposeResult
diff --git a/docs/css_types/_template.md b/docs/css_types/_template.md
index 079f1b7ef0..1e8be15123 100644
--- a/docs/css_types/_template.md
+++ b/docs/css_types/_template.md
@@ -51,7 +51,7 @@ If the type has many different syntaxes, cover all of them.
Add comments when needed/if helpful.
-->
-```sass
+```css
.some-class {
rule: type-value-1;
rule: type-value-2;
diff --git a/docs/css_types/border.md b/docs/css_types/border.md
index 012bfb598c..00be2f8a98 100644
--- a/docs/css_types/border.md
+++ b/docs/css_types/border.md
@@ -37,7 +37,7 @@ textual borders
### CSS
-```sass
+```css
#container {
border: heavy red;
}
diff --git a/docs/css_types/color.md b/docs/css_types/color.md
index 63b8e31eac..2394603ddb 100644
--- a/docs/css_types/color.md
+++ b/docs/css_types/color.md
@@ -106,7 +106,7 @@ For example, `hsla(128, 100%, 50%, 0.5)` is the color `hsl(128, 100%, 50%)` with
### CSS
-```sass
+```css
Header {
background: red; /* Color name */
}
diff --git a/docs/css_types/horizontal.md b/docs/css_types/horizontal.md
index ec6d02c5d8..b4783e6e15 100644
--- a/docs/css_types/horizontal.md
+++ b/docs/css_types/horizontal.md
@@ -16,7 +16,7 @@ The [``](./horizontal.md) type can take any of the following values:
### CSS
-```sass
+```css
.container {
align-horizontal: right;
}
diff --git a/docs/css_types/integer.md b/docs/css_types/integer.md
index 29c8ba2b3d..a4fdb009ec 100644
--- a/docs/css_types/integer.md
+++ b/docs/css_types/integer.md
@@ -14,7 +14,7 @@ An [``](./integer.md) is any valid integer number like `-10` or `42`.
### CSS
-```sass
+```css
.classname {
offset: 10 -20
}
diff --git a/docs/css_types/keyline.md b/docs/css_types/keyline.md
new file mode 100644
index 0000000000..9b84f32ef9
--- /dev/null
+++ b/docs/css_types/keyline.md
@@ -0,0 +1,30 @@
+# <keyline>
+
+The `` CSS type represents a line style used in the [keyline](../styles/keyline.md) rule.
+
+
+## Syntax
+
+| Value | Description |
+| -------- | -------------------------- |
+| `none` | No line (disable keyline). |
+| `thin` | A thin line. |
+| `heavy` | A heavy (thicker) line. |
+| `double` | A double line. |
+
+## Examples
+
+### CSS
+
+```css
+Vertical {
+ keyline: thin green;
+}
+```
+
+### Python
+
+```py
+# A tuple of and color
+widget.styles.keyline = ("thin", "green")
+```
diff --git a/docs/css_types/name.md b/docs/css_types/name.md
index 8a6f064b97..03b2d7a9fc 100644
--- a/docs/css_types/name.md
+++ b/docs/css_types/name.md
@@ -13,7 +13,7 @@ A [``](./name.md) is any non-empty sequence of characters:
### CSS
-```sass
+```css
Screen {
layers: onlyLetters Letters-and-hiphens _lead-under letters-1-digit;
}
diff --git a/docs/css_types/number.md b/docs/css_types/number.md
index ae3400fcfa..7159a74396 100644
--- a/docs/css_types/number.md
+++ b/docs/css_types/number.md
@@ -10,7 +10,7 @@ A [``](./number.md) is an [``](./integer.md), optionally follow
### CSS
-```sass
+```css
Grid {
grid-size: 3 6 /* Integers are numbers */
}
diff --git a/docs/css_types/overflow.md b/docs/css_types/overflow.md
index cb2cc76362..b5eec3a0d4 100644
--- a/docs/css_types/overflow.md
+++ b/docs/css_types/overflow.md
@@ -16,7 +16,7 @@ The [``](./overflow.md) type can take any of the following values:
### CSS
-```sass
+```css
#container {
overflow-y: hidden; /* Don't overflow */
}
diff --git a/docs/css_types/percentage.md b/docs/css_types/percentage.md
index aa2586d7dd..b886e880b5 100644
--- a/docs/css_types/percentage.md
+++ b/docs/css_types/percentage.md
@@ -16,7 +16,7 @@ Some rules may clamp the values between `0%` and `100%`.
### CSS
-```sass
+```css
#footer {
/* Integer followed by % */
color: red 70%;
diff --git a/docs/css_types/scalar.md b/docs/css_types/scalar.md
index 89c7dbf5d3..75ba4b4287 100644
--- a/docs/css_types/scalar.md
+++ b/docs/css_types/scalar.md
@@ -98,7 +98,7 @@ For example, if its container is big enough, a label with `width: auto` will be
### CSS
-```sass
+```css
Horizontal {
width: 60; /* 60 cells */
height: 1fr; /* proportional size of 1 */
diff --git a/docs/css_types/text_align.md b/docs/css_types/text_align.md
index 95119e676e..4289f695cb 100644
--- a/docs/css_types/text_align.md
+++ b/docs/css_types/text_align.md
@@ -27,9 +27,9 @@ A [``](./text_align.md) can be any of the following values:
### CSS
-```sass
+```css
Label {
- rule: justify;
+ text-align: justify;
}
```
diff --git a/docs/css_types/text_style.md b/docs/css_types/text_style.md
index 8309502f95..c03e92c067 100644
--- a/docs/css_types/text_style.md
+++ b/docs/css_types/text_style.md
@@ -23,7 +23,7 @@ or any _space-separated_ combination of the following values:
### CSS
-```sass
+```css
#label1 {
/* You can specify any value by itself. */
rule: strike;
diff --git a/docs/css_types/vertical.md b/docs/css_types/vertical.md
index b8ff482037..d1b0ffd6ca 100644
--- a/docs/css_types/vertical.md
+++ b/docs/css_types/vertical.md
@@ -16,7 +16,7 @@ The [``](./vertical.md) type can take any of the following values:
### CSS
-```sass
+```css
.container {
align-vertical: top;
}
diff --git a/docs/custom_theme/main.html b/docs/custom_theme/main.html
index fbbfd659ab..3219e100dd 100644
--- a/docs/custom_theme/main.html
+++ b/docs/custom_theme/main.html
@@ -7,7 +7,7 @@
-
+
@@ -30,4 +30,23 @@
+
+
{% endblock %}
diff --git a/docs/examples/events/on_decorator01.py b/docs/examples/events/on_decorator01.py
index ac8e2ccd28..6612d6ad6c 100644
--- a/docs/examples/events/on_decorator01.py
+++ b/docs/examples/events/on_decorator01.py
@@ -1,4 +1,3 @@
-from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Button
diff --git a/docs/examples/events/prevent.py b/docs/examples/events/prevent.py
index 39fe437c2d..61e48780d5 100644
--- a/docs/examples/events/prevent.py
+++ b/docs/examples/events/prevent.py
@@ -1,5 +1,4 @@
from textual.app import App, ComposeResult
-from textual.containers import Horizontal
from textual.widgets import Button, Input
diff --git a/docs/examples/guide/command_palette/command01.py b/docs/examples/guide/command_palette/command01.py
index f808f73224..2e0a9f82d6 100644
--- a/docs/examples/guide/command_palette/command01.py
+++ b/docs/examples/guide/command_palette/command01.py
@@ -1,6 +1,7 @@
-from pathlib import Path
+from __future__ import annotations
-from rich.syntax import Syntax
+from functools import partial
+from pathlib import Path
from textual.app import App, ComposeResult
from textual.command import Hit, Hits, Provider
@@ -34,7 +35,7 @@ async def search(self, query: str) -> Hits: # (2)!
yield Hit(
score,
matcher.highlight(command), # (5)!
- lambda: app.open_file(path),
+ partial(app.open_file, path),
help="Open this file in the viewer",
)
@@ -50,6 +51,8 @@ def compose(self) -> ComposeResult:
def open_file(self, path: Path) -> None:
"""Open and display a file with syntax highlighting."""
+ from rich.syntax import Syntax
+
syntax = Syntax.from_path(
str(path),
line_numbers=True,
diff --git a/docs/examples/guide/css/nesting01.py b/docs/examples/guide/css/nesting01.py
new file mode 100644
index 0000000000..56d6a85ffa
--- /dev/null
+++ b/docs/examples/guide/css/nesting01.py
@@ -0,0 +1,19 @@
+from textual.app import App, ComposeResult
+from textual.containers import Horizontal
+from textual.widgets import Static
+
+
+class NestingDemo(App):
+ """App that doesn't have nested CSS."""
+
+ CSS_PATH = "nesting01.tcss"
+
+ def compose(self) -> ComposeResult:
+ with Horizontal(id="questions"):
+ yield Static("Yes", classes="button affirmative")
+ yield Static("No", classes="button negative")
+
+
+if __name__ == "__main__":
+ app = NestingDemo()
+ app.run()
diff --git a/docs/examples/guide/css/nesting01.tcss b/docs/examples/guide/css/nesting01.tcss
new file mode 100644
index 0000000000..53cc9b2375
--- /dev/null
+++ b/docs/examples/guide/css/nesting01.tcss
@@ -0,0 +1,24 @@
+/* Style the container */
+#questions {
+ border: heavy $primary;
+ align: center middle;
+}
+
+/* Style all buttons */
+#questions .button {
+ width: 1fr;
+ padding: 1 2;
+ margin: 1 2;
+ text-align: center;
+ border: heavy $panel;
+}
+
+/* Style the Yes button */
+#questions .button.affirmative {
+ border: heavy $success;
+}
+
+/* Style the No button */
+#questions .button.negative {
+ border: heavy $error;
+}
diff --git a/docs/examples/guide/css/nesting02.py b/docs/examples/guide/css/nesting02.py
new file mode 100644
index 0000000000..25cbc96669
--- /dev/null
+++ b/docs/examples/guide/css/nesting02.py
@@ -0,0 +1,19 @@
+from textual.app import App, ComposeResult
+from textual.containers import Horizontal
+from textual.widgets import Static
+
+
+class NestingDemo(App):
+ """App with nested CSS."""
+
+ CSS_PATH = "nesting02.tcss"
+
+ def compose(self) -> ComposeResult:
+ with Horizontal(id="questions"):
+ yield Static("Yes", classes="button affirmative")
+ yield Static("No", classes="button negative")
+
+
+if __name__ == "__main__":
+ app = NestingDemo()
+ app.run()
diff --git a/docs/examples/guide/css/nesting02.tcss b/docs/examples/guide/css/nesting02.tcss
new file mode 100644
index 0000000000..febcc77b61
--- /dev/null
+++ b/docs/examples/guide/css/nesting02.tcss
@@ -0,0 +1,24 @@
+/* Style the container */
+#questions {
+ border: heavy $primary;
+ align: center middle;
+
+ /* Style all buttons */
+ .button {
+ width: 1fr;
+ padding: 1 2;
+ margin: 1 2;
+ text-align: center;
+ border: heavy $panel;
+
+ /* Style the Yes button */
+ &.affirmative {
+ border: heavy $success;
+ }
+
+ /* Style the No button */
+ &.negative {
+ border: heavy $error;
+ }
+ }
+}
diff --git a/docs/examples/guide/input/mouse01.py b/docs/examples/guide/input/mouse01.py
index 88ce4a94f9..48036ec4b3 100644
--- a/docs/examples/guide/input/mouse01.py
+++ b/docs/examples/guide/input/mouse01.py
@@ -1,18 +1,8 @@
from textual import events
from textual.app import App, ComposeResult
-from textual.containers import Container
from textual.widgets import RichLog, Static
-class PlayArea(Container):
- def on_mount(self) -> None:
- self.capture_mouse()
-
- def on_mouse_move(self, event: events.MouseMove) -> None:
- self.screen.query_one(RichLog).write(event)
- self.query_one(Ball).offset = event.offset - (8, 2)
-
-
class Ball(Static):
pass
@@ -22,7 +12,11 @@ class MouseApp(App):
def compose(self) -> ComposeResult:
yield RichLog()
- yield PlayArea(Ball("Textual"))
+ yield Ball("Textual")
+
+ def on_mouse_move(self, event: events.MouseMove) -> None:
+ self.screen.query_one(RichLog).write(event)
+ self.query_one(Ball).offset = event.screen_offset - (8, 2)
if __name__ == "__main__":
diff --git a/docs/examples/guide/input/mouse01.tcss b/docs/examples/guide/input/mouse01.tcss
index a37245093e..40a38ac1a7 100644
--- a/docs/examples/guide/input/mouse01.tcss
+++ b/docs/examples/guide/input/mouse01.tcss
@@ -2,15 +2,10 @@ Screen {
layers: log ball;
}
-TextLog {
+RichLog {
layer: log;
}
-PlayArea {
- opacity: 0%;
- layer: ball;
-
-}
Ball {
layer: ball;
width: auto;
diff --git a/docs/examples/guide/reactivity/dynamic_watch.py b/docs/examples/guide/reactivity/dynamic_watch.py
new file mode 100644
index 0000000000..bb231fa6e5
--- /dev/null
+++ b/docs/examples/guide/reactivity/dynamic_watch.py
@@ -0,0 +1,35 @@
+from textual.app import App, ComposeResult
+from textual.reactive import reactive
+from textual.widget import Widget
+from textual.widgets import Button, Label, ProgressBar
+
+
+class Counter(Widget):
+ DEFAULT_CSS = "Counter { height: auto; }"
+ counter = reactive(0) # (1)!
+
+ def compose(self) -> ComposeResult:
+ yield Label()
+ yield Button("+10")
+
+ def on_button_pressed(self) -> None:
+ self.counter += 10
+
+ def watch_counter(self, counter_value: int):
+ self.query_one(Label).update(str(counter_value))
+
+
+class WatchApp(App[None]):
+ def compose(self) -> ComposeResult:
+ yield Counter()
+ yield ProgressBar(total=100, show_eta=False)
+
+ def on_mount(self):
+ def update_progress(counter_value: int): # (2)!
+ self.query_one(ProgressBar).update(progress=counter_value)
+
+ self.watch(self.query_one(Counter), "counter", update_progress) # (3)!
+
+
+if __name__ == "__main__":
+ WatchApp().run()
diff --git a/docs/examples/guide/reactivity/validate01.py b/docs/examples/guide/reactivity/validate01.py
index 65d8113c07..e6ac1a75f2 100644
--- a/docs/examples/guide/reactivity/validate01.py
+++ b/docs/examples/guide/reactivity/validate01.py
@@ -30,7 +30,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
self.count += 1
else:
self.count -= 1
- self.query_one(RichLog).write(f"{self.count=}")
+ self.query_one(RichLog).write(f"count = {self.count}")
if __name__ == "__main__":
diff --git a/docs/examples/guide/screens/questions01.py b/docs/examples/guide/screens/questions01.py
new file mode 100644
index 0000000000..cc699200c6
--- /dev/null
+++ b/docs/examples/guide/screens/questions01.py
@@ -0,0 +1,45 @@
+from textual import on, work
+from textual.app import App, ComposeResult
+from textual.screen import Screen
+from textual.widgets import Button, Label
+
+
+class QuestionScreen(Screen[bool]):
+ """Screen with a parameter."""
+
+ def __init__(self, question: str) -> None:
+ self.question = question
+ super().__init__()
+
+ def compose(self) -> ComposeResult:
+ yield Label(self.question)
+ yield Button("Yes", id="yes", variant="success")
+ yield Button("No", id="no")
+
+ @on(Button.Pressed, "#yes")
+ def handle_yes(self) -> None:
+ self.dismiss(True) # (1)!
+
+ @on(Button.Pressed, "#no")
+ def handle_no(self) -> None:
+ self.dismiss(False) # (2)!
+
+
+class QuestionsApp(App):
+ """Demonstrates wait_for_dismiss"""
+
+ CSS_PATH = "questions01.tcss"
+
+ @work # (3)!
+ async def on_mount(self) -> None:
+ if await self.push_screen_wait( # (4)!
+ QuestionScreen("Do you like Textual?"),
+ ):
+ self.notify("Good answer!")
+ else:
+ self.notify(":-(", severity="error")
+
+
+if __name__ == "__main__":
+ app = QuestionsApp()
+ app.run()
diff --git a/docs/examples/guide/screens/questions01.tcss b/docs/examples/guide/screens/questions01.tcss
new file mode 100644
index 0000000000..e56b2a949c
--- /dev/null
+++ b/docs/examples/guide/screens/questions01.tcss
@@ -0,0 +1,17 @@
+QuestionScreen {
+ layout: grid;
+ grid-size: 2 2;
+ align: center bottom;
+}
+
+QuestionScreen > Label {
+ margin: 1;
+ text-align: center;
+ column-span: 2;
+ width: 1fr;
+}
+
+QuestionScreen Button {
+ margin: 2;
+ width: 1fr;
+}
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/guide/widgets/loading01.py b/docs/examples/guide/widgets/loading01.py
new file mode 100644
index 0000000000..3e25899cbe
--- /dev/null
+++ b/docs/examples/guide/widgets/loading01.py
@@ -0,0 +1,54 @@
+from asyncio import sleep
+from random import randint
+
+from textual import work
+from textual.app import App, ComposeResult
+from textual.widgets import DataTable
+
+ROWS = [
+ ("lane", "swimmer", "country", "time"),
+ (4, "Joseph Schooling", "Singapore", 50.39),
+ (2, "Michael Phelps", "United States", 51.14),
+ (5, "Chad le Clos", "South Africa", 51.14),
+ (6, "László Cseh", "Hungary", 51.14),
+ (3, "Li Zhuhao", "China", 51.26),
+ (8, "Mehdy Metella", "France", 51.58),
+ (7, "Tom Shields", "United States", 51.73),
+ (1, "Aleksandr Sadovnikov", "Russia", 51.84),
+ (10, "Darren Burns", "Scotland", 51.84),
+]
+
+
+class DataApp(App):
+ CSS = """
+ Screen {
+ layout: grid;
+ grid-size: 2;
+ }
+ DataTable {
+ height: 1fr;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ yield DataTable()
+ yield DataTable()
+ yield DataTable()
+ yield DataTable()
+
+ def on_mount(self) -> None:
+ for data_table in self.query(DataTable):
+ data_table.loading = True # (1)!
+ self.load_data(data_table)
+
+ @work
+ async def load_data(self, data_table: DataTable) -> None:
+ await sleep(randint(2, 10)) # (2)!
+ data_table.add_columns(*ROWS[0])
+ data_table.add_rows(ROWS[1:])
+ data_table.loading = False # (3)!
+
+
+if __name__ == "__main__":
+ app = DataApp()
+ app.run()
diff --git a/docs/examples/guide/workers/weather05.py b/docs/examples/guide/workers/weather05.py
index c1da80cb4d..a4d8049965 100644
--- a/docs/examples/guide/workers/weather05.py
+++ b/docs/examples/guide/workers/weather05.py
@@ -1,3 +1,4 @@
+from urllib.parse import quote
from urllib.request import Request, urlopen
from rich.text import Text
@@ -30,7 +31,7 @@ def update_weather(self, city: str) -> None:
worker = get_current_worker()
if city:
# Query the network API
- url = f"https://wttr.in/{city}"
+ url = f"https://wttr.in/{quote(city)}"
request = Request(url)
request.add_header("User-agent", "CURL")
response_text = urlopen(request).read().decode("utf-8")
diff --git a/docs/examples/how-to/render_compose.py b/docs/examples/how-to/render_compose.py
new file mode 100644
index 0000000000..b1413d3d0c
--- /dev/null
+++ b/docs/examples/how-to/render_compose.py
@@ -0,0 +1,57 @@
+from time import time
+
+from textual.app import App, ComposeResult, RenderableType
+from textual.containers import Container
+from textual.renderables.gradient import LinearGradient
+from textual.widgets import Static
+
+COLORS = [
+ "#881177",
+ "#aa3355",
+ "#cc6666",
+ "#ee9944",
+ "#eedd00",
+ "#99dd55",
+ "#44dd88",
+ "#22ccbb",
+ "#00bbcc",
+ "#0099cc",
+ "#3366bb",
+ "#663399",
+]
+STOPS = [(i / (len(COLORS) - 1), color) for i, color in enumerate(COLORS)]
+
+
+class Splash(Container):
+ """Custom widget that extends Container."""
+
+ DEFAULT_CSS = """
+ Splash {
+ align: center middle;
+ }
+ Static {
+ width: 40;
+ padding: 2 4;
+ }
+ """
+
+ def on_mount(self) -> None:
+ self.auto_refresh = 1 / 30 # (1)!
+
+ def compose(self) -> ComposeResult:
+ yield Static("Making a splash with Textual!") # (2)!
+
+ def render(self) -> RenderableType:
+ return LinearGradient(time() * 90, STOPS) # (3)!
+
+
+class SplashApp(App):
+ """Simple app to show our custom widget."""
+
+ def compose(self) -> ComposeResult:
+ yield Splash()
+
+
+if __name__ == "__main__":
+ app = SplashApp()
+ app.run()
diff --git a/docs/examples/styles/align.py b/docs/examples/styles/align.py
index a19a803f64..8115b026ce 100644
--- a/docs/examples/styles/align.py
+++ b/docs/examples/styles/align.py
@@ -3,9 +3,13 @@
class AlignApp(App):
+ CSS_PATH = "align.tcss"
+
def compose(self):
yield Label("Vertical alignment with [b]Textual[/]", classes="box")
yield Label("Take note, browsers.", classes="box")
-app = AlignApp(css_path="align.tcss")
+if __name__ == "__main__":
+ app = AlignApp()
+ app.run()
diff --git a/docs/examples/styles/align_all.py b/docs/examples/styles/align_all.py
index 2d409414f1..757445f719 100644
--- a/docs/examples/styles/align_all.py
+++ b/docs/examples/styles/align_all.py
@@ -18,3 +18,7 @@ def compose(self) -> ComposeResult:
yield Container(Label("left bottom"), id="left-bottom")
yield Container(Label("center bottom"), id="center-bottom")
yield Container(Label("right bottom"), id="right-bottom")
+
+
+if __name__ == "__main__":
+ AlignAllApp().run()
diff --git a/docs/examples/styles/background.py b/docs/examples/styles/background.py
index 5c5db8bc76..996ee6ac2b 100644
--- a/docs/examples/styles/background.py
+++ b/docs/examples/styles/background.py
@@ -3,10 +3,14 @@
class BackgroundApp(App):
+ CSS_PATH = "background.tcss"
+
def compose(self):
yield Label("Widget 1", id="static1")
yield Label("Widget 2", id="static2")
yield Label("Widget 3", id="static3")
-app = BackgroundApp(css_path="background.tcss")
+if __name__ == "__main__":
+ app = BackgroundApp()
+ app.run()
diff --git a/docs/examples/styles/background_transparency.py b/docs/examples/styles/background_transparency.py
index abcdc30375..f753e001b4 100644
--- a/docs/examples/styles/background_transparency.py
+++ b/docs/examples/styles/background_transparency.py
@@ -5,6 +5,8 @@
class BackgroundTransparencyApp(App):
"""Simple app to exemplify different transparency settings."""
+ CSS_PATH = "background_transparency.tcss"
+
def compose(self) -> ComposeResult:
yield Static("10%", id="t10")
yield Static("20%", id="t20")
@@ -18,4 +20,6 @@ def compose(self) -> ComposeResult:
yield Static("100%", id="t100")
-app = BackgroundTransparencyApp(css_path="background_transparency.tcss")
+if __name__ == "__main__":
+ app = BackgroundTransparencyApp()
+ app.run()
diff --git a/docs/examples/styles/border.py b/docs/examples/styles/border.py
index 31d244f2c1..cf05a3510b 100644
--- a/docs/examples/styles/border.py
+++ b/docs/examples/styles/border.py
@@ -3,10 +3,14 @@
class BorderApp(App):
+ CSS_PATH = "border.tcss"
+
def compose(self):
yield Label("My border is solid red", id="label1")
yield Label("My border is dashed green", id="label2")
yield Label("My border is tall blue", id="label3")
-app = BorderApp(css_path="border.tcss")
+if __name__ == "__main__":
+ app = BorderApp()
+ app.run()
diff --git a/docs/examples/styles/border_all.py b/docs/examples/styles/border_all.py
index 2fab42f352..4e5a80675e 100644
--- a/docs/examples/styles/border_all.py
+++ b/docs/examples/styles/border_all.py
@@ -4,6 +4,8 @@
class AllBordersApp(App):
+ CSS_PATH = "border_all.tcss"
+
def compose(self):
yield Grid(
Label("ascii", id="ascii"),
@@ -24,4 +26,6 @@ def compose(self):
)
-app = AllBordersApp(css_path="border_all.tcss")
+if __name__ == "__main__":
+ app = AllBordersApp()
+ app.run()
diff --git a/docs/examples/styles/border_sub_title_align_all.py b/docs/examples/styles/border_sub_title_align_all.py
index 1ec8340433..1832d97f74 100644
--- a/docs/examples/styles/border_sub_title_align_all.py
+++ b/docs/examples/styles/border_sub_title_align_all.py
@@ -13,6 +13,8 @@ def make_label_container( # (11)!
class BorderSubTitleAlignAll(App[None]):
+ CSS_PATH = "border_sub_title_align_all.tcss"
+
def compose(self):
with Grid():
yield make_label_container( # (1)!
@@ -68,7 +70,6 @@ def compose(self):
)
-app = BorderSubTitleAlignAll(css_path="border_sub_title_align_all.tcss")
-
if __name__ == "__main__":
+ app = BorderSubTitleAlignAll()
app.run()
diff --git a/docs/examples/styles/border_subtitle_align.py b/docs/examples/styles/border_subtitle_align.py
index 4c858b3df1..0633fcec3f 100644
--- a/docs/examples/styles/border_subtitle_align.py
+++ b/docs/examples/styles/border_subtitle_align.py
@@ -3,6 +3,8 @@
class BorderSubtitleAlignApp(App):
+ CSS_PATH = "border_subtitle_align.tcss"
+
def compose(self):
lbl = Label("My subtitle is on the left.", id="label1")
lbl.border_subtitle = "< Left"
@@ -17,4 +19,6 @@ def compose(self):
yield lbl
-app = BorderSubtitleAlignApp(css_path="border_subtitle_align.tcss")
+if __name__ == "__main__":
+ app = BorderSubtitleAlignApp()
+ app.run()
diff --git a/docs/examples/styles/border_title_align.py b/docs/examples/styles/border_title_align.py
index ba790104f8..fb819a5d83 100644
--- a/docs/examples/styles/border_title_align.py
+++ b/docs/examples/styles/border_title_align.py
@@ -3,6 +3,8 @@
class BorderTitleAlignApp(App):
+ CSS_PATH = "border_title_align.tcss"
+
def compose(self):
lbl = Label("My title is on the left.", id="label1")
lbl.border_title = "< Left"
@@ -17,4 +19,6 @@ def compose(self):
yield lbl
-app = BorderTitleAlignApp(css_path="border_title_align.tcss")
+if __name__ == "__main__":
+ app = BorderTitleAlignApp()
+ app.run()
diff --git a/docs/examples/styles/box_sizing.py b/docs/examples/styles/box_sizing.py
index 9bd4511891..83904a884d 100644
--- a/docs/examples/styles/box_sizing.py
+++ b/docs/examples/styles/box_sizing.py
@@ -3,9 +3,13 @@
class BoxSizingApp(App):
+ CSS_PATH = "box_sizing.tcss"
+
def compose(self):
yield Static("I'm using border-box!", id="static1")
yield Static("I'm using content-box!", id="static2")
-app = BoxSizingApp(css_path="box_sizing.tcss")
+if __name__ == "__main__":
+ app = BoxSizingApp()
+ app.run()
diff --git a/docs/examples/styles/color.py b/docs/examples/styles/color.py
index bef97429f8..4fada0d33e 100644
--- a/docs/examples/styles/color.py
+++ b/docs/examples/styles/color.py
@@ -3,10 +3,14 @@
class ColorApp(App):
+ CSS_PATH = "color.tcss"
+
def compose(self):
yield Label("I'm red!", id="label1")
yield Label("I'm rgb(0, 255, 0)!", id="label2")
yield Label("I'm hsl(240, 100%, 50%)!", id="label3")
-app = ColorApp(css_path="color.tcss")
+if __name__ == "__main__":
+ app = ColorApp()
+ app.run()
diff --git a/docs/examples/styles/color_auto.py b/docs/examples/styles/color_auto.py
index 4bb18f6e49..22d259138a 100644
--- a/docs/examples/styles/color_auto.py
+++ b/docs/examples/styles/color_auto.py
@@ -3,6 +3,8 @@
class ColorApp(App):
+ CSS_PATH = "color_auto.tcss"
+
def compose(self):
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl1")
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl2")
@@ -11,4 +13,6 @@ def compose(self):
yield Label("The quick brown fox jumps over the lazy dog!", id="lbl5")
-app = ColorApp(css_path="color_auto.tcss")
+if __name__ == "__main__":
+ app = ColorApp()
+ app.run()
diff --git a/docs/examples/styles/column_span.py b/docs/examples/styles/column_span.py
index 6d9b582ba5..6d120525ec 100644
--- a/docs/examples/styles/column_span.py
+++ b/docs/examples/styles/column_span.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "column_span.tcss"
+
def compose(self):
yield Grid(
Placeholder(id="p1"),
@@ -16,4 +18,6 @@ def compose(self):
)
-app = MyApp(css_path="column_span.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/content_align.py b/docs/examples/styles/content_align.py
index 71348d3032..47f61e24a5 100644
--- a/docs/examples/styles/content_align.py
+++ b/docs/examples/styles/content_align.py
@@ -3,10 +3,14 @@
class ContentAlignApp(App):
+ CSS_PATH = "content_align.tcss"
+
def compose(self):
yield Label("With [i]content-align[/] you can...", id="box1")
yield Label("...[b]Easily align content[/]...", id="box2")
yield Label("...Horizontally [i]and[/] vertically!", id="box3")
-app = ContentAlignApp(css_path="content_align.tcss")
+if __name__ == "__main__":
+ app = ContentAlignApp()
+ app.run()
diff --git a/docs/examples/styles/content_align_all.py b/docs/examples/styles/content_align_all.py
index 5ba2bce7d6..8ad7a87e93 100644
--- a/docs/examples/styles/content_align_all.py
+++ b/docs/examples/styles/content_align_all.py
@@ -3,6 +3,8 @@
class AllContentAlignApp(App):
+ CSS_PATH = "content_align_all.tcss"
+
def compose(self):
yield Label("left top", id="left-top")
yield Label("center top", id="center-top")
@@ -15,4 +17,6 @@ def compose(self):
yield Label("right bottom", id="right-bottom")
-app = AllContentAlignApp(css_path="content_align_all.tcss")
+if __name__ == "__main__":
+ app = AllContentAlignApp()
+ app.run()
diff --git a/docs/examples/styles/display.py b/docs/examples/styles/display.py
index 4da6aa2cae..b8fb0cd18e 100644
--- a/docs/examples/styles/display.py
+++ b/docs/examples/styles/display.py
@@ -3,10 +3,14 @@
class DisplayApp(App):
+ CSS_PATH = "display.tcss"
+
def compose(self):
yield Static("Widget 1")
yield Static("Widget 2", classes="remove")
yield Static("Widget 3")
-app = DisplayApp(css_path="display.tcss")
+if __name__ == "__main__":
+ app = DisplayApp()
+ app.run()
diff --git a/docs/examples/styles/dock_all.py b/docs/examples/styles/dock_all.py
index f1b024f239..f9b0080e8e 100644
--- a/docs/examples/styles/dock_all.py
+++ b/docs/examples/styles/dock_all.py
@@ -4,6 +4,8 @@
class DockAllApp(App):
+ CSS_PATH = "dock_all.tcss"
+
def compose(self):
yield Container(
Container(Label("left"), id="left"),
@@ -14,4 +16,6 @@ def compose(self):
)
-app = DockAllApp(css_path="dock_all.tcss")
+if __name__ == "__main__":
+ app = DockAllApp()
+ app.run()
diff --git a/docs/examples/styles/grid.py b/docs/examples/styles/grid.py
index 0c43607c09..fd3355bb37 100644
--- a/docs/examples/styles/grid.py
+++ b/docs/examples/styles/grid.py
@@ -3,6 +3,8 @@
class GridApp(App):
+ CSS_PATH = "grid.tcss"
+
def compose(self):
yield Static("Grid cell 1\n\nrow-span: 3;\ncolumn-span: 2;", id="static1")
yield Static("Grid cell 2", id="static2")
@@ -13,4 +15,6 @@ def compose(self):
yield Static("Grid cell 7", id="static7")
-app = GridApp(css_path="grid.tcss")
+if __name__ == "__main__":
+ app = GridApp()
+ app.run()
diff --git a/docs/examples/styles/grid_columns.py b/docs/examples/styles/grid_columns.py
index 6abbbc5a4d..fa564583af 100644
--- a/docs/examples/styles/grid_columns.py
+++ b/docs/examples/styles/grid_columns.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "grid_columns.tcss"
+
def compose(self):
yield Grid(
Label("1fr"),
@@ -19,4 +21,6 @@ def compose(self):
)
-app = MyApp(css_path="grid_columns.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/grid_gutter.py b/docs/examples/styles/grid_gutter.py
index 211b0e8c09..7cf39e26f3 100644
--- a/docs/examples/styles/grid_gutter.py
+++ b/docs/examples/styles/grid_gutter.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "grid_gutter.tcss"
+
def compose(self):
yield Grid(
Label("1"),
@@ -17,4 +19,6 @@ def compose(self):
)
-app = MyApp(css_path="grid_gutter.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/grid_rows.py b/docs/examples/styles/grid_rows.py
index 508c0143a4..f183fedd6c 100644
--- a/docs/examples/styles/grid_rows.py
+++ b/docs/examples/styles/grid_rows.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "grid_rows.tcss"
+
def compose(self):
yield Grid(
Label("1fr"),
@@ -19,4 +21,6 @@ def compose(self):
)
-app = MyApp(css_path="grid_rows.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/grid_size_both.py b/docs/examples/styles/grid_size_both.py
index 0e60188191..d64944aa3c 100644
--- a/docs/examples/styles/grid_size_both.py
+++ b/docs/examples/styles/grid_size_both.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "grid_size_both.tcss"
+
def compose(self):
yield Grid(
Label("1"),
@@ -14,4 +16,6 @@ def compose(self):
)
-app = MyApp(css_path="grid_size_both.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/grid_size_columns.py b/docs/examples/styles/grid_size_columns.py
index c6d3392d5b..19608a1c3e 100644
--- a/docs/examples/styles/grid_size_columns.py
+++ b/docs/examples/styles/grid_size_columns.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "grid_size_columns.tcss"
+
def compose(self):
yield Grid(
Label("1"),
@@ -14,4 +16,6 @@ def compose(self):
)
-app = MyApp(css_path="grid_size_columns.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/height.py b/docs/examples/styles/height.py
index 7eba3bbe10..6d58d76ff4 100644
--- a/docs/examples/styles/height.py
+++ b/docs/examples/styles/height.py
@@ -3,8 +3,12 @@
class HeightApp(App):
+ CSS_PATH = "height.tcss"
+
def compose(self):
yield Widget()
-app = HeightApp(css_path="height.tcss")
+if __name__ == "__main__":
+ app = HeightApp()
+ app.run()
diff --git a/docs/examples/styles/height_comparison.py b/docs/examples/styles/height_comparison.py
index 5fc72b237f..857ab823c9 100644
--- a/docs/examples/styles/height_comparison.py
+++ b/docs/examples/styles/height_comparison.py
@@ -10,6 +10,8 @@ def compose(self):
class HeightComparisonApp(App):
+ CSS_PATH = "height_comparison.tcss"
+
def compose(self):
yield VerticalScroll(
Placeholder(id="cells"), # (1)!
@@ -25,4 +27,6 @@ def compose(self):
yield Ruler()
-app = HeightComparisonApp(css_path="height_comparison.tcss")
+if __name__ == "__main__":
+ app = HeightComparisonApp()
+ app.run()
diff --git a/docs/examples/styles/keyline.py b/docs/examples/styles/keyline.py
new file mode 100644
index 0000000000..60a699c6ff
--- /dev/null
+++ b/docs/examples/styles/keyline.py
@@ -0,0 +1,19 @@
+from textual.app import App, ComposeResult
+from textual.containers import Grid
+from textual.widgets import Placeholder
+
+
+class KeylineApp(App):
+ CSS_PATH = "keyline.tcss"
+
+ def compose(self) -> ComposeResult:
+ with Grid():
+ yield Placeholder(id="foo")
+ yield Placeholder(id="bar")
+ yield Placeholder()
+ yield Placeholder(classes="hidden")
+ yield Placeholder(id="baz")
+
+
+if __name__ == "__main__":
+ KeylineApp().run()
diff --git a/docs/examples/styles/keyline.tcss b/docs/examples/styles/keyline.tcss
new file mode 100644
index 0000000000..41bf30d5e9
--- /dev/null
+++ b/docs/examples/styles/keyline.tcss
@@ -0,0 +1,21 @@
+Grid {
+ grid-size: 3 3;
+ grid-gutter: 1;
+ padding: 2 3;
+ keyline: heavy green;
+}
+Placeholder {
+ height: 1fr;
+}
+.hidden {
+ visibility: hidden;
+}
+#foo {
+ column-span: 2;
+}
+#bar {
+ row-span: 2;
+}
+#baz {
+ column-span:3;
+}
diff --git a/docs/examples/styles/keyline_horizontal.py b/docs/examples/styles/keyline_horizontal.py
new file mode 100644
index 0000000000..a0660df668
--- /dev/null
+++ b/docs/examples/styles/keyline_horizontal.py
@@ -0,0 +1,18 @@
+from textual.app import App, ComposeResult
+from textual.containers import Horizontal
+from textual.widgets import Placeholder
+
+
+class KeylineApp(App):
+ CSS_PATH = "keyline_horizontal.tcss"
+
+ def compose(self) -> ComposeResult:
+ with Horizontal():
+ yield Placeholder()
+ yield Placeholder()
+ yield Placeholder()
+
+
+if __name__ == "__main__":
+ app = KeylineApp()
+ app.run()
diff --git a/docs/examples/styles/keyline_horizontal.tcss b/docs/examples/styles/keyline_horizontal.tcss
new file mode 100644
index 0000000000..d4b02cd0c2
--- /dev/null
+++ b/docs/examples/styles/keyline_horizontal.tcss
@@ -0,0 +1,8 @@
+Placeholder {
+ margin: 1;
+ width: 1fr;
+}
+
+Horizontal {
+ keyline: thin $secondary;
+}
diff --git a/docs/examples/styles/layout.py b/docs/examples/styles/layout.py
index 07be94c630..bb9a2546f1 100644
--- a/docs/examples/styles/layout.py
+++ b/docs/examples/styles/layout.py
@@ -4,6 +4,8 @@
class LayoutApp(App):
+ CSS_PATH = "layout.tcss"
+
def compose(self):
yield Container(
Label("Layout"),
@@ -19,4 +21,6 @@ def compose(self):
)
-app = LayoutApp(css_path="layout.tcss")
+if __name__ == "__main__":
+ app = LayoutApp()
+ app.run()
diff --git a/docs/examples/styles/link_background.py b/docs/examples/styles/link_background.py
index 6cc0161ef5..6516f1b6aa 100644
--- a/docs/examples/styles/link_background.py
+++ b/docs/examples/styles/link_background.py
@@ -3,6 +3,8 @@
class LinkBackgroundApp(App):
+ CSS_PATH = "link_background.tcss"
+
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
@@ -22,4 +24,6 @@ def compose(self):
)
-app = LinkBackgroundApp(css_path="link_background.tcss")
+if __name__ == "__main__":
+ app = LinkBackgroundApp()
+ app.run()
diff --git a/docs/examples/styles/link_hover_background.py b/docs/examples/styles/link_background_hover.py
similarity index 84%
rename from docs/examples/styles/link_hover_background.py
rename to docs/examples/styles/link_background_hover.py
index d7d4d4928b..fc33e576d7 100644
--- a/docs/examples/styles/link_hover_background.py
+++ b/docs/examples/styles/link_background_hover.py
@@ -3,6 +3,8 @@
class LinkHoverBackgroundApp(App):
+ CSS_PATH = "link_background_hover.tcss"
+
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
@@ -22,4 +24,6 @@ def compose(self):
)
-app = LinkHoverBackgroundApp(css_path="link_hover_background.tcss")
+if __name__ == "__main__":
+ app = LinkHoverBackgroundApp()
+ app.run()
diff --git a/docs/examples/styles/link_hover_background.tcss b/docs/examples/styles/link_background_hover.tcss
similarity index 52%
rename from docs/examples/styles/link_hover_background.tcss
rename to docs/examples/styles/link_background_hover.tcss
index 68d564ae23..53ec6b8151 100644
--- a/docs/examples/styles/link_hover_background.tcss
+++ b/docs/examples/styles/link_background_hover.tcss
@@ -1,9 +1,9 @@
#lbl1, #lbl2 {
- link-hover-background: red; /* (1)! */
+ link-background-hover: red; /* (1)! */
}
#lbl3 {
- link-hover-background: hsl(60,100%,50%) 50%;
+ link-background-hover: hsl(60,100%,50%) 50%;
}
#lbl4 {
diff --git a/docs/examples/styles/link_color.py b/docs/examples/styles/link_color.py
index bd093093b1..3d6a83cc7f 100644
--- a/docs/examples/styles/link_color.py
+++ b/docs/examples/styles/link_color.py
@@ -3,6 +3,8 @@
class LinkColorApp(App):
+ CSS_PATH = "link_color.tcss"
+
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
@@ -22,4 +24,6 @@ def compose(self):
)
-app = LinkColorApp(css_path="link_color.tcss")
+if __name__ == "__main__":
+ app = LinkColorApp()
+ app.run()
diff --git a/docs/examples/styles/link_hover_color.py b/docs/examples/styles/link_color_hover.py
similarity index 85%
rename from docs/examples/styles/link_hover_color.py
rename to docs/examples/styles/link_color_hover.py
index 67b3acd21e..7344123ae0 100644
--- a/docs/examples/styles/link_hover_color.py
+++ b/docs/examples/styles/link_color_hover.py
@@ -3,6 +3,8 @@
class LinkHoverColorApp(App):
+ CSS_PATH = "link_color_hover.tcss"
+
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
@@ -22,4 +24,6 @@ def compose(self):
)
-app = LinkHoverColorApp(css_path="link_hover_color.tcss")
+if __name__ == "__main__":
+ app = LinkHoverColorApp()
+ app.run()
diff --git a/docs/examples/styles/link_color_hover.tcss b/docs/examples/styles/link_color_hover.tcss
new file mode 100644
index 0000000000..d4ddd07d64
--- /dev/null
+++ b/docs/examples/styles/link_color_hover.tcss
@@ -0,0 +1,11 @@
+#lbl1, #lbl2 {
+ link-color-hover: red; /* (1)! */
+}
+
+#lbl3 {
+ link-color-hover: hsl(60,100%,50%) 50%;
+}
+
+#lbl4 {
+ link-color-hover: black;
+}
diff --git a/docs/examples/styles/link_hover_color.tcss b/docs/examples/styles/link_hover_color.tcss
deleted file mode 100644
index 234184f474..0000000000
--- a/docs/examples/styles/link_hover_color.tcss
+++ /dev/null
@@ -1,11 +0,0 @@
-#lbl1, #lbl2 {
- link-hover-color: red; /* (1)! */
-}
-
-#lbl3 {
- link-hover-color: hsl(60,100%,50%) 50%;
-}
-
-#lbl4 {
- link-hover-color: black;
-}
diff --git a/docs/examples/styles/link_hover_style.tcss b/docs/examples/styles/link_hover_style.tcss
deleted file mode 100644
index 169e69d290..0000000000
--- a/docs/examples/styles/link_hover_style.tcss
+++ /dev/null
@@ -1,11 +0,0 @@
-#lbl1, #lbl2 {
- link-hover-style: bold italic; /* (1)! */
-}
-
-#lbl3 {
- link-hover-style: reverse strike;
-}
-
-#lbl4 {
- link-hover-style: bold;
-}
diff --git a/docs/examples/styles/link_style.py b/docs/examples/styles/link_style.py
index bab0d7eb8c..405666ca24 100644
--- a/docs/examples/styles/link_style.py
+++ b/docs/examples/styles/link_style.py
@@ -3,6 +3,8 @@
class LinkStyleApp(App):
+ CSS_PATH = "link_style.tcss"
+
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
@@ -22,4 +24,6 @@ def compose(self):
)
-app = LinkStyleApp(css_path="link_style.tcss")
+if __name__ == "__main__":
+ app = LinkStyleApp()
+ app.run()
diff --git a/docs/examples/styles/link_hover_style.py b/docs/examples/styles/link_style_hover.py
similarity index 85%
rename from docs/examples/styles/link_hover_style.py
rename to docs/examples/styles/link_style_hover.py
index 6ffe727d37..3d47406b48 100644
--- a/docs/examples/styles/link_hover_style.py
+++ b/docs/examples/styles/link_style_hover.py
@@ -3,6 +3,8 @@
class LinkHoverStyleApp(App):
+ CSS_PATH = "link_style_hover.tcss"
+
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
@@ -22,4 +24,6 @@ def compose(self):
)
-app = LinkHoverStyleApp(css_path="link_hover_style.tcss")
+if __name__ == "__main__":
+ app = LinkHoverStyleApp()
+ app.run()
diff --git a/docs/examples/styles/link_style_hover.tcss b/docs/examples/styles/link_style_hover.tcss
new file mode 100644
index 0000000000..b6c5d4356b
--- /dev/null
+++ b/docs/examples/styles/link_style_hover.tcss
@@ -0,0 +1,11 @@
+#lbl1, #lbl2 {
+ link-style-hover: bold italic; /* (1)! */
+}
+
+#lbl3 {
+ link-style-hover: reverse strike;
+}
+
+#lbl4 {
+ link-style-hover: bold;
+}
diff --git a/docs/examples/styles/links.py b/docs/examples/styles/links.py
index 93e9eead39..7884d719d4 100644
--- a/docs/examples/styles/links.py
+++ b/docs/examples/styles/links.py
@@ -7,9 +7,13 @@
class LinksApp(App):
+ CSS_PATH = "links.tcss"
+
def compose(self) -> ComposeResult:
yield Static(TEXT)
yield Static(TEXT, id="custom")
-app = LinksApp(css_path="links.tcss")
+if __name__ == "__main__":
+ app = LinksApp()
+ app.run()
diff --git a/docs/examples/styles/margin.py b/docs/examples/styles/margin.py
index 03cd13d21d..ed8fae3136 100644
--- a/docs/examples/styles/margin.py
+++ b/docs/examples/styles/margin.py
@@ -11,8 +11,12 @@
class MarginApp(App):
+ CSS_PATH = "margin.tcss"
+
def compose(self):
yield Label(TEXT)
-app = MarginApp(css_path="margin.tcss")
+if __name__ == "__main__":
+ app = MarginApp()
+ app.run()
diff --git a/docs/examples/styles/margin_all.py b/docs/examples/styles/margin_all.py
index 11d6ae3fad..0c6aa2fd78 100644
--- a/docs/examples/styles/margin_all.py
+++ b/docs/examples/styles/margin_all.py
@@ -4,6 +4,8 @@
class MarginAllApp(App):
+ CSS_PATH = "margin_all.tcss"
+
def compose(self):
yield Grid(
Container(Placeholder("no margin", id="p1"), classes="bordered"),
@@ -17,4 +19,6 @@ def compose(self):
)
-app = MarginAllApp(css_path="margin_all.tcss")
+if __name__ == "__main__":
+ app = MarginAllApp()
+ app.run()
diff --git a/docs/examples/styles/max_height.py b/docs/examples/styles/max_height.py
index b0b0bce391..cc431b248d 100644
--- a/docs/examples/styles/max_height.py
+++ b/docs/examples/styles/max_height.py
@@ -4,6 +4,8 @@
class MaxHeightApp(App):
+ CSS_PATH = "max_height.tcss"
+
def compose(self):
yield Horizontal(
Placeholder("max-height: 10w", id="p1"),
@@ -13,4 +15,6 @@ def compose(self):
)
-app = MaxHeightApp(css_path="max_height.tcss")
+if __name__ == "__main__":
+ app = MaxHeightApp()
+ app.run()
diff --git a/docs/examples/styles/max_width.py b/docs/examples/styles/max_width.py
index c944ff795b..4c09087085 100644
--- a/docs/examples/styles/max_width.py
+++ b/docs/examples/styles/max_width.py
@@ -4,6 +4,8 @@
class MaxWidthApp(App):
+ CSS_PATH = "max_width.tcss"
+
def compose(self):
yield VerticalScroll(
Placeholder("max-width: 50h", id="p1"),
@@ -13,4 +15,6 @@ def compose(self):
)
-app = MaxWidthApp(css_path="max_width.tcss")
+if __name__ == "__main__":
+ app = MaxWidthApp()
+ app.run()
diff --git a/docs/examples/styles/min_height.py b/docs/examples/styles/min_height.py
index 6df7b24522..7635be7d61 100644
--- a/docs/examples/styles/min_height.py
+++ b/docs/examples/styles/min_height.py
@@ -4,6 +4,8 @@
class MinHeightApp(App):
+ CSS_PATH = "min_height.tcss"
+
def compose(self):
yield Horizontal(
Placeholder("min-height: 25%", id="p1"),
@@ -13,4 +15,6 @@ def compose(self):
)
-app = MinHeightApp(css_path="min_height.tcss")
+if __name__ == "__main__":
+ app = MinHeightApp()
+ app.run()
diff --git a/docs/examples/styles/min_width.py b/docs/examples/styles/min_width.py
index 197dbe40e0..0a29abf598 100644
--- a/docs/examples/styles/min_width.py
+++ b/docs/examples/styles/min_width.py
@@ -4,6 +4,8 @@
class MinWidthApp(App):
+ CSS_PATH = "min_width.tcss"
+
def compose(self):
yield VerticalScroll(
Placeholder("min-width: 25%", id="p1"),
@@ -13,4 +15,6 @@ def compose(self):
)
-app = MinWidthApp(css_path="min_width.tcss")
+if __name__ == "__main__":
+ app = MinWidthApp()
+ app.run()
diff --git a/docs/examples/styles/offset.py b/docs/examples/styles/offset.py
index 5593f9e9af..faadd020cc 100644
--- a/docs/examples/styles/offset.py
+++ b/docs/examples/styles/offset.py
@@ -3,10 +3,14 @@
class OffsetApp(App):
+ CSS_PATH = "offset.tcss"
+
def compose(self):
yield Label("Paul (offset 8 2)", classes="paul")
yield Label("Duncan (offset 4 10)", classes="duncan")
yield Label("Chani (offset 0 -3)", classes="chani")
-app = OffsetApp(css_path="offset.tcss")
+if __name__ == "__main__":
+ app = OffsetApp()
+ app.run()
diff --git a/docs/examples/styles/opacity.py b/docs/examples/styles/opacity.py
index e3cdd1db7c..e701a9ee42 100644
--- a/docs/examples/styles/opacity.py
+++ b/docs/examples/styles/opacity.py
@@ -3,6 +3,8 @@
class OpacityApp(App):
+ CSS_PATH = "opacity.tcss"
+
def compose(self):
yield Label("opacity: 0%", id="zero-opacity")
yield Label("opacity: 25%", id="quarter-opacity")
@@ -11,4 +13,6 @@ def compose(self):
yield Label("opacity: 100%", id="full-opacity")
-app = OpacityApp(css_path="opacity.tcss")
+if __name__ == "__main__":
+ app = OpacityApp()
+ app.run()
diff --git a/docs/examples/styles/outline.py b/docs/examples/styles/outline.py
index b2e679a0b6..ac7f7fd420 100644
--- a/docs/examples/styles/outline.py
+++ b/docs/examples/styles/outline.py
@@ -11,8 +11,12 @@
class OutlineApp(App):
+ CSS_PATH = "outline.tcss"
+
def compose(self):
yield Label(TEXT)
-app = OutlineApp(css_path="outline.tcss")
+if __name__ == "__main__":
+ app = OutlineApp()
+ app.run()
diff --git a/docs/examples/styles/outline_all.py b/docs/examples/styles/outline_all.py
index c64645e98f..b841b220f3 100644
--- a/docs/examples/styles/outline_all.py
+++ b/docs/examples/styles/outline_all.py
@@ -4,6 +4,8 @@
class AllOutlinesApp(App):
+ CSS_PATH = "outline_all.tcss"
+
def compose(self):
yield Grid(
Label("ascii", id="ascii"),
@@ -24,4 +26,6 @@ def compose(self):
)
-app = AllOutlinesApp(css_path="outline_all.tcss")
+if __name__ == "__main__":
+ app = AllOutlinesApp()
+ app.run()
diff --git a/docs/examples/styles/outline_vs_border.py b/docs/examples/styles/outline_vs_border.py
index 80e656bcf5..0b82c0c043 100644
--- a/docs/examples/styles/outline_vs_border.py
+++ b/docs/examples/styles/outline_vs_border.py
@@ -11,10 +11,14 @@
class OutlineBorderApp(App):
+ CSS_PATH = "outline_vs_border.tcss"
+
def compose(self):
yield Label(TEXT, classes="outline")
yield Label(TEXT, classes="border")
yield Label(TEXT, classes="outline border")
-app = OutlineBorderApp(css_path="outline_vs_border.tcss")
+if __name__ == "__main__":
+ app = OutlineBorderApp()
+ app.run()
diff --git a/docs/examples/styles/overflow.py b/docs/examples/styles/overflow.py
index 9fe7cf9253..ea440da189 100644
--- a/docs/examples/styles/overflow.py
+++ b/docs/examples/styles/overflow.py
@@ -12,6 +12,8 @@
class OverflowApp(App):
+ CSS_PATH = "overflow.tcss"
+
def compose(self):
yield Horizontal(
VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id="left"),
@@ -19,4 +21,6 @@ def compose(self):
)
-app = OverflowApp(css_path="overflow.tcss")
+if __name__ == "__main__":
+ app = OverflowApp()
+ app.run()
diff --git a/docs/examples/styles/padding.py b/docs/examples/styles/padding.py
index e6ed1a9f6d..616c41523e 100644
--- a/docs/examples/styles/padding.py
+++ b/docs/examples/styles/padding.py
@@ -11,8 +11,12 @@
class PaddingApp(App):
+ CSS_PATH = "padding.tcss"
+
def compose(self):
yield Label(TEXT)
-app = PaddingApp(css_path="padding.tcss")
+if __name__ == "__main__":
+ app = PaddingApp()
+ app.run()
diff --git a/docs/examples/styles/padding_all.py b/docs/examples/styles/padding_all.py
index c857c26c1a..d9af47b8f4 100644
--- a/docs/examples/styles/padding_all.py
+++ b/docs/examples/styles/padding_all.py
@@ -1,9 +1,11 @@
from textual.app import App
-from textual.containers import Container, Grid
+from textual.containers import Grid
from textual.widgets import Placeholder
class PaddingAllApp(App):
+ CSS_PATH = "padding_all.tcss"
+
def compose(self):
yield Grid(
Placeholder("no padding", id="p1"),
@@ -17,4 +19,6 @@ def compose(self):
)
-app = PaddingAllApp(css_path="padding_all.tcss")
+if __name__ == "__main__":
+ app = PaddingAllApp()
+ app.run()
diff --git a/docs/examples/styles/row_span.py b/docs/examples/styles/row_span.py
index adfca09099..c1721f98cf 100644
--- a/docs/examples/styles/row_span.py
+++ b/docs/examples/styles/row_span.py
@@ -4,6 +4,8 @@
class MyApp(App):
+ CSS_PATH = "row_span.tcss"
+
def compose(self):
yield Grid(
Placeholder(id="p1"),
@@ -16,4 +18,6 @@ def compose(self):
)
-app = MyApp(css_path="row_span.tcss")
+if __name__ == "__main__":
+ app = MyApp()
+ app.run()
diff --git a/docs/examples/styles/scrollbar_corner_color.py b/docs/examples/styles/scrollbar_corner_color.py
index 4247099adb..462de93e52 100644
--- a/docs/examples/styles/scrollbar_corner_color.py
+++ b/docs/examples/styles/scrollbar_corner_color.py
@@ -12,8 +12,12 @@
class ScrollbarCornerColorApp(App):
+ CSS_PATH = "scrollbar_corner_color.tcss"
+
def compose(self):
yield Label(TEXT.replace("\n", " ") + "\n" + TEXT * 10)
-app = ScrollbarCornerColorApp(css_path="scrollbar_corner_color.tcss")
+if __name__ == "__main__":
+ app = ScrollbarCornerColorApp()
+ app.run()
diff --git a/docs/examples/styles/scrollbar_gutter.py b/docs/examples/styles/scrollbar_gutter.py
index 42bc81d495..aab689376b 100644
--- a/docs/examples/styles/scrollbar_gutter.py
+++ b/docs/examples/styles/scrollbar_gutter.py
@@ -11,8 +11,12 @@
class ScrollbarGutterApp(App):
+ CSS_PATH = "scrollbar_gutter.tcss"
+
def compose(self):
yield Static(TEXT, id="text-box")
-app = ScrollbarGutterApp(css_path="scrollbar_gutter.tcss")
+if __name__ == "__main__":
+ app = ScrollbarGutterApp()
+ app.run()
diff --git a/docs/examples/styles/scrollbar_size.py b/docs/examples/styles/scrollbar_size.py
index 0191a1a111..e7217052f3 100644
--- a/docs/examples/styles/scrollbar_size.py
+++ b/docs/examples/styles/scrollbar_size.py
@@ -13,8 +13,12 @@
class ScrollbarApp(App):
+ CSS_PATH = "scrollbar_size.tcss"
+
def compose(self):
yield ScrollableContainer(Label(TEXT * 5), classes="panel")
-app = ScrollbarApp(css_path="scrollbar_size.tcss")
+if __name__ == "__main__":
+ app = ScrollbarApp()
+ app.run()
diff --git a/docs/examples/styles/scrollbar_size2.py b/docs/examples/styles/scrollbar_size2.py
index d7c9c55e98..d6bd1d2449 100644
--- a/docs/examples/styles/scrollbar_size2.py
+++ b/docs/examples/styles/scrollbar_size2.py
@@ -13,6 +13,8 @@
class ScrollbarApp(App):
+ CSS_PATH = "scrollbar_size2.tcss"
+
def compose(self):
yield Horizontal(
ScrollableContainer(Label(TEXT * 5), id="v1"),
@@ -21,6 +23,6 @@ def compose(self):
)
-app = ScrollbarApp(css_path="scrollbar_size2.tcss")
if __name__ == "__main__":
+ app = ScrollbarApp()
app.run()
diff --git a/docs/examples/styles/scrollbars.py b/docs/examples/styles/scrollbars.py
index 2762313b5e..505e3b4a92 100644
--- a/docs/examples/styles/scrollbars.py
+++ b/docs/examples/styles/scrollbars.py
@@ -13,6 +13,8 @@
class ScrollbarApp(App):
+ CSS_PATH = "scrollbars.tcss"
+
def compose(self):
yield Horizontal(
ScrollableContainer(Label(TEXT * 10)),
@@ -20,6 +22,6 @@ def compose(self):
)
-app = ScrollbarApp(css_path="scrollbars.tcss")
if __name__ == "__main__":
+ app = ScrollbarApp()
app.run()
diff --git a/docs/examples/styles/scrollbars2.py b/docs/examples/styles/scrollbars2.py
index be26ca4c00..c53af75367 100644
--- a/docs/examples/styles/scrollbars2.py
+++ b/docs/examples/styles/scrollbars2.py
@@ -12,8 +12,12 @@
class Scrollbar2App(App):
+ CSS_PATH = "scrollbars2.tcss"
+
def compose(self):
yield Label(TEXT * 10)
-app = Scrollbar2App(css_path="scrollbars2.tcss")
+if __name__ == "__main__":
+ app = Scrollbar2App()
+ app.run()
diff --git a/docs/examples/styles/text_align.py b/docs/examples/styles/text_align.py
index 3608f2cfe6..f397c52ad2 100644
--- a/docs/examples/styles/text_align.py
+++ b/docs/examples/styles/text_align.py
@@ -10,6 +10,8 @@
class TextAlign(App):
+ CSS_PATH = "text_align.tcss"
+
def compose(self):
yield Grid(
Label("[b]Left aligned[/]\n" + TEXT, id="one"),
@@ -19,4 +21,6 @@ def compose(self):
)
-app = TextAlign(css_path="text_align.tcss")
+if __name__ == "__main__":
+ app = TextAlign()
+ app.run()
diff --git a/docs/examples/styles/text_opacity.py b/docs/examples/styles/text_opacity.py
index f34340c2dd..26d44ea7a1 100644
--- a/docs/examples/styles/text_opacity.py
+++ b/docs/examples/styles/text_opacity.py
@@ -3,6 +3,8 @@
class TextOpacityApp(App):
+ CSS_PATH = "text_opacity.tcss"
+
def compose(self):
yield Label("text-opacity: 0%", id="zero-opacity")
yield Label("text-opacity: 25%", id="quarter-opacity")
@@ -11,4 +13,6 @@ def compose(self):
yield Label("text-opacity: 100%", id="full-opacity")
-app = TextOpacityApp(css_path="text_opacity.tcss")
+if __name__ == "__main__":
+ app = TextOpacityApp()
+ app.run()
diff --git a/docs/examples/styles/text_style.py b/docs/examples/styles/text_style.py
index 01f7610d2f..fc22b89719 100644
--- a/docs/examples/styles/text_style.py
+++ b/docs/examples/styles/text_style.py
@@ -11,10 +11,14 @@
class TextStyleApp(App):
+ CSS_PATH = "text_style.tcss"
+
def compose(self):
yield Label(TEXT, id="lbl1")
yield Label(TEXT, id="lbl2")
yield Label(TEXT, id="lbl3")
-app = TextStyleApp(css_path="text_style.tcss")
+if __name__ == "__main__":
+ app = TextStyleApp()
+ app.run()
diff --git a/docs/examples/styles/text_style_all.py b/docs/examples/styles/text_style_all.py
index c4533a7f6e..48cba3a2d3 100644
--- a/docs/examples/styles/text_style_all.py
+++ b/docs/examples/styles/text_style_all.py
@@ -12,6 +12,8 @@
class AllTextStyleApp(App):
+ CSS_PATH = "text_style_all.tcss"
+
def compose(self):
yield Grid(
Label("none\n" + TEXT, id="lbl1"),
@@ -25,4 +27,6 @@ def compose(self):
)
-app = AllTextStyleApp(css_path="text_style_all.tcss")
+if __name__ == "__main__":
+ app = AllTextStyleApp()
+ app.run()
diff --git a/docs/examples/styles/tint.py b/docs/examples/styles/tint.py
index ea512b9226..eafa240a55 100644
--- a/docs/examples/styles/tint.py
+++ b/docs/examples/styles/tint.py
@@ -4,6 +4,8 @@
class TintApp(App):
+ CSS_PATH = "tint.tcss"
+
def compose(self):
color = Color.parse("green")
for tint_alpha in range(0, 101, 10):
@@ -12,4 +14,6 @@ def compose(self):
yield widget
-app = TintApp(css_path="tint.tcss")
+if __name__ == "__main__":
+ app = TintApp()
+ app.run()
diff --git a/docs/examples/styles/visibility.py b/docs/examples/styles/visibility.py
index fe67aa31c8..f83169d8a1 100644
--- a/docs/examples/styles/visibility.py
+++ b/docs/examples/styles/visibility.py
@@ -3,10 +3,14 @@
class VisibilityApp(App):
+ CSS_PATH = "visibility.tcss"
+
def compose(self):
yield Label("Widget 1")
yield Label("Widget 2", classes="invisible")
yield Label("Widget 3")
-app = VisibilityApp(css_path="visibility.tcss")
+if __name__ == "__main__":
+ app = VisibilityApp()
+ app.run()
diff --git a/docs/examples/styles/visibility_containers.py b/docs/examples/styles/visibility_containers.py
index 8be5633867..547cc5fe1c 100644
--- a/docs/examples/styles/visibility_containers.py
+++ b/docs/examples/styles/visibility_containers.py
@@ -4,6 +4,8 @@
class VisibilityContainersApp(App):
+ CSS_PATH = "visibility_containers.tcss"
+
def compose(self):
yield VerticalScroll(
Horizontal(
@@ -27,4 +29,6 @@ def compose(self):
)
-app = VisibilityContainersApp(css_path="visibility_containers.tcss")
+if __name__ == "__main__":
+ app = VisibilityContainersApp()
+ app.run()
diff --git a/docs/examples/styles/width.py b/docs/examples/styles/width.py
index 736f527495..993c86d43d 100644
--- a/docs/examples/styles/width.py
+++ b/docs/examples/styles/width.py
@@ -3,8 +3,12 @@
class WidthApp(App):
+ CSS_PATH = "width.tcss"
+
def compose(self):
yield Widget()
-app = WidthApp(css_path="width.tcss")
+if __name__ == "__main__":
+ app = WidthApp()
+ app.run()
diff --git a/docs/examples/styles/width_comparison.py b/docs/examples/styles/width_comparison.py
index 509479b155..44d7f27c74 100644
--- a/docs/examples/styles/width_comparison.py
+++ b/docs/examples/styles/width_comparison.py
@@ -10,6 +10,8 @@ def compose(self):
class WidthComparisonApp(App):
+ CSS_PATH = "width_comparison.tcss"
+
def compose(self):
yield Horizontal(
Placeholder(id="cells"), # (1)!
@@ -25,6 +27,6 @@ def compose(self):
yield Ruler()
-app = WidthComparisonApp(css_path="width_comparison.tcss")
if __name__ == "__main__":
+ app = WidthComparisonApp()
app.run()
diff --git a/docs/examples/widgets/content_switcher.py b/docs/examples/widgets/content_switcher.py
index 82cb43aace..8e235fe564 100644
--- a/docs/examples/widgets/content_switcher.py
+++ b/docs/examples/widgets/content_switcher.py
@@ -1,5 +1,3 @@
-from rich.align import VerticalCenter
-
from textual.app import App, ComposeResult
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Button, ContentSwitcher, DataTable, Markdown
diff --git a/docs/examples/widgets/data_table_sort.py b/docs/examples/widgets/data_table_sort.py
new file mode 100644
index 0000000000..599a629394
--- /dev/null
+++ b/docs/examples/widgets/data_table_sort.py
@@ -0,0 +1,92 @@
+from rich.text import Text
+
+from textual.app import App, ComposeResult
+from textual.widgets import DataTable, Footer
+
+ROWS = [
+ ("lane", "swimmer", "country", "time 1", "time 2"),
+ (4, "Joseph Schooling", Text("Singapore", style="italic"), 50.39, 51.84),
+ (2, "Michael Phelps", Text("United States", style="italic"), 50.39, 51.84),
+ (5, "Chad le Clos", Text("South Africa", style="italic"), 51.14, 51.73),
+ (6, "László Cseh", Text("Hungary", style="italic"), 51.14, 51.58),
+ (3, "Li Zhuhao", Text("China", style="italic"), 51.26, 51.26),
+ (8, "Mehdy Metella", Text("France", style="italic"), 51.58, 52.15),
+ (7, "Tom Shields", Text("United States", style="italic"), 51.73, 51.12),
+ (1, "Aleksandr Sadovnikov", Text("Russia", style="italic"), 51.84, 50.85),
+ (10, "Darren Burns", Text("Scotland", style="italic"), 51.84, 51.55),
+]
+
+
+class TableApp(App):
+ BINDINGS = [
+ ("a", "sort_by_average_time", "Sort By Average Time"),
+ ("n", "sort_by_last_name", "Sort By Last Name"),
+ ("c", "sort_by_country", "Sort By Country"),
+ ("d", "sort_by_columns", "Sort By Columns (Only)"),
+ ]
+
+ current_sorts: set = set()
+
+ def compose(self) -> ComposeResult:
+ yield DataTable()
+ yield Footer()
+
+ def on_mount(self) -> None:
+ table = self.query_one(DataTable)
+ for col in ROWS[0]:
+ table.add_column(col, key=col)
+ table.add_rows(ROWS[1:])
+
+ def sort_reverse(self, sort_type: str):
+ """Determine if `sort_type` is ascending or descending."""
+ reverse = sort_type in self.current_sorts
+ if reverse:
+ self.current_sorts.remove(sort_type)
+ else:
+ self.current_sorts.add(sort_type)
+ return reverse
+
+ def action_sort_by_average_time(self) -> None:
+ """Sort DataTable by average of times (via a function) and
+ passing of column data through positional arguments."""
+
+ def sort_by_average_time_then_last_name(row_data):
+ name, *scores = row_data
+ return (sum(scores) / len(scores), name.split()[-1])
+
+ table = self.query_one(DataTable)
+ table.sort(
+ "swimmer",
+ "time 1",
+ "time 2",
+ key=sort_by_average_time_then_last_name,
+ reverse=self.sort_reverse("time"),
+ )
+
+ def action_sort_by_last_name(self) -> None:
+ """Sort DataTable by last name of swimmer (via a lambda)."""
+ table = self.query_one(DataTable)
+ table.sort(
+ "swimmer",
+ key=lambda swimmer: swimmer.split()[-1],
+ reverse=self.sort_reverse("swimmer"),
+ )
+
+ def action_sort_by_country(self) -> None:
+ """Sort DataTable by country which is a `Rich.Text` object."""
+ table = self.query_one(DataTable)
+ table.sort(
+ "country",
+ key=lambda country: country.plain,
+ reverse=self.sort_reverse("country"),
+ )
+
+ def action_sort_by_columns(self) -> None:
+ """Sort DataTable without a key."""
+ table = self.query_one(DataTable)
+ table.sort("swimmer", "lane", reverse=self.sort_reverse("columns"))
+
+
+app = TableApp()
+if __name__ == "__main__":
+ 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/input_types.py b/docs/examples/widgets/input_types.py
new file mode 100644
index 0000000000..0544f2562d
--- /dev/null
+++ b/docs/examples/widgets/input_types.py
@@ -0,0 +1,13 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Input
+
+
+class InputApp(App):
+ def compose(self) -> ComposeResult:
+ yield Input(placeholder="An integer", type="integer")
+ yield Input(placeholder="A number", type="number")
+
+
+if __name__ == "__main__":
+ app = InputApp()
+ app.run()
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/select_from_values_widget.py b/docs/examples/widgets/select_from_values_widget.py
new file mode 100644
index 0000000000..23ba16acbc
--- /dev/null
+++ b/docs/examples/widgets/select_from_values_widget.py
@@ -0,0 +1,26 @@
+from textual import on
+from textual.app import App, ComposeResult
+from textual.widgets import Header, Select
+
+LINES = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.""".splitlines()
+
+
+class SelectApp(App):
+ CSS_PATH = "select.tcss"
+
+ def compose(self) -> ComposeResult:
+ yield Header()
+ yield Select.from_values(LINES)
+
+ @on(Select.Changed)
+ def select_changed(self, event: Select.Changed) -> None:
+ self.title = str(event.value)
+
+
+if __name__ == "__main__":
+ app = SelectApp()
+ app.run()
diff --git a/docs/examples/widgets/select_widget_no_blank.py b/docs/examples/widgets/select_widget_no_blank.py
new file mode 100644
index 0000000000..8fab93667f
--- /dev/null
+++ b/docs/examples/widgets/select_widget_no_blank.py
@@ -0,0 +1,38 @@
+from textual import on
+from textual.app import App, ComposeResult
+from textual.widgets import Header, Select
+
+LINES = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.""".splitlines()
+
+ALTERNATE_LINES = """Twinkle, twinkle, little star,
+How I wonder what you are!
+Up above the world so high,
+Like a diamond in the sky.
+Twinkle, twinkle, little star,
+How I wonder what you are!""".splitlines()
+
+
+class SelectApp(App):
+ CSS_PATH = "select.tcss"
+
+ BINDINGS = [("s", "swap", "Swap Select options")]
+
+ def compose(self) -> ComposeResult:
+ yield Header()
+ yield Select(zip(LINES, LINES), allow_blank=False)
+
+ @on(Select.Changed)
+ def select_changed(self, event: Select.Changed) -> None:
+ self.title = str(event.value)
+
+ def action_swap(self) -> None:
+ self.query_one(Select).set_options(zip(ALTERNATE_LINES, ALTERNATE_LINES))
+
+
+if __name__ == "__main__":
+ app = SelectApp()
+ app.run()
diff --git a/docs/examples/widgets/tabbed_content_label_color.py b/docs/examples/widgets/tabbed_content_label_color.py
new file mode 100644
index 0000000000..573944b948
--- /dev/null
+++ b/docs/examples/widgets/tabbed_content_label_color.py
@@ -0,0 +1,25 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Label, TabbedContent, TabPane
+
+
+class ColorTabsApp(App):
+ CSS = """
+ TabbedContent #--content-tab-green {
+ color: green;
+ }
+
+ TabbedContent #--content-tab-red {
+ color: red;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ with TabbedContent():
+ with TabPane("Red", id="red"):
+ yield Label("Red!")
+ with TabPane("Green", id="green"):
+ yield Label("Green!")
+
+
+if __name__ == "__main__":
+ ColorTabsApp().run()
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/getting_started.md b/docs/getting_started.md
index 0e031a2660..46addc0bc2 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -2,7 +2,7 @@ All you need to get started building Textual apps.
## Requirements
-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.
+Textual requires Python 3.8 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.
!!! info inline end "Your platform"
diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md
index 0d38a616cb..5eccb2391a 100644
--- a/docs/guide/CSS.md
+++ b/docs/guide/CSS.md
@@ -12,7 +12,7 @@ CSS stands for _Cascading Stylesheet_. A stylesheet is a list of styles and rule
Let's look at some Textual CSS.
-```sass
+```css
Header {
dock: top;
height: 3;
@@ -26,7 +26,7 @@ This is an example of a CSS _rule set_. There may be many such sections in any g
Let's break this CSS code down a bit.
-```sass hl_lines="1"
+```css hl_lines="1"
Header {
dock: top;
height: 3;
@@ -38,7 +38,7 @@ Header {
The first line is a _selector_ which tells Textual which widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class `Header`.
-```sass hl_lines="2 3 4 5 6"
+```css hl_lines="2 3 4 5 6"
Header {
dock: top;
height: 3;
@@ -153,7 +153,7 @@ These are used by the CSS to identify parts of the DOM. We will cover these in t
Here's the CSS file we are applying:
-```sass title="dom4.tcss"
+```css title="dom4.tcss"
--8<-- "docs/examples/guide/dom4.tcss"
```
@@ -206,7 +206,7 @@ class Button(Static):
The following rule applies a border to this widget:
-```sass
+```css
Button {
border: solid blue;
}
@@ -214,7 +214,7 @@ Button {
The type selector will also match a widget's base classes. Consequently, a `Static` selector will also style the button because the `Button` Python class extends `Static`.
-```sass
+```css
Static {
background: blue;
border: rounded white;
@@ -239,7 +239,7 @@ yield Button(id="next")
You can match an ID with a selector starting with a hash (`#`). Here is how you might draw a red outline around the above button:
-```sass
+```css
#next {
outline: red;
}
@@ -267,7 +267,7 @@ yield Button(classes="error disabled")
To match a Widget with a given class in CSS you can precede the class name with a dot (`.`). Here's a rule with a class selector to match the `"success"` class name:
-```sass
+```css
.success {
background: green;
color: white;
@@ -280,7 +280,7 @@ To match a Widget with a given class in CSS you can precede the class name with
Class name selectors may be _chained_ together by appending another full stop and class name. The selector will match a widget that has _all_ of the class names set. For instance, the following sets a red background on widgets that have both `error` _and_ `disabled` class names.
-```sass
+```css
.error.disabled {
background: darkred;
}
@@ -301,7 +301,7 @@ The _universal_ selector is denoted by an asterisk and will match _all_ widgets.
For example, the following will draw a red outline around all widgets:
-```sass
+```css
* {
outline: solid red;
}
@@ -311,7 +311,7 @@ For example, the following will draw a red outline around all widgets:
Pseudo classes can be used to match widgets in a particular state. Pseudo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the `:hover` pseudo selector.
-```sass
+```css
Button:hover {
background: green;
}
@@ -324,7 +324,10 @@ Here are some other pseudo classes:
- `:disabled` Matches widgets which are in a disabled state.
- `: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.
+- `:blur` Matches widgets which *do not* have input focus.
+- `:focus-within` Matches widgets with a focused child widget.
+- `:dark` Matches widgets in dark mode (where `App.dark == True`).
+- `:light` Matches widgets in dark mode (where `App.dark == False`).
## Combinators
@@ -342,7 +345,7 @@ Here's a section of DOM to illustrate this combinator:
Let's say we want to make the text of the buttons in the dialog bold, but we _don't_ want to change the Button in the sidebar. We can do this with the following rule:
-```sass hl_lines="1"
+```css hl_lines="1"
#dialog Button {
text-style: bold;
}
@@ -352,7 +355,7 @@ The `#dialog Button` selector matches all buttons that are below the widget with
As with all selectors, you can combine as many as you wish. The following will match a `Button` that is under a `Horizontal` widget _and_ under a widget with an id of `"dialog"`:
-```sass
+```css
#dialog Horizontal Button {
text-style: bold;
}
@@ -370,7 +373,7 @@ Let's use this to match the Button in the sidebar given the following DOM:
We can use the following CSS to style all buttons which have a parent with an ID of `sidebar`:
-```sass
+```css
#sidebar > Button {
text-style: underline;
}
@@ -396,7 +399,7 @@ The specificity rules are usually enough to fix any conflicts in your stylesheet
Here's an example that makes buttons blue when hovered over with the mouse, regardless of any other selectors that match Buttons:
-```sass hl_lines="2"
+```css hl_lines="2"
Button:hover {
background: blue !important;
}
@@ -408,14 +411,14 @@ You can define variables to reduce repetition and encourage consistency in your
Variables in Textual CSS are prefixed with `$`.
Here's an example of how you might define a variable called `$border`:
-```sass
+```css
$border: wide green;
```
With our variable assigned, we can write `$border` and it will be substituted with `wide green`.
Consider the following snippet:
-```sass
+```css
#foo {
border: $border;
}
@@ -423,7 +426,7 @@ Consider the following snippet:
This will be translated into:
-```sass
+```css
#foo {
border: wide green;
}
@@ -440,3 +443,126 @@ Variables can refer to other variables.
Let's say we define a variable `$success: lime;`.
Our `$border` variable could then be updated to `$border: wide $success;`, which will
be translated to `$border: wide lime;`.
+
+## Initial value
+
+All CSS rules support a special value called `initial`, which will reset a value back to its default.
+
+Let's look at an example.
+The following will set the background of a button to green:
+
+```css
+Button {
+ background: green;
+}
+```
+
+If we want a specific button (or buttons) to use the default color, we can set the value to `initial`.
+For instance, if we have a widget with a (CSS) class called `dialog`, we could reset the background color of all buttons inside the dialog with the following CSS:
+
+```css
+.dialog Button {
+ background: initial;
+}
+```
+
+Note that `initial` will set the value back to the value defined in any [default css](./widgets.md#default-css).
+If you use `initial` within default css, it will treat the rule as completely unstyled.
+
+
+## Nesting CSS
+
+!!! tip "Added in version 0.47.0"
+
+CSS rule sets may be *nested*, i.e. they can contain other rule sets.
+When a rule set occurs within an existing rule set, it inherits the selector from the enclosing rule set.
+
+Let's put this into practical terms.
+The following example will display two boxes containing the text "Yes" and "No" respectively.
+These could eventually form the basis for buttons, but for this demonstration we are only interested in the CSS.
+
+=== "nesting01.tcss (no nesting)"
+
+ ```css
+ --8<-- "docs/examples/guide/css/nesting01.tcss"
+ ```
+
+=== "nesting01.py"
+
+ ```python
+ --8<-- "docs/examples/guide/css/nesting01.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/guide/css/nesting01.py"}
+ ```
+
+The CSS is quite straightforward; there is one rule for the container, one for all buttons, and one rule for each of the buttons.
+However it is easy to imagine this stylesheet growing more rules as we add features.
+
+Nesting allows us to group rule sets which have common selectors.
+In the example above, the rules all start with `#questions`.
+When we see a common prefix on the selectors, this is a good indication that we can use nesting.
+
+The following produces identical results to the previous example, but adds nesting of the rules.
+
+=== "nesting02.tcss (with nesting)"
+
+ ```css
+ --8<-- "docs/examples/guide/css/nesting02.tcss"
+ ```
+
+=== "nesting02.py"
+
+ ```python
+ --8<-- "docs/examples/guide/css/nesting02.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/guide/css/nesting02.py"}
+ ```
+
+!!! tip
+
+ Indenting the rule sets is not strictly required, but it does make it easier to understand how the rule sets are related to each other.
+
+In the first example we had a rule set that began with the selector `#questions .button`, which would match any widget with a class called "button" that is inside a container with id `questions`.
+
+In the second example, the button rule selector is simply `.button`, but it is *within* the rule set with selector `#questions`.
+The nesting means that the button rule set will inherit the selector from the outer rule set, so it is equivalent to `#questions .button`.
+
+### Nesting selector
+
+The two remaining rules are nested within the button rule, which means they will inherit their selectors from the button rule set *and* the outer `#questions` rule set.
+
+You may have noticed that the rules for the button styles contain a syntax we haven't seen before.
+The rule for the Yes button is `&.affirmative`.
+The ampersand (`&`) is known as the *nesting selector* and it tells Textual that the selector should be combined with the selector from the outer rule set.
+
+So `&.affirmative` in the example above, produces the equivalent of `#questions .button.affirmative` which selects a widget with both the `button` and `affirmative` classes.
+Without `&` it would be equivalent to `#questions .button .affirmative` (note the additional space) which would only match a widget with class `affirmative` inside a container with class `button`.
+
+
+For reference, lets see those two CSS files side-by-side:
+
+=== "nesting01.tcss"
+
+ ```css
+ --8<-- "docs/examples/guide/css/nesting01.tcss"
+ ```
+
+=== "nesting02.tcss"
+
+ ```sass
+ --8<-- "docs/examples/guide/css/nesting02.tcss"
+ ```
+
+
+Note how nesting bundles related rules together.
+If we were to add other selectors for additional screens or widgets, it would be easier to find the rules which will be applied.
+
+### Why use nesting?
+
+There is no requirement to use nested CSS, but it can help to group related rule sets together (which makes it easier to edit). Nested CSS can also help you avoid some repetition in your selectors, i.e. in the nested CSS we only need to type `#questions` once, rather than four times in the non-nested CSS.
diff --git a/docs/guide/actions.md b/docs/guide/actions.md
index a59440a359..ca5ea8b824 100644
--- a/docs/guide/actions.md
+++ b/docs/guide/actions.md
@@ -10,13 +10,13 @@ Action methods are methods on your app or widgets prefixed with `action_`. Aside
Action methods may be coroutines (defined with the `async` keyword).
-Let's write an app with a simple action.
+Let's write an app with a simple action method.
```python title="actions01.py" hl_lines="6-7 11"
--8<-- "docs/examples/guide/actions/actions01.py"
```
-The `action_set_background` method is an action which sets the background of the screen. The key handler above will call this action if you press the ++r++ key.
+The `action_set_background` method is an action method which sets the background of the screen. The key handler above will call this action method if you press the ++r++ key.
Although it is possible (and occasionally useful) to call action methods in this way, they are intended to be parsed from an _action string_. For instance, the string `"set_background('red')"` is an action string which would call `self.action_set_background('red')`.
@@ -40,9 +40,9 @@ Action strings have a simple syntax, which for the most part replicates Python's
Action strings have the following format:
-- The name of an action on is own will call the action method with no parameters. For example, an action string of `"bell"` will call `action_bell()`.
-- Actions may be followed by braces containing Python objects. For example, the action string `set_background("red")` will call `action_set_background("red")`.
-- Actions may be prefixed with a _namespace_ (see below) followed by a dot.
+- The name of an action on its own will call the action method with no parameters. For example, an action string of `"bell"` will call `action_bell()`.
+- Action strings may be followed by parenthesis containing Python objects. For example, the action string `set_background("red")` will call `action_set_background("red")`.
+- Action strings may be prefixed with a _namespace_ ([see below](#namespaces)) and a dot.
--8<-- "docs/images/actions/format.excalidraw.svg"
@@ -50,13 +50,13 @@ Action strings have the following format:
### Parameters
-If the action string contains parameters, these must be valid Python literals. Which means you can include numbers, strings, dicts, lists etc. but you can't include variables or references to any other Python symbols.
+If the action string contains parameters, these must be valid Python literals, which means you can include numbers, strings, dicts, lists, etc., but you can't include variables or references to any other Python symbols.
Consequently `"set_background('blue')"` is a valid action string, but `"set_background(new_color)"` is not — because `new_color` is a variable and not a literal.
## Links
-Actions may be embedded as links within console markup. You can create such links with a `@click` tag.
+Actions may be embedded as links within console markup. You can create such links with a `@click` tag.
The following example mounts simple static text with embedded action links.
@@ -106,11 +106,11 @@ The following example defines a custom widget with its own `set_background` acti
=== "actions05.tcss"
- ```sass title="actions05.tcss"
+ ```css title="actions05.tcss"
--8<-- "docs/examples/guide/actions/actions05.tcss"
```
-There are two instances of the custom widget mounted. If you click the links in either of them it will changed the background for that widget only. The ++r++, ++g++, and ++b++ key bindings are set on the App so will set the background for the screen.
+There are two instances of the custom widget mounted. If you click the links in either of them it will change the background for that widget only. The ++r++, ++g++, and ++b++ key bindings are set on the App so will set the background for the screen.
You can optionally prefix an action with a _namespace_, which tells Textual to run actions for a different object.
diff --git a/docs/guide/app.md b/docs/guide/app.md
index 648847ec28..c78b8b52a7 100644
--- a/docs/guide/app.md
+++ b/docs/guide/app.md
@@ -262,7 +262,7 @@ The following example enables loading of CSS by adding a `CSS_PATH` class variab
If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.tcss"` in the same directory as the Python code. Here is that CSS file:
-```sass title="question02.tcss"
+```css title="question02.tcss"
--8<-- "docs/examples/app/question02.tcss"
```
diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md
index 126ebd6046..0dd15af0f1 100644
--- a/docs/guide/command_palette.md
+++ b/docs/guide/command_palette.md
@@ -57,7 +57,7 @@ The following example will display a blank screen initially, but if you bring up
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="11-39 45"
+ ```python title="command01.py" hl_lines="14-42 45"
--8<-- "docs/examples/guide/command_palette/command01.py"
```
diff --git a/docs/guide/design.md b/docs/guide/design.md
index 46b8d4d04d..36723969f9 100644
--- a/docs/guide/design.md
+++ b/docs/guide/design.md
@@ -16,7 +16,7 @@ Textual pre-defines a number of colors as [CSS variables](../guide/CSS.md#css-va
Here's an example of CSS that uses color variables:
-```sass
+```css
MyWidget {
background: $primary;
color: $text;
@@ -93,10 +93,13 @@ textual colors
Here's a list of the colors defined in the default light and dark themes.
+!!! note
+
+ `$boost` will look different on different backgrounds because of its alpha channel.
+
```{.rich title="Textual Theme Colors"}
from rich import print
from textual.app import DEFAULT_COLORS
from textual.design import show_design
output = show_design(DEFAULT_COLORS["light"], DEFAULT_COLORS["dark"])
```
-
diff --git a/docs/guide/events.md b/docs/guide/events.md
index bd89515f37..d778473e35 100644
--- a/docs/guide/events.md
+++ b/docs/guide/events.md
@@ -240,8 +240,8 @@ The following example uses the decorator approach to write individual message ha
While there are a few more lines of code, it is clearer what will happen when you click any given button.
-Note that the decorator requires that the message class has a `control` attribute which should be the widget associated with the message.
-Messages from builtin controls will have this attribute, but you may need to add `control` to any [custom messages](#custom-messages) you write.
+Note that the decorator requires that the message class has a `control` property which should return the widget associated with the message.
+Messages from builtin controls will have this attribute, but you may need to add a `control` property to any [custom messages](#custom-messages) you write.
!!! note
diff --git a/docs/guide/layout.md b/docs/guide/layout.md
index 3ef77ced7e..8c80a14b00 100644
--- a/docs/guide/layout.md
+++ b/docs/guide/layout.md
@@ -27,7 +27,7 @@ The example below demonstrates how children are arranged inside a container with
=== "vertical_layout.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/guide/layout/vertical_layout.tcss"
```
@@ -92,7 +92,7 @@ The example below shows how we can arrange widgets horizontally, with minimal ch
=== "horizontal_layout.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/guide/layout/horizontal_layout.tcss"
```
@@ -125,7 +125,7 @@ To enable horizontal scrolling, we can use the `overflow-x: auto;` declaration:
=== "horizontal_layout_overflow.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/guide/layout/horizontal_layout_overflow.tcss"
```
@@ -154,7 +154,7 @@ In other words, we have a single row containing two columns.
=== "utility_containers.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/guide/layout/utility_containers.tcss"
```
@@ -193,7 +193,7 @@ Let's update the [utility containers](#utility-containers) example to use the co
=== "utility_containers.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/layout/utility_containers.tcss"
```
@@ -235,7 +235,7 @@ The following example creates a 3 x 2 grid and adds six widgets to it
=== "grid_layout1.tcss"
- ```sass hl_lines="2 3"
+ ```css hl_lines="2 3"
--8<-- "docs/examples/guide/layout/grid_layout1.tcss"
```
@@ -256,7 +256,7 @@ If we were to yield a seventh widget from our `compose` method, it would not be
=== "grid_layout2.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/guide/layout/grid_layout2.tcss"
```
@@ -288,7 +288,7 @@ We'll make the first column take up half of the screen width, with the other two
=== "grid_layout3_row_col_adjust.tcss"
- ```sass hl_lines="4"
+ ```css hl_lines="4"
--8<-- "docs/examples/guide/layout/grid_layout3_row_col_adjust.tcss"
```
@@ -317,7 +317,7 @@ and the second row to `75%` height (while retaining the `grid-columns` change fr
=== "grid_layout4_row_col_adjust.tcss"
- ```sass hl_lines="5"
+ ```css hl_lines="5"
--8<-- "docs/examples/guide/layout/grid_layout4_row_col_adjust.tcss"
```
@@ -345,7 +345,7 @@ Let's modify the previous example to make the first column an `auto` column.
=== "grid_layout_auto.tcss"
- ```sass hl_lines="4"
+ ```css hl_lines="4"
--8<-- "docs/examples/guide/layout/grid_layout_auto.tcss"
```
@@ -377,7 +377,7 @@ We'll also add a slight tint using `tint: magenta 40%;` to draw attention to it.
=== "grid_layout5_col_span.tcss"
- ```sass hl_lines="6-9"
+ ```css hl_lines="6-9"
--8<-- "docs/examples/guide/layout/grid_layout5_col_span.tcss"
```
@@ -410,7 +410,7 @@ We again target widget `#two` in our CSS, and add a `row-span: 2;` declaration t
=== "grid_layout6_row_span.tcss"
- ```sass hl_lines="8"
+ ```css hl_lines="8"
--8<-- "docs/examples/guide/layout/grid_layout6_row_span.tcss"
```
@@ -442,7 +442,7 @@ Now if we add `grid-gutter: 1;` to our grid, one cell of spacing appears between
=== "grid_layout7_gutter.tcss"
- ```sass hl_lines="4"
+ ```css hl_lines="4"
--8<-- "docs/examples/guide/layout/grid_layout7_gutter.tcss"
```
@@ -482,7 +482,7 @@ The code below shows a simple sidebar implementation.
=== "dock_layout1_sidebar.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/guide/layout/dock_layout1_sidebar.tcss"
```
@@ -506,7 +506,7 @@ This new sidebar is double the width of the one previous one, and has a `deeppin
=== "dock_layout2_sidebar.tcss"
- ```sass hl_lines="1-6"
+ ```css hl_lines="1-6"
--8<-- "docs/examples/guide/layout/dock_layout2_sidebar.tcss"
```
@@ -530,7 +530,7 @@ We can yield it inside `compose`, and without any additional CSS, we get a heade
=== "dock_layout3_sidebar_header.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/layout/dock_layout3_sidebar_header.tcss"
```
@@ -573,7 +573,7 @@ However, in this case, both `#box1` and `#box2` are assigned to layers which def
=== "layers.tcss"
- ```sass hl_lines="3 14 19"
+ ```css hl_lines="3 14 19"
--8<-- "docs/examples/guide/layout/layers.tcss"
```
@@ -614,7 +614,7 @@ The example below shows how an advanced layout can be built by combining the var
=== "combining_layouts.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/layout/combining_layouts.tcss"
```
diff --git a/docs/guide/queries.md b/docs/guide/queries.md
index ff58ca2774..0b1e5fc105 100644
--- a/docs/guide/queries.md
+++ b/docs/guide/queries.md
@@ -2,7 +2,7 @@
In the previous chapter we introduced the [DOM](../guide/CSS.md#the-dom) which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS [selectors](./CSS.md#selectors).
-Selectors are a very useful idea and can do more that apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!
+Selectors are a very useful idea and can do more than apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!
## Query one
@@ -116,7 +116,7 @@ If the last widget is *not* a button, Textual will raise a [WrongType][textual.c
## Filter
-Query objects have a [filter][textual.css.query.DOMQuery.filter] method which further refines a query. This method will return a new query object with widgets that match both the original query _and_ the new selector
+Query objects have a [filter][textual.css.query.DOMQuery.filter] method which further refines a query. This method will return a new query object with widgets that match both the original query _and_ the new selector.
Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:
diff --git a/docs/guide/reactivity.md b/docs/guide/reactivity.md
index 4a7c2cd41b..c84050ad40 100644
--- a/docs/guide/reactivity.md
+++ b/docs/guide/reactivity.md
@@ -81,7 +81,7 @@ Let's look at an example which illustrates this. In the following app, the value
=== "refresh01.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/reactivity/refresh01.tcss"
```
@@ -125,7 +125,7 @@ The following example modifies "refresh01.py" so that the greeting has an automa
=== "refresh02.tcss"
- ```sass hl_lines="7-9"
+ ```css hl_lines="7-9"
--8<-- "docs/examples/guide/reactivity/refresh02.tcss"
```
@@ -152,7 +152,7 @@ A common use for this is to restrict numbers to a given range. The following exa
=== "validate01.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/reactivity/validate01.tcss"
```
@@ -185,7 +185,7 @@ The following app will display any color you type in to the input. Try it with a
=== "watch01.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/reactivity/watch01.tcss"
```
@@ -202,6 +202,31 @@ Textual only calls watch methods if the value of a reactive attribute _changes_.
If the newly assigned value is the same as the previous value, the watch method is not called.
You can override this behaviour by passing `always_update=True` to `reactive`.
+
+### Dynamically watching reactive attributes
+
+You can programmatically add watchers to reactive attributes with the method [`watch`][textual.dom.DOMNode.watch].
+This is useful when you want to react to changes to reactive attributes for which you can't edit the watch methods.
+
+The example below shows a widget `Counter` that defines a reactive attribute `counter`.
+The app that uses `Counter` uses the method `watch` to keep its progress bar synced with the reactive attribute:
+
+=== "dynamic_watch.py"
+
+ ```python hl_lines="9 28-29 31"
+ --8<-- "docs/examples/guide/reactivity/dynamic_watch.py"
+ ```
+
+ 1. `counter` is a reactive attribute defined inside `Counter`.
+ 2. `update_progress` is a custom callback that will update the progress bar when `counter` changes.
+ 3. We use the method `watch` to set `update_progress` as an additional watcher for the reactive attribute `counter`.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/guide/reactivity/dynamic_watch.py" press="enter,enter,enter"}
+ ```
+
+
## Compute methods
Compute methods are the final superpower offered by the `reactive` descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with `compute_` followed by the name of the reactive value.
@@ -221,7 +246,7 @@ The following example uses a computed attribute. It displays three inputs for ea
=== "computed01.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/guide/reactivity/computed01.tcss"
```
diff --git a/docs/guide/screens.md b/docs/guide/screens.md
index b9aefdc175..4f57412fd0 100644
--- a/docs/guide/screens.md
+++ b/docs/guide/screens.md
@@ -26,7 +26,7 @@ Let's look at a simple example of writing a screen class to simulate Window's [b
=== "screen01.tcss"
- ```sass title="screen01.tcss"
+ ```css title="screen01.tcss"
--8<-- "docs/examples/guide/screens/screen01.tcss"
```
@@ -55,7 +55,7 @@ You can also _install_ new named screens dynamically with the [install_screen][t
=== "screen02.tcss"
- ```sass title="screen02.tcss"
+ ```css title="screen02.tcss"
--8<-- "docs/examples/guide/screens/screen02.tcss"
```
@@ -171,7 +171,7 @@ From the quit screen you can click either Quit to exit the app immediately, or C
=== "modal01.tcss"
- ```sass title="modal01.tcss"
+ ```css title="modal01.tcss"
--8<-- "docs/examples/guide/screens/modal01.tcss"
```
@@ -213,7 +213,7 @@ Let's see what happens when we use `ModalScreen`.
=== "modal01.tcss"
- ```sass title="modal01.tcss"
+ ```css title="modal01.tcss"
--8<-- "docs/examples/guide/screens/modal01.tcss"
```
@@ -240,7 +240,7 @@ Let's modify the previous example to use `dismiss` rather than an explicit `pop_
=== "modal01.tcss"
- ```sass title="modal01.tcss"
+ ```css title="modal01.tcss"
--8<-- "docs/examples/guide/screens/modal01.tcss"
```
@@ -258,6 +258,45 @@ You may have noticed in the previous example that we changed the base class to `
The addition of `[bool]` adds typing information that tells the type checker to expect a boolean in the call to `dismiss`, and that any callback set in `push_screen` should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs.
+### Waiting for screens
+
+It is also possible to wait on a screen to be dismissed, which can feel like a more natural way of expressing logic than a callback.
+The [`push_screen_wait()`][textual.app.App.push_screen_wait] method will push a screen and wait for its result (the value from [`Screen.dismiss()`][textual.screen.Screen.dismiss]).
+
+This can only be done from a [worker](./workers.md), so that waiting for the screen doesn't prevent your app from updating.
+
+Let's look at an example that uses `push_screen_wait` to ask a question and waits for the user to reply by clicking a button.
+
+
+=== "questions01.py"
+
+ ```python title="questions01.py" hl_lines="35-37"
+ --8<-- "docs/examples/guide/screens/questions01.py"
+ ```
+
+ 1. Dismiss with `True` when pressing the Yes button.
+ 2. Dismiss with `False` when pressing the No button.
+ 3. The `work` decorator will make this method run in a worker (background task).
+ 4. Will return a result when the user clicks one of the buttons.
+
+
+=== "questions01.tcss"
+
+ ```css title="questions01.tcss"
+ --8<-- "docs/examples/guide/screens/questions01.tcss"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/guide/screens/questions01.py"}
+ ```
+
+The mount handler on the app is decorated with `@work`, which makes the code run in a worker (background task).
+In the mount handler we push the screen with the `push_screen_wait`.
+When the user presses one of the buttons, the screen calls [`dismiss()`][textual.screen.Screen.dismiss] with either `True` or `False`.
+This value is then returned from the `push_screen_wait` method in the mount handler.
+
+
## Modes
Some apps may benefit from having multiple screen stacks, rather than just one.
diff --git a/docs/guide/styles.md b/docs/guide/styles.md
index 22ac00d0b8..33566708af 100644
--- a/docs/guide/styles.md
+++ b/docs/guide/styles.md
@@ -302,7 +302,7 @@ When you set padding or border it reduces the size of the widget's content area.
This is generally desirable when you arrange things on screen as you can add border or padding without breaking your layout. Occasionally though you may want to keep the size of the content area constant and grow the size of the widget to fit padding and border. The [box-sizing](../styles/box_sizing.md) style allows you to switch between these two modes.
-If you set `box_sizing` to `"content-box"` then space required for padding and border will be added to the widget dimensions. The default value of `box_sizing` is `"border-box"`. Compare the box model diagram for `content-box` to the to the box model for `border-box`.
+If you set `box_sizing` to `"content-box"` then the space required for padding and border will be added to the widget dimensions. The default value of `box_sizing` is `"border-box"`. Compare the box model diagram for `content-box` to the box model for `border-box`.
=== "content-box"
diff --git a/docs/guide/testing.md b/docs/guide/testing.md
new file mode 100644
index 0000000000..f22645b604
--- /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..d703761caf 100644
--- a/docs/guide/widgets.md
+++ b/docs/guide/widgets.md
@@ -24,7 +24,7 @@ Let's create a simple custom widget to display a greeting.
--8<-- "docs/examples/guide/widgets/hello01.py"
```
-The three highlighted lines define a custom widget class with just a [render()][textual.widget.Widget.render] method. Textual will display whatever is returned from render in the content area of your widget. We have returned a string in the code above, but there are other possible return types which we will cover later.
+The highlighted lines define a custom widget class with just a [render()][textual.widget.Widget.render] method. Textual will display whatever is returned from render in the content area of your widget. We have returned a string in the code above, but there are other possible return types which we will cover later.
Note that the text contains tags in square brackets, i.e. `[b]`. This is [console markup](https://rich.readthedocs.io/en/latest/markup.html) which allows you to embed various styles within your content. If you run this you will find that `World` is in bold.
@@ -42,7 +42,7 @@ This (very simple) custom widget may be [styled](./styles.md) in the same way as
=== "hello02.tcss"
- ```sass title="hello02.tcss"
+ ```css title="hello02.tcss"
--8<-- "docs/examples/guide/widgets/hello02.tcss"
```
@@ -59,13 +59,13 @@ Let's use Static to create a widget which cycles through "hello" in various lang
=== "hello03.py"
- ```python title="hello03.py" hl_lines="24-36"
+ ```python title="hello03.py" hl_lines="23-35"
--8<-- "docs/examples/guide/widgets/hello03.py"
```
=== "hello03.tcss"
- ```sass title="hello03.tcss"
+ ```css title="hello03.tcss"
--8<-- "docs/examples/guide/widgets/hello03.tcss"
```
@@ -88,13 +88,13 @@ Here's the Hello example again, this time the widget has embedded default CSS:
=== "hello04.py"
- ```python title="hello04.py" hl_lines="27-36"
+ ```python title="hello04.py" hl_lines="26-35"
--8<-- "docs/examples/guide/widgets/hello04.py"
```
=== "hello04.tcss"
- ```sass title="hello04.tcss"
+ ```css title="hello04.tcss"
--8<-- "docs/examples/guide/widgets/hello04.tcss"
```
@@ -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
@@ -124,13 +131,13 @@ Let's use markup links in the hello example so that the greeting becomes a link
=== "hello05.py"
- ```python title="hello05.py" hl_lines="24-33"
+ ```python title="hello05.py" hl_lines="23-32"
--8<-- "docs/examples/guide/widgets/hello05.py"
```
=== "hello05.tcss"
- ```sass title="hello05.tcss"
+ ```css title="hello05.tcss"
--8<-- "docs/examples/guide/widgets/hello05.tcss"
```
@@ -168,7 +175,7 @@ Let's demonstrate setting a title, both as a class variable and a instance varia
=== "hello06.tcss"
- ```sass title="hello06.tcss"
+ ```css title="hello06.tcss"
--8<-- "docs/examples/guide/widgets/hello06.tcss"
```
@@ -199,7 +206,7 @@ This app will "play" fizz buzz by displaying a table of the first 15 numbers and
=== "fizzbuzz01.tcss"
- ```sass title="fizzbuzz01.tcss" hl_lines="32-35"
+ ```css title="fizzbuzz01.tcss" hl_lines="32-35"
--8<-- "docs/examples/guide/widgets/fizzbuzz01.tcss"
```
@@ -223,7 +230,7 @@ Let's modify the default width for the fizzbuzz example. By default, the table w
=== "fizzbuzz02.tcss"
- ```sass title="fizzbuzz02.tcss"
+ ```css title="fizzbuzz02.tcss"
--8<-- "docs/examples/guide/widgets/fizzbuzz02.tcss"
```
@@ -287,6 +294,37 @@ Add a rule to your CSS that targets `Tooltip`. Here's an example:
```{.textual path="docs/examples/guide/widgets/tooltip02.py" hover="Button"}
```
+## Loading indicator
+
+Widgets have a [`loading`][textual.widget.Widget.loading] reactive which when set to `True` will temporarily replace your widget with a [`LoadingIndicator`](../widgets/loading_indicator.md).
+
+You can use this to indicate to the user that the app is currently working on getting data, and there will be content when that data is available.
+Let's look at an example of this.
+
+=== "loading01.py"
+
+ ```python title="loading01.py"
+ --8<-- "docs/examples/guide/widgets/loading01.py"
+ ```
+
+ 1. Shows the loading indicator in place of the data table.
+ 2. Insert a random sleep to simulate a network request.
+ 3. Show the new data.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/guide/widgets/loading01.py"}
+ ```
+
+
+In this example we have four [DataTable](../widgets/data_table.md) widgets, which we put into a loading state by setting the widget's `loading` property to `True`.
+This will temporarily replace the widget with a loading indicator animation.
+When the (simulated) data has been retrieved, we reset the `loading` property to show the new data.
+
+!!! tip
+
+ See the guide on [Workers](./workers.md) if you want to know more about the `@work` decorator.
+
## Line API
A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size.
@@ -513,7 +551,7 @@ In this section we will show how to design and build a fully-working app, while
### Designing the app
-We are going to build a *byte editor* which allows you to enter a number in both decimal and binary. You could use this a teaching aid for binary numbers.
+We are going to build a *byte editor* which allows you to enter a number in both decimal and binary. You could use this as a teaching aid for binary numbers.
Here's a sketch of what the app should ultimately look like:
@@ -526,7 +564,11 @@ Here's a sketch of what the app should ultimately look like:
--8<-- "docs/images/byte01.excalidraw.svg"
-There are three types of built-in widget in the sketch, namely ([Input](../widgets/input.md), [Label](../widgets/label.md), and [Switch](../widgets/switch.md)). Rather than manage these as a single collection of widgets, we can arrange them in to logical groups with compound widgets. This will make our app easier to work with.
+There are three types of built-in widget in the sketch, namely ([Input](../widgets/input.md), [Label](../widgets/label.md), and [Switch](../widgets/switch.md)). Rather than manage these as a single collection of widgets, we can arrange them into logical groups with compound widgets. This will make our app easier to work with.
+
+??? textualize "Try in Textual-web"
+
+
### Identifying components
@@ -563,7 +605,7 @@ Note the `compose()` methods of each of the widgets.
- The `ByteInput` yields 8 `BitSwitch` widgets and arranges them horizontally. It also adds a `focus-within` style in its CSS to draw an accent border when any of the switches are focused.
-- The `ByteEditor` yields a `ByteInput` and an `Input` control. The default CSS stacks the two controls on top of each other to divide the screen in to two parts.
+- The `ByteEditor` yields a `ByteInput` and an `Input` control. The default CSS stacks the two controls on top of each other to divide the screen into two parts.
With these three widgets, the [DOM](CSS.md#the-dom) for our app will look like this:
diff --git a/docs/guide/workers.md b/docs/guide/workers.md
index a8eff8432d..71a06c093e 100644
--- a/docs/guide/workers.md
+++ b/docs/guide/workers.md
@@ -28,7 +28,7 @@ The following app uses [httpx](https://www.python-httpx.org/) to get the current
=== "weather.tcss"
- ```sass title="weather.tcss"
+ ```css title="weather.tcss"
--8<-- "docs/examples/guide/workers/weather.tcss"
```
@@ -158,7 +158,7 @@ The second difference is that you can't cancel threads in the same way as corout
Let's demonstrate thread workers by replacing `httpx` with `urllib.request` (in the standard library).
The `urllib` module is not async aware, so we will need to use threads:
-```python title="weather05.py" hl_lines="1 26-43"
+```python title="weather05.py" hl_lines="1-2 27-44"
--8<-- "docs/examples/guide/workers/weather05.py"
```
diff --git a/docs/how-to/design-a-layout.md b/docs/how-to/design-a-layout.md
index b9f17c51c7..80c82c9d7e 100644
--- a/docs/how-to/design-a-layout.md
+++ b/docs/how-to/design-a-layout.md
@@ -27,6 +27,11 @@ Here's our sketch:
It's rough, but it's all we need.
+??? textualize "Try in Textual-web"
+
+
+
+
## Tip 2. Work outside in
Like a sculpture with a block of marble, it is best to work from the outside towards the center.
diff --git a/docs/how-to/render-and-compose.md b/docs/how-to/render-and-compose.md
new file mode 100644
index 0000000000..de43f26e98
--- /dev/null
+++ b/docs/how-to/render-and-compose.md
@@ -0,0 +1,63 @@
+# Render and compose
+
+A common question that comes up on the [Textual Discord server](https://discord.gg/Enf6Z3qhVr) is what is the difference between [`render`][textual.widget.Widget.render] and [`compose`][textual.widget.Widget.compose] methods on a widget?
+In this article we will clarify the differences, and use both these methods to build something fun.
+
+
+
+
+
+## Which method to use?
+
+Render and compose are easy to confuse because they both ultimately define what a widget will look like, but they have quite different uses.
+
+The `render` method on a widget returns a [Rich](https://rich.readthedocs.io/en/latest/) renderable, which is anything you could print with Rich.
+The simplest renderable is just text; so `render()` methods often return a string, but could equally return a [`Text`](https://rich.readthedocs.io/en/latest/text.html) instance, a [`Table`](https://rich.readthedocs.io/en/latest/tables.html), or anything else from Rich (or third party library).
+Whatever is returned from `render()` will be combined with any styles from CSS and displayed within the widget's borders.
+
+The `compose` method is used to build [*compound* widgets](../guide/widgets.md#compound-widgets) (widgets composed of other widgets).
+
+A general rule of thumb, is that if you implement a `compose` method, there is no need for a `render` method because it is the widgets yielded from `compose` which define how the custom widget will look.
+However, you *can* mix these two methods.
+If you implement both, the `render` method will set the custom widget's *background* and `compose` will add widgets on top of that background.
+
+## Combining render and compose
+
+Let's look at an example that combines both these methods.
+We will create a custom widget with a [linear gradient][textual.renderables.gradient.LinearGradient] as a background.
+The background will be animated (I did promise *fun*)!
+
+=== "render_compose.py"
+
+ ```python
+ --8<-- "docs/examples/how-to/render_compose.py"
+ ```
+
+ 1. Refresh the widget 30 times a second.
+ 2. Compose our compound widget, which contains a single Static.
+ 3. Render a linear gradient in the background.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/how-to/render_compose.py" columns="100" lines="40"}
+ ```
+
+The `Splash` custom widget has a `compose` method which adds a simple `Static` widget to display a message.
+Additionally there is a `render` method which returns a renderable to fill the background with a gradient.
+
+!!! tip
+
+ As fun as this is, spinning animated gradients may be too distracting for most apps!
+
+## Summary
+
+Keep the following in mind when building [custom widgets](../guide/widgets.md).
+
+1. Use `render` to return simple text, or a Rich renderable.
+2. Use `compose` to create a widget out of other widgets.
+3. If you define both, then `render` will be used as a *background*.
+
+
+---
+
+We are here to [help](../help.md)!
diff --git a/docs/images/icons/logo light transparent.svg b/docs/images/icons/logo light transparent.svg
new file mode 100644
index 0000000000..411625e4a7
--- /dev/null
+++ b/docs/images/icons/logo light transparent.svg
@@ -0,0 +1,12 @@
+
+
+
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..1c06781407 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](https://github.com/Textualize/textual-web)!
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 90e05d1e1d..d5a9f1a3b6 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
@@ -75,8 +74,8 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c
- [X] Spark-lines
- [X] Switch
- [X] Tabs
-- [ ] TextArea (multi-line input)
- * [ ] Basic controls
+- [X] TextArea (multi-line input)
+ * [X] Basic controls
* [ ] Indentation guides
* [ ] Smart features for various languages
- * [ ] Syntax highlighting
+ * [X] Syntax highlighting
diff --git a/docs/snippets/border_sub_title_align_all_example.md b/docs/snippets/border_sub_title_align_all_example.md
index 6550a92ca0..4d916ce982 100644
--- a/docs/snippets/border_sub_title_align_all_example.md
+++ b/docs/snippets/border_sub_title_align_all_example.md
@@ -8,7 +8,7 @@ Open the code tabs to see the details of the code examples.
=== "border_sub_title_align_all.py"
- ```py hl_lines="6 18 24 30 39 40 42 45 51 57 63"
+ ```py hl_lines="6 20 26 32 41 42 44 47 53 59 65"
--8<-- "docs/examples/styles/border_sub_title_align_all.py"
```
@@ -26,7 +26,7 @@ Open the code tabs to see the details of the code examples.
=== "border_sub_title_align_all.tcss"
- ```sass hl_lines="12 16 30 34 41 46"
+ ```css hl_lines="12 16 30 34 41 46"
--8<-- "docs/examples/styles/border_sub_title_align_all.tcss"
```
diff --git a/docs/snippets/border_title_color.md b/docs/snippets/border_title_color.md
index 36b473c24a..edcc9c5c0d 100644
--- a/docs/snippets/border_title_color.md
+++ b/docs/snippets/border_title_color.md
@@ -13,6 +13,6 @@ The following examples demonstrates customization of the border color and text s
=== "border_title_colors.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/border_title_colors.tcss"
```
diff --git a/docs/snippets/border_vs_outline_example.md b/docs/snippets/border_vs_outline_example.md
index 55d035f47d..239a4c79a0 100644
--- a/docs/snippets/border_vs_outline_example.md
+++ b/docs/snippets/border_vs_outline_example.md
@@ -16,6 +16,6 @@ This example also shows that a widget cannot contain both a `border` and an `out
=== "outline_vs_border.tcss"
- ```sass hl_lines="5-7 9-11"
+ ```css hl_lines="5-7 9-11"
--8<-- "docs/examples/styles/outline_vs_border.tcss"
```
diff --git a/docs/styles/_template.md b/docs/styles/_template.md
index 5cddb8919c..b08423bb3b 100644
--- a/docs/styles/_template.md
+++ b/docs/styles/_template.md
@@ -46,7 +46,7 @@ Short description of the first example.
=== "style.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/style.tcss"
```
-->
@@ -68,7 +68,7 @@ Short description of the second example.
=== "style.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/style.tcss"
```
@@ -84,7 +84,7 @@ Include comments when relevant.
Include all variations.
List all values, if possible and sensible.
-```sass
+```css
rule-name: value1
rule-name: value2
rule-name: different-syntax-value shown-here
diff --git a/docs/styles/align.md b/docs/styles/align.md
index 810e26303a..72da641027 100644
--- a/docs/styles/align.md
+++ b/docs/styles/align.md
@@ -34,7 +34,7 @@ This example contains a simple app with two labels centered on the screen with `
=== "align.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/styles/align.tcss"
```
@@ -56,13 +56,13 @@ Each label has been aligned differently inside its container, and its text shows
=== "align_all.tcss"
- ```sass hl_lines="2 6 10 14 18 22 26 30 34"
+ ```css hl_lines="2 6 10 14 18 22 26 30 34"
--8<-- "docs/examples/styles/align_all.tcss"
```
## CSS
-```sass
+```css
/* Align child widgets to the center. */
align: center middle;
/* Align child widget to the top right */
diff --git a/docs/styles/background.md b/docs/styles/background.md
index 9a1c4f04f2..420d08eae8 100644
--- a/docs/styles/background.md
+++ b/docs/styles/background.md
@@ -29,7 +29,7 @@ This example creates three widgets and applies a different background to each.
=== "background.tcss"
- ```sass hl_lines="9 13 17"
+ ```css hl_lines="9 13 17"
--8<-- "docs/examples/styles/background.tcss"
```
@@ -50,13 +50,13 @@ The next example creates ten widgets laid out side by side to show the effect of
=== "background_transparency.tcss"
- ```sass hl_lines="2 6 10 14 18 22 26 30 34 38"
+ ```css hl_lines="2 6 10 14 18 22 26 30 34 38"
--8<-- "docs/examples/styles/background_transparency.tcss"
```
## CSS
-```sass
+```css
/* Blue background */
background: blue;
diff --git a/docs/styles/border.md b/docs/styles/border.md
index 71643fd4fe..eb3e4991df 100644
--- a/docs/styles/border.md
+++ b/docs/styles/border.md
@@ -53,7 +53,7 @@ This examples shows three widgets with different border styles.
=== "border.tcss"
- ```sass hl_lines="4 10 16"
+ ```css hl_lines="4 10 16"
--8<-- "docs/examples/styles/border.tcss"
```
@@ -68,13 +68,13 @@ The next example shows a grid with all the available border types.
=== "border_all.py"
- ```py hl_lines="2 6 10 14 18 22 26 30 34 38 42 46 50 54 58"
+ ```py
--8<-- "docs/examples/styles/border_all.py"
```
=== "border_all.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/border_all.tcss"
```
@@ -84,7 +84,7 @@ The next example shows a grid with all the available border types.
## CSS
-```sass
+```css
/* Set a heavy white border */
border: heavy white;
diff --git a/docs/styles/border_subtitle_align.md b/docs/styles/border_subtitle_align.md
index 7723062cdb..6484d277f6 100644
--- a/docs/styles/border_subtitle_align.md
+++ b/docs/styles/border_subtitle_align.md
@@ -35,7 +35,7 @@ This example shows three labels, each with a different border subtitle alignment
=== "border_subtitle_align.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/border_subtitle_align.tcss"
```
@@ -47,7 +47,7 @@ This example shows three labels, each with a different border subtitle alignment
## CSS
-```sass
+```css
border-subtitle-align: left;
border-subtitle-align: center;
border-subtitle-align: right;
diff --git a/docs/styles/border_subtitle_background.md b/docs/styles/border_subtitle_background.md
index 8d60b3e150..21a407c67e 100644
--- a/docs/styles/border_subtitle_background.md
+++ b/docs/styles/border_subtitle_background.md
@@ -18,7 +18,7 @@ border-subtitle-background: (<color> |
## CSS
-```sass
+```css
border-subtitle-background: blue;
```
diff --git a/docs/styles/border_subtitle_color.md b/docs/styles/border_subtitle_color.md
index 3880111595..0629896ea3 100644
--- a/docs/styles/border_subtitle_color.md
+++ b/docs/styles/border_subtitle_color.md
@@ -17,7 +17,7 @@ border-subtitle-color: (<color> | auto
## CSS
-```sass
+```css
border-subtitle-color: red;
```
diff --git a/docs/styles/border_subtitle_style.md b/docs/styles/border_subtitle_style.md
index 3b826568f8..0c8c16afb2 100644
--- a/docs/styles/border_subtitle_style.md
+++ b/docs/styles/border_subtitle_style.md
@@ -17,7 +17,7 @@ border-subtitle-style: <text-style>
## CSS
-```sass
+```css
border-subtitle-style: bold underline;
```
diff --git a/docs/styles/border_title_align.md b/docs/styles/border_title_align.md
index f2d9a61f9f..b30efcab2b 100644
--- a/docs/styles/border_title_align.md
+++ b/docs/styles/border_title_align.md
@@ -35,7 +35,7 @@ This example shows three labels, each with a different border title alignment:
=== "border_title_align.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/border_title_align.tcss"
```
@@ -47,7 +47,7 @@ This example shows three labels, each with a different border title alignment:
## CSS
-```sass
+```css
border-title-align: left;
border-title-align: center;
border-title-align: right;
diff --git a/docs/styles/border_title_background.md b/docs/styles/border_title_background.md
index 19d33e578f..9067db1f66 100644
--- a/docs/styles/border_title_background.md
+++ b/docs/styles/border_title_background.md
@@ -17,7 +17,7 @@ border-title-background: (<color> | au
## CSS
-```sass
+```css
border-title-background: blue;
```
diff --git a/docs/styles/border_title_color.md b/docs/styles/border_title_color.md
index 2272de9c6f..5831295984 100644
--- a/docs/styles/border_title_color.md
+++ b/docs/styles/border_title_color.md
@@ -15,7 +15,7 @@ border-title-color: (<color> | auto) [
## CSS
-```sass
+```css
border-title-color: red;
```
diff --git a/docs/styles/border_title_style.md b/docs/styles/border_title_style.md
index 09e8319cca..7d16fce9ba 100644
--- a/docs/styles/border_title_style.md
+++ b/docs/styles/border_title_style.md
@@ -18,7 +18,7 @@ border-title-style: <text-style>;
## CSS
-```sass
+```css
border-title-style: bold underline;
```
diff --git a/docs/styles/box_sizing.md b/docs/styles/box_sizing.md
index 147929f8ba..b2fe4bf52f 100644
--- a/docs/styles/box_sizing.md
+++ b/docs/styles/box_sizing.md
@@ -34,13 +34,13 @@ The bottom widget has `box-sizing: content-box` which increases the size of the
=== "box_sizing.tcss"
- ```sass hl_lines="2 6"
+ ```css hl_lines="2 6"
--8<-- "docs/examples/styles/box_sizing.tcss"
```
## CSS
-```sass
+```css
/* Set box sizing to border-box (default) */
box-sizing: border-box;
diff --git a/docs/styles/color.md b/docs/styles/color.md
index 49b55dbb00..3a1f18c8b7 100644
--- a/docs/styles/color.md
+++ b/docs/styles/color.md
@@ -31,7 +31,7 @@ This example sets a different text color for each of three different widgets.
=== "color.tcss"
- ```sass hl_lines="8 12 16"
+ ```css hl_lines="8 12 16"
--8<-- "docs/examples/styles/color.tcss"
```
@@ -52,13 +52,13 @@ The next example shows how `auto` chooses between a lighter or a darker text col
=== "color_auto.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/styles/color_auto.tcss"
```
## CSS
-```sass
+```css
/* Blue text */
color: blue;
diff --git a/docs/styles/content_align.md b/docs/styles/content_align.md
index 63d0ba298f..88a10ea6f7 100644
--- a/docs/styles/content_align.md
+++ b/docs/styles/content_align.md
@@ -39,7 +39,7 @@ This first example shows three labels stacked vertically, each with different co
=== "content_align.tcss"
- ```sass hl_lines="2 7-8 13"
+ ```css hl_lines="2 7-8 13"
--8<-- "docs/examples/styles/content_align.tcss"
```
@@ -61,13 +61,13 @@ Each label has its text aligned differently.
=== "content_align_all.tcss"
- ```sass hl_lines="2 5 8 11 14 17 20 23 26"
+ ```css hl_lines="2 5 8 11 14 17 20 23 26"
--8<-- "docs/examples/styles/content_align_all.tcss"
```
## CSS
-```sass
+```css
/* Align content in the very center of a widget */
content-align: center middle;
/* Align content at the top right of a widget */
diff --git a/docs/styles/display.md b/docs/styles/display.md
index 6a40dfcb54..c2fe31fc81 100644
--- a/docs/styles/display.md
+++ b/docs/styles/display.md
@@ -32,13 +32,13 @@ Note that the second widget is hidden by adding the `"remove"` class which sets
=== "display.tcss"
- ```sass hl_lines="13"
+ ```css hl_lines="13"
--8<-- "docs/examples/styles/display.tcss"
```
## CSS
-```sass
+```css
/* Widget is shown */
display: block;
diff --git a/docs/styles/dock.md b/docs/styles/dock.md
index 25464c5f7b..00cb0e4195 100644
--- a/docs/styles/dock.md
+++ b/docs/styles/dock.md
@@ -30,7 +30,7 @@ Notice that even though the content is scrolled, the sidebar remains fixed.
=== "dock_layout1_sidebar.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/guide/layout/dock_layout1_sidebar.tcss"
```
@@ -52,13 +52,13 @@ The labels will remain in that position (docked) even if the container they are
=== "dock_all.tcss"
- ```sass hl_lines="2-5 8-11 14-17 20-23"
+ ```css hl_lines="2-5 8-11 14-17 20-23"
--8<-- "docs/examples/styles/dock_all.tcss"
```
## CSS
-```sass
+```css
dock: bottom; /* Docks on the bottom edge of the parent container. */
dock: left; /* Docks on the left edge of the parent container. */
dock: right; /* Docks on the right edge of the parent container. */
diff --git a/docs/styles/grid/column_span.md b/docs/styles/grid/column_span.md
index d712edb835..3ed7af7c3b 100644
--- a/docs/styles/grid/column_span.md
+++ b/docs/styles/grid/column_span.md
@@ -31,13 +31,13 @@ The example below shows a 4 by 4 grid where many placeholders span over several
=== "column_span.tcss"
- ```sass hl_lines="2 5 8 11 14 20"
+ ```css hl_lines="2 5 8 11 14 20"
--8<-- "docs/examples/styles/column_span.tcss"
```
## CSS
-```sass
+```css
column-span: 3;
```
diff --git a/docs/styles/grid/grid_columns.md b/docs/styles/grid/grid_columns.md
index 89b589c6d6..47e31b849e 100644
--- a/docs/styles/grid/grid_columns.md
+++ b/docs/styles/grid/grid_columns.md
@@ -42,13 +42,13 @@ Because there are more rows than scalars in the style definition, the scalars wi
=== "grid_columns.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/grid_columns.tcss"
```
## CSS
-```sass
+```css
/* Set all columns to have 50% width */
grid-columns: 50%;
diff --git a/docs/styles/grid/grid_gutter.md b/docs/styles/grid/grid_gutter.md
index 39e8981c55..21264848af 100644
--- a/docs/styles/grid/grid_gutter.md
+++ b/docs/styles/grid/grid_gutter.md
@@ -37,7 +37,7 @@ The example below employs a common trick to apply visually consistent spacing ar
=== "grid_gutter.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/grid_gutter.tcss"
```
@@ -45,7 +45,7 @@ The example below employs a common trick to apply visually consistent spacing ar
## CSS
-```sass
+```css
/* Set vertical and horizontal gutters to be the same */
grid-gutter: 5;
diff --git a/docs/styles/grid/grid_rows.md b/docs/styles/grid/grid_rows.md
index 816ce708ef..c76cfda56f 100644
--- a/docs/styles/grid/grid_rows.md
+++ b/docs/styles/grid/grid_rows.md
@@ -42,13 +42,13 @@ Because there are more rows than scalars in the style definition, the scalars wi
=== "grid_rows.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/grid_rows.tcss"
```
## CSS
-```sass
+```css
/* Set all rows to have 50% height */
grid-rows: 50%;
diff --git a/docs/styles/grid/grid_size.md b/docs/styles/grid/grid_size.md
index b225b858fc..33c6a67b4e 100644
--- a/docs/styles/grid/grid_size.md
+++ b/docs/styles/grid/grid_size.md
@@ -37,7 +37,7 @@ In the first example, we create a grid with 2 columns and 5 rows, although we do
=== "grid_size_both.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/styles/grid_size_both.tcss"
```
@@ -60,7 +60,7 @@ In the second example, we create a grid with 2 columns and however many rows are
=== "grid_size_columns.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/styles/grid_size_columns.tcss"
```
@@ -68,7 +68,7 @@ In the second example, we create a grid with 2 columns and however many rows are
## CSS
-```sass
+```css
/* Grid with 3 rows and 5 columns */
grid-size: 3 5;
diff --git a/docs/styles/grid/index.md b/docs/styles/grid/index.md
index 012365e00e..23a0a6ae84 100644
--- a/docs/styles/grid/index.md
+++ b/docs/styles/grid/index.md
@@ -51,7 +51,7 @@ The spacing between grid cells is defined by the `grid-gutter` style.
=== "grid.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/grid.tcss"
```
diff --git a/docs/styles/grid/row_span.md b/docs/styles/grid/row_span.md
index 145015e434..9b9dd0da58 100644
--- a/docs/styles/grid/row_span.md
+++ b/docs/styles/grid/row_span.md
@@ -34,13 +34,13 @@ After placing the placeholders `#p1`, `#p2`, `#p3`, and `#p4`, the next availabl
=== "row_span.tcss"
- ```sass hl_lines="2 5 8 11 14 17 20"
+ ```css hl_lines="2 5 8 11 14 17 20"
--8<-- "docs/examples/styles/row_span.tcss"
```
## CSS
-```sass
+```css
row-span: 3
```
diff --git a/docs/styles/height.md b/docs/styles/height.md
index e3f8b980c0..7fe8d17158 100644
--- a/docs/styles/height.md
+++ b/docs/styles/height.md
@@ -30,7 +30,7 @@ This examples creates a widget with a height of 50% of the screen.
=== "height.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/height.tcss"
```
@@ -47,7 +47,7 @@ Open the CSS file tab to see the comments that explain how each height is comput
=== "height_comparison.py"
- ```py hl_lines="15-23"
+ ```py hl_lines="17-25"
--8<-- "docs/examples/styles/height_comparison.py"
```
@@ -55,7 +55,7 @@ Open the CSS file tab to see the comments that explain how each height is comput
=== "height_comparison.tcss"
- ```sass hl_lines="2 5 8 11 14 17 20 23 26"
+ ```css hl_lines="2 5 8 11 14 17 20 23 26"
--8<-- "docs/examples/styles/height_comparison.tcss"
```
@@ -73,7 +73,7 @@ Open the CSS file tab to see the comments that explain how each height is comput
## CSS
-```sass
+```css
/* Explicit cell height */
height: 10;
diff --git a/docs/styles/keyline.md b/docs/styles/keyline.md
new file mode 100644
index 0000000000..77c2f43ed5
--- /dev/null
+++ b/docs/styles/keyline.md
@@ -0,0 +1,83 @@
+# Keyline
+
+The `keyline` style is applied to a container and will draw lines around child widgets.
+
+A keyline is superficially like the [border](./border.md) rule, but rather than draw inside the widget, a keyline is drawn outside of the widget's border. Additionally, unlike `border`, keylines can overlap and cross to create dividing lines between widgets.
+
+Because keylines are drawn in the widget's margin, you will need to apply the [margin](./margin.md) or [grid-gutter](./grid/grid_gutter.md) rule to see the effect.
+
+
+## Syntax
+
+--8<-- "docs/snippets/syntax_block_start.md"
+keyline: [<keyline>] [<color>];
+--8<-- "docs/snippets/syntax_block_end.md"
+
+
+## Examples
+
+### Horizontal Keyline
+
+The following examples shows a simple horizontal layout with a thin keyline.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/keyline_horizontal.py"}
+ ```
+
+=== "keyline.py"
+
+ ```python
+ --8<-- "docs/examples/styles/keyline_horizontal.py"
+ ```
+
+=== "keyline.tcss"
+
+ ```css
+ --8<-- "docs/examples/styles/keyline_horizontal.tcss"
+ ```
+
+
+
+### Grid keyline
+
+The following examples shows a grid layout with a *heavy* keyline.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/keyline.py"}
+ ```
+
+=== "keyline.py"
+
+ ```python
+ --8<-- "docs/examples/styles/keyline.py"
+ ```
+
+=== "keyline.tcss"
+
+ ```css
+ --8<-- "docs/examples/styles/keyline.tcss"
+ ```
+
+
+## CSS
+
+```css
+/* Set a thin green keyline */
+/* Note: Must be set on a container or a widget with a layout. */
+keyline: thin green;
+```
+
+## Python
+
+You can set a keyline in Python with a tuple of type and color:
+
+```python
+widget.styles.keyline = ("thin", "green")
+```
+
+
+## See also
+
+ - [`border`](./border.md) to add a border around a widget.
diff --git a/docs/styles/layer.md b/docs/styles/layer.md
index d1504dd592..bdb56b9be6 100644
--- a/docs/styles/layer.md
+++ b/docs/styles/layer.md
@@ -37,13 +37,13 @@ However, since `#box1` is on the higher layer, it is drawn on top of `#box2`.
=== "layers.tcss"
- ```sass hl_lines="3 14 19"
+ ```css hl_lines="3 14 19"
--8<-- "docs/examples/guide/layout/layers.tcss"
```
## CSS
-```sass
+```css
/* Draw the widget on the layer called 'below' */
layer: below;
```
diff --git a/docs/styles/layers.md b/docs/styles/layers.md
index 685b5659cf..c31f1f5d36 100644
--- a/docs/styles/layers.md
+++ b/docs/styles/layers.md
@@ -35,13 +35,13 @@ However, since `#box1` is on the higher layer, it is drawn on top of `#box2`.
=== "layers.tcss"
- ```sass hl_lines="3 14 19"
+ ```css hl_lines="3 14 19"
--8<-- "docs/examples/guide/layout/layers.tcss"
```
## CSS
-```sass
+```css
/* Bottom layer is called 'below', layer above it is called 'above' */
layers: below above;
```
diff --git a/docs/styles/layout.md b/docs/styles/layout.md
index deda25d0cf..590ea3ec5d 100644
--- a/docs/styles/layout.md
+++ b/docs/styles/layout.md
@@ -23,7 +23,7 @@ See the [layout](../guide/layout.md) guide for more information.
## Example
Note how the `layout` style affects the arrangement of widgets in the example below.
-To learn more about the grid layout, you can see the [layout guide](../guide/layout.md) or the [grid reference](./grid.md).
+To learn more about the grid layout, you can see the [layout guide](../guide/layout.md) or the [grid reference](../grid.md).
=== "Output"
@@ -38,13 +38,13 @@ To learn more about the grid layout, you can see the [layout guide](../guide/lay
=== "layout.tcss"
- ```sass hl_lines="2 8"
+ ```css hl_lines="2 8"
--8<-- "docs/examples/styles/layout.tcss"
```
## CSS
-```sass
+```css
layout: horizontal;
```
diff --git a/docs/styles/links/demos/link_hover_background_demo.gif b/docs/styles/links/demos/link_background_hover_demo.gif
similarity index 100%
rename from docs/styles/links/demos/link_hover_background_demo.gif
rename to docs/styles/links/demos/link_background_hover_demo.gif
diff --git a/docs/styles/links/demos/link_hover_color_demo.gif b/docs/styles/links/demos/link_color_hover_demo.gif
similarity index 100%
rename from docs/styles/links/demos/link_hover_color_demo.gif
rename to docs/styles/links/demos/link_color_hover_demo.gif
diff --git a/docs/styles/links/demos/link_hover_style_demo.gif b/docs/styles/links/demos/link_style_hover_demo.gif
similarity index 100%
rename from docs/styles/links/demos/link_hover_style_demo.gif
rename to docs/styles/links/demos/link_style_hover_demo.gif
diff --git a/docs/styles/links/index.md b/docs/styles/links/index.md
index f2984ba046..87070a5ba0 100644
--- a/docs/styles/links/index.md
+++ b/docs/styles/links/index.md
@@ -10,11 +10,11 @@ There are a number of styles which influence the appearance of these links withi
| Property | Description |
|-------------------------------------------------------|-------------------------------------------------------------------|
| [`link-background`](./link_background.md) | The background color of the link text. |
+| [`link-background-hover`](./link_background_hover.md) | The background color of the link text when the cursor is over it. |
| [`link-color`](./link_color.md) | The color of the link text. |
-| [`link-hover-background`](./link_hover_background.md) | The background color of the link text when the cursor is over it. |
-| [`link-hover-color`](./link_hover_color.md) | The color of the link text when the cursor is over it. |
-| [`link-hover-style`](./link_hover_style.md) | The style of the link text when the cursor is over it. |
+| [`link-color-hover`](./link_color_hover.md) | The color of the link text when the cursor is over it. |
| [`link-style`](./link_style.md) | The style of the link text (e.g. underline). |
+| [`link-style-hover`](./link_style_hover.md) | The style of the link text when the cursor is over it. |
## Syntax
@@ -25,11 +25,11 @@ There are a number of styles which influence the appearance of these links withi
link-style: <text-style>;
-link-hover-background: <color> [<percentage>];
+link-background-hover: <color> [<percentage>];
-link-hover-color: <color> [<percentage>];
+link-color-hover: <color> [<percentage>];
-link-hover-style: <text-style>;
+link-style-hover: <text-style>;
--8<-- "docs/snippets/syntax_block_end.md"
Visit each style's reference page to learn more about how the values are used.
@@ -52,7 +52,7 @@ The second label uses CSS to customize the link color, background, and style.
=== "links.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/links.tcss"
```
diff --git a/docs/styles/links/link_background.md b/docs/styles/links/link_background.md
index a9ccc96cbb..c6cbdc3ad3 100644
--- a/docs/styles/links/link_background.md
+++ b/docs/styles/links/link_background.md
@@ -26,18 +26,18 @@ It also shows that `link-background` does not affect hyperlinks.
=== "link_background.py"
- ```py hl_lines="8-9 12-13 16-17 20-21"
+ ```py hl_lines="10-11 14-15 18-20 22-23"
--8<-- "docs/examples/styles/link_background.py"
```
- 1. This label has an hyperlink so it won't be affected by the `link-background` rule.
+ 1. This label has a hyperlink so it won't be affected by the `link-background` rule.
2. This label has an "action link" that can be styled with `link-background`.
3. This label has an "action link" that can be styled with `link-background`.
4. This label has an "action link" that can be styled with `link-background`.
=== "link_background.tcss"
- ```sass hl_lines="2 6 10"
+ ```css hl_lines="2 6 10"
--8<-- "docs/examples/styles/link_background.tcss"
```
@@ -45,7 +45,7 @@ It also shows that `link-background` does not affect hyperlinks.
## CSS
-```sass
+```css
link-background: red 70%;
link-background: $accent;
```
@@ -63,4 +63,4 @@ widget.styles.link_background = Color(100, 30, 173)
## See also
- [`link-color`](./link_color.md) to set the color of link text.
- - [`link-hover-background](./link_hover_background.md) to set the background color of link text when the mouse pointer is over it.
+ - [`link-background-hover](./link_background_hover.md) to set the background color of link text when the mouse pointer is over it.
diff --git a/docs/styles/links/link_hover_background.md b/docs/styles/links/link_background_hover.md
similarity index 55%
rename from docs/styles/links/link_hover_background.md
rename to docs/styles/links/link_background_hover.md
index e396e0c615..2d8fbe463e 100644
--- a/docs/styles/links/link_hover_background.md
+++ b/docs/styles/links/link_background_hover.md
@@ -1,53 +1,53 @@
-# Link-hover-background
+# Link-background-hover
-The `link-hover-background` style sets the background color of the link when the mouse cursor is over the link.
+The `link-background-hover` style sets the background color of the link when the mouse cursor is over the link.
!!! note
- `link-hover-background` only applies to Textual action links as described in the [actions guide](../../guide/actions.md#links) and not to regular hyperlinks.
+ `link-background-hover` only applies to Textual action links as described in the [actions guide](../../guide/actions.md#links) and not to regular hyperlinks.
## Syntax
--8<-- "docs/snippets/syntax_block_start.md"
-link-hover-background: <color> [<percentage>];
+link-background-hover: <color> [<percentage>];
--8<-- "docs/snippets/syntax_block_end.md"
-`link-hover-background` accepts a [``](../../css_types/color.md) (with an optional opacity level defined by a [``](../../css_types/percentage.md)) that is used to define the background color of text enclosed in Textual action links when the mouse pointer is over it.
+`link-background-hover` accepts a [``](../../css_types/color.md) (with an optional opacity level defined by a [``](../../css_types/percentage.md)) that is used to define the background color of text enclosed in Textual action links when the mouse pointer is over it.
### Defaults
-If not provided, a Textual action link will have `link-hover-background` set to `$accent`.
+If not provided, a Textual action link will have `link-background-hover` set to `$accent`.
## Example
-The example below shows some links that have their background colour changed when the mouse moves over it and it shows that there is a default color for `link-hover-background`.
+The example below shows some links that have their background colour changed when the mouse moves over it and it shows that there is a default color for `link-background-hover`.
-It also shows that `link-hover-background` does not affect hyperlinks.
+It also shows that `link-background-hover` does not affect hyperlinks.
=== "Output"
- ![](./demos/link_hover_background_demo.gif)
+ ![](./demos/link_background_hover_demo.gif)
!!! note
The GIF has reduced quality to make it easier to load in the documentation.
- Try running the example yourself with `textual run docs/examples/styles/link_hover_background.py`.
+ Try running the example yourself with `textual run docs/examples/styles/link_background_hover.py`.
-=== "link_hover_background.py"
+=== "link_background_hover.py"
- ```py hl_lines="8-9 12-13 16-17 20-21"
- --8<-- "docs/examples/styles/link_hover_background.py"
+ ```py hl_lines="10-11 14-15 18-19 22-23"
+ --8<-- "docs/examples/styles/link_background_hover.py"
```
- 1. This label has an hyperlink so it won't be affected by the `link-hover-background` rule.
- 2. This label has an "action link" that can be styled with `link-hover-background`.
- 3. This label has an "action link" that can be styled with `link-hover-background`.
- 4. This label has an "action link" that can be styled with `link-hover-background`.
+ 1. This label has a hyperlink so it won't be affected by the `link-background-hover` rule.
+ 2. This label has an "action link" that can be styled with `link-background-hover`.
+ 3. This label has an "action link" that can be styled with `link-background-hover`.
+ 4. This label has an "action link" that can be styled with `link-background-hover`.
-=== "link_hover_background.tcss"
+=== "link_background_hover.tcss"
- ```sass hl_lines="2 6 10"
- --8<-- "docs/examples/styles/link_hover_background.tcss"
+ ```css hl_lines="2 6 10"
+ --8<-- "docs/examples/styles/link_background_hover.tcss"
```
1. This will only affect one of the labels because action links are the only links that this rule affects.
@@ -55,23 +55,23 @@ It also shows that `link-hover-background` does not affect hyperlinks.
## CSS
-```sass
-link-hover-background: red 70%;
-link-hover-background: $accent;
+```css
+link-background-hover: red 70%;
+link-background-hover: $accent;
```
## Python
```py
-widget.styles.link_hover_background = "red 70%"
-widget.styles.link_hover_background = "$accent"
+widget.styles.link_background_hover = "red 70%"
+widget.styles.link_background_hover = "$accent"
# You can also use a `Color` object directly:
-widget.styles.link_hover_background = Color(100, 30, 173)
+widget.styles.link_background_hover = Color(100, 30, 173)
```
## See also
- [`link-background`](./link_background.md) to set the background color of link text.
- - [`link-hover-color](./link_hover_color.md) to set the color of link text when the mouse pointer is over it.
- - [`link-hover-style](./link_hover_style.md) to set the style of link text when the mouse pointer is over it.
+ - [`link-color-hover](./link_color_hover.md) to set the color of link text when the mouse pointer is over it.
+ - [`link-style-hover](./link_style_hover.md) to set the style of link text when the mouse pointer is over it.
diff --git a/docs/styles/links/link_color.md b/docs/styles/links/link_color.md
index 44e0cd72ac..8d25fdd16d 100644
--- a/docs/styles/links/link_color.md
+++ b/docs/styles/links/link_color.md
@@ -26,18 +26,18 @@ It also shows that `link-color` does not affect hyperlinks.
=== "link_color.py"
- ```py hl_lines="8-9 12-13 16-17 20-21"
+ ```py hl_lines="10-11 14-15 18-19 22-23"
--8<-- "docs/examples/styles/link_color.py"
```
- 1. This label has an hyperlink so it won't be affected by the `link-color` rule.
+ 1. This label has a hyperlink so it won't be affected by the `link-color` rule.
2. This label has an "action link" that can be styled with `link-color`.
3. This label has an "action link" that can be styled with `link-color`.
4. This label has an "action link" that can be styled with `link-color`.
=== "link_color.tcss"
- ```sass hl_lines="2 6 10"
+ ```css hl_lines="2 6 10"
--8<-- "docs/examples/styles/link_color.tcss"
```
@@ -45,7 +45,7 @@ It also shows that `link-color` does not affect hyperlinks.
## CSS
-```sass
+```css
link-color: red 70%;
link-color: $accent;
```
@@ -63,4 +63,4 @@ widget.styles.link_color = Color(100, 30, 173)
## See also
- [`link-background`](./link_background.md) to set the background color of link text.
- - [`link-hover-color](./link_hover_color.md) to set the color of link text when the mouse pointer is over it.
+ - [`link-color-hover](./link_color_hover.md) to set the color of link text when the mouse pointer is over it.
diff --git a/docs/styles/links/link_hover_color.md b/docs/styles/links/link_color_hover.md
similarity index 53%
rename from docs/styles/links/link_hover_color.md
rename to docs/styles/links/link_color_hover.md
index b525647314..adb2fa78c2 100644
--- a/docs/styles/links/link_hover_color.md
+++ b/docs/styles/links/link_color_hover.md
@@ -1,80 +1,80 @@
-# Link-hover-color
+# Link-color-hover
-The `link-hover-color` style sets the color of the link text when the mouse cursor is over the link.
+The `link-color-hover` style sets the color of the link text when the mouse cursor is over the link.
!!! note
- `link-hover-color` only applies to Textual action links as described in the [actions guide](../../guide/actions.md#links) and not to regular hyperlinks.
+ `link-color-hover` only applies to Textual action links as described in the [actions guide](../../guide/actions.md#links) and not to regular hyperlinks.
## Syntax
--8<-- "docs/snippets/syntax_block_start.md"
-link-hover-color: <color> [<percentage>];
+link-color-hover: <color> [<percentage>];
--8<-- "docs/snippets/syntax_block_end.md"
-`link-hover-color` accepts a [``](../../css_types/color.md) (with an optional opacity level defined by a [``](../../css_types/percentage.md)) that is used to define the color of text enclosed in Textual action links when the mouse pointer is over it.
+`link-color-hover` accepts a [``](../../css_types/color.md) (with an optional opacity level defined by a [``](../../css_types/percentage.md)) that is used to define the color of text enclosed in Textual action links when the mouse pointer is over it.
### Defaults
-If not provided, a Textual action link will have `link-hover-color` set to `white`.
+If not provided, a Textual action link will have `link-color-hover` set to `white`.
## Example
The example below shows some links that have their colour changed when the mouse moves over it.
-It also shows that `link-hover-color` does not affect hyperlinks.
+It also shows that `link-color-hover` does not affect hyperlinks.
=== "Output"
- ![](./demos/link_hover_color_demo.gif)
+ ![](./demos/link_color_hover_demo.gif)
!!! note
The background color also changes when the mouse moves over the links because that is the default behavior.
- That can be customised by setting [`link-hover-background`](./link_hover_background.md) but we haven't done so in this example.
+ That can be customised by setting [`link-background-hover`](./link_background_hover.md) but we haven't done so in this example.
!!! note
The GIF has reduced quality to make it easier to load in the documentation.
- Try running the example yourself with `textual run docs/examples/styles/link_hover_color.py`.
+ Try running the example yourself with `textual run docs/examples/styles/link_color_hover.py`.
-=== "link_hover_color.py"
+=== "link_color_hover.py"
- ```py hl_lines="8-9 12-13 16-17 20-21"
- --8<-- "docs/examples/styles/link_hover_color.py"
+ ```py hl_lines="10-11 14-15 18-19 22-23"
+ --8<-- "docs/examples/styles/link_color_hover.py"
```
- 1. This label has an hyperlink so it won't be affected by the `link-hover-color` rule.
- 2. This label has an "action link" that can be styled with `link-hover-color`.
- 3. This label has an "action link" that can be styled with `link-hover-color`.
- 4. This label has an "action link" that can be styled with `link-hover-color`.
+ 1. This label has a hyperlink so it won't be affected by the `link-color-hover` rule.
+ 2. This label has an "action link" that can be styled with `link-color-hover`.
+ 3. This label has an "action link" that can be styled with `link-color-hover`.
+ 4. This label has an "action link" that can be styled with `link-color-hover`.
-=== "link_hover_color.tcss"
+=== "link_color_hover.tcss"
- ```sass hl_lines="2 6 10"
- --8<-- "docs/examples/styles/link_hover_color.tcss"
+ ```css hl_lines="2 6 10"
+ --8<-- "docs/examples/styles/link_color_hover.tcss"
```
1. This will only affect one of the labels because action links are the only links that this rule affects.
## CSS
-```sass
-link-hover-color: red 70%;
-link-hover-color: black;
+```css
+link-color-hover: red 70%;
+link-color-hover: black;
```
## Python
```py
-widget.styles.link_hover_color = "red 70%"
-widget.styles.link_hover_color = "black"
+widget.styles.link_color_hover = "red 70%"
+widget.styles.link_color_hover = "black"
# You can also use a `Color` object directly:
-widget.styles.link_hover_color = Color(100, 30, 173)
+widget.styles.link_color_hover = Color(100, 30, 173)
```
## See also
- [`link-color`](./link_color.md) to set the color of link text.
- - [`link-hover-background](./link_hover_background.md) to set the background color of link text when the mouse pointer is over it.
- - [`link-hover-style](./link_hover_style.md) to set the style of link text when the mouse pointer is over it.
+ - [`link-background-hover](./link_background_hover.md) to set the background color of link text when the mouse pointer is over it.
+ - [`link-style-hover](./link_style_hover.md) to set the style of link text when the mouse pointer is over it.
diff --git a/docs/styles/links/link_style.md b/docs/styles/links/link_style.md
index b5d100b8c5..dce4153fbe 100644
--- a/docs/styles/links/link_style.md
+++ b/docs/styles/links/link_style.md
@@ -30,18 +30,18 @@ It also shows that `link-style` does not affect hyperlinks.
=== "link_style.py"
- ```py hl_lines="8-9 12-13 16-17 20-21"
+ ```py hl_lines="10-11 14-15 18-19 22-23"
--8<-- "docs/examples/styles/link_style.py"
```
- 1. This label has an hyperlink so it won't be affected by the `link-style` rule.
+ 1. This label has a hyperlink so it won't be affected by the `link-style` rule.
2. This label has an "action link" that can be styled with `link-style`.
3. This label has an "action link" that can be styled with `link-style`.
4. This label has an "action link" that can be styled with `link-style`.
=== "link_style.tcss"
- ```sass hl_lines="2 6 10"
+ ```css hl_lines="2 6 10"
--8<-- "docs/examples/styles/link_style.tcss"
```
@@ -49,7 +49,7 @@ It also shows that `link-style` does not affect hyperlinks.
## CSS
-```sass
+```css
link-style: bold;
link-style: bold italic reverse;
```
@@ -63,5 +63,5 @@ widget.styles.link_style = "bold italic reverse"
## See also
- - [`link-hover-style](./link_hover_style.md) to set the style of link text when the mouse pointer is over it.
+ - [`link-style-hover](./link_style_hover.md) to set the style of link text when the mouse pointer is over it.
- [`text-style`](../text_style.md) to set the style of text in a widget.
diff --git a/docs/styles/links/link_hover_style.md b/docs/styles/links/link_style_hover.md
similarity index 55%
rename from docs/styles/links/link_hover_style.md
rename to docs/styles/links/link_style_hover.md
index 53cee01ec4..5598994f0f 100644
--- a/docs/styles/links/link_hover_style.md
+++ b/docs/styles/links/link_style_hover.md
@@ -1,57 +1,57 @@
-# Link-hover-style
+# Link-style-hover
-The `link-hover-style` style sets the text style for the link text when the mouse cursor is over the link.
+The `link-style-hover` style sets the text style for the link text when the mouse cursor is over the link.
!!! note
- `link-hover-style` only applies to Textual action links as described in the [actions guide](../../guide/actions.md#links) and not to regular hyperlinks.
+ `link-style-hover` only applies to Textual action links as described in the [actions guide](../../guide/actions.md#links) and not to regular hyperlinks.
## Syntax
--8<-- "docs/snippets/syntax_block_start.md"
-link-hover-style: <text-style>;
+link-style-hover: <text-style>;
--8<-- "docs/snippets/syntax_block_end.md"
-`link-hover-style` applies its [``](../../css_types/text_style.md) to the text of Textual action links when the mouse pointer is over them.
+`link-style-hover` applies its [``](../../css_types/text_style.md) to the text of Textual action links when the mouse pointer is over them.
### Defaults
-If not provided, a Textual action link will have `link-hover-style` set to `bold`.
+If not provided, a Textual action link will have `link-style-hover` set to `bold`.
## Example
The example below shows some links that have their colour changed when the mouse moves over it.
-It also shows that `link-hover-style` does not affect hyperlinks.
+It also shows that `link-style-hover` does not affect hyperlinks.
=== "Output"
- ![](./demos/link_hover_style_demo.gif)
+ ![](./demos/link_style_hover_demo.gif)
!!! note
The background color also changes when the mouse moves over the links because that is the default behavior.
- That can be customised by setting [`link-hover-background`](./link_hover_background.md) but we haven't done so in this example.
+ That can be customised by setting [`link-background-hover`](./link_background_hover.md) but we haven't done so in this example.
!!! note
The GIF has reduced quality to make it easier to load in the documentation.
- Try running the example yourself with `textual run docs/examples/styles/link_hover_style.py`.
+ Try running the example yourself with `textual run docs/examples/styles/link_style_hover.py`.
-=== "link_hover_style.py"
+=== "link_style_hover.py"
- ```py hl_lines="8-9 12-13 16-17 20-21"
- --8<-- "docs/examples/styles/link_hover_style.py"
+ ```py hl_lines="10-11 14-15 18-19 22-23"
+ --8<-- "docs/examples/styles/link_style_hover.py"
```
- 1. This label has an hyperlink so it won't be affected by the `link-hover-style` rule.
- 2. This label has an "action link" that can be styled with `link-hover-style`.
- 3. This label has an "action link" that can be styled with `link-hover-style`.
- 4. This label has an "action link" that can be styled with `link-hover-style`.
+ 1. This label has a hyperlink so it won't be affected by the `link-style-hover` rule.
+ 2. This label has an "action link" that can be styled with `link-style-hover`.
+ 3. This label has an "action link" that can be styled with `link-style-hover`.
+ 4. This label has an "action link" that can be styled with `link-style-hover`.
-=== "link_hover_style.tcss"
+=== "link_style_hover.tcss"
- ```sass hl_lines="2 6 10"
- --8<-- "docs/examples/styles/link_hover_style.tcss"
+ ```css hl_lines="2 6 10"
+ --8<-- "docs/examples/styles/link_style_hover.tcss"
```
1. This will only affect one of the labels because action links are the only links that this rule affects.
@@ -59,21 +59,21 @@ It also shows that `link-hover-style` does not affect hyperlinks.
## CSS
-```sass
-link-hover-style: bold;
-link-hover-style: bold italic reverse;
+```css
+link-style-hover: bold;
+link-style-hover: bold italic reverse;
```
## Python
```py
-widget.styles.link_hover_style = "bold"
-widget.styles.link_hover_style = "bold italic reverse"
+widget.styles.link_style_hover = "bold"
+widget.styles.link_style_hover = "bold italic reverse"
```
## See also
- - [`link-hover-background](./link_hover_background.md) to set the background color of link text when the mouse pointer is over it.
- - [`link-hover-color](./link_hover_color.md) to set the color of link text when the mouse pointer is over it.
+ - [`link-background-hover](./link_background_hover.md) to set the background color of link text when the mouse pointer is over it.
+ - [`link-color-hover](./link_color_hover.md) to set the color of link text when the mouse pointer is over it.
- [`link-style`](./link_style.md) to set the style of link text.
- [`text-style`](../text_style.md) to set the style of text in a widget.
diff --git a/docs/styles/margin.md b/docs/styles/margin.md
index a8f47832ea..e6b4b8b63e 100644
--- a/docs/styles/margin.md
+++ b/docs/styles/margin.md
@@ -51,7 +51,7 @@ In the example below we add a large margin to a label, which makes it move away
=== "margin.tcss"
- ```sass hl_lines="7"
+ ```css hl_lines="7"
--8<-- "docs/examples/styles/margin.tcss"
```
@@ -73,13 +73,13 @@ In each cell, we have a placeholder that has its margins set in different ways.
=== "margin_all.tcss"
- ```sass hl_lines="25 29 33 37 41 45 49 53"
+ ```css hl_lines="25 29 33 37 41 45 49 53"
--8<-- "docs/examples/styles/margin_all.tcss"
```
## CSS
-```sass
+```css
/* Set margin of 1 around all edges */
margin: 1;
/* Set margin of 2 on the top and bottom edges, and 4 on the left and right */
diff --git a/docs/styles/max_height.md b/docs/styles/max_height.md
index d23faa9bab..a90c7888c3 100644
--- a/docs/styles/max_height.md
+++ b/docs/styles/max_height.md
@@ -29,7 +29,7 @@ Then, we set `max-height` individually on each placeholder.
=== "max_height.tcss"
- ```sass hl_lines="12 16 20 24"
+ ```css hl_lines="12 16 20 24"
--8<-- "docs/examples/styles/max_height.tcss"
```
@@ -37,7 +37,7 @@ Then, we set `max-height` individually on each placeholder.
## CSS
-```sass
+```css
/* Set the maximum height to 10 rows */
max-height: 10;
diff --git a/docs/styles/max_width.md b/docs/styles/max_width.md
index 5d4596ad05..6c6e6b908a 100644
--- a/docs/styles/max_width.md
+++ b/docs/styles/max_width.md
@@ -29,7 +29,7 @@ Then, we set `max-width` individually on each placeholder.
=== "max_width.tcss"
- ```sass hl_lines="12 16 20 24"
+ ```css hl_lines="12 16 20 24"
--8<-- "docs/examples/styles/max_width.tcss"
```
@@ -37,7 +37,7 @@ Then, we set `max-width` individually on each placeholder.
## CSS
-```sass
+```css
/* Set the maximum width to 10 rows */
max-width: 10;
diff --git a/docs/styles/min_height.md b/docs/styles/min_height.md
index 6c23958cc1..9c98083a4a 100644
--- a/docs/styles/min_height.md
+++ b/docs/styles/min_height.md
@@ -29,7 +29,7 @@ Then, we set `min-height` individually on each placeholder.
=== "min_height.tcss"
- ```sass hl_lines="13 17 21 25"
+ ```css hl_lines="13 17 21 25"
--8<-- "docs/examples/styles/min_height.tcss"
```
@@ -37,7 +37,7 @@ Then, we set `min-height` individually on each placeholder.
## CSS
-```sass
+```css
/* Set the minimum height to 10 rows */
min-height: 10;
diff --git a/docs/styles/min_width.md b/docs/styles/min_width.md
index a8771fc0b3..1c08ee35c3 100644
--- a/docs/styles/min_width.md
+++ b/docs/styles/min_width.md
@@ -29,7 +29,7 @@ Then, we set `min-width` individually on each placeholder.
=== "min_width.tcss"
- ```sass hl_lines="13 17 21 25"
+ ```css hl_lines="13 17 21 25"
--8<-- "docs/examples/styles/min_width.tcss"
```
@@ -37,7 +37,7 @@ Then, we set `min-width` individually on each placeholder.
## CSS
-```sass
+```css
/* Set the minimum width to 10 rows */
min-width: 10;
diff --git a/docs/styles/offset.md b/docs/styles/offset.md
index 47c836166b..eee43e81be 100644
--- a/docs/styles/offset.md
+++ b/docs/styles/offset.md
@@ -32,13 +32,13 @@ In this example, we have 3 widgets with differing offsets.
=== "offset.tcss"
- ```sass hl_lines="13 20 27"
+ ```css hl_lines="13 20 27"
--8<-- "docs/examples/styles/offset.tcss"
```
## CSS
-```sass
+```css
/* Move the widget 8 cells in the x direction and 2 in the y direction */
offset: 8 2;
diff --git a/docs/styles/opacity.md b/docs/styles/opacity.md
index 69b657401e..b5e6a8b2a1 100644
--- a/docs/styles/opacity.md
+++ b/docs/styles/opacity.md
@@ -36,13 +36,13 @@ When the opacity is zero, all we see is the (black) background.
=== "opacity.tcss"
- ```sass hl_lines="2 6 10 14 18"
+ ```css hl_lines="2 6 10 14 18"
--8<-- "docs/examples/styles/opacity.tcss"
```
## CSS
-```sass
+```css
/* Fade the widget to 50% against its parent's background */
opacity: 50%;
```
diff --git a/docs/styles/outline.md b/docs/styles/outline.md
index b3fb75ff2a..66e760bf96 100644
--- a/docs/styles/outline.md
+++ b/docs/styles/outline.md
@@ -50,7 +50,7 @@ Note how the outline occludes the text area.
=== "outline.tcss"
- ```sass hl_lines="8"
+ ```css hl_lines="8"
--8<-- "docs/examples/styles/outline.tcss"
```
@@ -71,7 +71,7 @@ The next example shows a grid with all the available outline types.
=== "outline_all.tcss"
- ```sass hl_lines="2 6 10 14 18 22 26 30 34 38 42 46 50 54 58"
+ ```css hl_lines="2 6 10 14 18 22 26 30 34 38 42 46 50 54 58"
--8<-- "docs/examples/styles/outline_all.tcss"
```
@@ -81,7 +81,7 @@ The next example shows a grid with all the available outline types.
## CSS
-```sass
+```css
/* Set a heavy white outline */
outline:heavy white;
diff --git a/docs/styles/overflow.md b/docs/styles/overflow.md
index d4807ae4dd..caf3b9ccdc 100644
--- a/docs/styles/overflow.md
+++ b/docs/styles/overflow.md
@@ -47,13 +47,13 @@ The right side has `overflow-y: hidden` which will prevent a scrollbar from bein
=== "overflow.tcss"
- ```sass hl_lines="19"
+ ```css hl_lines="19"
--8<-- "docs/examples/styles/overflow.tcss"
```
## CSS
-```sass
+```css
/* Automatic scrollbars on both axes (the default) */
overflow: auto auto;
diff --git a/docs/styles/padding.md b/docs/styles/padding.md
index a26d767b9a..ca34e84a79 100644
--- a/docs/styles/padding.md
+++ b/docs/styles/padding.md
@@ -50,7 +50,7 @@ This example adds padding around some text.
=== "padding.tcss"
- ```sass hl_lines="7"
+ ```css hl_lines="7"
--8<-- "docs/examples/styles/padding.tcss"
```
@@ -73,13 +73,13 @@ The effect of each padding setting is noticeable in the colored background aroun
=== "padding_all.tcss"
- ```sass hl_lines="16 20 24 28 32 36 40 44"
+ ```css hl_lines="16 20 24 28 32 36 40 44"
--8<-- "docs/examples/styles/padding_all.tcss"
```
## CSS
-```sass
+```css
/* Set padding of 1 around all edges */
padding: 1;
/* Set padding of 2 on the top and bottom edges, and 4 on the left and right */
diff --git a/docs/styles/scrollbar_colors/index.md b/docs/styles/scrollbar_colors/index.md
index c0ef25a37e..1ee1b04de0 100644
--- a/docs/styles/scrollbar_colors/index.md
+++ b/docs/styles/scrollbar_colors/index.md
@@ -51,6 +51,6 @@ The right panel sets `scrollbar-background`, `scrollbar-color`, and `scrollbar-c
=== "scrollbars.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/scrollbars.tcss"
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_background.md b/docs/styles/scrollbar_colors/scrollbar_background.md
index 5dff38c3e1..443bf84189 100644
--- a/docs/styles/scrollbar_colors/scrollbar_background.md
+++ b/docs/styles/scrollbar_colors/scrollbar_background.md
@@ -28,13 +28,13 @@ The `scrollbar-background` style sets the background color of the scrollbar.
=== "scrollbars2.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/styles/scrollbars2.tcss"
```
## CSS
-```sass
+```css
scrollbar-backround: blue;
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_background_active.md b/docs/styles/scrollbar_colors/scrollbar_background_active.md
index 41e687f582..f54e9e6e3b 100644
--- a/docs/styles/scrollbar_colors/scrollbar_background_active.md
+++ b/docs/styles/scrollbar_colors/scrollbar_background_active.md
@@ -29,13 +29,13 @@ The `scrollbar-background-active` style sets the background color of the scrollb
=== "scrollbars2.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/scrollbars2.tcss"
```
## CSS
-```sass
+```css
scrollbar-backround-active: red;
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_background_hover.md b/docs/styles/scrollbar_colors/scrollbar_background_hover.md
index caaa552a10..b02d8b25ef 100644
--- a/docs/styles/scrollbar_colors/scrollbar_background_hover.md
+++ b/docs/styles/scrollbar_colors/scrollbar_background_hover.md
@@ -29,13 +29,13 @@ The `scrollbar-background-hover` style sets the background color of the scrollba
=== "scrollbars2.tcss"
- ```sass hl_lines="4"
+ ```css hl_lines="4"
--8<-- "docs/examples/styles/scrollbars2.tcss"
```
## CSS
-```sass
+```css
scrollbar-background-hover: purple;
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_color.md b/docs/styles/scrollbar_colors/scrollbar_color.md
index dac2d0daa7..f5c2ea9d95 100644
--- a/docs/styles/scrollbar_colors/scrollbar_color.md
+++ b/docs/styles/scrollbar_colors/scrollbar_color.md
@@ -29,13 +29,13 @@ The `scrollbar-color` style sets the color of the scrollbar.
=== "scrollbars2.tcss"
- ```sass hl_lines="5"
+ ```css hl_lines="5"
--8<-- "docs/examples/styles/scrollbars2.tcss"
```
## CSS
-```sass
+```css
scrollbar-color: cyan;
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_color_active.md b/docs/styles/scrollbar_colors/scrollbar_color_active.md
index 34ffeff813..3e373e303d 100644
--- a/docs/styles/scrollbar_colors/scrollbar_color_active.md
+++ b/docs/styles/scrollbar_colors/scrollbar_color_active.md
@@ -29,13 +29,13 @@ The `scrollbar-color-active` style sets the color of the scrollbar when the thum
=== "scrollbars2.tcss"
- ```sass hl_lines="6"
+ ```css hl_lines="6"
--8<-- "docs/examples/styles/scrollbars2.tcss"
```
## CSS
-```sass
+```css
scrollbar-color-active: yellow;
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_color_hover.md b/docs/styles/scrollbar_colors/scrollbar_color_hover.md
index 25e06b436e..fb88b47acf 100644
--- a/docs/styles/scrollbar_colors/scrollbar_color_hover.md
+++ b/docs/styles/scrollbar_colors/scrollbar_color_hover.md
@@ -29,13 +29,13 @@ The `scrollbar-color-hover` style sets the color of the scrollbar when the curso
=== "scrollbars2.tcss"
- ```sass hl_lines="7"
+ ```css hl_lines="7"
--8<-- "docs/examples/styles/scrollbars2.tcss"
```
## CSS
-```sass
+```css
scrollbar-color-hover: pink;
```
diff --git a/docs/styles/scrollbar_colors/scrollbar_corner_color.md b/docs/styles/scrollbar_colors/scrollbar_corner_color.md
index 7482cd62a1..8c2c9c3140 100644
--- a/docs/styles/scrollbar_colors/scrollbar_corner_color.md
+++ b/docs/styles/scrollbar_colors/scrollbar_corner_color.md
@@ -27,13 +27,13 @@ The example below sets the scrollbar corner (bottom-right corner of the screen)
=== "scrollbar_corner_color.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/scrollbar_corner_color.tcss"
```
## CSS
-```sass
+```css
scrollbar-corner-color: white;
```
diff --git a/docs/styles/scrollbar_gutter.md b/docs/styles/scrollbar_gutter.md
index 1666f8a03a..22cd8069c7 100644
--- a/docs/styles/scrollbar_gutter.md
+++ b/docs/styles/scrollbar_gutter.md
@@ -35,13 +35,13 @@ terminal window.
=== "scrollbar_gutter.tcss"
- ```sass hl_lines="2"
+ ```css hl_lines="2"
--8<-- "docs/examples/styles/scrollbar_gutter.tcss"
```
## CSS
-```sass
+```css
scrollbar-gutter: auto; /* Don't reserve space for a vertical scrollbar. */
scrollbar-gutter: stable; /* Reserve space for a vertical scrollbar. */
```
diff --git a/docs/styles/scrollbar_size.md b/docs/styles/scrollbar_size.md
index cd392b1fcc..25281214bd 100644
--- a/docs/styles/scrollbar_size.md
+++ b/docs/styles/scrollbar_size.md
@@ -36,7 +36,7 @@ In this example we modify the size of the widget's scrollbar to be _much_ larger
=== "scrollbar_size.tcss"
- ```sass hl_lines="13"
+ ```css hl_lines="13"
--8<-- "docs/examples/styles/scrollbar_size.tcss"
```
@@ -44,6 +44,11 @@ In this example we modify the size of the widget's scrollbar to be _much_ larger
In the next example we show three containers with differently sized scrollbars.
+!!! tip
+
+ If you want to hide the scrollbar but still allow the container to scroll
+ using the mousewheel or keyboard, you can set the scrollbar size to `0`.
+
=== "Output"
```{.textual path="docs/examples/styles/scrollbar_size2.py"}
@@ -57,13 +62,13 @@ In the next example we show three containers with differently sized scrollbars.
=== "scrollbar_size2.tcss"
- ```sass hl_lines="6 11 16"
+ ```css hl_lines="6 11 16"
--8<-- "docs/examples/styles/scrollbar_size2.tcss"
```
## CSS
-```sass
+```css
/* Set horizontal scrollbar to 10, and vertical scrollbar to 4 */
scrollbar-size: 10 4;
diff --git a/docs/styles/text_align.md b/docs/styles/text_align.md
index d503f6de2a..f0a3977b1a 100644
--- a/docs/styles/text_align.md
+++ b/docs/styles/text_align.md
@@ -31,7 +31,7 @@ This example shows, from top to bottom: `left`, `center`, `right`, and `justify`
=== "text_align.tcss"
- ```sass hl_lines="2 7 12 17"
+ ```css hl_lines="2 7 12 17"
--8<-- "docs/examples/styles/text_align.tcss"
```
@@ -39,7 +39,7 @@ This example shows, from top to bottom: `left`, `center`, `right`, and `justify`
## CSS
-```sass
+```css
/* Set text in the widget to be right aligned */
text-align: right;
```
diff --git a/docs/styles/text_opacity.md b/docs/styles/text_opacity.md
index d178800c32..5c21cf7f92 100644
--- a/docs/styles/text_opacity.md
+++ b/docs/styles/text_opacity.md
@@ -38,13 +38,13 @@ This example shows, from top to bottom, increasing `text-opacity` values.
=== "text_opacity.tcss"
- ```sass hl_lines="2 6 10 14 18"
+ ```css hl_lines="2 6 10 14 18"
--8<-- "docs/examples/styles/text_opacity.tcss"
```
## CSS
-```sass
+```css
/* Set the text to be "half-faded" against the background of the widget */
text-opacity: 50%;
```
diff --git a/docs/styles/text_style.md b/docs/styles/text_style.md
index e684b440e1..d252140b35 100644
--- a/docs/styles/text_style.md
+++ b/docs/styles/text_style.md
@@ -29,7 +29,7 @@ Each of the three text panels has a different text style, respectively `bold`, `
=== "text_style.tcss"
- ```sass hl_lines="9 13 17"
+ ```css hl_lines="9 13 17"
--8<-- "docs/examples/styles/text_style.tcss"
```
@@ -50,13 +50,13 @@ The next example shows all different text styles on their own, as well as some c
=== "text_style_all.tcss"
- ```sass hl_lines="2 6 10 14 18 22 26 30"
+ ```css hl_lines="2 6 10 14 18 22 26 30"
--8<-- "docs/examples/styles/text_style_all.tcss"
```
## CSS
-```sass
+```css
text-style: italic;
```
diff --git a/docs/styles/tint.md b/docs/styles/tint.md
index cc2b29f46b..b692dacbf8 100644
--- a/docs/styles/tint.md
+++ b/docs/styles/tint.md
@@ -21,7 +21,7 @@ This examples shows a green tint with gradually increasing alpha.
=== "tint.py"
- ```python hl_lines="11"
+ ```python hl_lines="13"
--8<-- "docs/examples/styles/tint.py"
```
@@ -29,13 +29,13 @@ This examples shows a green tint with gradually increasing alpha.
=== "tint.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/styles/tint.tcss"
```
## CSS
-```sass
+```css
/* A red tint (could indicate an error) */
tint: red 20%;
diff --git a/docs/styles/visibility.md b/docs/styles/visibility.md
index b80105a48c..78dfe932c4 100644
--- a/docs/styles/visibility.md
+++ b/docs/styles/visibility.md
@@ -47,7 +47,7 @@ Note that the second widget is hidden while leaving a space where it would have
=== "visibility.tcss"
- ```sass hl_lines="14"
+ ```css hl_lines="14"
--8<-- "docs/examples/styles/visibility.tcss"
```
@@ -74,7 +74,7 @@ The containers all have a white background, and then:
=== "visibility_containers.tcss"
- ```sass hl_lines="2-3 6 8-10 12-14 16-18"
+ ```css hl_lines="2-3 7 9-11 13-15 17-19"
--8<-- "docs/examples/styles/visibility_containers.tcss"
```
@@ -86,7 +86,7 @@ The containers all have a white background, and then:
## CSS
-```sass
+```css
/* Widget is invisible */
visibility: hidden;
diff --git a/docs/styles/width.md b/docs/styles/width.md
index a0f7553bac..d694b0169a 100644
--- a/docs/styles/width.md
+++ b/docs/styles/width.md
@@ -30,7 +30,7 @@ This example adds a widget with 50% width of the screen.
=== "width.tcss"
- ```sass hl_lines="3"
+ ```css hl_lines="3"
--8<-- "docs/examples/styles/width.tcss"
```
@@ -51,7 +51,7 @@ This example adds a widget with 50% width of the screen.
=== "width_comparison.tcss"
- ```sass hl_lines="2 5 8 11 14 17 20 23 26"
+ ```css hl_lines="2 5 8 11 14 17 20 23 26"
--8<-- "docs/examples/styles/width_comparison.tcss"
```
@@ -74,7 +74,7 @@ This example adds a widget with 50% width of the screen.
## CSS
-```sass
+```css
/* Explicit cell width */
width: 10;
diff --git a/docs/stylesheets/custom.css b/docs/stylesheets/custom.css
index ee10d06544..6d54ce45b4 100644
--- a/docs/stylesheets/custom.css
+++ b/docs/stylesheets/custom.css
@@ -14,6 +14,7 @@ h3 .doc-heading code {
}
body[data-md-color-primary="black"] .excalidraw svg {
+ will-change: filter;
filter: invert(100%) hue-rotate(180deg);
}
@@ -72,3 +73,45 @@ td code {
opacity: 0.85;
}
+
+.textual-web-demo iframe {
+ border: none;
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ padding: 0;
+ margin: 0;
+}
+
+
+.textual-web-demo {
+ display: flex;
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ padding: 0;
+ margin: 0;
+ opacity: 0;
+ transition: 0.3s opacity;
+}
+
+.textual-web-demo.-loaded {
+ opacity: 1.0;
+ transition: 0.3s opacity;
+}
+
+:root {
+ --md-admonition-icon--textualize: url('/images/icons/logo light transparent.svg')
+}
+.md-typeset .admonition.textualize,
+.md-typeset details.textualize {
+ border-color: rgb(43, 155, 70);
+}
+.md-typeset .textualize > .admonition-title,
+.md-typeset .textualize > summary {
+ background-color: rgba(43, 155, 70, 0.1);
+}
+.md-typeset .textualize > .admonition-title::before,
+.md-typeset .textualize > summary::before {
+ background-color: rgb(43, 155, 70);
+ -webkit-mask-image: var(--md-admonition-icon--textualize);
+ mask-image: var(--md-admonition-icon--textualize);
+}
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 6c875aa059..ce7ed15d5a 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -31,6 +31,21 @@ Here's what the finished app will look like:
```{.textual path="docs/examples/tutorial/stopwatch.py" title="stopwatch.py" press="tab,enter,tab,enter,tab,enter,tab,enter"}
```
+### Try it out!
+
+The following is *not* a screenshot, but a fully interactive Textual app running in your browser.
+
+
+!!! textualize "Try in Textual-web"
+
+
+
+
+!!! tip
+
+ See [textual-web](https://github.com/Textualize/textual-web) if you are interested in publishing your Textual apps on the web.
+
+
### Get the code
If you want to try the finished Stopwatch app and follow along with the code, first make sure you have [Textual installed](getting_started.md) then check out the [Textual](https://github.com/Textualize/textual) repository:
@@ -216,7 +231,7 @@ Let's add a CSS file to our application.
Adding the `CSS_PATH` class variable tells Textual to load the following file when the app starts:
-```sass title="stopwatch03.tcss"
+```css title="stopwatch03.tcss"
--8<-- "docs/examples/tutorial/stopwatch03.tcss"
```
@@ -231,7 +246,7 @@ This app looks much more like our sketch. Let's look at how Textual uses `stopwa
CSS files contain a number of _declaration blocks_. Here's the first such block from `stopwatch03.tcss` again:
-```sass
+```css
Stopwatch {
layout: horizontal;
background: $boost;
@@ -260,7 +275,7 @@ Here's how this CSS code changes how the `Stopwatch` widget is displayed.
Here's the rest of `stopwatch03.tcss` which contains further declaration blocks:
-```sass
+```css
TimeDisplay {
content-align: center middle;
opacity: 60%;
@@ -308,7 +323,7 @@ We can accomplish this with a CSS _class_. Not to be confused with a Python clas
Here's the new CSS:
-```sass title="stopwatch04.tcss" hl_lines="33-53"
+```css title="stopwatch04.tcss" hl_lines="33-53"
--8<-- "docs/examples/tutorial/stopwatch04.tcss"
```
@@ -316,7 +331,7 @@ These new rules are prefixed with `.started`. The `.` indicates that `.started`
Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:
-```sass
+```css
.started #start {
display: none
}
diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md
index 559fc62929..ca82b5d4e3 100644
--- a/docs/widget_gallery.md
+++ b/docs/widget_gallery.md
@@ -281,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 }
@@ -307,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..497db695da 100644
--- a/docs/widgets/_template.md
+++ b/docs/widgets/_template.md
@@ -25,13 +25,14 @@ Example app showing the widget:
=== "checkbox.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/checkbox.tcss"
```
-## Reactive attributes
+## Reactive Attributes
+## Messages
## Bindings
diff --git a/docs/widgets/button.md b/docs/widgets/button.md
index 290895d374..4e8c7b01c5 100644
--- a/docs/widgets/button.md
+++ b/docs/widgets/button.md
@@ -25,7 +25,7 @@ Clicking any of the non-disabled buttons in the example app below will result in
=== "button.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/button.tcss"
```
@@ -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..fd47c918d6 100644
--- a/docs/widgets/checkbox.md
+++ b/docs/widgets/checkbox.md
@@ -24,7 +24,7 @@ The example below shows check boxes in various states.
=== "checkbox.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/checkbox.tcss"
```
@@ -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
index 6ff479582d..9ba79f8b47 100644
--- a/docs/widgets/collapsible.md
+++ b/docs/widgets/collapsible.md
@@ -120,11 +120,29 @@ The following example shows `Collapsible` widgets with custom expand/collapse sy
--8<-- "docs/examples/widgets/collapsible_custom_symbol.py"
```
-## Reactive attributes
+## Reactive Attributes
-| Name | Type | Default | Description |
-| ----------- | ------ | ------- | ---------------------------------------------------- |
-| `collapsed` | `bool` | `True` | Controls the collapsed/expanded state of the widget. |
+| Name | Type | Default | Description |
+| ----------- | ------ | ------------| ---------------------------------------------------- |
+| `collapsed` | `bool` | `True` | Controls the collapsed/expanded state of the widget. |
+| `title` | `str` | `"Toggle"` | Title of the collapsed/expanded contents. |
+
+## Messages
+
+- [Collapsible.Toggled][textual.widgets.Collapsible.Toggled]
+
+## 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
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/data_table.md b/docs/widgets/data_table.md
index 05c7409982..ab1981c0f1 100644
--- a/docs/widgets/data_table.md
+++ b/docs/widgets/data_table.md
@@ -10,7 +10,7 @@ A table widget optimized for displaying a lot of data.
### Adding data
The following example shows how to fill a table with data.
-First, we use [add_columns][textual.widgets.DataTable.add_rows] to include the `lane`, `swimmer`, `country`, and `time` columns in the table.
+First, we use [add_columns][textual.widgets.DataTable.add_columns] to include the `lane`, `swimmer`, `country`, and `time` columns in the table.
After that, we use the [add_rows][textual.widgets.DataTable.add_rows] method to insert the rows into the table.
=== "Output"
@@ -143,11 +143,22 @@ visible as you scroll through the data table.
### Sorting
-The `DataTable` can be sorted using the [sort][textual.widgets.DataTable.sort] method.
-In order to sort your data by a column, you must have supplied a `key` to the `add_column` method
-when you added it.
-You can then pass this key to the `sort` method to sort by that column.
-Additionally, you can sort by multiple columns by passing multiple keys to `sort`.
+The `DataTable` can be sorted using the [sort][textual.widgets.DataTable.sort] method. In order to sort your data by a column, you can provide the `key` you supplied to the `add_column` method or a `ColumnKey`. You can then pass one more column keys to the `sort` method to sort by one or more columns.
+
+Additionally, you can sort your `DataTable` with a custom function (or other callable) via the `key` argument. Similar to the `key` parameter of the built-in [sorted()](https://docs.python.org/3/library/functions.html#sorted) function, your function (or other callable) should take a single argument (row) and return a key to use for sorting purposes.
+
+Providing both `columns` and `key` will limit the row information sent to your `key` function (or other callable) to only the columns specified.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/data_table_sort.py"}
+ ```
+
+=== "data_table_sort.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/data_table_sort.py"
+ ```
### Labelled rows
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..1a7b8aa65f 100644
--- a/docs/widgets/input.md
+++ b/docs/widgets/input.md
@@ -22,6 +22,47 @@ The example below shows how you might create a simple form using two `Input` wid
--8<-- "docs/examples/widgets/input.py"
```
+
+### Input Types
+
+The `Input` widget supports a `type` parameter which will prevent the user from typing invalid characters.
+You can set `type` to any of the following values:
+
+
+| input.type | Description |
+| ----------- | ------------------------------------------- |
+| `"integer"` | Restricts input to integers. |
+| `"number"` | Restricts input to a floating point number. |
+| `"text"` | Allow all text (no restrictions). |
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/input_types.py" press="1234"}
+ ```
+
+=== "input_types.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/input_types.py"
+ ```
+
+If you set `type` to something other than `"text"`, then the `Input` will apply the appropriate [validator](#validating-input).
+
+### Restricting Input
+
+You can limit input to particular characters by supplying the `restrict` parameter, which should be a regular expression.
+The `Input` widget will prevent the addition of any characters that would cause the regex to no longer match.
+For instance, if you wanted to limit characters to binary you could set `restrict=r"[01]*"`.
+
+!!! note
+
+ The `restrict` regular expression is applied to the full value and not just to the new character.
+
+### Maximum Length
+
+You can limit the length of the input by setting `max_length` to a value greater than zero.
+This will prevent the user from typing any more characters when the maximum has been reached.
+
### Validating Input
You can supply one or more *[validators][textual.validation.Validator]* to the `Input` widget to validate the value.
@@ -71,15 +112,23 @@ Textual offers several [built-in validators][textual.validation] for common requ
but you can easily roll your own by extending [Validator][textual.validation.Validator],
as seen for `Palindrome` in the example above.
+#### Validate Empty
+
+If you set `valid_empty=True` then empty values will bypass any validators, and empty values will be considered valid.
+
## Reactive Attributes
-| Name | Type | Default | Description |
-|-------------------|--------|---------|-----------------------------------------------------------------|
-| `cursor_blink` | `bool` | `True` | True if cursor blinking is enabled. |
-| `value` | `str` | `""` | The value currently in the text input. |
-| `cursor_position` | `int` | `0` | The index of the cursor in the value string. |
-| `placeholder` | `str` | `str` | The dimmed placeholder text to display when the input is empty. |
-| `password` | `bool` | `False` | True if the input should be masked. |
+| Name | Type | Default | Description |
+| ----------------- | ------ | -------- | --------------------------------------------------------------- |
+| `cursor_blink` | `bool` | `True` | True if cursor blinking is enabled. |
+| `value` | `str` | `""` | The value currently in the text input. |
+| `cursor_position` | `int` | `0` | The index of the cursor in the value string. |
+| `placeholder` | `str` | `""` | The dimmed placeholder text to display when the input is empty. |
+| `password` | `bool` | `False` | True if the input should be masked. |
+| `restrict` | `str` | `None` | Optional regular expression to restrict input. |
+| `type` | `str` | `"text"` | The type of the input. |
+| `max_length` | `int` | `None` | Maximum length of the input value. |
+| `valid_empty` | `bool` | `False` | Allow empty values to bypass validation. |
## Messages
@@ -88,7 +137,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..ad92a3dd40 100644
--- a/docs/widgets/list_view.md
+++ b/docs/widgets/list_view.md
@@ -25,15 +25,15 @@ The example below shows an app with a simple `ListView`.
=== "list_view.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/list_view.tcss"
```
## 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..0e2f6f43fe 100644
--- a/docs/widgets/loading_indicator.md
+++ b/docs/widgets/loading_indicator.md
@@ -7,27 +7,49 @@ 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:
-```sass
+```css
LoadingIndicator {
color: red;
}
```
+## 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..b4bd53ba51 100644
--- a/docs/widgets/markdown.md
+++ b/docs/widgets/markdown.md
@@ -4,7 +4,7 @@
A widget to display a Markdown document.
-- [x] Focusable
+- [ ] Focusable
- [ ] Container
@@ -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..cb209b2801 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
@@ -45,3 +57,9 @@ The following example displays Markdown from a string and a Table of Contents.
::: textual.widgets.MarkdownViewer
options:
heading_level: 2
+
+
+::: textual.widgets.markdown
+ options:
+ show_root_heading: true
+ show_root_toc_entry: true
diff --git a/docs/widgets/option_list.md b/docs/widgets/option_list.md
index b0a3170857..100c3052aa 100644
--- a/docs/widgets/option_list.md
+++ b/docs/widgets/option_list.md
@@ -90,7 +90,7 @@ tables](https://rich.readthedocs.io/en/latest/tables.html):
- [OptionList.OptionHighlighted][textual.widgets.OptionList.OptionHighlighted]
- [OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected]
-Both of the messages above inherit from the common base [`OptionList`][textual.widgets.OptionList.OptionMessage], so refer to its documentation to see what attributes are available.
+Both of the messages above inherit from the common base [`OptionList.OptionMessage`][textual.widgets.OptionList.OptionMessage], so refer to its documentation to see what attributes are available.
## Bindings
diff --git a/docs/widgets/placeholder.md b/docs/widgets/placeholder.md
index c566b871dd..9dce8ac0a6 100644
--- a/docs/widgets/placeholder.md
+++ b/docs/widgets/placeholder.md
@@ -28,7 +28,7 @@ The example below shows each placeholder variant.
=== "placeholder.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/placeholder.tcss"
```
@@ -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..ba3f5a346f 100644
--- a/docs/widgets/progress_bar.md
+++ b/docs/widgets/progress_bar.md
@@ -67,7 +67,7 @@ The example below shows a simple app with a progress bar that is keeping track o
=== "progress_bar.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/progress_bar.tcss"
```
@@ -100,19 +100,10 @@ Refer to the [section below](#styling-the-progress-bar) for more information.
=== "progress_bar_styled.tcss"
- ```sass
+ ```css
--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..aa2b1c7205 100644
--- a/docs/widgets/radiobutton.md
+++ b/docs/widgets/radiobutton.md
@@ -26,7 +26,7 @@ The example below shows radio buttons, used within a [`RadioSet`](./radioset.md)
=== "radio_button.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/radio_button.tcss"
```
@@ -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..4285b4ffa0 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.
@@ -25,15 +27,11 @@ The example below shows two radio sets, one built using a collection of
=== "radio_set.tcss"
- ```sass
+ ```css
--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`:
@@ -50,10 +48,27 @@ Here is an example of using the message to react to changes in a `RadioSet`:
=== "radio_set_changed.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/radio_set_changed.tcss"
```
+## Messages
+
+- [RadioSet.Changed][textual.widgets.RadioSet.Changed]
+
+## Bindings
+
+The `RadioSet` widget defines the following bindings:
+
+::: textual.widgets.RadioSet.BINDINGS
+ options:
+ show_root_heading: false
+ show_root_toc_entry: false
+
+## 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..5cf4cec983 100644
--- a/docs/widgets/rule.md
+++ b/docs/widgets/rule.md
@@ -26,7 +26,7 @@ The example below shows horizontal rules with all the available line styles.
=== "horizontal_rules.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/horizontal_rules.tcss"
```
@@ -47,7 +47,7 @@ The example below shows vertical rules with all the available line styles.
=== "vertical_rules.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/vertical_rules.tcss"
```
@@ -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..2ba1f1bb72 100644
--- a/docs/widgets/select.md
+++ b/docs/widgets/select.md
@@ -31,7 +31,9 @@ my_select: Select[int] = Select(options)
If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.
-## Example
+## Examples
+
+### Basic Example
The following example presents a `Select` with a number of options.
@@ -45,7 +47,6 @@ The following example presents a `Select` with a number of options.
```{.textual path="docs/examples/widgets/select_widget.py" press="tab,enter,down,down"}
```
-
=== "select_widget.py"
```python
@@ -54,23 +55,55 @@ The following example presents a `Select` with a number of options.
=== "select.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/select.tcss"
```
-## Messages
+### Example using Class Method
-- [Select.Changed][textual.widgets.Select.Changed]
+The following example presents a `Select` created using the `from_values` class method.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/select_from_values_widget.py"}
+ ```
+=== "Output (expanded)"
+
+ ```{.textual path="docs/examples/widgets/select_from_values_widget.py" press="tab,enter,down,down"}
+ ```
-## Reactive attributes
+=== "select_from_values_widget.py"
-| Name | Type | Default | Description |
-|------------|------------------------|---------|-------------------------------------|
-| `expanded` | `bool` | `False` | True to expand the options overlay. |
-| `value` | `SelectType` \| `None` | `None` | Current value of the Select. |
+ ```python
+ --8<-- "docs/examples/widgets/select_from_values_widget.py"
+ ```
+=== "select.tcss"
+
+ ```css
+ --8<-- "docs/examples/widgets/select.tcss"
+ ```
+
+## Blank state
+
+The widget `Select` has an option `allow_blank` for its constructor.
+If set to `True`, the widget may be in a state where there is no selection, in which case its value will be the special constant [`Select.BLANK`][textual.widgets.Select.BLANK].
+The auxiliary methods [`Select.is_blank`][textual.widgets.Select.is_blank] and [`Select.clear`][textual.widgets.Select.clear] provide a convenient way to check if the widget is in this state and to set this state, respectively.
+
+
+## Reactive Attributes
+
+
+| Name | Type | Default | Description |
+|------------|--------------------------------|------------------------------------------------|-------------------------------------|
+| `expanded` | `bool` | `False` | True to expand the options overlay. |
+| `value` | `SelectType` \| `_NoSelection` | [`Select.BLANK`][textual.widgets.Select.BLANK] | Current value of the Select. |
+
+## Messages
+
+- [Select.Changed][textual.widgets.Select.Changed]
## Bindings
@@ -81,6 +114,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.
---
@@ -88,3 +124,7 @@ The Select widget defines the following bindings:
::: textual.widgets.Select
options:
heading_level: 2
+
+::: textual.widgets.select
+ options:
+ heading_level: 2
diff --git a/docs/widgets/sparkline.md b/docs/widgets/sparkline.md
index 98790f9c65..8c0bbb2fa9 100644
--- a/docs/widgets/sparkline.md
+++ b/docs/widgets/sparkline.md
@@ -38,7 +38,7 @@ The example below illustrates the relationship between the data, its length, the
=== "sparkline_basic.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/sparkline_basic.tcss"
```
@@ -66,7 +66,7 @@ The summary function is what determines the height of each bar.
=== "sparkline.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/sparkline.tcss"
```
@@ -87,7 +87,7 @@ The example below shows how to use component classes to change the colors of the
=== "sparkline_colors.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/sparkline_colors.tcss"
```
@@ -102,7 +102,20 @@ 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
+
+The sparkline widget provides the following component classes:
+
+::: textual.widgets.Sparkline.COMPONENT_CLASSES
+ options:
+ show_root_heading: false
+ show_root_toc_entry: false
---
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..e7543299a9 100644
--- a/docs/widgets/switch.md
+++ b/docs/widgets/switch.md
@@ -22,7 +22,7 @@ The example below shows switches in various states.
=== "switch.tcss"
- ```sass
+ ```css
--8<-- "docs/examples/widgets/switch.tcss"
```
@@ -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..15164e7907 100644
--- a/docs/widgets/tabbed_content.md
+++ b/docs/widgets/tabbed_content.md
@@ -94,7 +94,31 @@ The following example contains a `TabbedContent` with three tabs.
--8<-- "docs/examples/widgets/tabbed_content.py"
```
-## Reactive attributes
+## Styling
+
+The `TabbedContent` widget is composed of two main sub-widgets: a
+[`Tabs`](tabs.md) and a [`ContentSwitcher`]((content_switcher.md)); you can
+style them accordingly.
+
+The tabs within the `Tabs` widget will have prefixed IDs; each ID being the
+ID of the `TabPane` the `Tab` is for, prefixed with `--content-tab-`. If you
+wish to style individual tabs within the `TabbedContent` widget you will
+need to use that prefix for the `Tab` IDs.
+
+For example, to create a `TabbedContent` that has red and green labels:
+
+=== "Output"
+
+ ```{.textual path="docs/examples/widgets/tabbed_content_label_color.py"}
+ ```
+
+=== "tabbed_content.py"
+
+ ```python
+ --8<-- "docs/examples/widgets/tabbed_content_label_color.py"
+ ```
+
+## Reactive Attributes
| Name | Type | Default | Description |
| -------- | ----- | ------- | -------------------------------------------------------------- |
@@ -105,6 +129,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..bc3a5e25ad
--- /dev/null
+++ b/docs/widgets/text_area.md
@@ -0,0 +1,484 @@
+
+# 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
+
+### Syntax highlighting dependencies
+
+To enable syntax highlighting, you'll need to install the `syntax` extra dependencies:
+
+=== "pip"
+
+ ```
+ pip install "textual[syntax]"
+ ```
+
+=== "poetry"
+
+ ```
+ poetry add "textual[syntax]"
+ ```
+
+This will install `tree-sitter` and `tree-sitter-languages`.
+These packages are distributed as binary wheels, so it may limit your applications ability to run in environments where these wheels are not supported.
+
+### 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
+ 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")
+```
+
+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. |
+
+## Messages
+
+- [TextArea.Changed][textual.widgets._text_area.TextArea.Changed]
+- [TextArea.SelectionChanged][textual.widgets._text_area.TextArea.SelectionChanged]
+
+## 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..a324150e7f 100644
--- a/docs/widgets/toast.md
+++ b/docs/widgets/toast.md
@@ -7,9 +7,7 @@ A widget which displays a notification message.
- [ ] Focusable
- [ ] Container
-Note that `Toast` isn't designed to be used directly in your applications,
-but it is instead used by [`notify`][textual.app.App.notify] to
-display a message when using Textual's built-in notification system.
+!!! warning "Note that `Toast` isn't designed to be used directly in your applications, but it is instead used by [`notify`][textual.app.App.notify] to display a message when using Textual's built-in notification system."
## Styling
@@ -71,9 +69,30 @@ 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
+::: textual.widgets._toast.Toast
options:
show_root_heading: true
show_root_toc_entry: true
diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md
index 70d2822321..e1c4f33d1e 100644
--- a/docs/widgets/tree.md
+++ b/docs/widgets/tree.md
@@ -69,6 +69,6 @@ The tree widget provides the following component classes:
---
-::: textual.widgets.tree.TreeNode
+::: textual.widgets.tree
options:
heading_level: 2
diff --git a/examples/dictionary.tcss b/examples/dictionary.tcss
index 151fa019d0..79d7851490 100644
--- a/examples/dictionary.tcss
+++ b/examples/dictionary.tcss
@@ -9,8 +9,7 @@ Input {
#results {
width: 100%;
- height: auto;
-
+ height: auto;
}
#results-container {
diff --git a/examples/merlin.py b/examples/merlin.py
new file mode 100644
index 0000000000..0a0287a4ee
--- /dev/null
+++ b/examples/merlin.py
@@ -0,0 +1,173 @@
+from __future__ import annotations
+
+import random
+from datetime import timedelta
+from time import monotonic
+
+from textual import events
+from textual.app import App, ComposeResult
+from textual.containers import Grid
+from textual.reactive import var
+from textual.renderables.gradient import LinearGradient
+from textual.widget import Widget
+from textual.widgets import Digits, Label, Switch
+
+# A nice rainbow of colors.
+COLORS = [
+ "#881177",
+ "#aa3355",
+ "#cc6666",
+ "#ee9944",
+ "#eedd00",
+ "#99dd55",
+ "#44dd88",
+ "#22ccbb",
+ "#00bbcc",
+ "#0099cc",
+ "#3366bb",
+ "#663399",
+]
+
+
+# Maps a switch number on to other switch numbers, which should be toggled.
+TOGGLES: dict[int, tuple[int, ...]] = {
+ 1: (2, 4, 5),
+ 2: (1, 3),
+ 3: (2, 5, 6),
+ 4: (1, 7),
+ 5: (2, 4, 6, 8),
+ 6: (3, 9),
+ 7: (4, 5, 8),
+ 8: (7, 9),
+ 9: (5, 6, 8),
+}
+
+
+class LabelSwitch(Widget):
+ """Switch with a numeric label."""
+
+ DEFAULT_CSS = """
+ LabelSwitch Label {
+ text-align: center;
+ width: 1fr;
+ text-style: bold;
+ }
+
+ LabelSwitch Label#label-5 {
+ color: $text-disabled;
+ }
+ """
+
+ def __init__(self, switch_no: int) -> None:
+ self.switch_no = switch_no
+ super().__init__()
+
+ def compose(self) -> ComposeResult:
+ """Compose the label and a switch."""
+ yield Label(str(self.switch_no), id=f"label-{self.switch_no}")
+ yield Switch(id=f"switch-{self.switch_no}", name=str(self.switch_no))
+
+
+class Timer(Digits):
+ """Displays a timer that stops when you win."""
+
+ DEFAULT_CSS = """
+ Timer {
+ text-align: center;
+ width: auto;
+ margin: 2 8;
+ color: $warning;
+ }
+ """
+ start_time = var(0.0)
+ running = var(True)
+
+ def on_mount(self) -> None:
+ """Start the timer on mount."""
+ self.start_time = monotonic()
+ self.set_interval(1, self.tick)
+ self.tick()
+
+ def tick(self) -> None:
+ """Called from `set_interval` to update the clock."""
+ if self.start_time == 0 or not self.running:
+ return
+ time_elapsed = timedelta(seconds=int(monotonic() - self.start_time))
+ self.update(str(time_elapsed))
+
+
+class MerlinApp(App):
+ """A simple reproduction of one game on the Merlin hand held console."""
+
+ CSS = """
+ Screen {
+ align: center middle;
+ }
+
+ Screen.-win {
+ background: transparent;
+ }
+
+ Screen.-win Timer {
+ color: $success;
+ }
+
+ Grid {
+ width: auto;
+ height: auto;
+ border: thick $primary;
+ padding: 1 2;
+ grid-size: 3 3;
+ grid-rows: auto;
+ grid-columns: auto;
+ grid-gutter: 1 1;
+ background: $surface;
+ }
+ """
+
+ def render(self) -> LinearGradient:
+ """Renders a gradient, when the background is transparent."""
+ stops = [(i / (len(COLORS) - 1), c) for i, c in enumerate(COLORS)]
+ return LinearGradient(30.0, stops)
+
+ def compose(self) -> ComposeResult:
+ """Compose a timer, and a grid of 9 switches."""
+ yield Timer()
+ with Grid():
+ for switch in (7, 8, 9, 4, 5, 6, 1, 2, 3):
+ yield LabelSwitch(switch)
+
+ def on_mount(self) -> None:
+ """Randomize the switches on mount."""
+ for switch_no in range(1, 10):
+ if random.randint(0, 1):
+ self.query_one(f"#switch-{switch_no}", Switch).toggle()
+
+ def check_win(self) -> bool:
+ """Check for a win."""
+ on_switches = {
+ int(switch.name or "0") for switch in self.query(Switch) if switch.value
+ }
+ return on_switches == {1, 2, 3, 4, 6, 7, 8, 9}
+
+ def on_switch_changed(self, event: Switch.Changed) -> None:
+ """Called when a switch is toggled."""
+ # The switch that was pressed
+ switch_no = int(event.switch.name or "0")
+ # Also toggle corresponding switches
+ with self.prevent(Switch.Changed):
+ for toggle_no in TOGGLES[switch_no]:
+ self.query_one(f"#switch-{toggle_no}", Switch).toggle()
+ # Check the win
+ if self.check_win():
+ self.query_one("Screen").add_class("-win")
+ self.query_one(Timer).running = False
+
+ def on_key(self, event: events.Key) -> None:
+ """Maps switches to keys, so we can use the keyboard as well."""
+ if event.character and event.character.isdigit():
+ self.query_one(f"#switch-{event.character}", Switch).toggle()
+
+
+if __name__ == "__main__":
+ MerlinApp().run()
diff --git a/examples/splash.py b/examples/splash.py
new file mode 100644
index 0000000000..54b031c48a
--- /dev/null
+++ b/examples/splash.py
@@ -0,0 +1,58 @@
+from time import time
+
+from textual.app import App, ComposeResult, RenderableType
+from textual.containers import Container
+from textual.renderables.gradient import LinearGradient
+from textual.widgets import Static
+
+COLORS = [
+ "#881177",
+ "#aa3355",
+ "#cc6666",
+ "#ee9944",
+ "#eedd00",
+ "#99dd55",
+ "#44dd88",
+ "#22ccbb",
+ "#00bbcc",
+ "#0099cc",
+ "#3366bb",
+ "#663399",
+]
+STOPS = [(i / (len(COLORS) - 1), color) for i, color in enumerate(COLORS)]
+
+
+class Splash(Container):
+ """Custom widget that extends Container."""
+
+ DEFAULT_CSS = """
+ Splash {
+ align: center middle;
+ }
+ Static {
+ width: 40;
+ padding: 2 4;
+ }
+ """
+
+ def on_mount(self) -> None:
+ self.auto_refresh = 1 / 30
+
+ def compose(self) -> ComposeResult:
+ yield Static("Making a splash with Textual!")
+
+ def render(self) -> RenderableType:
+ return LinearGradient(time() * 90, STOPS)
+
+
+class SplashApp(App):
+ """Simple app to show our custom widget."""
+
+ def compose(self) -> ComposeResult:
+ yield Splash()
+
+
+if __name__ == "__main__":
+ app = SplashApp()
+ app.run()
+ print("https://textual.textualize.io/how-to/render-and-compose/")
diff --git a/mkdocs-common.yml b/mkdocs-common.yml
index 50c574073d..6834c0ff50 100644
--- a/mkdocs-common.yml
+++ b/mkdocs-common.yml
@@ -3,12 +3,13 @@ site_name: Textual
markdown_extensions:
- attr_list
- pymdownx.emoji:
- emoji_index: !!python/name:materialx.emoji.twemoji
- emoji_generator: !!python/name:materialx.emoji.to_svg
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
- md_in_html
- admonition
- def_list
- meta
+ - footnotes
- toc:
permalink: true
@@ -34,10 +35,12 @@ markdown_extensions:
alternate_style: true
- pymdownx.snippets
- markdown.extensions.attr_list
+ - pymdownx.details
theme:
name: material
custom_dir: docs/custom_theme
+ logo: images/icons/logo light transparent.svg
features:
- navigation.tabs
- navigation.indexes
@@ -80,7 +83,8 @@ plugins:
- "!^can_replace$"
# Hide some methods that Widget subclasses implement but that we don't want
# to be shown in the docs.
- # This is then overridden in widget.md so that it shows in the base class.
+ # This is then overridden in widget.md and app.md so that it shows in the
+ # base class.
- "!^compose$"
- "!^render$"
- "!^render_line$"
diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml
index 2e688c3088..68f1723917 100644
--- a/mkdocs-nav.yml
+++ b/mkdocs-nav.yml
@@ -1,212 +1,222 @@
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"
- - "guide/command_palette.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/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
+ - "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/keyline.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"
+ - "styles/keyline.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_background_hover.md"
+ - "styles/links/link_color.md"
+ - "styles/links/link_color_hover.md"
+ - "styles/links/link_style.md"
+ - "styles/links/link_style_hover.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/toast.md"
+ - "widgets/tree.md"
+ - API:
+ - "api/index.md"
+ - "api/app.md"
+ - "api/await_complete.md"
+ - "api/await_remove.md"
+ - "api/binding.md"
+ - "api/color.md"
+ - "api/command.md"
+ - "api/containers.md"
+ - "api/content_switcher.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/lazy.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/renderables.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"
+ - "how-to/render-and-compose.md"
+ - "FAQ.md"
+ - "roadmap.md"
+ - "Blog":
+ - blog/index.md
diff --git a/poetry.lock b/poetry.lock
index 85f0779436..3dfffadb4c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,114 +1,100 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "aiohttp"
-version = "3.8.5"
+version = "3.9.1"
description = "Async http client/server framework (asyncio)"
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"},
+python-versions = ">=3.8"
+files = [
+ {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"},
+ {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"},
+ {file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"},
+ {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"},
+ {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"},
+ {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"},
+ {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"},
+ {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"},
+ {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"},
+ {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"},
+ {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"},
+ {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"},
+ {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"},
+ {file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"},
+ {file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"},
+ {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"},
+ {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"},
+ {file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"},
+ {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"},
+ {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"},
+ {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"},
+ {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"},
+ {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"},
+ {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"},
+ {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"},
+ {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"},
+ {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"},
+ {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"},
+ {file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"},
+ {file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"},
+ {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"},
+ {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"},
+ {file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"},
+ {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"},
+ {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"},
+ {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"},
+ {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"},
+ {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"},
+ {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"},
+ {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"},
+ {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"},
+ {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"},
+ {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"},
+ {file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"},
+ {file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"},
+ {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"},
+ {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"},
+ {file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"},
+ {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"},
+ {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"},
+ {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"},
+ {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"},
+ {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"},
+ {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"},
+ {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"},
+ {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"},
+ {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"},
+ {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"},
+ {file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"},
+ {file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"},
+ {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"},
+ {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"},
+ {file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"},
+ {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"},
+ {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"},
+ {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"},
+ {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"},
+ {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"},
+ {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"},
+ {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"},
+ {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"},
+ {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"},
+ {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"},
+ {file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"},
+ {file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"},
+ {file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"},
]
[package.dependencies]
aiosignal = ">=1.1.2"
-async-timeout = ">=4.0.0a3,<5.0"
-asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
+async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
-charset-normalizer = ">=2.0,<4.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
yarl = ">=1.0,<2.0"
[package.extras]
-speedups = ["Brotli", "aiodns", "cchardet"]
+speedups = ["Brotli", "aiodns", "brotlicffi"]
[[package]]
name = "aiosignal"
@@ -126,25 +112,24 @@ frozenlist = ">=1.1.0"
[[package]]
name = "anyio"
-version = "3.7.1"
+version = "4.1.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
- {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
+ {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"},
+ {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"},
]
[package.dependencies]
-exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
+exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
-typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
-doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
-test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
-trio = ["trio (<0.22)"]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
[[package]]
name = "async-timeout"
@@ -157,20 +142,6 @@ files = [
{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\""}
-
-[[package]]
-name = "asynctest"
-version = "0.13.0"
-description = "Enhance the standard unittest package with features for testing asyncio libraries"
-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"
@@ -182,9 +153,6 @@ files = [
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
]
-[package.dependencies]
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[docs,tests]", "pre-commit"]
@@ -194,50 +162,47 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
[[package]]
name = "babel"
-version = "2.12.1"
+version = "2.13.1"
description = "Internationalization utilities"
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"},
+ {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"},
+ {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"},
]
[package.dependencies]
pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
+setuptools = {version = "*", markers = "python_version >= \"3.12\""}
+
+[package.extras]
+dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "black"
-version = "23.3.0"
+version = "23.11.0"
description = "The uncompromising code formatter."
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"},
+python-versions = ">=3.8"
+files = [
+ {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"},
+ {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"},
+ {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"},
+ {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"},
+ {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
+ {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
+ {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
+ {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
+ {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"},
+ {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"},
+ {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"},
+ {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"},
+ {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"},
+ {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"},
+ {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"},
+ {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"},
+ {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
+ {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
]
[package.dependencies]
@@ -247,8 +212,7 @@ packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
@@ -256,121 +220,125 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
-[[package]]
-name = "cached-property"
-version = "1.5.2"
-description = "A decorator for caching properties in classes."
-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"
+version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
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"},
+ {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
+ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[package]]
name = "cfgv"
-version = "3.3.1"
+version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.8"
files = [
- {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
- {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "charset-normalizer"
-version = "3.2.0"
+version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
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"},
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
@@ -386,7 +354,6 @@ files = [
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "colorama"
@@ -411,71 +378,63 @@ files = [
[[package]]
name = "coverage"
-version = "7.2.7"
+version = "7.3.2"
description = "Code coverage measurement for Python"
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"},
+python-versions = ">=3.8"
+files = [
+ {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"},
+ {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"},
+ {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"},
+ {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"},
+ {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"},
+ {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"},
+ {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"},
+ {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"},
+ {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"},
+ {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"},
+ {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"},
+ {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"},
+ {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"},
+ {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"},
+ {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"},
+ {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"},
+ {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"},
+ {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"},
+ {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"},
+ {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"},
+ {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"},
+ {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"},
]
[package.extras]
@@ -494,13 +453,13 @@ files = [
[[package]]
name = "exceptiongroup"
-version = "1.1.3"
+version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
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"},
+ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
]
[package.extras]
@@ -508,100 +467,88 @@ test = ["pytest (>=6)"]
[[package]]
name = "filelock"
-version = "3.12.2"
+version = "3.13.1"
description = "A platform independent file lock."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
- {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
+ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
+ {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
]
[package.extras]
-docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "frozenlist"
-version = "1.3.3"
+version = "1.4.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
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"},
+python-versions = ">=3.8"
+files = [
+ {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"},
+ {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"},
+ {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"},
+ {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"},
+ {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"},
+ {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"},
+ {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"},
+ {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"},
+ {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"},
+ {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"},
+ {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"},
+ {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"},
+ {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"},
+ {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"},
+ {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"},
+ {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"},
+ {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"},
+ {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"},
+ {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"},
+ {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"},
+ {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"},
+ {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"},
+ {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"},
+ {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"},
+ {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"},
+ {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"},
+ {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"},
+ {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"},
+ {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"},
+ {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"},
+ {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"},
+ {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"},
+ {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"},
+ {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"},
+ {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"},
+ {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"},
+ {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"},
+ {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"},
+ {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"},
+ {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"},
+ {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"},
+ {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"},
+ {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"},
+ {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"},
+ {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"},
+ {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"},
+ {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"},
+ {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"},
+ {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"},
+ {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"},
+ {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"},
+ {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"},
+ {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"},
+ {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"},
+ {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"},
+ {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"},
+ {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"},
+ {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"},
+ {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"},
+ {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"},
+ {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"},
]
[[package]]
@@ -623,13 +570,13 @@ dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "gitdb"
-version = "4.0.10"
+version = "4.0.11"
description = "Git Object Database"
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"},
+ {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"},
+ {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"},
]
[package.dependencies]
@@ -637,32 +584,33 @@ smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
-version = "3.1.34"
+version = "3.1.40"
description = "GitPython is a Python library used to interact with Git repositories"
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"},
+ {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"},
+ {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"},
]
[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)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"]
[[package]]
name = "griffe"
-version = "0.30.1"
+version = "0.32.3"
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."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"},
- {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"},
+ {file = "griffe-0.32.3-py3-none-any.whl", hash = "sha256:d9471934225818bf8f309822f70451cc6abb4b24e59e0bb27402a45f9412510f"},
+ {file = "griffe-0.32.3.tar.gz", hash = "sha256:14983896ad581f59d5ad7b6c9261ff12bdaa905acccc1129341d13e545da8521"},
]
[package.dependencies]
-cached-property = {version = "*", markers = "python_version < \"3.8\""}
colorama = ">=0.4"
[[package]]
@@ -676,9 +624,6 @@ files = [
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
-[package.dependencies]
-typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
-
[[package]]
name = "httpcore"
version = "0.16.3"
@@ -725,13 +670,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "identify"
-version = "2.5.24"
+version = "2.5.33"
description = "File identification library for Python"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
- {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
+ {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
+ {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
]
[package.extras]
@@ -739,34 +684,33 @@ license = ["ukkonen"]
[[package]]
name = "idna"
-version = "3.4"
+version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
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"},
+ {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "importlib-metadata"
-version = "6.7.0"
+version = "7.0.0"
description = "Read metadata from Python packages"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
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"},
+ {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"},
+ {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"},
]
[package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]]
name = "iniconfig"
@@ -818,38 +762,37 @@ test = ["coverage", "pytest", "pytest-cov"]
[[package]]
name = "markdown"
-version = "3.4.4"
+version = "3.5.1"
description = "Python implementation of John Gruber's Markdown."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"},
- {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"},
+ {file = "Markdown-3.5.1-py3-none-any.whl", hash = "sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc"},
+ {file = "Markdown-3.5.1.tar.gz", hash = "sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd"},
]
[package.dependencies]
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
[package.extras]
-docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markdown-it-py"
-version = "2.2.0"
+version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
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"},
+ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+ {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""}
mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""}
mdurl = ">=0.1,<1.0"
-typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
@@ -858,7 +801,7 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
-rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
@@ -888,6 +831,16 @@ files = [
{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-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{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"},
@@ -922,21 +875,21 @@ files = [
[[package]]
name = "mdit-py-plugins"
-version = "0.3.5"
+version = "0.4.0"
description = "Collection of plugins for markdown-it-py"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
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"},
+ {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"},
+ {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"},
]
[package.dependencies]
-markdown-it-py = ">=1.0.0,<3.0.0"
+markdown-it-py = ">=1.0.0,<4.0.0"
[package.extras]
code-style = ["pre-commit"]
-rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"]
+rtd = ["myst-parser", "sphinx-book-theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
@@ -963,13 +916,13 @@ files = [
[[package]]
name = "mkdocs"
-version = "1.5.2"
+version = "1.5.3"
description = "Project documentation with Markdown."
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"},
+ {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"},
+ {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"},
]
[package.dependencies]
@@ -986,7 +939,6 @@ pathspec = ">=0.11.1"
platformdirs = ">=2.2.0"
pyyaml = ">=5.1"
pyyaml-env-tag = ">=0.1"
-typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
watchdog = ">=2.0"
[package.extras]
@@ -995,13 +947,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
[[package]]
name = "mkdocs-autorefs"
-version = "0.4.1"
+version = "0.5.0"
description = "Automatically link across pages in MkDocs."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
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"},
+ {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"},
+ {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"},
]
[package.dependencies]
@@ -1023,13 +975,13 @@ mkdocs = "*"
[[package]]
name = "mkdocs-material"
-version = "9.2.7"
+version = "9.5.1"
description = "Documentation that simply works"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
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"},
+ {file = "mkdocs_material-9.5.1-py3-none-any.whl", hash = "sha256:2e01249bc41813afe2479a4a659f8ba899c3355ccaf9310b5b782952df9c1dea"},
+ {file = "mkdocs_material-9.5.1.tar.gz", hash = "sha256:7ec5d20ed5eee97bb090823a33b33e177ad0704d74bad5937b53acca571ddb3d"},
]
[package.dependencies]
@@ -1037,45 +989,51 @@ babel = ">=2.10,<3.0"
colorama = ">=0.4,<1.0"
jinja2 = ">=3.0,<4.0"
markdown = ">=3.2,<4.0"
-mkdocs = ">=1.5,<2.0"
-mkdocs-material-extensions = ">=1.1,<2.0"
+mkdocs = ">=1.5.3,<2.0"
+mkdocs-material-extensions = ">=1.3,<2.0"
paginate = ">=0.5,<1.0"
pygments = ">=2.16,<3.0"
pymdown-extensions = ">=10.2,<11.0"
-regex = ">=2022.4,<2023.0"
+regex = ">=2022.4"
requests = ">=2.26,<3.0"
+[package.extras]
+git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"]
+imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"]
+recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
+
[[package]]
name = "mkdocs-material-extensions"
-version = "1.1.1"
+version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
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"},
+ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
+ {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
]
[[package]]
name = "mkdocs-rss-plugin"
-version = "1.5.0"
+version = "1.9.0"
description = "MkDocs plugin which generates a static RSS feed using git log and page.meta."
optional = false
-python-versions = ">=3.7, <4"
+python-versions = ">=3.8, <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"},
+ {file = "mkdocs-rss-plugin-1.9.0.tar.gz", hash = "sha256:eeb576945d3d9990cdf8aa3545062669892ea4410e5a960072d44cec867dba42"},
+ {file = "mkdocs_rss_plugin-1.9.0-py2.py3-none-any.whl", hash = "sha256:8c3eda30ec59e6b51c6c0ed2b27e6f5c907583d8828122c81140b4505f42b72c"},
]
[package.dependencies]
GitPython = ">=3.1,<3.2"
-mkdocs = ">=1.1,<2"
+mkdocs = ">=1.4,<2"
pytz = {version = "==2022.*", markers = "python_version < \"3.9\""}
-tzdata = {version = "==2022.*", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
+tzdata = {version = "==2023.*", 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", "flake8 (>=6,<7)", "flake8-bugbear (>=23.12)", "flake8-builtins (>=2.1)", "flake8-eradicate (>=1)", "flake8-isort (>=6)", "pre-commit (>=3,<4)"]
+doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (==0.7.*)", "pygments (>=2.5,<3)", "pymdown-extensions (>=10,<11)"]
+test = ["feedparser (>=6.0,<6.1)", "mkdocs-material (>=9)", "pytest-cov (>=4,<4.2)", "validator-collection (>=1.5,<1.6)"]
[[package]]
name = "mkdocstrings"
@@ -1119,74 +1077,67 @@ mkdocstrings = ">=0.20"
[[package]]
name = "msgpack"
-version = "1.0.5"
+version = "1.0.7"
description = "MessagePack serializer"
optional = false
-python-versions = "*"
-files = [
- {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"},
- {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"},
- {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"},
- {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"},
- {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"},
- {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"},
- {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"},
- {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"},
- {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"},
- {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"},
- {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"},
- {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"},
- {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"},
- {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"},
- {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"},
- {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"},
- {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"},
- {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"},
- {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"},
- {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"},
- {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"},
- {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"},
- {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"},
- {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"},
- {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"},
- {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"},
- {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"},
- {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"},
- {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"},
- {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"},
- {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"},
- {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"},
- {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"},
- {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"},
- {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"},
- {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"},
- {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"},
- {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"},
- {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"},
- {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"},
- {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"},
- {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"},
- {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"},
- {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"},
- {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"},
- {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"},
- {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"},
- {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"},
- {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"},
- {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"},
- {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"},
- {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"},
- {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"},
- {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"},
- {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"},
- {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"},
- {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"},
- {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"},
- {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"},
- {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"},
- {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"},
+python-versions = ">=3.8"
+files = [
+ {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"},
+ {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"},
+ {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"},
+ {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"},
+ {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"},
+ {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"},
+ {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"},
+ {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"},
+ {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"},
+ {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"},
+ {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"},
+ {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"},
+ {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"},
+ {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"},
+ {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"},
+ {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"},
+ {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"},
+ {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"},
+ {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"},
+ {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"},
+ {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"},
+ {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"},
+ {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"},
+ {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"},
+ {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"},
+ {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"},
+ {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"},
+ {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"},
+ {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"},
+ {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"},
+ {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"},
+ {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"},
+ {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"},
+ {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"},
+ {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"},
+ {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"},
+ {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"},
+ {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"},
+ {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"},
+ {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"},
+ {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"},
+ {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"},
+ {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"},
+ {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"},
+ {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"},
+ {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"},
+ {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"},
+ {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"},
+ {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"},
+ {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"},
+ {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"},
+ {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"},
+ {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"},
+ {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"},
+ {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"},
+ {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"},
]
[[package]]
@@ -1274,49 +1225,49 @@ files = [
[[package]]
name = "mypy"
-version = "1.4.1"
+version = "1.7.1"
description = "Optional static typing for Python"
optional = false
-python-versions = ">=3.7"
-files = [
- {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"},
- {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"},
- {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"},
- {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"},
- {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"},
- {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"},
- {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"},
- {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"},
- {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"},
- {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"},
- {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"},
- {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"},
- {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"},
- {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"},
- {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"},
- {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"},
- {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"},
- {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"},
- {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"},
- {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"},
- {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"},
- {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"},
- {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"},
- {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"},
+python-versions = ">=3.8"
+files = [
+ {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"},
+ {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"},
+ {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"},
+ {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"},
+ {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"},
+ {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
+ {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
+ {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
+ {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
+ {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
+ {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
+ {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
+ {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
+ {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
+ {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
+ {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"},
+ {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"},
+ {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"},
+ {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"},
+ {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"},
+ {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"},
+ {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"},
+ {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"},
+ {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"},
+ {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"},
+ {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
+ {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
]
[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)"]
+mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
@@ -1346,13 +1297,13 @@ setuptools = "*"
[[package]]
name = "packaging"
-version = "23.1"
+version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
- {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
- {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
@@ -1378,36 +1329,30 @@ files = [
[[package]]
name = "platformdirs"
-version = "3.10.0"
+version = "4.1.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
- {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
+ {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
+ {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
]
-[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"
+version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
- {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
+ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
+ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
@@ -1426,38 +1371,38 @@ files = [
[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"
+version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.7"
files = [
- {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
- {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
+ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+ {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
]
[package.extras]
plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pymdown-extensions"
-version = "10.2.1"
+version = "10.5"
description = "Extension pack for Python Markdown."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4"},
- {file = "pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591"},
+ {file = "pymdown_extensions-10.5-py3-none-any.whl", hash = "sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879"},
+ {file = "pymdown_extensions-10.5.tar.gz", hash = "sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b"},
]
[package.dependencies]
-markdown = ">=3.2"
+markdown = ">=3.5"
pyyaml = "*"
[package.extras]
@@ -1465,19 +1410,18 @@ extra = ["pygments (>=2.12)"]
[[package]]
name = "pytest"
-version = "7.4.1"
+version = "7.4.3"
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"},
+ {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
+ {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
]
[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"
@@ -1486,43 +1430,23 @@ 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"},
-]
-
-[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"
+version = "0.23.2"
description = "Pytest support for asyncio"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
- {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
+ {file = "pytest-asyncio-0.23.2.tar.gz", hash = "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc"},
+ {file = "pytest_asyncio-0.23.2-py3-none-any.whl", hash = "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f"},
]
[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)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-cov"
@@ -1598,6 +1522,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -1605,8 +1530,15 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -1623,6 +1555,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -1630,6 +1563,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -1651,99 +1585,99 @@ pyyaml = "*"
[[package]]
name = "regex"
-version = "2022.10.31"
+version = "2023.10.3"
description = "Alternative regular expression module, to replace re."
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
files = [
- {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"},
- {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"},
- {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"},
- {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"},
- {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"},
- {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"},
- {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"},
- {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"},
- {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"},
- {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"},
- {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"},
- {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"},
- {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"},
- {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"},
- {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"},
- {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"},
- {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"},
- {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"},
- {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"},
- {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"},
- {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"},
- {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"},
- {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"},
- {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"},
- {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"},
- {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"},
- {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"},
- {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"},
- {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"},
- {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"},
- {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"},
- {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"},
- {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"},
- {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"},
- {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"},
- {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"},
- {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"},
- {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"},
- {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"},
- {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"},
- {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"},
- {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"},
- {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"},
- {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"},
- {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"},
- {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"},
- {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"},
- {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"},
- {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"},
- {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"},
- {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"},
- {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"},
- {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"},
- {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"},
- {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"},
- {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"},
- {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"},
- {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"},
- {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"},
- {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"},
- {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"},
- {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"},
- {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"},
- {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"},
- {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"},
- {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"},
- {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"},
- {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"},
- {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"},
- {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"},
- {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"},
- {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"},
- {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"},
- {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"},
- {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"},
- {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"},
- {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"},
- {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"},
- {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"},
- {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"},
- {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"},
- {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"},
- {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"},
- {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"},
- {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"},
- {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"},
+ {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"},
+ {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"},
+ {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"},
+ {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"},
+ {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"},
+ {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"},
+ {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"},
+ {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"},
+ {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"},
+ {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"},
+ {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"},
+ {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"},
+ {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"},
+ {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"},
+ {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"},
+ {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"},
+ {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"},
+ {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"},
+ {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"},
+ {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"},
+ {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"},
+ {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"},
+ {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"},
+ {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"},
]
[[package]]
@@ -1786,13 +1720,13 @@ idna2008 = ["idna"]
[[package]]
name = "rich"
-version = "13.5.2"
+version = "13.7.0"
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"},
+ {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
+ {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
]
[package.dependencies]
@@ -1805,19 +1739,19 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "setuptools"
-version = "68.0.0"
+version = "69.0.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
- {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
+ {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"},
+ {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"},
]
[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"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "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] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
@@ -1832,13 +1766,13 @@ files = [
[[package]]
name = "smmap"
-version = "5.0.0"
+version = "5.0.1"
description = "A pure Python implementation of a sliding window memory map manager"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
files = [
- {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
- {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+ {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"},
+ {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"},
]
[[package]]
@@ -1869,83 +1803,85 @@ pytest = ">=5.1.0,<8.0.0"
[[package]]
name = "textual-dev"
-version = "1.1.0"
+version = "1.2.1"
description = "Development tools for working with Textual"
optional = false
python-versions = ">=3.7,<4.0"
files = [
- {file = "textual_dev-1.1.0-py3-none-any.whl", hash = "sha256:c57320636098e31fa5d5c29fc3bc60829bb420da3c76bfed24db6eacf178dbc6"},
- {file = "textual_dev-1.1.0.tar.gz", hash = "sha256:e2f8ce4e1c18a16b80282f3257cd2feb49a7ede289a78908c9063ce071bb77ce"},
+ {file = "textual_dev-1.2.1-py3-none-any.whl", hash = "sha256:a96ff43841cadf853dd689d68c2fc920a23ad71cfa9a33917ca53e96d1cc81f3"},
+ {file = "textual_dev-1.2.1.tar.gz", hash = "sha256:0bda11adfc541e0cc9e49bdf37a8b852281dc2387bb6ff3d01f40c7a3f841684"},
]
[package.dependencies]
aiohttp = ">=3.8.1"
click = ">=8.1.2"
msgpack = ">=1.0.3"
-textual = ">=0.32.0"
+textual = ">=0.33.0"
typing-extensions = ">=4.4.0,<5.0.0"
[[package]]
name = "time-machine"
-version = "2.10.0"
+version = "2.13.0"
description = "Travel through time in your tests."
optional = false
-python-versions = ">=3.7"
-files = [
- {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"},
- {file = "time_machine-2.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:860279c7f9413bc763b3d1aee622937c4538472e2e58ad668546b49a797cb9fb"},
- {file = "time_machine-2.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f451be286d50ec9b685198c7f76cea46538b8c57ec816f60edf5eb68d71c4f4"},
- {file = "time_machine-2.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1b07f5da833b2d8ea170cdf15a322c6fa2c6f7e9097a1bea435adc597cdcb5d"},
- {file = "time_machine-2.10.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6b3a529ecc819488783e371df5ad315e790b9558c6945a236b13d7cb9ab73b9a"},
- {file = "time_machine-2.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51e36491bd4a43f8a937ca7c0d1a2287b8998f41306f47ebed250a02f93d2fe4"},
- {file = "time_machine-2.10.0-cp310-cp310-win32.whl", hash = "sha256:1e9973091ad3272c719dafae35a5bb08fa5433c2902224d0f745657f9e3ac327"},
- {file = "time_machine-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab82ea5a59faa1faa7397465f2edd94789a13f543daa02d16244906339100080"},
- {file = "time_machine-2.10.0-cp310-cp310-win_arm64.whl", hash = "sha256:55bc6d666966fa2e6283d7433ebe875be37684a847eaa802075433c1ab3a377a"},
- {file = "time_machine-2.10.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:99fc366cb4fa26d81f12fa36a929db0da89d99909e28231c045e0f1277e0db84"},
- {file = "time_machine-2.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5969f325c20bdcb7f8917a6ac2ef328ec41cc2e256320a99dfe38b4080eeae71"},
- {file = "time_machine-2.10.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:a1a5e283ab47b28205f33fa3c5a2df3fd9f07f09add63dbe76637c3633893a23"},
- {file = "time_machine-2.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4083ec185ab9ece3e5a7ca7a7589114a555f04bcff31b29d4eb47a37e87d97fe"},
- {file = "time_machine-2.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cbe45f88399b8af299136435a2363764d5fa6d16a936e4505081b6ea32ff3e18"},
- {file = "time_machine-2.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d149a3fae8a06a3593361496ec036a27906fed478ade23ffc01dd402acd0b37"},
- {file = "time_machine-2.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2e05306f63df3c7760170af6e77e1b37405b7c7c4a97cc9fdf0105f1094b1b1c"},
- {file = "time_machine-2.10.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d6d7b7680e34dbe60da34d75d6d5f31b6206c7149c0de8a7b0f0311d0ef7e3a"},
- {file = "time_machine-2.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:91b8b06e09e1dfd53dafe272d41b60690d6f8806d7194c62982b003a088dc423"},
- {file = "time_machine-2.10.0-cp311-cp311-win32.whl", hash = "sha256:6241a1742657622ebdcd66cf6045c92e0ec6ca6365c55434cc7fea945008192c"},
- {file = "time_machine-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:48cce6dcb7118ba4a58537c6de4d1dd6e7ad6ea15d0257d6e0003b45c4a839c2"},
- {file = "time_machine-2.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:8cb6285095efa0833fd0301e159748a06e950c7744dc3d38e92e7607e2232d5a"},
- {file = "time_machine-2.10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8829ca7ed939419c2a23c360101edc51e3b57f40708d304b6aed16214d8b2a1f"},
- {file = "time_machine-2.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b5b60bc00ad2efa5fefee117e5611a28b26f563f1a64df118d1d2f2590a679a"},
- {file = "time_machine-2.10.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1491fb647568134d38b06e844783d3069f5811405e9a3906eff88d55403e327"},
- {file = "time_machine-2.10.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e78f2759a63fcc7660d283e22054c7cfa7468fad1ad86d0846819b6ea958d63f"},
- {file = "time_machine-2.10.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:30881f263332245a665a49d0e30fda135597c4e18f2efa9c6759c224419c36a5"},
- {file = "time_machine-2.10.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e93750309093275340e0e95bb270801ec9cbf2ee8702d71031f4ccd8cc91dd7f"},
- {file = "time_machine-2.10.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a906bb338a6be978b83f09f09d8b24737239330f280c890ecbf1c13828e1838c"},
- {file = "time_machine-2.10.0-cp37-cp37m-win32.whl", hash = "sha256:10c8b170920d3f83dad2268ae8d5e1d8bb431a85198e32d778e6f3a1f93b172d"},
- {file = "time_machine-2.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5efc4cc914d93138944c488fdebd6e4290273e3ac795d5c7a744af29eb04ce0f"},
- {file = "time_machine-2.10.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1787887168e36f57d5ca1abf1b9d065a55eb67067df2fa23aaa4382da36f7098"},
- {file = "time_machine-2.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26a8cc1f8e9f4f69ea3f50b9b9e3a699e80e44ac9359a867208be6adac30fc60"},
- {file = "time_machine-2.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07e2c6c299c5509c72cc221a19f4bf680c87c793727a3127a29e18ddad3db13"},
- {file = "time_machine-2.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f3e5f263a623148a448756a332aad45e65a59876fcb2511f7f61213e6d3ec3e"},
- {file = "time_machine-2.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3abcb48d7ca7ed95e5d99220317b7ce31378636bb020cabfa62f9099e7dad"},
- {file = "time_machine-2.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:545a813b7407c33dee388aa380449e79f57f02613ea149c6e907fc9ca3d53e64"},
- {file = "time_machine-2.10.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:458b52673ec83d10da279d989d7a6ad1e60c93e4ba986210d72e6c78e17102f4"},
- {file = "time_machine-2.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:acb2ca50d779d39eab1d0fab48697359e4ffc1aedfa58b79cd3a86ee13253834"},
- {file = "time_machine-2.10.0-cp38-cp38-win32.whl", hash = "sha256:648fec54917a7e67acca38ed8e736b206e8a9688730e13e1cf7a74bcce89dec7"},
- {file = "time_machine-2.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3ed92d2a6e2c2b7a0c8161ecca5d012041b7ba147cbdfb2b7f62f45c02615111"},
- {file = "time_machine-2.10.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6d2588581d3071d556f96954d084b7b99701e54120bb29dfadaab04791ef6ae4"},
- {file = "time_machine-2.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:185f7a4228e993ddae610e24fb3c7e7891130ebb6a40f42d58ea3be0bfafe1b1"},
- {file = "time_machine-2.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8225eb813ea9488de99e61569fc1b2d148d236473a84c6758cc436ffef4c043"},
- {file = "time_machine-2.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f03ac22440b00abd1027bfb7dd793dfeffb72dda26f336f4d561835e0ce6117"},
- {file = "time_machine-2.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4252f4daef831556e6685853d7a61b02910d0465528c549f179ea4e36aaeb14c"},
- {file = "time_machine-2.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:58c65bf4775fca62e1678cb234f1ca90254e811d978971c819d2cd24e1b7f136"},
- {file = "time_machine-2.10.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8527ac8fca7b92556c3c4c0f08e0bea995202db4be5b7d95b9b2ccbcb63649f2"},
- {file = "time_machine-2.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4684308d749fdb0c22af173b081206d2a5a85d2154a683a7f4a60c4b667f7a65"},
- {file = "time_machine-2.10.0-cp39-cp39-win32.whl", hash = "sha256:2adc24cf25b7e8d08aea2b109cc42c5db76817b07ee709fae5c66afa4ec7bc6e"},
- {file = "time_machine-2.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:36f5be6f3042734fca043bedafbfbb6ad4809352e40b3283cb46b151a823674c"},
- {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"},
+python-versions = ">=3.8"
+files = [
+ {file = "time_machine-2.13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:685d98593f13649ad5e7ce3e58efe689feca1badcf618ba397d3ab877ee59326"},
+ {file = "time_machine-2.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ccbce292380ebf63fb9a52e6b03d91677f6a003e0c11f77473efe3913a75f289"},
+ {file = "time_machine-2.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:679cbf9b15bfde1654cf48124128d3fbe52f821fa158a98fcee5fe7e05db1917"},
+ {file = "time_machine-2.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a26bdf3462d5f12a4c1009fdbe54366c6ef22c7b6f6808705b51dedaaeba8296"},
+ {file = "time_machine-2.13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dabb3b155819811b4602f7e9be936e2024e20dc99a90f103e36b45768badf9c3"},
+ {file = "time_machine-2.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0db97f92be3efe0ac62fd3f933c91a78438cef13f283b6dfc2ee11123bfd7d8a"},
+ {file = "time_machine-2.13.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:12eed2e9171c85b703d75c985dab2ecad4fe7025b7d2f842596fce1576238ece"},
+ {file = "time_machine-2.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bdfe4a7f033e6783c3e9a7f8d8fc0b115367330762e00a03ff35fedf663994f3"},
+ {file = "time_machine-2.13.0-cp310-cp310-win32.whl", hash = "sha256:3a7a0a49ce50d9c306c4343a7d6a3baa11092d4399a4af4355c615ccc321a9d3"},
+ {file = "time_machine-2.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1812e48c6c58707db9988445a219a908a710ea065b2cc808d9a50636291f27d4"},
+ {file = "time_machine-2.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:5aee23cd046abf9caeddc982113e81ba9097a01f3972e9560f5ed64e3495f66d"},
+ {file = "time_machine-2.13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e9a9d150e098be3daee5c9f10859ab1bd14a61abebaed86e6d71f7f18c05b9d7"},
+ {file = "time_machine-2.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2bd4169b808745d219a69094b3cb86006938d45e7293249694e6b7366225a186"},
+ {file = "time_machine-2.13.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:8d526cdcaca06a496877cfe61cc6608df2c3a6fce210e076761964ebac7f77cc"},
+ {file = "time_machine-2.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfef4ebfb4f055ce3ebc7b6c1c4d0dbfcffdca0e783ad8c6986c992915a57ed3"},
+ {file = "time_machine-2.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f128db8997c3339f04f7f3946dd9bb2a83d15e0a40d35529774da1e9e501511"},
+ {file = "time_machine-2.13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21bef5854d49b62e2c33848b5c3e8acf22a3b46af803ef6ff19529949cb7cf9f"},
+ {file = "time_machine-2.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:32b71e50b07f86916ac04bd1eefc2bd2c93706b81393748b08394509ee6585dc"},
+ {file = "time_machine-2.13.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ac8ff145c63cd0dcfd9590fe694b5269aacbc130298dc7209b095d101f8cdde"},
+ {file = "time_machine-2.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:19a3b10161c91ca8e0fd79348665cca711fd2eac6ce336ff9e6b447783817f93"},
+ {file = "time_machine-2.13.0-cp311-cp311-win32.whl", hash = "sha256:5f87787d562e42bf1006a87eb689814105b98c4d5545874a281280d0f8b9a2d9"},
+ {file = "time_machine-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:62fd14a80b8b71726e07018628daaee0a2e00937625083f96f69ed6b8e3304c0"},
+ {file = "time_machine-2.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:e9935aff447f5400a2665ab10ed2da972591713080e1befe1bb8954e7c0c7806"},
+ {file = "time_machine-2.13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:34dcdbbd25c1e124e17fe58050452960fd16a11f9d3476aaa87260e28ecca0fd"},
+ {file = "time_machine-2.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e58d82fe0e59d6e096ada3281d647a2e7420f7da5453b433b43880e1c2e8e0c5"},
+ {file = "time_machine-2.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71acbc1febbe87532c7355eca3308c073d6e502ee4ce272b5028967847c8e063"},
+ {file = "time_machine-2.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dec0ec2135a4e2a59623e40c31d6e8a8ae73305ade2634380e4263d815855750"},
+ {file = "time_machine-2.13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e3a2611f8788608ebbcb060a5e36b45911bc3b8adc421b1dc29d2c81786ce4d"},
+ {file = "time_machine-2.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:42ef5349135626ad6cd889a0a81400137e5c6928502b0817ea9e90bb10702000"},
+ {file = "time_machine-2.13.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6c16d90a597a8c2d3ce22d6be2eb3e3f14786974c11b01886e51b3cf0d5edaf7"},
+ {file = "time_machine-2.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f2ae8d0e359b216b695f1e7e7256f208c390db0480601a439c5dd1e1e4e16ce"},
+ {file = "time_machine-2.13.0-cp312-cp312-win32.whl", hash = "sha256:f5fa9610f7e73fff42806a2ed8b06d862aa59ce4d178a52181771d6939c3e237"},
+ {file = "time_machine-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:02b33a8c19768c94f7ffd6aa6f9f64818e88afce23250016b28583929d20fb12"},
+ {file = "time_machine-2.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:0cc116056a8a2a917a4eec85661dfadd411e0d8faae604ef6a0e19fe5cd57ef1"},
+ {file = "time_machine-2.13.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de01f33aa53da37530ad97dcd17e9affa25a8df4ab822506bb08101bab0c2673"},
+ {file = "time_machine-2.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:67fa45cd813821e4f5bec0ac0820869e8e37430b15509d3f5fad74ba34b53852"},
+ {file = "time_machine-2.13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a2d3db2c3b8e519d5ef436cd405abd33542a7b7761fb05ef5a5f782a8ce0b1"},
+ {file = "time_machine-2.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7558622a62243be866a7e7c41da48eacd82c874b015ecf67d18ebf65ca3f7436"},
+ {file = "time_machine-2.13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab04cf4e56e1ee65bee2adaa26a04695e92eb1ed1ccc65fbdafd0d114399595a"},
+ {file = "time_machine-2.13.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0c8f24ae611a58782773af34dd356f1f26756272c04be2be7ea73b47e5da37d"},
+ {file = "time_machine-2.13.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ca20f85a973a4ca8b00cf466cd72c27ccc72372549b138fd48d7e70e5a190ab"},
+ {file = "time_machine-2.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9fad549521c4c13bdb1e889b2855a86ec835780d534ffd8f091c2647863243be"},
+ {file = "time_machine-2.13.0-cp38-cp38-win32.whl", hash = "sha256:20205422fcf2caf9a7488394587df86e5b54fdb315c1152094fbb63eec4e9304"},
+ {file = "time_machine-2.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:2dc76ee55a7d915a55960a726ceaca7b9097f67e4b4e681ef89871bcf98f00be"},
+ {file = "time_machine-2.13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7693704c0f2f6b9beed912ff609781edf5fcf5d63aff30c92be4093e09d94b8e"},
+ {file = "time_machine-2.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:918f8389de29b4f41317d121f1150176fae2cdb5fa41f68b2aee0b9dc88df5c3"},
+ {file = "time_machine-2.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fe3fda5fa73fec74278912e438fce1612a79c36fd0cc323ea3dc2d5ce629f31"},
+ {file = "time_machine-2.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6245db573863b335d9ca64b3230f623caf0988594ae554c0c794e7f80e3e66"},
+ {file = "time_machine-2.13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e433827eccd6700a34a2ab28fd9361ff6e4d4923f718d2d1dac6d1dcd9d54da6"},
+ {file = "time_machine-2.13.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:924377d398b1c48e519ad86a71903f9f36117f69e68242c99fb762a2465f5ad2"},
+ {file = "time_machine-2.13.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66fb3877014dca0b9286b0f06fa74062357bd23f2d9d102d10e31e0f8fa9b324"},
+ {file = "time_machine-2.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0c9829b2edfcf6b5d72a6ff330d4380f36a937088314c675531b43d3423dd8af"},
+ {file = "time_machine-2.13.0-cp39-cp39-win32.whl", hash = "sha256:1a22be4df364f49a507af4ac9ea38108a0105f39da3f9c60dce62d6c6ea4ccdc"},
+ {file = "time_machine-2.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:88601de1da06c7cab3d5ed3d5c3801ef683366e769e829e96383fdab6ae2fe42"},
+ {file = "time_machine-2.13.0-cp39-cp39-win_arm64.whl", hash = "sha256:3c87856105dcb25b5bbff031d99f06ef4d1c8380d096222e1bc63b496b5258e6"},
+ {file = "time_machine-2.13.0.tar.gz", hash = "sha256:c23b2408e3adcedec84ea1131e238f0124a5bc0e491f60d1137ad7239b37c01a"},
]
[package.dependencies]
@@ -1974,55 +1910,163 @@ files = [
]
[[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"
+name = "tree-sitter"
+version = "0.20.4"
+description = "Python bindings for the Tree-Sitter parsing library"
+optional = true
+python-versions = ">=3.3"
+files = [
+ {file = "tree_sitter-0.20.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c259b9bcb596e54f54713eb3951226fc834d65289940f4bfdcdf519f08e8e876"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88da7e2e4c69881cd63916cc24ae0b809f96aae331da45b418ae6b2d1ed2ca19"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66a68b156ba131e9d8dff4a1f72037f4b368cc50c58f18905a91743ae1d1c795"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28e25d551f406807011487bdfb9728041e656b30b554fa7f3391ab64ed69f9"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36b10c9c69e825ba65cf9b0f77668bf33e70d2a5764b64ad6f133f8cc9220f09"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7c18c64ddd44b75b7e1660b9793753eda427e4b145b6216d4b2d2e9b200c74f2"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9e9e594bbefb76ad9ea256f5c87eba7591b4758854d3df83ce4df415933a006"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-win32.whl", hash = "sha256:b4755229dc18644fe48bcab974bde09b171fcb6ef625d3cb5ece5c6198f4223e"},
+ {file = "tree_sitter-0.20.4-cp310-cp310-win_amd64.whl", hash = "sha256:f792684cee8a46d9194d9f4223810e54ccc704470c5777538d59fbde0a4c91bf"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d22ee75f45836554ee6a11e50dd8f9827941e67c49fce9a0790245b899811a9"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a0ffd76dd991ba745bb5d0ba1d583bec85726d3ddef8c9685dc8636a619adde"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:060d4e5b803be0975f1ac46e54a292eab0701296ccd912f6cdac3f7331e29143"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822e02366dbf223697b2b56b8f91aa5b60571f9fe7c998988a381db1c69604e9"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:527ca72c6a8f60fa719af37fa86f58b7ad0e07b8f74d1c1c7e926c5c888a7e6b"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a418ca71309ea7052e076f08d623f33f58eae01a8e8cdc1e6d3a01b5b8ddebfe"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c3ba2561b61a83c28ca06a0bce2a5ffcfb6b39f9d27a45e5ebd9cad2bedb7f"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-win32.whl", hash = "sha256:8d04c75a389b2de94952d602264852acff8cd3ed1ccf8a2492a080973d5ddd58"},
+ {file = "tree_sitter-0.20.4-cp311-cp311-win_amd64.whl", hash = "sha256:ba9215c0e7529d9eb370528e5d99b7389d14a7eae94f07d14fa9dab18f267c62"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c4c1af5ed4306071d30970c83ec882520a7bf5d8053996dbc4aa5c59238d4990"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9d70bfa550cf22c9cea9b3c0d18b889fc4f2a7e9dcf1d6cc93f49fa9d4a94954"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6de537bca0641775d8d175d37303d54998980fc0d997dd9aa89e16b415bf0cc3"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1c0f8c0e3e50267566f5116cdceedf4e23e8c08b55ef3becbe954a11b16e84"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef2ee6d9bb8e21713949e5ff769ed670fe1217f95b7eeb6c675788438c1e6e"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b6fd1c881ab0de5faa67168db2d001eee32be5482cb4e0b21b217689a05b6fe4"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf47047420021d50aec529cb66387c90562350b499ddf56ecef1fc8255439e30"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-win32.whl", hash = "sha256:c16b48378041fc9702b6aa3480f2ffa49ca8ea58141a862acd569e5a0679655f"},
+ {file = "tree_sitter-0.20.4-cp312-cp312-win_amd64.whl", hash = "sha256:973e871167079a1b1d7304d361449253efbe2a6974728ad563cf407bd02ddccb"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9d33a55598dd18a4d8b869a3417de82a4812c3a7dc7e61cb025ece3e9c3e4e96"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cee6955c2c97fc5927a41c7a8b06647c4b4d9b99b8a1581bf1183435c8cec3e"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5022bea67e479ad212be7c05b983a72e297a013efb4e8ea5b5b4d7da79a9fdef"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:640f60a5b966f0990338f1bf559455c3dcb822bc4329d82b3d42f32a48374dfe"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0e83f641fe6f27d91bd4d259fff5d35de1567d3f581b9efe9bbd5be50fe4ddc7"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-win32.whl", hash = "sha256:ce6a85027c66fa3f09d482cc6d41927ea40955f7f33b86aedd26dd932709a2c9"},
+ {file = "tree_sitter-0.20.4-cp36-cp36m-win_amd64.whl", hash = "sha256:fe10779347a6c067af29cb37fd4b75fa96c5cb68f587cc9530b70fe3f2a51a55"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28d5f84e34e276887e3a240b60906ca7e2b51e975f3145c3149ceed977a69508"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c913b65cbe10996116988ac436748f24883b5097e58274223e89bb2c5d1bb1a"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecaed46241e071752195a628bb97d2b740f2fde9e34f8a74456a4ea8bb26df88"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b641e88a97eab002a1736d93ef5a4beac90ea4fd6e25affd1831319b99f456c9"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:327c40f439c6155e4eee54c4657e4701a04f5f4816d9defdcb836bf65bf83d21"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-win32.whl", hash = "sha256:1b7c1d95f006b3de42fbf4045bd00c273d113e372fcb6a5378e74ed120c12032"},
+ {file = "tree_sitter-0.20.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6140d037239a41046f5d34fba5e0374ee697adb4b48b90579c618b5402781c11"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f42fd1104efaad8151370f1936e2a488b7337a5d24544a9ab59ba4c4010b1272"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7859717c5d62ee386b3d036cab8ed0f88f8c027b6b4ae476a55a8c5fb8aab713"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdd361fe1cc68db68b4d85165641275e34b86cc26b2bab932790204fa14824dc"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b8d7539075606027b67764543463ff2bc4e52f4158ef6dc419c9f5625aa5383"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78e76307f05aca6cde72f3307b4d53701f34ae45f2248ceb83d1626051e201fd"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd8c352f4577f61098d06cf3feb7fd214259f41b5036b81003860ed54d16b448"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:281f3e5382d1bd7fccc88d1afe68c915565bc24f8b8dd4844079d46c7815b8a7"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-win32.whl", hash = "sha256:6a77ac3cdcddd80cdd1fd394318bff99f94f37e08d235aaefccb87e1224946e5"},
+ {file = "tree_sitter-0.20.4-cp38-cp38-win_amd64.whl", hash = "sha256:8eee8adf54033dc48eab84b040f4d7b32355a964c4ae0aae5dfbdc4dbc3364ca"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e89f6508e30fce05e2c724725d022db30d877817b9d64f933506ffb3a3f4a2c2"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fb6286bb1fae663c45ff0700ec88fb9b50a81eed2bae8a291f95fcf8cc19547"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11e93f8b4bbae04070416a82257a7ab2eb0afb76e093ae3ea73bd63b792f6846"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8250725c5f78929aeb2c71db5dca76f1ef448389ca16f9439161f90978bb8478"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d404a8ca9de9b0843844f0cd4d423f46bc46375ab8afb63b1d8ec01201457ac8"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0f2422c9ee70ba972dfc3943746e6cf7fc03725a866908950245bda9ccfc7301"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:21a937942e4729abbe778a609d2c218574436cb351c36fba89ef3c8c6066ec78"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-win32.whl", hash = "sha256:427a9a39360cc1816e28f8182550e478e4ba983595a2565ab9dfe32ea1b03fd7"},
+ {file = "tree_sitter-0.20.4-cp39-cp39-win_amd64.whl", hash = "sha256:7095bb9aff297fa9c6026bf8914fd295997d714d1a6ee9a1edf7282c772f9f64"},
+ {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:859260b90f0e3867ae840e39f54e830f607b3bc531bc21deeeeaa8a30cbb89ad"},
+ {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dfc14be73cf46126660a3aecdd0396e69562ad1a902245225ca7bd29649594e"},
+ {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46355bf3ff23f54d5e365871ffd3e05cfbc65d1b36a8be7c0bcbda30a1d43"},
+ {file = "tree_sitter-0.20.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d933a942fde39876b99c36f12aa3764e4a555ae9366c10ce6cca8c16341c1bbf"},
+ {file = "tree_sitter-0.20.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a7eec3b55135fe851a38fa248c9fd75fc3d58ceb6e1865b795e416e4d598c2a1"},
+ {file = "tree_sitter-0.20.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc76225529ee14a53e84413480ce81ec3c44eaa0455c140e961c90ac3118ead"},
+ {file = "tree_sitter-0.20.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf0396e47efffc0b528959a8f2e2346a98297579f867e9e1834c2aad4be829c"},
+ {file = "tree_sitter-0.20.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a15fbabd3bc8e29c48289c156d743e69f5ec72bb125cf44f7adbdaa1937c3da6"},
+ {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:36f8adf2126f496cf376b6e4b707cba061c25beb17841727eef6f0e083e53e1f"},
+ {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841efb40c116ab0a066924925409a8a4dcffeb39a151c0b2a1c2abe56ad4fb42"},
+ {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2051e8a70fd8426f27a43dad71d11929a62ce30a9b1eb65bba0ed79e82481592"},
+ {file = "tree_sitter-0.20.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99a3c2824d4cfcffd9f961176891426bde2cb36ece5280c61480be93319c23c4"},
+ {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:72830dc85a10430eca3d56739b7efcd7a05459c8d425f08c1aee6179ab7f13a9"},
+ {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4992dd226055b6cd0a4f5661c66b799a73d3eff716302e0f7ab06594ee12d49f"},
+ {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66d95bbf92175cdc295d6d77f330942811f02e3aaf3fc64431cb749683b2f7d"},
+ {file = "tree_sitter-0.20.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a25b1087e4f7825b2458dacf5f4b0be2938f78e850e822edca1ff4994b56081a"},
+ {file = "tree_sitter-0.20.4.tar.gz", hash = "sha256:6adb123e2f3e56399bbf2359924633c882cc40ee8344885200bca0922f713be5"},
+]
+
+[package.dependencies]
+setuptools = {version = ">=60.0.0", markers = "python_version >= \"3.12\""}
+
+[[package]]
+name = "tree-sitter-languages"
+version = "1.8.0"
+description = "Binary Python wheels for all tree sitter languages."
+optional = true
+python-versions = "*"
files = [
- {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"},
- {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"},
- {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"},
- {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"},
- {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"},
- {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"},
- {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"},
- {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"},
- {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"},
- {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"},
- {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"},
- {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"},
- {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"},
- {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"},
- {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"},
- {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"},
- {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"},
- {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"},
- {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"},
- {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"},
- {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"},
- {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"},
- {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"},
- {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"},
- {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"},
- {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"},
- {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"},
- {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"},
- {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"},
- {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"},
- {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"},
- {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"},
- {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"},
- {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"},
- {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"},
- {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"},
- {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"},
- {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"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a20045f0c7a8394ac0c085c3a7da88438f9e62c6a8b661ebf63c3edb8c3f2bf6"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ef80d5896b420d434f7322abbc2c5a5548a37b3821c5486ed0612d2bd760d5a"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e7c7100c7b4a364035417e811ab8d43c8ee4e38d0c6ab9cad9c4d8133c0abd"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9618bfb5874c43fcb4da43cd71bc24f01f4f94cd55bb9923c4210c7f9e977eb5"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7b0b606be0c61155bde8e913528b7dc038e8476891f5b198996f780c678ecc0"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:306b49d60afb8c08f95a55e38744687521aa9350a97e9d6d1512db47ea401c51"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b561b979d1dc15a0b2bc35586fe4ccf95049812944042ea5760d8450b63c3fe0"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c46c82a5649c41fd4ce7483534fe548a98af6ef6490b5c9f066e2df43e40aa9"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-win32.whl", hash = "sha256:4d84b2bf63f8dc51188f83a6dfc7d70365e1c720310c1222f44d0cd2ec76e4d0"},
+ {file = "tree_sitter_languages-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:c59b81123fa73e7d66d3a8bc0e64af2f2a8fcbbce1b08676d9188ec5edb4fb49"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a5816a1e394d717a86b9f5cbb0af08ad92a9badbb4b95678d75052e6bd7402"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:912a12a56361077715b231f1931cf7d472f7d6cfdc76abb806e6b1bdf11d3835"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33838baa8583b2c9f9df4d672237158dcc9d845782413569b51cc8dfed2fb4de"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b6f148e459e8af180be68e9f9c8f8c4db0db170850482b083fd078fba3f4076"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96dbdaff9d317d193451bc5b566098717096381d67674f9e65fb8f0ebe98c847"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c719535ebdd39f94c26f2182b0d16c45a2996b03b5ad7b78a863178eca1546d"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d5c4cb2f4231135d038155787c96f4ecdf44f63eeee8d9e36b100b96a80a7764"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:524bfa0bcbf0fe8cbb93712336d1de0a3073f08c004bb920270d69c0c3eaaf14"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-win32.whl", hash = "sha256:26a0b923c47eeed551e4c307b7badb337564523cca36f9c40e188a308f471c72"},
+ {file = "tree_sitter_languages-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3f0ed6297878f9f335f652843e9ab48c561f9a5b312a41a868b5fc127567447b"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0f18d0d98b92bfa40ec15fc4cc5eb5e1f39b9f2f8986cf4cb3e1f8a8e31b06cf"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c742b0733be6d057d323252c56b8419fa2e120510baf601f710363971ae99ae7"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4417710db978edf6bad1e1e59efba04693919ed45c4115bae7da359354d9d8af"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a051e1cceddd1126ce0fa0d3faa12873e5b52cafae0893cc82d22b21348fc83c"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2665768f7ef6d00ab3847c5a3a5fdd54fbc62a9abf80475bff26dcc7a4e8544f"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76be6fd0d1e514e496eb3430b05ce0efd2f7d09fc3dfe47cc99afc653313c36a"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:510c5ba5dd3ce502f2963c46cc56ad4a0acd1b776be9b119da03f392bda9f8bf"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-win32.whl", hash = "sha256:f852ff7b77df5c7a3f8b825c31673aee59456a93347b58cfa43fdda81fe1cb63"},
+ {file = "tree_sitter_languages-1.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:53934c8b09650e576ad5724b84c6891d84b69508ad71a78bb2d4dc88b63543fc"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:400ba190fd08cec9412d70efa09e2f1791a0db82a3e9b31f677e145ad2e48a9a"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:937b0e8cc07fb6574b475fcaded8dd16fa445c66f40bf449b4e50684fd8c380b"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c165c5d13ee335c74a2b6dc6edfcf85045839fa2f7254d2aae3ae9f76020e87d"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124117c6184653cdd381c70a16e5d6a45a41c3f6470d9d756452ea50aa6bb472"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4c12232c93d4c5c8b3b6324850085971fa93c2226842778f07fe3fba9a7683c1"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b9baf99c00366fe2c8e61bf7489d86eaab4c884f669abdb30ba2450cfabb77f7"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f97baf3d574fc44872c1de8c941888c940a0376c8f80a15ec6931d19b4fe2091"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:c40267904f734d8a7e9a05ce60f04ea95db59cad183207c4af34e6bc1f5bbd1f"},
+ {file = "tree_sitter_languages-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:06b8d11ea550d3c4f0ce0774d6b521c44f2e83d1a77d50f85bea3ed150e66c28"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a151d4f2637309f1780b9a0422cdeea3c0a8a6209800f587fe4374ebe13e6a1"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1a3afb35a316495ff1b848aadeb4f9f7ef6522e9b730a7a35cfe28361398404e"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22eb91d745b96936c13fc1c100d78e6dcbaa14e9fbe54e180cdc6ca1b262c0f"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54a3a83474d3abb44a178aa1f0a5ef73002c014e7e489977fd39624c1ac0a476"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a13aa1e6f0fc76268e8fed282fb433ca4b8f6644bb75476a10d28cc19d6cf3"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:68872fcea16f7ddbfeec52120b7070e18a820407d16f6b513ec95ede4110df82"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:43928c43d8a25204297c43bbaab0c4b567a7e85901a19ef9317a3964ad8eb76e"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cca84cacd5530f23ae5d05e4904c2d42f7479fd80541eda34c27cadbf9611d6b"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-win32.whl", hash = "sha256:9d043fdbaf260d0f36f8843acf43096765bed913be71ad705265dccb8e381e1c"},
+ {file = "tree_sitter_languages-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f5bbccf1250dc07e74fd86f08a9ed614efd64986a48c142846cd21e84267d46b"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:10046058a4213304e3ba78a52ab88d8d5a2703f5d193e7e976d0a53c2fa12f4b"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fc84bb37ca0bb1f45f808a38733f6bb9c2e8fc8a02712fe8658fe3d31ed74e7"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b13199282d71d2a841f404f58ccf914b3917b27a99917b0a79b80c93f8a24e"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94f5f5ac57591004823385bd7f4cc1b62c7b0b08efc1c39a5e33fb2f8c201bf"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a796a359bd6fb4f2b67e29f86c9130bd6ae840d75d31d356594f92d5505f43d"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:45a6edf0106ff653940fe52fb8a47f8c03d0c5981312ac036888d44102840452"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f077fe6099bb310a247514b68d7103c6dbafef552856fcd225d0867f78b620b7"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3842ef8d05e3368c227fd5a57f08f636374b4b870070916d08c4aafb99d04cd1"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-win32.whl", hash = "sha256:3e9eafc7079114783b5385a769fd190c93525bcae3cf6791fd819c617067394e"},
+ {file = "tree_sitter_languages-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:9d30b7f48f18a60eea9a0f9494e0f0ea6f560d861770a84c3faab8d7a446fc55"},
]
+[package.dependencies]
+tree-sitter = "*"
+
[[package]]
name = "types-setuptools"
version = "67.8.0.0"
@@ -2035,25 +2079,50 @@ files = [
]
[[package]]
-name = "typing-extensions"
-version = "4.7.1"
-description = "Backported and Experimental Type Hints for Python 3.7+"
+name = "types-tree-sitter"
+version = "0.20.1.6"
+description = "Typing stubs for tree-sitter"
optional = false
python-versions = ">=3.7"
files = [
- {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
- {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+ {file = "types-tree-sitter-0.20.1.6.tar.gz", hash = "sha256:310a97916adf73553fd1bda8107884da9b638550ddc76085ae0875c8f520520c"},
+ {file = "types_tree_sitter-0.20.1.6-py3-none-any.whl", hash = "sha256:40eae13bc44f4e36d4e97b52db674fe808c6ccb3036a7aed9a736313411fd057"},
+]
+
+[[package]]
+name = "types-tree-sitter-languages"
+version = "1.8.0.0"
+description = "Typing stubs for tree-sitter-languages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "types-tree-sitter-languages-1.8.0.0.tar.gz", hash = "sha256:a066d1c91d5fe8b8fce08669816d9e8c41bbe348085b3cb9799fa74070a30604"},
+ {file = "types_tree_sitter_languages-1.8.0.0-py3-none-any.whl", hash = "sha256:9d4a8e2a435a4a0d356e643fb53993e3c491749ce0b7a628c22cb87904c6daca"},
+]
+
+[package.dependencies]
+types-tree-sitter = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "4.8.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
+ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]]
name = "tzdata"
-version = "2022.7"
+version = "2023.3"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
- {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"},
- {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"},
+ {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
+ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
]
[[package]]
@@ -2072,37 +2141,35 @@ test = ["coverage", "pytest", "pytest-cov"]
[[package]]
name = "urllib3"
-version = "2.0.4"
+version = "2.1.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
- {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
+ {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
+ {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
]
[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"
+version = "20.25.0"
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"},
+ {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
+ {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
]
[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"
+platformdirs = ">=3.9.1,<5"
[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)"]
@@ -2149,108 +2216,126 @@ watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "yarl"
-version = "1.9.2"
+version = "1.9.4"
description = "Yet another URL library"
optional = false
python-versions = ">=3.7"
files = [
- {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"},
- {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"},
- {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"},
- {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"},
- {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"},
- {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"},
- {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"},
- {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"},
- {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"},
- {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"},
- {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"},
- {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"},
- {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"},
- {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"},
- {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"},
- {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"},
- {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"},
- {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"},
- {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"},
- {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"},
- {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"},
- {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"},
- {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"},
- {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"},
- {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"},
- {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"},
- {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"},
- {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"},
- {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"},
- {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"},
- {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"},
- {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"},
- {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"},
- {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"},
- {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"},
- {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"},
- {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"},
- {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"},
- {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"},
- {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"},
- {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"},
- {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"},
- {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"},
- {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"},
- {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"},
- {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"},
- {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"},
- {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"},
- {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"},
- {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"},
- {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"},
- {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"},
- {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"},
- {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"},
- {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"},
- {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"},
- {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"},
- {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"},
- {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"},
- {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"},
- {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"},
- {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"},
- {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"},
- {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"},
- {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"},
- {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"},
- {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"},
- {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"},
- {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"},
- {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"},
- {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
- {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
+ {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
+ {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
+ {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
+ {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
+ {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
+ {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
+ {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
+ {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
+ {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
+ {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
+ {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
+ {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
+ {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
+ {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
+ {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
+ {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
+ {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
+ {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
+ {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
+ {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
+ {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
+ {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
+ {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
+ {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
+ {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
+ {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
+ {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
+ {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
+ {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
+ {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
+ {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
+ {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
+ {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
+ {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
+ {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
+ {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
+ {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
+ {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
+ {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
+ {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
+ {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
+ {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
+ {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
+ {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
+ {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
+ {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
+ {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
+ {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
+ {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
+ {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
+ {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
+ {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
+ {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
+ {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
+ {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
+ {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
+ {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
+ {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
+ {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
+ {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
+ {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
+ {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
+ {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
+ {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
+ {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
+ {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
+ {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
+ {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
+ {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
+ {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
+ {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
+ {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
+ {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
+ {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
+ {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
+ {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
+ {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
+ {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
+ {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
+ {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
+ {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
+ {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
+ {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
+ {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
+ {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
+ {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
+ {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
+ {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
+ {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
+ {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
]
[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"
+version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
- {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+ {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
+ {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
]
[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)"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+
+[extras]
+syntax = ["tree-sitter", "tree_sitter_languages"]
[metadata]
lock-version = "2.0"
-python-versions = "^3.7"
-content-hash = "3817b3d8b678845abb17cddd49d5a6ea5fb9d0083faa356ef232184a94312ba6"
+python-versions = "^3.8"
+content-hash = "c4c26f6d0bd1266a7a38b9236c99cf51bf658447a18ffc2c96fb5da442762d6a"
diff --git a/pyproject.toml b/pyproject.toml
index f343aea290..19261bafca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
-version = "0.37.1"
+version = "0.47.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
@@ -17,11 +17,11 @@ classifiers = [
"Operating System :: Microsoft :: Windows :: Windows 11",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Typing :: Typed",
]
include = [
@@ -40,12 +40,16 @@ include = [
"Bug Tracker" = "https://github.com/Textualize/textual/issues"
[tool.poetry.dependencies]
-python = "^3.7"
+python = "^3.8"
rich = ">=13.3.3"
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 = { version = "^0.20.1", optional = true }
+tree_sitter_languages = { version = ">=1.7.0", optional = true }
+
+[tool.poetry.extras]
+syntax = ["tree-sitter", "tree_sitter_languages"]
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
@@ -58,14 +62,16 @@ mkdocstrings-python = "0.10.1"
mkdocs-material = "^9.0.11"
mkdocs-exclude = "^1.0.2"
pre-commit = "^2.13.0"
-pytest-aiohttp = "^1.0.4"
time-machine = "^2.6.0"
mkdocs-rss-plugin = "^1.5.0"
httpx = "^0.23.1"
types-setuptools = "^67.2.0.1"
-textual-dev = "^1.1.0"
+textual-dev = "^1.2.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"
+griffe = "0.32.3"
[tool.black]
includes = "src"
@@ -76,6 +82,7 @@ testpaths = ["tests"]
addopts = "--strict-markers"
markers = [
"integration_test: marks tests as slow integration tests (deselect with '-m \"not integration_test\"')",
+ "syntax: marks tests that require syntax highlighting (deselect with '-m \"not syntax\"')",
]
[build-system]
diff --git a/reference/_devtools.md b/reference/_devtools.md
deleted file mode 100644
index 964c69ed3b..0000000000
--- a/reference/_devtools.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Devtools
-
-## Installation
-
-Using the Textual Devtools requires installation of the `dev` [optional dependency group](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#dependencies-optional-dependencies).
-
-## Running
-
-TODO: Note how we run the devtools themselves and how we run our Textual apps
-such that they can connect. Don't forget Windows instructions :)
-We might also add a link to the documentation from the exception that gets
-raised when the "dev" extra dependencies aren't installed.
diff --git a/src/textual/__init__.py b/src/textual/__init__.py
index 103f9db2fe..0b93010125 100644
--- a/src/textual/__init__.py
+++ b/src/textual/__init__.py
@@ -28,16 +28,11 @@
def __getattr__(name: str) -> str:
- """Lazily get the version from whatever API is available."""
+ """Lazily get the version."""
if name == "__version__":
- try:
- from importlib.metadata import version
- except ImportError:
- import pkg_resources
+ from importlib.metadata import version
- return pkg_resources.get_distribution("textual").version
- else:
- return version("textual")
+ return version("textual")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/textual/_animator.py b/src/textual/_animator.py
index 54a3cf6cf3..3561b1ba58 100644
--- a/src/textual/_animator.py
+++ b/src/textual/_animator.py
@@ -349,6 +349,7 @@ def _animate(
duration is None and speed is not None
), "An Animation should have a duration OR a speed"
+ # If an animation is already scheduled for this attribute, unschedule it.
animation_key = (id(obj), attribute)
try:
del self._scheduled[animation_key]
@@ -359,9 +360,7 @@ def _animate(
final_value = value
start_time = self._get_time()
-
easing_function = EASING[easing] if isinstance(easing, str) else easing
-
animation: Animation | None = None
if hasattr(obj, "__textual_animation__"):
diff --git a/src/textual/_ansi_sequences.py b/src/textual/_ansi_sequences.py
index fc3b8de624..a504ea718d 100644
--- a/src/textual/_ansi_sequences.py
+++ b/src/textual/_ansi_sequences.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from typing import Mapping, Tuple
from .keys import Keys
# Mapping of vt100 escape codes to Keys.
-ANSI_SEQUENCES_KEYS: Mapping[str, Tuple[Keys, ...]] = {
+ANSI_SEQUENCES_KEYS: Mapping[str, Tuple[Keys, ...] | str] = {
# Control keys.
" ": (Keys.Space,),
"\r": (Keys.Enter,),
@@ -98,7 +100,9 @@
# Xterm
"\x1b[1;2P": (Keys.F13,),
"\x1b[1;2Q": (Keys.F14,),
- # '\x1b[1;2R': Keys.F15, # Conflicts with CPR response.
+ "\x1b[1;2R": (
+ Keys.F15,
+ ), # Conflicts with CPR response; enabled after https://github.com/Textualize/textual/issues/3440.
"\x1b[1;2S": (Keys.F16,),
"\x1b[15;2~": (Keys.F17,),
"\x1b[17;2~": (Keys.F18,),
@@ -112,7 +116,9 @@
# Control + function keys.
"\x1b[1;5P": (Keys.ControlF1,),
"\x1b[1;5Q": (Keys.ControlF2,),
- # "\x1b[1;5R": Keys.ControlF3, # Conflicts with CPR response.
+ "\x1b[1;5R": (
+ Keys.ControlF3,
+ ), # Conflicts with CPR response; enabled after https://github.com/Textualize/textual/issues/3440.
"\x1b[1;5S": (Keys.ControlF4,),
"\x1b[15;5~": (Keys.ControlF5,),
"\x1b[17;5~": (Keys.ControlF6,),
@@ -124,7 +130,9 @@
"\x1b[24;5~": (Keys.ControlF12,),
"\x1b[1;6P": (Keys.ControlF13,),
"\x1b[1;6Q": (Keys.ControlF14,),
- # "\x1b[1;6R": Keys.ControlF15, # Conflicts with CPR response.
+ "\x1b[1;6R": (
+ Keys.ControlF15,
+ ), # Conflicts with CPR response; enabled after https://github.com/Textualize/textual/issues/3440.
"\x1b[1;6S": (Keys.ControlF16,),
"\x1b[15;6~": (Keys.ControlF17,),
"\x1b[17;6~": (Keys.ControlF18,),
@@ -134,6 +142,30 @@
"\x1b[21;6~": (Keys.ControlF22,),
"\x1b[23;6~": (Keys.ControlF23,),
"\x1b[24;6~": (Keys.ControlF24,),
+ # rxvt-unicode control function keys:
+ "\x1b[11^": (Keys.ControlF1,),
+ "\x1b[12^": (Keys.ControlF2,),
+ "\x1b[13^": (Keys.ControlF3,),
+ "\x1b[14^": (Keys.ControlF4,),
+ "\x1b[15^": (Keys.ControlF5,),
+ "\x1b[17^": (Keys.ControlF6,),
+ "\x1b[18^": (Keys.ControlF7,),
+ "\x1b[19^": (Keys.ControlF8,),
+ "\x1b[20^": (Keys.ControlF9,),
+ "\x1b[21^": (Keys.ControlF10,),
+ "\x1b[23^": (Keys.ControlF11,),
+ "\x1b[24^": (Keys.ControlF12,),
+ # rxvt-unicode control+shift function keys:
+ "\x1b[25^": (Keys.ControlF13,),
+ "\x1b[26^": (Keys.ControlF14,),
+ "\x1b[28^": (Keys.ControlF15,),
+ "\x1b[29^": (Keys.ControlF16,),
+ "\x1b[31^": (Keys.ControlF17,),
+ "\x1b[32^": (Keys.ControlF18,),
+ "\x1b[33^": (Keys.ControlF19,),
+ "\x1b[34^": (Keys.ControlF20,),
+ "\x1b[23@": (Keys.ControlF21,),
+ "\x1b[24@": (Keys.ControlF22,),
# --
# Tmux (Win32 subsystem) sends the following scroll events.
"\x1b[62~": (Keys.ScrollUp,),
@@ -146,6 +178,7 @@
# --
# Meta/control/escape + pageup/pagedown/insert/delete.
"\x1b[3;2~": (Keys.ShiftDelete,), # xterm, gnome-terminal.
+ "\x1b[3$": (Keys.ShiftDelete,), # rxvt
"\x1b[5;2~": (Keys.ShiftPageUp,),
"\x1b[6;2~": (Keys.ShiftPageDown,),
"\x1b[2;3~": (Keys.Escape, Keys.Insert),
@@ -157,8 +190,11 @@
"\x1b[5;4~": (Keys.Escape, Keys.ShiftPageUp),
"\x1b[6;4~": (Keys.Escape, Keys.ShiftPageDown),
"\x1b[3;5~": (Keys.ControlDelete,), # xterm, gnome-terminal.
+ "\x1b[3^": (Keys.ControlDelete,), # rxvt
"\x1b[5;5~": (Keys.ControlPageUp,),
"\x1b[6;5~": (Keys.ControlPageDown,),
+ "\x1b[5^": (Keys.ControlPageUp,), # rxvt
+ "\x1b[6^": (Keys.ControlPageDown,), # rxvt
"\x1b[3;6~": (Keys.ControlShiftDelete,),
"\x1b[5;6~": (Keys.ControlShiftPageUp,),
"\x1b[6;6~": (Keys.ControlShiftPageDown,),
@@ -194,6 +230,13 @@
"\x1b[1;2D": (Keys.ShiftLeft,),
"\x1b[1;2F": (Keys.ShiftEnd,),
"\x1b[1;2H": (Keys.ShiftHome,),
+ # Shift+navigation in rxvt
+ "\x1b[a": (Keys.ShiftUp,),
+ "\x1b[b": (Keys.ShiftDown,),
+ "\x1b[c": (Keys.ShiftRight,),
+ "\x1b[d": (Keys.ShiftLeft,),
+ "\x1b[7$": (Keys.ShiftHome,),
+ "\x1b[8$": (Keys.ShiftEnd,),
# Meta + arrow keys. Several terminals handle this differently.
# The following sequences are for xterm and gnome-terminal.
# (Iterm sends ESC followed by the normal arrow_up/down/left/right
@@ -221,8 +264,13 @@
"\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,),
+ # rxvt
+ "\x1b[7^": (Keys.ControlEnd,),
+ "\x1b[8^": (Keys.ControlHome,),
# Tmux sends following keystrokes when control+arrow is pressed, but for
# Emacs ansi-term sends the same sequences for normal arrow keys. Consider
# it a normal arrow press, because that's more important.
@@ -230,8 +278,11 @@
"\x1b[5B": (Keys.ControlDown,),
"\x1b[5C": (Keys.ControlRight,),
"\x1b[5D": (Keys.ControlLeft,),
- "\x1bOc": (Keys.ControlRight,), # rxvt
- "\x1bOd": (Keys.ControlLeft,), # rxvt
+ # Control arrow keys in rxvt
+ "\x1bOa": (Keys.ControlUp,),
+ "\x1bOb": (Keys.ControlUp,),
+ "\x1bOc": (Keys.ControlRight,),
+ "\x1bOd": (Keys.ControlLeft,),
# Control + shift + arrows.
"\x1b[1;6A": (Keys.ControlShiftUp,),
"\x1b[1;6B": (Keys.ControlShiftDown,),
@@ -301,6 +352,24 @@
"\x1b[1;8w": (Keys.Escape, Keys.ControlShift7),
"\x1b[1;8x": (Keys.Escape, Keys.ControlShift8),
"\x1b[1;8y": (Keys.Escape, Keys.ControlShift9),
+ # Simplify some sequences that appear to be unique to rxvt; see
+ # https://github.com/Textualize/textual/issues/3741 for context.
+ "\x1bOj": "*",
+ "\x1bOk": "+",
+ "\x1bOm": "-",
+ "\x1bOn": ".",
+ "\x1bOo": "/",
+ "\x1bOp": "0",
+ "\x1bOq": "1",
+ "\x1bOr": "2",
+ "\x1bOs": "3",
+ "\x1bOt": "4",
+ "\x1bOu": "5",
+ "\x1bOv": "6",
+ "\x1bOw": "7",
+ "\x1bOx": "8",
+ "\x1bOy": "9",
+ "\x1bOM": (Keys.Enter,),
}
# https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
diff --git a/src/textual/_asyncio.py b/src/textual/_asyncio.py
deleted file mode 100644
index 351ff650d5..0000000000
--- a/src/textual/_asyncio.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""
-Compatibility layer for asyncio.
-"""
-
-from __future__ import annotations
-
-import sys
-
-__all__ = ["create_task"]
-
-if sys.version_info >= (3, 8):
- from asyncio import create_task
-
-else:
- import asyncio
- from asyncio import create_task as _create_task
- from typing import Awaitable
-
- def create_task(coroutine: Awaitable, *, name: str | None = None) -> asyncio.Task:
- """Schedule the execution of a coroutine object in a spawn task."""
- return _create_task(coroutine)
diff --git a/src/textual/_box_drawing.py b/src/textual/_box_drawing.py
new file mode 100644
index 0000000000..e30d73b4c6
--- /dev/null
+++ b/src/textual/_box_drawing.py
@@ -0,0 +1,366 @@
+"""
+Box drawing utilities for Canvas.
+
+The box drawing characters have zero to four lines radiating from the center of the glyph.
+There are three line types: thin, heavy, and double. These are indicated by 1, 2, and 3 respectively (0 for no line).
+
+This code represents the characters as a tuple of 4 integers, (, , , ). This format
+makes it possible to logically combine characters together, as there is no mathematical relationship in the unicode db.
+
+Note that not all combinations are possible. Characters can have a maximum of 2 border types in a single glyph.
+There are also fewer characters for the "double" line type.
+
+"""
+
+from __future__ import annotations
+
+from functools import lru_cache
+
+from typing_extensions import TypeAlias
+
+Quad: TypeAlias = "tuple[int, int, int, int]"
+"""Four values indicating the composition of the box character."""
+
+# Yes, I typed this out by hand. - WM
+BOX_CHARACTERS: dict[Quad, str] = {
+ (0, 0, 0, 0): " ",
+ (0, 0, 0, 1): "╴",
+ (0, 0, 0, 2): "╸",
+ (0, 0, 0, 3): "╸",
+ #
+ (0, 0, 1, 0): "╷",
+ (0, 0, 1, 1): "┐",
+ (0, 0, 1, 2): "┑",
+ (0, 0, 1, 3): "╕",
+ #
+ (0, 0, 2, 0): "╻",
+ (0, 0, 2, 1): "┒",
+ (0, 0, 2, 2): "┓",
+ (0, 0, 2, 3): "╕",
+ #
+ (0, 0, 3, 0): "╻",
+ (0, 0, 3, 1): "╖",
+ (0, 0, 3, 2): "╖",
+ (0, 0, 3, 3): "╗",
+ #
+ (0, 1, 0, 0): "╶",
+ (0, 1, 0, 1): "─",
+ (0, 1, 0, 2): "╾",
+ (0, 1, 0, 3): "╼",
+ #
+ (0, 1, 1, 0): "┌",
+ (0, 1, 1, 1): "┬",
+ (0, 1, 1, 2): "┭",
+ (0, 1, 1, 3): "╤",
+ #
+ (0, 1, 2, 0): "┎",
+ (0, 1, 2, 1): "┰",
+ (0, 1, 2, 2): "┱",
+ (0, 1, 2, 3): "┱",
+ #
+ (0, 1, 3, 0): "╓",
+ (0, 1, 3, 1): "╥",
+ (0, 1, 3, 2): "╥",
+ (0, 1, 3, 3): "╥",
+ #
+ (0, 2, 0, 0): "╺",
+ (0, 2, 0, 1): "╼",
+ (0, 2, 0, 2): "━",
+ (0, 2, 0, 3): "━",
+ #
+ (0, 2, 1, 0): "┍",
+ (0, 2, 1, 1): "┮",
+ (0, 2, 1, 2): "┯",
+ (0, 2, 1, 3): "┯",
+ #
+ (0, 2, 2, 0): "┏",
+ (0, 2, 2, 1): "┲",
+ (0, 2, 2, 2): "┳",
+ (0, 2, 2, 3): "╦",
+ #
+ (0, 2, 3, 0): "╒",
+ (0, 2, 3, 1): "╥",
+ (0, 2, 3, 2): "╥",
+ (0, 2, 3, 3): "╦",
+ #
+ (0, 3, 0, 0): "╺",
+ (0, 3, 0, 1): "╾",
+ (0, 3, 0, 2): "╾",
+ (0, 3, 0, 3): "═",
+ #
+ (0, 3, 1, 0): "╒",
+ (0, 3, 1, 1): "╤",
+ (0, 3, 1, 2): "╤",
+ (0, 3, 1, 3): "╤",
+ #
+ (0, 3, 2, 0): "╒",
+ (0, 3, 2, 1): "╤",
+ (0, 3, 2, 2): "╤",
+ (0, 3, 2, 3): "╤",
+ #
+ (0, 3, 3, 0): "╔",
+ (0, 3, 3, 1): "╦",
+ (0, 3, 3, 2): "╦",
+ (0, 3, 3, 3): "╦",
+ #
+ (1, 0, 0, 0): "╵",
+ (1, 0, 0, 1): "┘",
+ (1, 0, 0, 2): "┙",
+ (1, 0, 0, 3): "╛",
+ #
+ (1, 0, 1, 0): "│",
+ (1, 0, 1, 1): "┤",
+ (1, 0, 1, 2): "┥",
+ (1, 0, 1, 3): "╡",
+ #
+ (1, 0, 2, 0): "╽",
+ (1, 0, 2, 1): "┧",
+ (1, 0, 2, 2): "┪",
+ (1, 0, 2, 3): "┪",
+ #
+ (1, 0, 3, 0): "╽",
+ (1, 0, 3, 1): "┧",
+ (1, 0, 3, 2): "┪",
+ (1, 0, 3, 3): "┪",
+ #
+ (1, 1, 0, 0): "└",
+ (1, 1, 0, 1): "┴",
+ (1, 1, 0, 2): "┵",
+ (1, 1, 0, 3): "┵",
+ #
+ (1, 1, 1, 0): "├",
+ (1, 1, 1, 1): "┼",
+ (1, 1, 1, 2): "┽",
+ (1, 1, 1, 3): "┽",
+ #
+ (1, 1, 2, 0): "┟",
+ (1, 1, 2, 1): "╁",
+ (1, 1, 2, 2): "╅",
+ (1, 1, 2, 3): "╅",
+ #
+ (1, 1, 3, 0): "┟",
+ (1, 1, 3, 1): "╁",
+ (1, 1, 3, 2): "╅",
+ (1, 1, 3, 3): "╅",
+ #
+ (1, 2, 0, 0): "┕",
+ (1, 2, 0, 1): "┶",
+ (1, 2, 0, 2): "┷",
+ (1, 2, 0, 3): "╧",
+ #
+ (1, 2, 1, 0): "┝",
+ (1, 2, 1, 1): "┾",
+ (1, 2, 1, 2): "┿",
+ (1, 2, 1, 3): "┿",
+ #
+ (1, 2, 2, 0): "┢",
+ (1, 2, 2, 1): "╆",
+ (1, 2, 2, 2): "╈",
+ (1, 2, 2, 3): "╈",
+ #
+ (1, 2, 3, 0): "┢",
+ (1, 2, 3, 1): "╆",
+ (1, 2, 3, 2): "╈",
+ (1, 2, 3, 3): "╈",
+ #
+ (1, 3, 0, 0): "╘",
+ (1, 3, 0, 1): "╧",
+ (1, 3, 0, 2): "╧",
+ (1, 3, 0, 3): "╧",
+ #
+ (1, 3, 1, 0): "╞",
+ (1, 3, 1, 1): "╬",
+ (1, 3, 1, 2): "╪",
+ (1, 3, 1, 3): "╪",
+ #
+ (1, 3, 2, 0): "╟",
+ (1, 3, 2, 1): "┾",
+ (1, 3, 2, 2): "┾",
+ (1, 3, 2, 3): "╪",
+ #
+ (1, 3, 3, 0): "╞",
+ (1, 3, 3, 1): "╆",
+ (1, 3, 3, 2): "╆",
+ (1, 3, 3, 3): "╈",
+ #
+ (2, 0, 0, 0): "╹",
+ (2, 0, 0, 1): "┚",
+ (2, 0, 0, 2): "┛",
+ (2, 0, 0, 3): "╛",
+ #
+ (2, 0, 1, 0): "╿",
+ (2, 0, 1, 1): "┦",
+ (2, 0, 1, 2): "┩",
+ (2, 0, 1, 3): "┩",
+ #
+ (2, 0, 2, 0): "┃",
+ (2, 0, 2, 1): "┨",
+ (2, 0, 2, 2): "┫",
+ (2, 0, 2, 3): "╢",
+ #
+ (2, 0, 3, 0): "║",
+ (2, 0, 3, 1): "╢",
+ (2, 0, 3, 2): "╢",
+ (2, 0, 3, 3): "╢",
+ #
+ (2, 1, 0, 0): "┖",
+ (2, 1, 0, 1): "┸",
+ (2, 1, 0, 2): "┹",
+ (2, 1, 0, 3): "┹",
+ #
+ (2, 1, 1, 0): "┞",
+ (2, 1, 1, 1): "╀",
+ (2, 1, 1, 2): "╃",
+ (2, 1, 1, 3): "╃",
+ #
+ (2, 1, 2, 0): "┠",
+ (2, 1, 2, 1): "╂",
+ (2, 1, 2, 2): "╉",
+ (2, 1, 2, 3): "╉",
+ #
+ (2, 1, 3, 0): "╟",
+ (2, 1, 3, 1): "╫",
+ (2, 1, 3, 2): "╫",
+ (2, 1, 3, 3): "╫",
+ #
+ (2, 2, 0, 0): "┗",
+ (2, 2, 0, 1): "┺",
+ (2, 2, 0, 2): "┻",
+ (2, 2, 0, 3): "┻",
+ #
+ (2, 2, 1, 0): "┡",
+ (2, 2, 1, 1): "╄",
+ (2, 2, 1, 2): "╇",
+ (2, 2, 1, 3): "╇",
+ #
+ (2, 2, 2, 0): "┣",
+ (2, 2, 2, 1): "╊",
+ (2, 2, 2, 2): "╋",
+ (2, 2, 2, 3): "╬",
+ #
+ (2, 2, 3, 0): "╠",
+ (2, 2, 3, 1): "╬",
+ (2, 2, 3, 2): "╬",
+ (2, 2, 3, 3): "╬",
+ #
+ (2, 3, 0, 0): "╚",
+ (2, 3, 0, 1): "╩",
+ (2, 3, 0, 2): "╩",
+ (2, 3, 0, 3): "╩",
+ #
+ (2, 3, 1, 0): "╞",
+ (2, 3, 1, 1): "╬",
+ (2, 3, 1, 2): "╬",
+ (2, 3, 1, 3): "╬",
+ #
+ (2, 3, 2, 0): "╞",
+ (2, 3, 2, 1): "╬",
+ (2, 3, 2, 2): "╬",
+ (2, 3, 2, 3): "╬",
+ #
+ (2, 3, 3, 0): "╠",
+ (2, 3, 3, 1): "╬",
+ (2, 3, 3, 2): "╬",
+ (2, 3, 3, 3): "╬",
+ #
+ (3, 0, 0, 0): "╹",
+ (3, 0, 0, 1): "╜",
+ (3, 0, 0, 2): "╜",
+ (3, 0, 0, 3): "╝",
+ #
+ (3, 0, 1, 0): "╿",
+ (3, 0, 1, 1): "┦",
+ (3, 0, 1, 2): "┦",
+ (3, 0, 1, 3): "┩",
+ #
+ (3, 0, 2, 0): "║",
+ (3, 0, 2, 1): "╢",
+ (3, 0, 2, 2): "╢",
+ (3, 0, 2, 3): "╣",
+ #
+ (3, 0, 3, 0): "║",
+ (3, 0, 3, 1): "╢",
+ (3, 0, 3, 2): "╢",
+ (3, 0, 3, 3): "╣",
+ #
+ (3, 1, 0, 0): "╙",
+ (3, 1, 0, 1): "╨",
+ (3, 1, 0, 2): "╨",
+ (3, 1, 0, 3): "╩",
+ #
+ (3, 1, 1, 0): "╟",
+ (3, 1, 1, 1): "╬",
+ (3, 1, 1, 2): "╬",
+ (3, 1, 1, 3): "╬",
+ #
+ (3, 1, 2, 0): "╟",
+ (3, 1, 2, 1): "╬",
+ (3, 1, 2, 2): "╬",
+ (3, 1, 2, 3): "╬",
+ #
+ (3, 1, 3, 0): "╟",
+ (3, 1, 3, 1): "╫",
+ (3, 1, 3, 2): "╫",
+ (3, 1, 3, 3): "╉",
+ #
+ (3, 2, 0, 0): "╙",
+ (3, 2, 0, 1): "╨",
+ (3, 2, 0, 2): "╨",
+ (3, 2, 0, 3): "╩",
+ #
+ (3, 2, 1, 0): "╟",
+ (3, 2, 1, 1): "╬",
+ (3, 2, 1, 2): "╬",
+ (3, 2, 1, 3): "╬",
+ #
+ (3, 2, 2, 0): "╟",
+ (3, 2, 2, 1): "╬",
+ (3, 2, 2, 2): "╬",
+ (3, 2, 2, 3): "╬",
+ #
+ (3, 2, 3, 0): "╟",
+ (3, 2, 3, 1): "╫",
+ (3, 2, 3, 2): "╫",
+ (3, 2, 3, 3): "╬",
+ #
+ (3, 3, 0, 0): "╚",
+ (3, 3, 0, 1): "╩",
+ (3, 3, 0, 2): "╩",
+ (3, 3, 0, 3): "╩",
+ #
+ (3, 3, 1, 0): "╠",
+ (3, 3, 1, 1): "╄",
+ (3, 3, 1, 2): "╄",
+ (3, 3, 1, 3): "╇",
+ #
+ (3, 3, 2, 0): "╠",
+ (3, 3, 2, 1): "╬",
+ (3, 3, 2, 2): "╬",
+ (3, 3, 2, 3): "╬",
+ #
+ (3, 3, 3, 0): "╠",
+ (3, 3, 3, 1): "╊",
+ (3, 3, 3, 2): "╬",
+ (3, 3, 3, 3): "╬",
+}
+
+
+@lru_cache(1024)
+def combine_quads(box1: Quad, box2: Quad) -> Quad:
+ """Combine two box drawing quads.
+
+ Args:
+ box1: Existing box quad.
+ box2: New box quad.
+
+ Returns:
+ A new box quad.
+ """
+ top1, right1, bottom1, left1 = box1
+ top2, right2, bottom2, left2 = box2
+ return (
+ top2 or top1,
+ right2 or right1,
+ bottom2 or bottom1,
+ left2 or left1,
+ )
diff --git a/src/textual/_cache.py b/src/textual/_cache.py
index a4b440e602..79a589ff0e 100644
--- a/src/textual/_cache.py
+++ b/src/textual/_cache.py
@@ -183,14 +183,22 @@ def discard(self, key: CacheKey) -> None:
Args:
key: Cache key.
"""
- link = self._cache.get(key)
- if link is None:
+ if key not in self._cache:
return
+ link = self._cache[key]
+
# Remove link from list
link[0][1] = link[1] # type: ignore[index]
link[1][0] = link[0] # type: ignore[index]
# Remove link from cache
+
+ if self._head[2] == key:
+ self._head = self._head[1] # type: ignore[assignment]
+ if self._head[2] == key: # type: ignore[index]
+ self._head = []
+
del self._cache[key]
+ self._full = False
class FIFOCache(Generic[CacheKey, CacheValue]):
diff --git a/src/textual/_callback.py b/src/textual/_callback.py
index abefeae557..756a9352af 100644
--- a/src/textual/_callback.py
+++ b/src/textual/_callback.py
@@ -1,7 +1,7 @@
from __future__ import annotations
import asyncio
-from functools import lru_cache
+from functools import lru_cache, partial
from inspect import isawaitable, signature
from typing import TYPE_CHECKING, Any, Callable
@@ -14,8 +14,19 @@
INVOKE_TIMEOUT_WARNING = 3
-@lru_cache(maxsize=2048)
def count_parameters(func: Callable) -> int:
+ """Count the number of parameters in a callable"""
+ if isinstance(func, partial):
+ return _count_parameters(func.func) + len(func.args)
+ if hasattr(func, "__self__"):
+ # Bound method
+ func = func.__func__ # type: ignore
+ return _count_parameters(func) - 1
+ return _count_parameters(func)
+
+
+@lru_cache(maxsize=2048)
+def _count_parameters(func: Callable) -> int:
"""Count the number of parameters in a callable"""
return len(signature(func).parameters)
diff --git a/src/textual/_cells.py b/src/textual/_cells.py
index 2c23b0e3ea..716faaed66 100644
--- a/src/textual/_cells.py
+++ b/src/textual/_cells.py
@@ -1,6 +1,8 @@
from typing import Callable
-__all__ = ["cell_len"]
+from textual.expand_tabs import get_tab_widths
+
+__all__ = ["cell_len", "cell_width_to_column_index"]
cell_len: Callable[[str], int]
@@ -8,3 +10,35 @@
from rich.cells import cached_cell_len as cell_len
except ImportError:
from rich.cells import cell_len
+
+
+def cell_width_to_column_index(line: str, cell_width: int, tab_width: int) -> int:
+ """Retrieve the column index corresponding to the given cell width.
+
+ Args:
+ line: The line of text to search within.
+ cell_width: The cell width to convert to column index.
+ tab_width: The tab stop width to expand tabs contained within the line.
+
+ Returns:
+ The column corresponding to the cell width.
+ """
+ column_index = 0
+ total_cell_offset = 0
+ for part, expanded_tab_width in get_tab_widths(line, tab_width):
+ # Check if the click landed on a character within this part.
+ for character in part:
+ total_cell_offset += cell_len(character)
+ if total_cell_offset > cell_width:
+ return column_index
+ column_index += 1
+
+ # Account for the appearance of the tab character for this part
+ total_cell_offset += expanded_tab_width
+ # Check if the click falls within the boundary of the expanded tab.
+ if total_cell_offset > cell_width:
+ return column_index
+
+ column_index += 1
+
+ return len(line)
diff --git a/src/textual/_compose.py b/src/textual/_compose.py
index 022664d2b0..482b27fb6a 100644
--- a/src/textual/_compose.py
+++ b/src/textual/_compose.py
@@ -16,19 +16,57 @@ def compose(node: App | Widget) -> list[Widget]:
Returns:
A list of widgets.
"""
+ _rich_traceback_omit = True
+ from .widget import MountError, Widget
+
app = node.app
nodes: list[Widget] = []
compose_stack: list[Widget] = []
composed: list[Widget] = []
app._compose_stacks.append(compose_stack)
app._composed.append(composed)
+ iter_compose = iter(node.compose())
+ is_generator = hasattr(iter_compose, "throw")
try:
- for child in node.compose():
+ while True:
+ try:
+ child = next(iter_compose)
+ except StopIteration:
+ break
+
+ if not isinstance(child, Widget):
+ mount_error = MountError(
+ f"Can't mount {type(child)}; expected a Widget instance."
+ )
+ if is_generator:
+ iter_compose.throw(mount_error) # type: ignore
+ else:
+ raise mount_error from None
+
+ try:
+ child.id
+ except AttributeError:
+ mount_error = MountError(
+ "Widget is missing an 'id' attribute; did you forget to call super().__init__()?"
+ )
+ if is_generator:
+ iter_compose.throw(mount_error) # type: ignore
+ else:
+ raise mount_error from None
+
if composed:
nodes.extend(composed)
composed.clear()
if compose_stack:
- compose_stack[-1].compose_add_child(child)
+ try:
+ compose_stack[-1].compose_add_child(child)
+ except Exception as error:
+ if is_generator:
+ # So the error is raised inside the generator
+ # This will generate a more sensible traceback for the dev
+ iter_compose.throw(error) # type: ignore
+ else:
+ raise
else:
nodes.append(child)
if composed:
diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py
index ddfba87806..6b786e8b26 100644
--- a/src/textual/_compositor.py
+++ b/src/textual/_compositor.py
@@ -423,9 +423,7 @@ def reflow_visible(self, parent: Widget, size: Size) -> set[Widget]:
self.size = size
# Keep a copy of the old map because we're going to compare it with the update
- old_map = (
- self._visible_map if self._visible_map is not None else self._full_map or {}
- )
+ old_map = self._visible_map or {}
map, widgets = self._arrange_root(parent, size, visible_only=True)
# Replace map and widgets
@@ -573,6 +571,8 @@ def add_widget(
visible: Whether the widget should be visible by default.
This may be overridden by the CSS rule `visibility`.
"""
+ if not widget._is_mounted:
+ return
styles = widget.styles
visibility = styles.get_rule("visibility")
if visibility is not None:
@@ -594,10 +594,11 @@ def add_widget(
# Widgets with scrollbars (containers or scroll view) require additional processing
if widget.is_scrollable:
# The region that contains the content (container region minus scrollbars)
- child_region = widget._get_scrollable_region(container_region)
-
- # Adjust the clip region accordingly
- sub_clip = clip.intersection(child_region)
+ child_region = (
+ container_region
+ if widget.loading
+ else widget._get_scrollable_region(container_region)
+ )
# The region covered by children relative to parent widget
total_region = child_region.reset_offset
@@ -608,9 +609,12 @@ def add_widget(
arranged_widgets = arrange_result.widgets
widgets.update(arranged_widgets)
+ # Get the region that will be updated
+ sub_clip = clip.intersection(child_region)
+
if visible_only:
placements = arrange_result.get_visible_placements(
- container_size.region + widget.scroll_offset
+ sub_clip - child_region.offset + widget.scroll_offset
)
else:
placements = arrange_result.placements
@@ -620,9 +624,9 @@ def add_widget(
placement_offset = container_region.offset
placement_scroll_offset = placement_offset - widget.scroll_offset
- _layers = widget.layers
layers_to_index = {
- layer_name: index for index, layer_name in enumerate(_layers)
+ layer_name: index
+ for index, layer_name in enumerate(widget.layers)
}
get_layer_index = layers_to_index.get
@@ -660,7 +664,10 @@ def add_widget(
if visible:
# Add any scrollbars
- if any(widget.scrollbars_enabled):
+ if (
+ widget.show_vertical_scrollbar
+ or widget.show_horizontal_scrollbar
+ ):
for chrome_widget, chrome_region in widget._arrange_scrollbars(
container_region
):
@@ -874,16 +881,14 @@ def cuts(self) -> list[list[int]]:
return self._cuts
width, height = self.size
- screen_region = self.size.region
cuts = [[0, width] for _ in range(height)]
intersection = Region.intersection
extend = list.extend
for region, clip in self.visible_widgets.values():
- region = intersection(region, clip)
- if region and (region in screen_region):
- x, y, region_width, region_height = region
+ x, y, region_width, region_height = intersection(region, clip)
+ if region_width and region_height:
region_cuts = (x, x + region_width)
for cut in cuts[y : y + region_height]:
extend(cut, region_cuts)
@@ -934,15 +939,13 @@ def _get_renders(
_Region(0, 0, region.width, region.height)
)
else:
- clipped_region = intersection(region, clip)
- if not clipped_region:
- continue
- new_x, new_y, new_width, new_height = clipped_region
- delta_x = new_x - region.x
- delta_y = new_y - region.y
- yield region, clip, widget.render_lines(
- _Region(delta_x, delta_y, new_width, new_height)
- )
+ new_x, new_y, new_width, new_height = intersection(region, clip)
+ if new_width and new_height:
+ yield region, clip, widget.render_lines(
+ _Region(
+ new_x - region.x, new_y - region.y, new_width, new_height
+ )
+ )
def render_update(
self, full: bool = False, screen_stack: list[Screen] | None = None
diff --git a/src/textual/_doc.py b/src/textual/_doc.py
index 8639f1d25d..54aa7008be 100644
--- a/src/textual/_doc.py
+++ b/src/textual/_doc.py
@@ -127,6 +127,8 @@ async def auto_pilot(pilot: Pilot) -> None:
if wait_for_animation:
await pilot.wait_for_scheduled_animations()
await pilot.pause()
+ await pilot.pause()
+ await pilot.wait_for_scheduled_animations()
svg = app.export_screenshot(title=title)
app.exit(svg)
diff --git a/src/textual/_layout.py b/src/textual/_layout.py
index 575dc547fb..c147a0e419 100644
--- a/src/textual/_layout.py
+++ b/src/textual/_layout.py
@@ -5,7 +5,9 @@
from typing import TYPE_CHECKING, ClassVar, Iterable, NamedTuple
from ._spatial_map import SpatialMap
+from .canvas import Canvas, Rectangle
from .geometry import Offset, Region, Size, Spacing
+from .strip import StripRenderable
if TYPE_CHECKING:
from typing_extensions import TypeAlias
@@ -63,8 +65,17 @@ def get_visible_placements(self, region: Region) -> list[WidgetPlacement]:
Returns:
Set of placements.
"""
+ if self.total_region in region:
+ # Short circuit for when we want all the placements
+ return self.placements
visible_placements = self.spatial_map.get_values_in_region(region)
- return visible_placements
+ overlaps = region.overlaps
+ culled_placements = [
+ placement
+ for placement in visible_placements
+ if placement.fixed or overlaps(placement.region)
+ ]
+ return culled_placements
class WidgetPlacement(NamedTuple):
@@ -172,7 +183,54 @@ def get_content_height(
height = 0
else:
# Use a height of zero to ignore relative heights
- arrangement = widget._arrange(Size(width, 0))
+ styles_height = widget.styles.height
+ if widget._parent and len(widget._nodes) == 1:
+ # If it is an only child with height auto we want it to expand
+ height = (
+ container.height
+ if styles_height is not None and styles_height.is_auto
+ else 0
+ )
+ else:
+ height = 0
+ arrangement = widget._arrange(Size(width, height))
height = arrangement.total_region.bottom
return height
+
+ def render_keyline(self, container: Widget) -> StripRenderable:
+ """Render keylines around all widgets.
+
+ Args:
+ container: The container widget.
+
+ Returns:
+ A renderable to draw the keylines.
+ """
+ width, height = container.outer_size
+ canvas = Canvas(width, height)
+
+ line_style, keyline_color = container.styles.keyline
+
+ container_offset = container.content_region.offset
+
+ def get_rectangle(region: Region) -> Rectangle:
+ """Get a canvas Rectangle that wraps a region.
+
+ Args:
+ region: Widget region.
+
+ Returns:
+ A Rectangle that encloses the widget.
+ """
+ offset = region.offset - container_offset - (1, 1)
+ width, height = region.size
+ return Rectangle(offset, width + 2, height + 2, keyline_color, line_style)
+
+ primitives = [
+ get_rectangle(widget.region)
+ for widget in container.children
+ if widget.visible
+ ]
+ canvas_renderable = canvas.render(primitives, container.rich_style)
+ return canvas_renderable
diff --git a/src/textual/_on.py b/src/textual/_on.py
index 40e2a706c8..fd4c3d12a0 100644
--- a/src/textual/_on.py
+++ b/src/textual/_on.py
@@ -27,7 +27,7 @@ def on(
"""Decorator to declare that the method is a message handler.
The decorator accepts an optional CSS selector that will be matched against a widget exposed by
- a `control` attribute on the message.
+ a `control` property on the message.
Example:
```python
diff --git a/src/textual/_opacity.py b/src/textual/_opacity.py
index f2bc57508a..41e48137e7 100644
--- a/src/textual/_opacity.py
+++ b/src/textual/_opacity.py
@@ -1,4 +1,4 @@
-from typing import Iterable
+from typing import Iterable, cast
from rich.segment import Segment
from rich.style import Style
@@ -25,8 +25,8 @@ def _apply_opacity(
from_rich_color = Color.from_rich_color
from_color = Style.from_color
blend = base_background.blend
- for segment in segments:
- text, style, _ = segment
+ styled_segments = cast("Iterable[tuple[str, Style, object]]", segments)
+ for text, style, _ in styled_segments:
blended_style = style
if style.color is not None:
diff --git a/src/textual/_parser.py b/src/textual/_parser.py
index a1a187da46..812e063882 100644
--- a/src/textual/_parser.py
+++ b/src/textual/_parser.py
@@ -165,8 +165,6 @@ def parse(
test_parser = TestParser()
- import time
-
for n in range(0, len(data), 5):
for token in test_parser.feed(data[n : n + 5]):
print(token)
diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py
index 79f3bc0772..219c6dcadf 100644
--- a/src/textual/_segment_tools.py
+++ b/src/textual/_segment_tools.py
@@ -197,11 +197,23 @@ def align_lines(
Returns:
Aligned lines.
"""
-
+ if not lines:
+ return
width, height = size
- shape_width, shape_height = Segment.get_shape(lines)
+ get_line_length = Segment.get_line_length
+ line_lengths = [get_line_length(line) for line in lines]
+ shape_width = max(line_lengths)
+ shape_height = len(line_lengths)
def blank_lines(count: int) -> list[list[Segment]]:
+ """Create blank lines.
+
+ Args:
+ count: Desired number of blank lines.
+
+ Returns:
+ A list of blank lines.
+ """
return [[Segment(" " * width, style)]] * count
top_blank_lines = bottom_blank_lines = 0
@@ -211,31 +223,38 @@ def blank_lines(count: int) -> list[list[Segment]]:
bottom_blank_lines = vertical_excess_space
elif vertical == "middle":
top_blank_lines = vertical_excess_space // 2
- bottom_blank_lines = height - top_blank_lines
+ bottom_blank_lines = vertical_excess_space - top_blank_lines
elif vertical == "bottom":
top_blank_lines = vertical_excess_space
- yield from blank_lines(top_blank_lines)
+ if top_blank_lines:
+ yield from blank_lines(top_blank_lines)
horizontal_excess_space = max(0, width - shape_width)
- adjust_line_length = Segment.adjust_line_length
if horizontal == "left":
- for line in lines:
- yield adjust_line_length(line, width, style, pad=True)
+ for cell_length, line in zip(line_lengths, lines):
+ if cell_length == width:
+ yield line
+ else:
+ yield line_pad(line, 0, width - cell_length, style)
elif horizontal == "center":
left_space = horizontal_excess_space // 2
- for line in lines:
- yield [
- Segment(" " * left_space, style),
- *adjust_line_length(line, width - left_space, style, pad=True),
- ]
+ for cell_length, line in zip(line_lengths, lines):
+ if cell_length == width:
+ yield line
+ else:
+ yield line_pad(
+ line, left_space, width - cell_length - left_space, style
+ )
elif horizontal == "right":
- get_line_length = Segment.get_line_length
- for line in lines:
- left_space = width - get_line_length(line)
- yield [Segment(" " * left_space, style), *line]
-
- yield from blank_lines(bottom_blank_lines)
+ for cell_length, line in zip(line_lengths, lines):
+ if width == cell_length:
+ yield line
+ else:
+ yield line_pad(line, width - cell_length, 0, style)
+
+ if bottom_blank_lines:
+ yield from blank_lines(bottom_blank_lines)
diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py
index 3f778a06bb..9426e9b949 100644
--- a/src/textual/_spatial_map.py
+++ b/src/textual/_spatial_map.py
@@ -65,7 +65,7 @@ def insert(
indicates fixed regions. Fixed regions don't scroll and are always visible.
Args:
- regions_and_values: An iterable of (REGION, FIXED, VALUE).
+ regions_and_values: An iterable of (REGION, FIXED, OVERLAY, VALUE).
"""
append_fixed = self._fixed.append
get_grid_list = self._map.__getitem__
diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py
index c874015dc3..adea09045b 100644
--- a/src/textual/_styles_cache.py
+++ b/src/textual/_styles_cache.py
@@ -9,10 +9,12 @@
from rich.style import Style
from rich.text import Text
+from . import log
from ._border import get_box, render_border_label, render_row
from ._opacity import _apply_opacity
from ._segment_tools import line_pad, line_trim
from .color import Color
+from .constants import DEBUG
from .filter import LineFilter
from .geometry import Region, Size, Spacing
from .renderables.text_opacity import TextOpacity
@@ -148,10 +150,10 @@ def render_widget(self, widget: Widget, crop: Region) -> list[Strip]:
and hover_style._meta
and "@click" in hover_style.meta
):
- link_hover_style = widget.link_hover_style
- if link_hover_style:
+ link_style_hover = widget.link_style_hover
+ if link_style_hover:
strips = [
- strip.style_links(hover_style.link_id, link_hover_style)
+ strip.style_links(hover_style.link_id, link_style_hover)
for strip in strips
]
@@ -228,10 +230,17 @@ def render(
self._cache[y] = strip
else:
strip = self._cache[y]
+
if filters:
for filter in filters:
strip = strip.apply_filter(filter, background)
+
+ if DEBUG:
+ if any([not (segment.control or segment.text) for segment in strip]):
+ log.warning(f"Strip contains invalid empty Segments: {strip!r}.")
+
add_strip(strip)
+
self._dirty_lines.difference_update(crop.line_range)
if crop.column_span != (0, width):
diff --git a/src/textual/_system_commands.py b/src/textual/_system_commands.py
index c8b499c395..8cbb6ef017 100644
--- a/src/textual/_system_commands.py
+++ b/src/textual/_system_commands.py
@@ -17,7 +17,7 @@ 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.
+ query: The user input to be matched.
Yields:
Command hits for use in the command palette.
diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py
new file mode 100644
index 0000000000..33845e4494
--- /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 03f83f619d..b1ad7972f3 100644
--- a/src/textual/_types.py
+++ b/src/textual/_types.py
@@ -26,6 +26,10 @@ 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."""
diff --git a/src/textual/_work_decorator.py b/src/textual/_work_decorator.py
index dbfdba185e..cf4bc624df 100644
--- a/src/textual/_work_decorator.py
+++ b/src/textual/_work_decorator.py
@@ -1,5 +1,4 @@
"""
-
A decorator used to create [workers](/guide/workers).
"""
diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py
index edf587a3f9..1b2b47a64f 100644
--- a/src/textual/_xterm_parser.py
+++ b/src/textual/_xterm_parser.py
@@ -84,6 +84,13 @@ def parse_mouse_code(self, code: str) -> events.Event | None:
return event
return None
+ _reissued_sequence_debug_book: Callable[[str], None] | None = None
+ """INTERNAL USE ONLY!
+
+ If this property is set to a callable, it will be called *instead* of
+ the reissued sequence being emitted as key events.
+ """
+
def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]:
ESC = "\x1b"
read1 = self.read1
@@ -94,6 +101,9 @@ def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]:
use_prior_escape = False
def reissue_sequence_as_keys(reissue_sequence: str) -> None:
+ if self._reissued_sequence_debug_book is not None:
+ self._reissued_sequence_debug_book(reissue_sequence)
+ return
for character in reissue_sequence:
key_events = sequence_to_key_events(character)
for event in key_events:
@@ -233,10 +243,20 @@ def _sequence_to_key_events(
Keys
"""
keys = ANSI_SEQUENCES_KEYS.get(sequence)
- if keys is not None:
+ if isinstance(keys, tuple):
+ # If the sequence mapped to a tuple, then it's values from the
+ # `Keys` enum. Raise key events from what we find in the tuple.
for key in keys:
yield events.Key(key.value, sequence if len(sequence) == 1 else None)
- elif len(sequence) == 1:
+ return
+ # If keys is a string, the intention is that it's a mapping to a
+ # character, which should really be treated as the sequence for the
+ # purposes of the next step...
+ if isinstance(keys, str):
+ sequence = keys
+ # If the sequence is a single character, attempt to process it as a
+ # key.
+ if len(sequence) == 1:
try:
if not sequence.isalnum():
name = _character_to_key(sequence)
diff --git a/src/textual/app.py b/src/textual/app.py
index f98d98f482..c900f7b2ba 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -15,9 +15,8 @@
import platform
import sys
import threading
-import unicodedata
import warnings
-from asyncio import Task
+from asyncio import Task, create_task
from concurrent.futures import Future
from contextlib import (
asynccontextmanager,
@@ -53,14 +52,13 @@
import rich.repr
from rich import terminal_theme
from rich.console import Console, RenderableType
+from rich.control import Control
from rich.protocol import is_renderable
from rich.segment import Segment, Segments
-from rich.traceback import Traceback
from . import Logger, LogGroup, LogVerbosity, actions, constants, events, log, messages
from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction
from ._ansi_sequences import SYNC_END, SYNC_START
-from ._asyncio import create_task
from ._callback import invoke
from ._compose import compose
from ._compositor import CompositorUpdate
@@ -68,7 +66,6 @@
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 import SystemCommands
from ._wait import wait_for_idle
from ._worker_manager import WorkerManager
from .actions import ActionParseResult, SkipAction
@@ -76,14 +73,14 @@
from .binding import Binding, BindingType, _Bindings
from .command import CommandPalette, Provider
from .css.query import NoMatches
-from .css.stylesheet import Stylesheet
+from .css.stylesheet import RulesMap, Stylesheet
from .design import ColorSystem
from .dom import DOMNode
from .driver import Driver
from .drivers.headless_driver import HeadlessDriver
+from .errors import NoWidget
from .features import FeatureFlag, parse_features
from .file_monitor import FileMonitor
-from .filter import ANSIToTruecolor, DimFilter, LineFilter, Monochrome
from .geometry import Offset, Region, Size
from .keys import (
REPLACED_KEYS,
@@ -95,18 +92,26 @@
from .notifications import Notification, Notifications, Notify, SeverityLevel
from .reactive import Reactive
from .renderables.blank import Blank
-from .screen import Screen, ScreenResultCallbackType, ScreenResultType
+from .screen import (
+ Screen,
+ ScreenResultCallbackType,
+ ScreenResultType,
+ _SystemModalScreen,
+)
from .widget import AwaitMount, Widget
from .widgets._toast import ToastRack
+from .worker import NoActiveWorker, get_current_worker
if TYPE_CHECKING:
from textual_dev.client import DevtoolsClient
- from typing_extensions import Coroutine, TypeAlias
+ from typing_extensions import Coroutine, Literal, TypeAlias
+ from ._system_commands import SystemCommands
from ._types import MessageTarget
# Unused & ignored imports are needed for the docs to link to these objects:
from .css.query import WrongType # type: ignore # noqa: F401
+ from .filter import LineFilter
from .message import Message
from .pilot import Pilot
from .widget import MountError # type: ignore # noqa: F401
@@ -153,6 +158,17 @@
"""Signature for valid callbacks that can be used to control apps."""
+def get_system_commands() -> type[SystemCommands]:
+ """Callable to lazy load the system commands.
+
+ Returns:
+ System commands class.
+ """
+ from ._system_commands import SystemCommands
+
+ return SystemCommands
+
+
class AppError(Exception):
"""Base class for general App related exceptions."""
@@ -254,17 +270,14 @@ class App(Generic[ReturnType], DOMNode):
and therefore takes priority in the event of a specificity clash."""
# Default (the lowest priority) CSS
- DEFAULT_CSS: ClassVar[
- str
- ] = """
+ DEFAULT_CSS: ClassVar[str]
+ DEFAULT_CSS = """
App {
background: $background;
color: $text;
}
-
*:disabled:can-focus {
opacity: 0.7;
-
}
"""
@@ -297,7 +310,7 @@ class MyApp(App[None]):
...
```
"""
- SCREENS: ClassVar[dict[str, Screen | Callable[[], Screen]]] = {}
+ SCREENS: ClassVar[dict[str, Screen[Any] | Callable[[], Screen[Any]]]] = {}
"""Screens associated with the app for the lifetime of the app."""
AUTO_FOCUS: ClassVar[str | None] = "*"
@@ -328,8 +341,10 @@ class MyApp(App[None]):
ENABLE_COMMAND_PALETTE: ClassVar[bool] = True
"""Should the [command palette][textual.command.CommandPalette] be enabled for the application?"""
- COMMANDS: ClassVar[set[type[Provider]]] = {SystemCommands}
- """Command providers used by the [command palette](/guide/command).
+ COMMANDS: ClassVar[set[type[Provider] | Callable[[], type[Provider]]]] = {
+ get_system_commands
+ }
+ """Command providers used by the [command palette](/guide/command_palette).
Should be a set of [command.Provider][textual.command.Provider] classes.
"""
@@ -352,6 +367,12 @@ class MyApp(App[None]):
self.app.dark = not self.app.dark # Toggle dark mode
```
"""
+ app_focus = Reactive(True, compute=False)
+ """Indicates if the app has focus.
+
+ When run in the terminal, the app always has focus. When run in the web, the app will
+ get focus when the terminal widget has focus.
+ """
def __init__(
self,
@@ -373,6 +394,7 @@ def __init__(
Raises:
CssPathError: When the supplied CSS path(s) are an unexpected type.
"""
+ self._start_time = perf_counter()
super().__init__()
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
@@ -380,11 +402,15 @@ def __init__(
environ = dict(os.environ)
no_color = environ.pop("NO_COLOR", None)
if no_color is not None:
+ from .filter import Monochrome
+
self._filters.append(Monochrome())
for filter_name in constants.FILTERS.split(","):
filter = filter_name.lower().strip()
if filter == "dim":
+ from .filter import ANSIToTruecolor, DimFilter
+
self._filters.append(ANSIToTruecolor(terminal_theme.DIMMED_MONOKAI))
self._filters.append(DimFilter())
@@ -418,6 +444,15 @@ def __init__(
self._animate = self._animator.bind(self)
self.mouse_position = Offset(0, 0)
+ self._mouse_down_widget: Widget | None = None
+ """The widget that was most recently mouse downed (used to create click events)."""
+
+ self.cursor_position = Offset(0, 0)
+ """The position of the terminal cursor in screen-space.
+
+ This can be set by widgets and is useful for controlling the
+ positioning of OS IME and emoji popup menus."""
+
self._exception: Exception | None = None
"""The unhandled exception which is leading to the app shutting down,
or None if the app is still running with no unhandled exceptions."""
@@ -515,7 +550,7 @@ def __init__(
self.css_monitor = (
FileMonitor(self.css_path, self._on_css_change)
- if ((watch_css or self.debug) and self.css_path)
+ if watch_css or self.debug
else None
)
self._screenshot: str | None = None
@@ -527,8 +562,15 @@ def __init__(
self._capture_print: WeakKeyDictionary[
MessageTarget, tuple[bool, bool]
] = WeakKeyDictionary()
+ """Registry of the MessageTargets which are capturing output at any given time."""
self._capture_stdout = _PrintCapture(self, stderr=False)
+ """File-like object capturing data written to stdout."""
self._capture_stderr = _PrintCapture(self, stderr=True)
+ """File-like object capturing data written to stderr."""
+ self._original_stdout = sys.__stdout__
+ """The original stdout stream (before redirection etc)."""
+ self._original_stderr = sys.__stderr__
+ """The original stderr stream (before redirection etc)."""
self.set_class(self.dark, "-dark-mode")
self.set_class(not self.dark, "-light-mode")
@@ -543,7 +585,7 @@ def validate_sub_title(self, sub_title: Any) -> str:
@property
def workers(self) -> WorkerManager:
- """The [worker](guide/workers/) manager.
+ """The [worker](/guide/workers/) manager.
Returns:
An object to manage workers.
@@ -587,8 +629,14 @@ def children(self) -> Sequence["Widget"]:
A sequence of widgets.
"""
try:
- return (self.screen,)
- except ScreenError:
+ return (
+ next(
+ screen
+ for screen in reversed(self._screen_stack)
+ if not isinstance(screen, _SystemModalScreen)
+ ),
+ )
+ except StopIteration:
return ()
@contextmanager
@@ -728,7 +776,10 @@ def focused(self) -> Widget | None:
Returns:
The currently focused widget, or `None` if nothing is focused.
"""
- return self.screen.focused
+ focused = self.screen.focused
+ if focused is not None and focused.loading:
+ return None
+ return focused
@property
def namespace_bindings(self) -> dict[str, tuple[DOMNode, Binding]]:
@@ -778,8 +829,8 @@ def watch_dark(self, dark: bool) -> None:
This method handles the transition between light and dark mode when you
change the [dark][textual.app.App.dark] attribute.
"""
- self.set_class(dark, "-dark-mode")
- self.set_class(not dark, "-light-mode")
+ self.set_class(dark, "-dark-mode", update=False)
+ self.set_class(not dark, "-light-mode", update=False)
self.call_later(self.refresh_css)
def get_driver_class(self) -> Type[Driver]:
@@ -945,11 +996,23 @@ def _log(
except Exception as error:
self._handle_exception(error)
+ def get_loading_widget(self) -> Widget:
+ """Get a widget to be used as a loading indicator.
+
+ Extend this method if you want to display the loading state a little differently.
+
+ Returns:
+ A widget to display a loading state.
+ """
+ from .widgets import LoadingIndicator
+
+ return LoadingIndicator()
+
def call_from_thread(
self,
callback: Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]],
- *args: object,
- **kwargs: object,
+ *args: Any,
+ **kwargs: Any,
) -> CallThreadReturnType:
"""Run a callable from another thread, and return the result.
@@ -1113,14 +1176,18 @@ def get_key_display(self, key: str) -> str:
async def _press_keys(self, keys: Iterable[str]) -> None:
"""A task to send key events."""
+ import unicodedata
+
app = self
driver = app._driver
assert driver is not None
for key in keys:
if key.startswith("wait:"):
_, wait_ms = key.split(":")
- print(f"(pause {wait_ms}ms)")
await asyncio.sleep(float(wait_ms) / 1000)
+ await wait_for_idle(0)
+ await app._animator.wait_until_complete()
+ await wait_for_idle(0)
else:
if len(key) == 1 and not key.isalnum():
key = _character_to_key(key)
@@ -1130,7 +1197,6 @@ async def _press_keys(self, keys: Iterable[str]) -> None:
char = unicodedata.lookup(_get_unicode_name_from_key(original_key))
except KeyError:
char = key if len(key) == 1 else None
- print(f"press {key!r} (char={char!r})")
key_event = events.Key(key, char)
key_event._set_sender(app)
driver.send_event(key_event)
@@ -1149,14 +1215,27 @@ def _flush(self, stderr: bool = False) -> None:
self._devtools_redirector.flush()
def _print(self, text: str, stderr: bool = False) -> None:
- """Called with capture print.
+ """Called with captured print.
+
+ Dispatches printed content to appropriate destinations: devtools,
+ widgets currently capturing output, stdout/stderr.
Args:
text: Text that has been printed.
stderr: True if the print was to stderr, or False for stdout.
"""
if self._devtools_redirector is not None:
- self._devtools_redirector.write(text)
+ current_frame = inspect.currentframe()
+ self._devtools_redirector.write(
+ text, current_frame.f_back if current_frame is not None else None
+ )
+
+ # If we're in headless mode, we want printed text to still reach stdout/stderr.
+ if self.is_headless:
+ target_stream = self._original_stderr if stderr else self._original_stdout
+ target_stream.write(text)
+
+ # Send Print events to all widgets that are currently capturing output.
for target, (_stdout, _stderr) in self._capture_print.items():
if (_stderr and stderr) or (_stdout and not stderr):
target.post_message(events.Print(text, stderr=stderr))
@@ -1166,7 +1245,7 @@ def begin_capture_print(
) -> None:
"""Capture content that is printed (or written to stdout / stderr).
- If printing is captured, the `target` will be send an [events.Print][textual.events.Print] message.
+ If printing is captured, the `target` will be sent an [events.Print][textual.events.Print] message.
Args:
target: The widget where print content will be sent.
@@ -1195,10 +1274,14 @@ async def run_test(
tooltips: bool = False,
notifications: bool = False,
message_hook: Callable[[Message], None] | None = None,
- ) -> AsyncGenerator[Pilot, None]:
- """An asynchronous context manager for testing app.
+ ) -> AsyncGenerator[Pilot[ReturnType], None]:
+ """An asynchronous context manager for testing apps.
- Use this to run your app in "headless" (no output) mode and driver the app via a [Pilot][textual.pilot.Pilot] object.
+ !!! tip
+
+ 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:
@@ -1214,7 +1297,8 @@ async def run_test(
or None to auto-detect.
tooltips: Enable tooltips when testing.
notifications: Enable notifications when testing.
- message_hook: An optional callback that will called with every message going through the app.
+ message_hook: An optional callback that will be called each time any message arrives at any
+ message pump in the app.
"""
from .pilot import Pilot
@@ -1318,6 +1402,7 @@ async def run_auto_pilot(
try:
app._loop = asyncio.get_running_loop()
app._thread_id = threading.get_ident()
+
await app._process_messages(
ready_callback=None if auto_pilot is None else app_ready,
headless=headless,
@@ -1375,8 +1460,10 @@ async def run_app() -> None:
return self.return_value
async def _on_css_change(self) -> None:
- """Called when the CSS changes (if watch_css is True)."""
- css_paths = self.css_path
+ """Callback for the file monitor, called when CSS files change."""
+ css_paths = (
+ self.css_monitor._paths if self.css_monitor is not None else self.css_path
+ )
if css_paths:
try:
time = perf_counter()
@@ -1502,7 +1589,6 @@ def update_styles(self, node: DOMNode) -> None:
will be added, and this method is called to apply the corresponding
:hover styles.
"""
-
descendants = node.walk_children(with_self=True)
self.stylesheet.update_nodes(descendants, animate=True)
@@ -1565,28 +1651,40 @@ def mount_all(
"""
return self.mount(*widgets, before=before, after=after)
- def _init_mode(self, mode: str) -> None:
+ def _init_mode(self, mode: str) -> AwaitMount:
"""Do internal initialisation of a new screen stack mode.
Args:
mode: Name of the mode.
+
+ Returns:
+ An optionally awaitable object which can be awaited until the screen
+ associated with the mode has been mounted.
"""
stack = self._screen_stacks.get(mode, [])
- if not stack:
+ if stack:
+ await_mount = AwaitMount(stack[0], [])
+ else:
_screen = self.MODES[mode]
new_screen: Screen | str = _screen() if callable(_screen) else _screen
- screen, _ = self._get_screen(new_screen)
+ screen, await_mount = self._get_screen(new_screen)
stack.append(screen)
self._load_screen_css(screen)
+
self._screen_stacks[mode] = stack
+ return await_mount
- def switch_mode(self, mode: str) -> None:
+ def switch_mode(self, mode: str) -> AwaitMount:
"""Switch to a given mode.
Args:
mode: The mode to switch to.
+ Returns:
+ An optionally awaitable object which waits for the screen associated
+ with the mode to be mounted.
+
Raises:
UnknownModeError: If trying to switch to an unknown mode.
"""
@@ -1597,13 +1695,19 @@ def switch_mode(self, mode: str) -> None:
self.screen.refresh()
if mode not in self._screen_stacks:
- self._init_mode(mode)
+ await_mount = self._init_mode(mode)
+ else:
+ await_mount = AwaitMount(self.screen, [])
+
self._current_mode = mode
self.screen._screen_resized(self.size)
self.screen.post_message(events.ScreenResume())
+
self.log.system(f"{self._current_mode!r} is the current mode")
self.log.system(f"{self.screen} is active")
+ return await_mount
+
def add_mode(
self, mode: str, base_screen: str | Screen | Callable[[], Screen]
) -> None:
@@ -1721,19 +1825,22 @@ def _load_screen_css(self, screen: Screen):
update = False
for path in screen.css_path:
- if not self.stylesheet.has_source(path):
+ if not self.stylesheet.has_source(str(path), ""):
self.stylesheet.read(path)
update = True
if screen.CSS:
try:
- screen_css_path = (
- f"{inspect.getfile(screen.__class__)}:{screen.__class__.__name__}"
- )
+ screen_path = inspect.getfile(screen.__class__)
except (TypeError, OSError):
- screen_css_path = f"{screen.__class__.__name__}"
- if not self.stylesheet.has_source(screen_css_path):
+ screen_path = ""
+ screen_class_var = f"{screen.__class__.__name__}.CSS"
+ read_from = (screen_path, screen_class_var)
+ if not self.stylesheet.has_source(screen_path, screen_class_var):
self.stylesheet.add_source(
- screen.CSS, path=screen_css_path, is_default_css=False
+ screen.CSS,
+ read_from=read_from,
+ is_default_css=False,
+ scope=screen._css_type_name if screen.SCOPED_CSS else "",
)
update = True
if update:
@@ -1760,25 +1867,58 @@ def _replace_screen(self, screen: Screen) -> Screen:
self.log.system(f"{screen} REMOVED")
return screen
+ @overload
def push_screen(
self,
screen: Screen[ScreenResultType] | str,
callback: ScreenResultCallbackType[ScreenResultType] | None = None,
+ wait_for_dismiss: Literal[False] = False,
) -> AwaitMount:
+ ...
+
+ @overload
+ def push_screen(
+ self,
+ screen: Screen[ScreenResultType] | str,
+ callback: ScreenResultCallbackType[ScreenResultType] | None = None,
+ wait_for_dismiss: Literal[True] = True,
+ ) -> asyncio.Future[ScreenResultType]:
+ ...
+
+ def push_screen(
+ self,
+ screen: Screen[ScreenResultType] | str,
+ callback: ScreenResultCallbackType[ScreenResultType] | None = None,
+ wait_for_dismiss: bool = False,
+ ) -> AwaitMount | asyncio.Future[ScreenResultType]:
"""Push a new [screen](/guide/screens) on the screen stack, making it the current screen.
Args:
screen: A Screen instance or the name of an installed screen.
callback: An optional callback function that will be called if the screen is [dismissed][textual.screen.Screen.dismiss] with a result.
+ wait_for_dismiss: If `True`, awaiting this method will return the dismiss value from the screen. When set to `False`, awaiting
+ this method will wait for the screen to be mounted. Note that `wait_for_dismiss` should only be set to `True` when running in a worker.
+
+ Raises:
+ NoActiveWorker: If using `wait_for_dismiss` outside of a worker.
Returns:
- An optional awaitable that awaits the mounting of the screen and its children.
+ An optional awaitable that awaits the mounting of the screen and its children, or an asyncio Future
+ to await the result of the screen.
"""
if not isinstance(screen, (Screen, str)):
raise TypeError(
f"push_screen requires a Screen instance or str; not {screen!r}"
)
+ try:
+ loop = asyncio.get_running_loop()
+ except RuntimeError:
+ # Mainly for testing, when push_screen isn't called in an async context
+ future: asyncio.Future[ScreenResultType] = asyncio.Future()
+ else:
+ future = loop.create_future()
+
if self._screen_stack:
self.screen.post_message(events.ScreenSuspend())
self.screen.refresh()
@@ -1787,13 +1927,46 @@ def push_screen(
message_pump = active_message_pump.get()
except LookupError:
message_pump = self.app
- next_screen._push_result_callback(message_pump, callback)
+
+ next_screen._push_result_callback(message_pump, callback, future)
self._load_screen_css(next_screen)
self._screen_stack.append(next_screen)
self.stylesheet.update(next_screen)
next_screen.post_message(events.ScreenResume())
self.log.system(f"{self.screen} is current (PUSHED)")
- return await_mount
+ if wait_for_dismiss:
+ try:
+ get_current_worker()
+ except NoActiveWorker:
+ raise NoActiveWorker(
+ "push_screen must be run from a worker when `wait_for_dismiss` is True"
+ ) from None
+ return future
+ else:
+ return await_mount
+
+ @overload
+ async def push_screen_wait(
+ self, screen: Screen[ScreenResultType]
+ ) -> ScreenResultType:
+ ...
+
+ @overload
+ async def push_screen_wait(self, screen: str) -> Any:
+ ...
+
+ async def push_screen_wait(
+ self, screen: Screen[ScreenResultType] | str
+ ) -> ScreenResultType | Any:
+ """Push a screen and wait for the result (received from [`Screen.dismiss`][textual.screen.Screen.dismiss]).
+
+ Args:
+ screen: A screen or the name of an installed screen.
+
+ Returns:
+ The screen's result.
+ """
+ return await self.push_screen(screen, wait_for_dismiss=True)
def switch_screen(self, screen: Screen | str) -> AwaitMount:
"""Switch to another [screen](/guide/screens) by replacing the top of the screen stack with a new screen.
@@ -1841,7 +2014,7 @@ def install_screen(self, screen: Screen, name: str) -> None:
raise ScreenError(f"Can't install screen; {name!r} is already installed")
if screen in self._installed_screens.values():
raise ScreenError(
- "Can't install screen; {screen!r} has already been installed"
+ f"Can't install screen; {screen!r} has already been installed"
)
self._installed_screens[name] = screen
self.log.system(f"{screen} INSTALLED name={name!r}")
@@ -1951,7 +2124,6 @@ def panic(self, *renderables: RenderableType) -> None:
Args:
*renderables: Text or Rich renderable(s) to display on exit.
"""
-
assert all(
is_renderable(renderable) for renderable in renderables
), "Can only call panic with strings or Rich renderables"
@@ -1963,6 +2135,7 @@ def render(renderable: RenderableType) -> list[Segment]:
pre_rendered = [Segments(render(renderable)) for renderable in renderables]
self._exit_renderables.extend(pre_rendered)
+
self._close_messages_no_wait()
def _handle_exception(self, error: Exception) -> None:
@@ -1989,6 +2162,8 @@ def _handle_exception(self, error: Exception) -> None:
def _fatal_error(self) -> None:
"""Exits the app after an unhandled exception."""
+ from rich.traceback import Traceback
+
self.bell()
traceback = Traceback(
show_locals=True, width=None, locals_max_length=5, suppress=[rich]
@@ -2012,7 +2187,7 @@ def _print_error_renderables(self) -> None:
self.error_console.print(self._exit_renderables[0])
if error_count > 1:
self.error_console.print(
- f"\n[b]NOTE:[/b] 1 of {error_count} errors shown. Run with [b]--dev[/] to see all errors.",
+ f"\n[b]NOTE:[/b] 1 of {error_count} errors shown. Run with [b]textual run --dev[/] to see all errors.",
markup=True,
)
@@ -2049,19 +2224,22 @@ 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 read_from, 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,
+ read_from=read_from,
+ is_default_css=True,
+ tie_breaker=tie_breaker,
+ scope=scope,
)
if self.CSS:
try:
- app_css_path = (
- f"{inspect.getfile(self.__class__)}:{self.__class__.__name__}"
- )
+ app_path = inspect.getfile(self.__class__)
except (TypeError, OSError):
- app_css_path = f"{self.__class__.__name__}"
+ app_path = ""
+ read_from = (app_path, f"{self.__class__.__name__}.CSS")
self.stylesheet.add_source(
- self.CSS, path=app_css_path, is_default_css=False
+ self.CSS, read_from=read_from, is_default_css=False
)
except Exception as error:
self._handle_exception(error)
@@ -2090,12 +2268,13 @@ async def invoke_ready_callback() -> None:
self.check_idle()
finally:
self._mounted_event.set()
+ self._is_mounted = True
Reactive._initialize_object(self)
- self.stylesheet.update(self)
+ self.stylesheet.apply(self)
if self.screen is not default_screen:
- self.stylesheet.update(default_screen)
+ self.stylesheet.apply(default_screen)
await self.animator.start()
@@ -2149,8 +2328,9 @@ async def invoke_ready_callback() -> None:
except Exception as error:
self._handle_exception(error)
- async def _pre_process(self) -> None:
- pass
+ async def _pre_process(self) -> bool:
+ """Special case for the app, which doesn't need the functionality in MessagePump."""
+ return True
async def _ready(self) -> None:
"""Called immediately prior to processing messages.
@@ -2158,6 +2338,9 @@ async def _ready(self) -> None:
May be used as a hook for any operations that should run first.
"""
+ ready_time = (perf_counter() - self._start_time) * 1000
+ self.log.system(f"ready in {ready_time:0.0f} milliseconds")
+
async def take_screenshot() -> None:
"""Take a screenshot and exit."""
self.save_screenshot()
@@ -2169,6 +2352,7 @@ async def take_screenshot() -> None:
)
async def _on_compose(self) -> None:
+ _rich_traceback_omit = True
try:
widgets = [*self.screen._nodes, *compose(self)]
except TypeError as error:
@@ -2234,6 +2418,7 @@ def _register(
*widgets: Widget,
before: int | None = None,
after: int | None = None,
+ cache: dict[tuple, RulesMap] | None = None,
) -> list[Widget]:
"""Register widget(s) so they may receive events.
@@ -2242,6 +2427,7 @@ def _register(
*widgets: The widget(s) to register.
before: A location to mount before.
after: A location to mount after.
+ cache: Optional rules map cache.
Returns:
List of modified widgets.
@@ -2250,6 +2436,8 @@ def _register(
if not widgets:
return []
+ if cache is None:
+ cache = {}
widget_list: Iterable[Widget]
if before is not None or after is not None:
# There's a before or after, which means there's going to be an
@@ -2266,8 +2454,8 @@ def _register(
if widget not in self._registry:
self._register_child(parent, widget, before, after)
if widget._nodes:
- self._register(widget, *widget._nodes)
- apply_stylesheet(widget)
+ self._register(widget, *widget._nodes, cache=cache)
+ apply_stylesheet(widget, cache=cache)
if not self._running:
# If the app is not running, prevent awaiting of the widget tasks
@@ -2412,17 +2600,38 @@ def _display(self, screen: Screen, renderable: RenderableType | None) -> None:
try:
try:
if isinstance(renderable, CompositorUpdate):
+ cursor_x, cursor_y = self.cursor_position
terminal_sequence = renderable.render_segments(console)
+ terminal_sequence += Control.move_to(
+ cursor_x, cursor_y
+ ).segment.text
else:
segments = console.render(renderable)
terminal_sequence = console._render_buffer(segments)
except Exception as error:
self._handle_exception(error)
else:
- self._driver.write(terminal_sequence)
+ if WINDOWS:
+ # Combat a problem with Python on Windows.
+ #
+ # https://github.com/Textualize/textual/issues/2548
+ # https://github.com/python/cpython/issues/82052
+ CHUNK_SIZE = 8192
+ write = self._driver.write
+ for chunk in (
+ terminal_sequence[offset : offset + CHUNK_SIZE]
+ for offset in range(
+ 0, len(terminal_sequence), CHUNK_SIZE
+ )
+ ):
+ write(chunk)
+ else:
+ self._driver.write(terminal_sequence)
finally:
self._end_update()
+
self._driver.flush()
+
finally:
self.post_display_hook()
@@ -2507,17 +2716,44 @@ async def on_event(self, event: events.Event) -> None:
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Compose):
- screen = Screen(id=f"_default")
+ screen: Screen[Any] = Screen(id=f"_default")
self._register(self, screen)
self._screen_stack.append(screen)
screen.post_message(events.ScreenResume())
await super().on_event(event)
elif isinstance(event, events.InputEvent) and not event.is_forwarded:
+ if not self.app_focus and isinstance(event, (events.Key, events.MouseDown)):
+ self.app_focus = True
if isinstance(event, events.MouseEvent):
# Record current mouse position on App
self.mouse_position = Offset(event.x, event.y)
+
+ if isinstance(event, events.MouseDown):
+ try:
+ self._mouse_down_widget, _ = self.get_widget_at(
+ event.x, event.y
+ )
+ except NoWidget:
+ # Shouldn't occur, since at the very least this will find the Screen
+ self._mouse_down_widget = None
+
self.screen._forward_event(event)
+
+ if (
+ isinstance(event, events.MouseUp)
+ and self._mouse_down_widget is not None
+ ):
+ try:
+ if (
+ self.get_widget_at(event.x, event.y)[0]
+ is self._mouse_down_widget
+ ):
+ click_event = events.Click.from_event(event)
+ self.screen._forward_event(click_event)
+ except NoWidget:
+ pass
+
elif isinstance(event, events.Key):
if not await self.check_bindings(event.key, priority=True):
forward_target = self.focused or self.screen
@@ -2585,7 +2821,7 @@ async def _dispatch_action(
"""
_rich_traceback_guard = True
- log(
+ log.system(
"",
namespace=namespace,
action_name=action_name,
@@ -2601,13 +2837,13 @@ async def _dispatch_action(
if callable(public_method):
await invoke(public_method, *params)
return True
- log(
+ log.system(
f" {action_name!r} has no target."
f" Could not find methods '_action_{action_name}' or 'action_{action_name}'"
)
except SkipAction:
# The action method raised this to explicitly not handle the action
- log(f" {action_name!r} skipped.")
+ log.system(f" {action_name!r} skipped.")
return False
async def _broker_event(
@@ -2652,7 +2888,7 @@ async def _on_key(self, event: events.Key) -> None:
await self.dispatch_key(event)
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
- log("shutdown request")
+ log.system("shutdown request")
await self._close_messages()
async def _on_resize(self, event: events.Resize) -> None:
@@ -2661,6 +2897,16 @@ async def _on_resize(self, event: events.Resize) -> None:
for screen in self._background_screens:
screen.post_message(event)
+ async def _on_app_focus(self, event: events.AppFocus) -> None:
+ """App has focus."""
+ # Required by textual-web to manage focus in a web page.
+ self.app_focus = True
+
+ async def _on_app_blur(self, event: events.AppBlur) -> None:
+ """App has lost focus."""
+ # Required by textual-web to manage focus in a web page.
+ self.app_focus = False
+
def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]:
"""Detach a list of widgets from the DOM.
@@ -2818,6 +3064,15 @@ async def _prune_node(self, root: Widget) -> None:
await root._close_messages(wait=True)
self._unregister(root)
+ def _watch_app_focus(self, focus: bool) -> None:
+ """Respond to changes in app focus."""
+ if focus:
+ focused = self.screen.focused
+ self.screen.set_focus(None)
+ self.screen.set_focus(focused)
+ else:
+ self.screen.set_focus(None)
+
async def action_check_bindings(self, key: str) -> None:
"""An [action](/guide/actions) to handle a key press using the binding system.
@@ -2937,10 +3192,14 @@ def _end_update(self) -> None:
def _refresh_notifications(self) -> None:
"""Refresh the notifications on the current screen, if one is available."""
# If we've got a screen to hand...
- if self.screen is not None:
+ try:
+ screen = self.screen
+ except ScreenStackError:
+ pass
+ else:
try:
# ...see if it has a toast rack.
- toast_rack = self.screen.get_child_by_type(ToastRack)
+ toast_rack = screen.get_child_by_type(ToastRack)
except NoMatches:
# It doesn't. That's fine. Either there won't ever be one,
# or one will turn up. Things will work out later.
diff --git a/src/textual/await_complete.py b/src/textual/await_complete.py
new file mode 100644
index 0000000000..51d807f6d2
--- /dev/null
+++ b/src/textual/await_complete.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from asyncio import Future, gather
+from typing import Any, Coroutine, Iterator, TypeVar
+
+import rich.repr
+
+ReturnType = TypeVar("ReturnType")
+
+
+@rich.repr.auto(angular=True)
+class AwaitComplete:
+ """An 'optionally-awaitable' object."""
+
+ def __init__(self, *coroutines: Coroutine[Any, Any, Any]) -> None:
+ """Create an AwaitComplete.
+
+ Args:
+ coroutines: One or more coroutines to execute.
+ """
+ self.coroutines: tuple[Coroutine[Any, Any, Any], ...] = coroutines
+ self._future: Future = gather(*self.coroutines)
+
+ async def __call__(self) -> Any:
+ return await self
+
+ def __await__(self) -> Iterator[None]:
+ return self._future.__await__()
+
+ @property
+ def is_done(self) -> bool:
+ """Returns True if the task has completed."""
+ return self._future.done()
+
+ @property
+ def exception(self) -> BaseException | None:
+ """An exception if it occurred in any of the coroutines."""
+ if self._future.done():
+ return self._future.exception()
+ return None
+
+ @classmethod
+ def nothing(cls):
+ """Returns an already completed instance of AwaitComplete."""
+ instance = cls()
+ instance._future = Future()
+ instance._future.set_result(None) # Mark it as completed with no result
+ return instance
diff --git a/src/textual/await_remove.py b/src/textual/await_remove.py
index 854703623a..f02fe5b840 100644
--- a/src/textual/await_remove.py
+++ b/src/textual/await_remove.py
@@ -1,5 +1,4 @@
"""
-
An *optionally* awaitable object returned by methods that remove widgets.
"""
diff --git a/src/textual/canvas.py b/src/textual/canvas.py
new file mode 100644
index 0000000000..5cc5013f86
--- /dev/null
+++ b/src/textual/canvas.py
@@ -0,0 +1,278 @@
+"""
+A Canvas class used to render keylines.
+
+!!! note
+ This API is experimental, and may change in the near future.
+
+"""
+
+from __future__ import annotations
+
+from array import array
+from collections import defaultdict
+from dataclasses import dataclass
+from operator import itemgetter
+from typing import NamedTuple, Sequence
+
+from rich.segment import Segment
+from rich.style import Style
+from typing_extensions import Literal, TypeAlias
+
+from ._box_drawing import BOX_CHARACTERS, Quad, combine_quads
+from .color import Color
+from .geometry import Offset, clamp
+from .strip import Strip, StripRenderable
+
+CanvasLineType: TypeAlias = Literal["thin", "heavy", "double"]
+
+
+_LINE_TYPE_INDEX = {"thin": 1, "heavy": 2, "double": 3}
+
+
+class _Span(NamedTuple):
+ """Associates a sequence of character indices with a color."""
+
+ start: int
+ end: int # exclusive
+ color: Color
+
+
+class Primitive:
+ """Base class for a canvas primitive."""
+
+ def render(self, canvas: Canvas) -> None:
+ """Render to the canvas.
+
+ Args:
+ canvas: Canvas instance.
+ """
+ raise NotImplementedError()
+
+
+@dataclass
+class HorizontalLine(Primitive):
+ """A horizontal line."""
+
+ origin: Offset
+ length: int
+ color: Color
+ line_type: CanvasLineType = "thin"
+
+ def render(self, canvas: Canvas) -> None:
+ x, y = self.origin
+ if y < 0 or y > canvas.height - 1:
+ return
+ box = canvas.box
+ box_line = box[y]
+
+ line_type_index = _LINE_TYPE_INDEX[self.line_type]
+ _combine_quads = combine_quads
+
+ right = x + self.length - 1
+
+ x_range = canvas.x_range(x, x + self.length)
+
+ if x in x_range:
+ box_line[x] = _combine_quads(box_line[x], (0, line_type_index, 0, 0))
+ if right in x_range:
+ box_line[right] = _combine_quads(
+ box_line[right], (0, 0, 0, line_type_index)
+ )
+
+ line_quad = (0, line_type_index, 0, line_type_index)
+ for box_x in canvas.x_range(x + 1, x + self.length - 1):
+ box_line[box_x] = _combine_quads(box_line[box_x], line_quad)
+
+ canvas.spans[y].append(_Span(x, x + self.length, self.color))
+
+
+@dataclass
+class VerticalLine(Primitive):
+ """A vertical line."""
+
+ origin: Offset
+ length: int
+ color: Color
+ line_type: CanvasLineType = "thin"
+
+ def render(self, canvas: Canvas) -> None:
+ x, y = self.origin
+ if x < 0 or x >= canvas.width:
+ return
+ line_type_index = _LINE_TYPE_INDEX[self.line_type]
+ box = canvas.box
+ _combine_quads = combine_quads
+
+ y_range = canvas.y_range(y, y + self.length)
+
+ if y in y_range:
+ box[y][x] = _combine_quads(box[y][x], (0, 0, line_type_index, 0))
+ bottom = y + self.length - 1
+
+ if bottom in y_range:
+ box[bottom][x] = _combine_quads(box[bottom][x], (line_type_index, 0, 0, 0))
+ line_quad = (line_type_index, 0, line_type_index, 0)
+
+ for box_y in canvas.y_range(y + 1, y + self.length - 1):
+ box[box_y][x] = _combine_quads(box[box_y][x], line_quad)
+
+ spans = canvas.spans
+ span = _Span(x, x + 1, self.color)
+ for y in y_range:
+ spans[y].append(span)
+
+
+@dataclass
+class Rectangle(Primitive):
+ """A rectangle."""
+
+ origin: Offset
+ width: int
+ height: int
+ color: Color
+ line_type: CanvasLineType = "thin"
+
+ def render(self, canvas: Canvas) -> None:
+ origin = self.origin
+ width = self.width
+ height = self.height
+ color = self.color
+ line_type = self.line_type
+ HorizontalLine(origin, width, color, line_type).render(canvas)
+ HorizontalLine(origin + (0, height - 1), width, color, line_type).render(canvas)
+ VerticalLine(origin, height, color, line_type).render(canvas)
+ VerticalLine(origin + (width - 1, 0), height, color, line_type).render(canvas)
+
+
+class Canvas:
+ """A character canvas."""
+
+ def __init__(self, width: int, height: int) -> None:
+ """
+
+ Args:
+ width: Width of the canvas (in cells).
+ height Height of the canvas (in cells).
+ """
+ self._width = width
+ self._height = height
+ blank_line = " " * width
+ self.lines: list[array[str]] = [array("u", blank_line) for _ in range(height)]
+ self.box: list[defaultdict[int, Quad]] = [
+ defaultdict(lambda: (0, 0, 0, 0)) for _ in range(height)
+ ]
+ self.spans: list[list[_Span]] = [[] for _ in range(height)]
+
+ @property
+ def width(self) -> int:
+ """The canvas width."""
+ return self._width
+
+ @property
+ def height(self) -> int:
+ """The canvas height."""
+ return self._height
+
+ def x_range(self, start: int, end: int) -> range:
+ """Range of x values, clipped to the canvas dimensions.
+
+ Args:
+ start: Start index.
+ end: End index.
+
+ Returns:
+ A range object.
+ """
+ return range(
+ clamp(start, 0, self._width),
+ clamp(end, 0, self._width),
+ )
+
+ def y_range(self, start: int, end: int) -> range:
+ """Range of y values, clipped to the canvas dimensions.
+
+ Args:
+ start: Start index.
+ end: End index.
+
+ Returns:
+ A range object.
+ """
+ return range(
+ clamp(start, 0, self._height),
+ clamp(end, 0, self._height),
+ )
+
+ def render(
+ self, primitives: Sequence[Primitive], base_style: Style
+ ) -> StripRenderable:
+ """Render the canvas.
+
+ Args:
+ primitives: A sequence of primitives.
+ base_style: The base style of the canvas.
+
+ Returns:
+ A Rich renderable for the canvas.
+ """
+ for primitive in primitives:
+ primitive.render(self)
+
+ get_box = BOX_CHARACTERS.__getitem__
+ for box, line in zip(self.box, self.lines):
+ for offset, quad in box.items():
+ line[offset] = get_box(quad)
+
+ width = self._width
+ span_sort_key = itemgetter(0, 1)
+ strips: list[Strip] = []
+ color = (
+ Color.from_rich_color(base_style.bgcolor)
+ if base_style.bgcolor
+ else Color.parse("transparent")
+ )
+ _Segment = Segment
+ for raw_spans, line in zip(self.spans, self.lines):
+ text = line.tounicode()
+
+ if raw_spans:
+ segments: list[Segment] = []
+ colors = [color] + [span.color for span in raw_spans]
+ spans = [
+ (0, False, 0),
+ *(
+ (span.start, False, index)
+ for index, span in enumerate(raw_spans, 1)
+ ),
+ *(
+ (span.end, True, index)
+ for index, span in enumerate(raw_spans, 1)
+ ),
+ (width, True, 0),
+ ]
+ spans.sort(key=span_sort_key)
+ color_indices: set[int] = set()
+ color_remove = color_indices.discard
+ color_add = color_indices.add
+ for (offset, leaving, style_id), (next_offset, _, _) in zip(
+ spans, spans[1:]
+ ):
+ if leaving:
+ color_remove(style_id)
+ else:
+ color_add(style_id)
+ if next_offset > offset:
+ segments.append(
+ _Segment(
+ text[offset:next_offset],
+ base_style
+ + Style.from_color(
+ colors[max(color_indices)].rich_color
+ ),
+ )
+ )
+ strips.append(Strip(segments, width))
+ else:
+ strips.append(Strip([_Segment(text, base_style)], width))
+
+ return StripRenderable(strips, width)
diff --git a/src/textual/command.py b/src/textual/command.py
index 2aafa10abe..9136bf9d5b 100644
--- a/src/textual/command.py
+++ b/src/textual/command.py
@@ -7,11 +7,20 @@
from __future__ import annotations
from abc import ABC, abstractmethod
-from asyncio import CancelledError, Queue, Task, TimeoutError, wait, wait_for
+from asyncio import (
+ CancelledError,
+ Queue,
+ Task,
+ TimeoutError,
+ create_task,
+ wait,
+ wait_for,
+)
from dataclasses import dataclass
from functools import total_ordering
+from inspect import isclass
from time import monotonic
-from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar
+from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar, Iterable
import rich.repr
from rich.align import Align
@@ -19,17 +28,15 @@
from rich.emoji import Emoji
from rich.style import Style
from rich.text import Text
-from rich.traceback import Traceback
from typing_extensions import Final, TypeAlias
from . import on, work
-from ._asyncio import create_task
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 .screen import Screen, _SystemModalScreen
from .timer import Timer
from .types import CallbackType, IgnoreReturnCallbackType
from .widget import Widget
@@ -165,6 +172,8 @@ async def post_init_task() -> None:
try:
await self.startup()
except Exception:
+ from rich.traceback import Traceback
+
self.app.log.error(Traceback())
else:
self._init_success = True
@@ -211,6 +220,8 @@ async def _shutdown(self) -> None:
try:
await self.shutdown()
except Exception:
+ from rich.traceback import Traceback
+
self.app.log.error(Traceback())
async def shutdown(self) -> None:
@@ -329,7 +340,7 @@ class CommandInput(Input):
"""
-class CommandPalette(ModalScreen[CallbackType], inherit_css=False):
+class CommandPalette(_SystemModalScreen[CallbackType]):
"""The Textual command palette."""
COMPONENT_CLASSES: ClassVar[set[str]] = {
@@ -354,7 +365,7 @@ class CommandPalette(ModalScreen[CallbackType], inherit_css=False):
color: $text-muted;
}
- App.-dark-mode CommandPalette > .command-palette--highlight {
+ CommandPalette:dark > .command-palette--highlight {
text-style: bold;
color: $warning;
}
@@ -458,6 +469,8 @@ def __init__(self) -> 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._no_matches_timer: Timer | None = None
+ """Keeps track of if there are 'No matches found' message waiting to be displayed."""
self._providers: list[Provider] = []
"""List of Provider instances involved in searches."""
@@ -481,10 +494,27 @@ def _provider_classes(self) -> set[type[Provider]]:
application][textual.app.App.COMMANDS] and those [defined in
the current screen][textual.screen.Screen.COMMANDS].
"""
+
+ def get_providers(root: App | Screen) -> Iterable[type[Provider]]:
+ """Get providers from app or screen.
+
+ Args:
+ root: The app or screen.
+
+ Returns:
+ An iterable of providers.
+ """
+ for provider in root.COMMANDS:
+ if isclass(provider) and issubclass(provider, Provider):
+ yield provider
+ else:
+ # Lazy loaded providers
+ yield provider() # type: ignore
+
return (
set()
if self._calling_screen is None
- else self.app.COMMANDS | self._calling_screen.COMMANDS
+ else {*get_providers(self.app), *get_providers(self._calling_screen)}
)
def compose(self) -> ComposeResult:
@@ -513,11 +543,11 @@ def _on_click(self, event: Click) -> None:
method of dismissing the palette.
"""
if self.get_widget_at(event.screen_x, event.screen_y)[0] is self:
- self.workers.cancel_all()
+ self._cancel_gather_commands()
self.dismiss()
- def on_mount(self, _: Mount) -> None:
- """Capture the calling screen."""
+ def _on_mount(self, _: Mount) -> None:
+ """Configure the command palette once the DOM is ready."""
self._calling_screen = self.app.screen_stack[-2]
match_style = self.get_component_rich_style(
@@ -532,7 +562,7 @@ def on_mount(self, _: Mount) -> None:
for provider in self._providers:
provider._post_init()
- async def on_unmount(self) -> None:
+ async def _on_unmount(self) -> None:
"""Shutdown providers when command palette is closed."""
if self._providers:
await wait(
@@ -559,6 +589,38 @@ def _become_busy() -> None:
self._busy_timer = self.set_timer(self._BUSY_COUNTDOWN, _become_busy)
+ def _stop_no_matches_countdown(self) -> None:
+ """Stop any 'No matches' countdown that's in effect."""
+ if self._no_matches_timer is not None:
+ self._no_matches_timer.stop()
+ self._no_matches_timer = None
+
+ _NO_MATCHES_COUNTDOWN: Final[float] = 0.5
+ """How many seconds to wait before showing 'No matches found'."""
+
+ def _start_no_matches_countdown(self) -> None:
+ """Start a countdown to showing that there are no matches for the query.
+
+ Adds a 'No matches found' option to the command list after `_NO_MATCHES_COUNTDOWN` seconds.
+ """
+ self._stop_no_matches_countdown()
+
+ def _show_no_matches() -> None:
+ command_list = self.query_one(CommandList)
+ command_list.add_option(
+ Option(
+ Align.center(Text("No matches found")),
+ disabled=True,
+ id=self._NO_MATCHES,
+ )
+ )
+ self._list_visible = True
+
+ self._no_matches_timer = self.set_timer(
+ self._NO_MATCHES_COUNTDOWN,
+ _show_no_matches,
+ )
+
def _watch__list_visible(self) -> None:
"""React to the list visible flag being toggled."""
self.query_one(CommandList).set_class(self._list_visible, "--visible")
@@ -647,6 +709,8 @@ async def _search_for(self, search_value: str) -> AsyncGenerator[Hit, bool]:
if search.done():
exception = search.exception()
if exception is not None:
+ from rich.traceback import Traceback
+
self.log.error(
Traceback.from_exception(
type(exception), exception, exception.__traceback__
@@ -732,6 +796,7 @@ def _refresh_command_list(
command_list.clear_options().add_options(sorted(commands, reverse=True))
if highlighted is not None:
command_list.highlighted = command_list.get_option_index(highlighted.id)
+ self._list_visible = bool(command_list.option_count)
_RESULT_BATCH_TIME: Final[float] = 0.25
"""How long to wait before adding commands to the command list."""
@@ -739,7 +804,10 @@ def _refresh_command_list(
_NO_MATCHES: Final[str] = "--no-matches"
"""The ID to give the disabled option that shows there were no matches."""
- @work(exclusive=True)
+ _GATHER_COMMANDS_GROUP: Final[str] = "--textual-command-palette-gather-commands"
+ """The group name of the command gathering worker."""
+
+ @work(exclusive=True, group=_GATHER_COMMANDS_GROUP)
async def _gather_commands(self, search_value: str) -> None:
"""Gather up all of the commands that match the search value.
@@ -780,10 +848,7 @@ async def _gather_commands(self, search_value: str) -> None:
# grab a reference to that.
worker = get_current_worker()
- # We're ready to show results, ensure the list is visible.
- self._list_visible = True
-
- # Go into a busy mode.
+ # Reset busy mode.
self._show_busy = False
# A flag to keep track of if the current content of the command hit
@@ -861,13 +926,11 @@ async def _gather_commands(self, search_value: str) -> None:
# mean nothing was found. Give the user positive feedback to that
# effect.
if command_list.option_count == 0 and not worker.is_cancelled:
- command_list.add_option(
- Option(
- Align.center(Text("No matches found")),
- disabled=True,
- id=self._NO_MATCHES,
- )
- )
+ self._start_no_matches_countdown()
+
+ def _cancel_gather_commands(self) -> None:
+ """Cancel any operation that is gather commands."""
+ self.workers.cancel_group(self, self._GATHER_COMMANDS_GROUP)
@on(Input.Changed)
def _input(self, event: Input.Changed) -> None:
@@ -877,7 +940,9 @@ def _input(self, event: Input.Changed) -> None:
event: The input event.
"""
event.stop()
- self.workers.cancel_all()
+ self._cancel_gather_commands()
+ self._stop_no_matches_countdown()
+
search_value = event.value.strip()
if search_value:
self._gather_commands(search_value)
@@ -893,7 +958,7 @@ def _select_command(self, event: OptionList.OptionSelected) -> None:
event: The option selection event.
"""
event.stop()
- self.workers.cancel_all()
+ self._cancel_gather_commands()
input = self.query_one(CommandInput)
with self.prevent(Input.Changed):
assert isinstance(event.option, Command)
@@ -930,15 +995,20 @@ def _select_or_command(
if self._selected_command is not None:
# ...we should return it to the parent screen and let it
# decide what to do with it (hopefully it'll run it).
- self.workers.cancel_all()
+ self._cancel_gather_commands()
self.dismiss(self._selected_command.command)
+ @on(OptionList.OptionHighlighted)
+ def _stop_event_leak(self, event: OptionList.OptionHighlighted) -> None:
+ """Stop any unused events so they don't leak to the application."""
+ event.stop()
+
def _action_escape(self) -> None:
"""Handle a request to escape out of the command palette."""
if self._list_visible:
self._list_visible = False
else:
- self.workers.cancel_all()
+ self._cancel_gather_commands()
self.dismiss()
def _action_command_list(self, action: str) -> None:
diff --git a/src/textual/containers.py b/src/textual/containers.py
index 6ba1d2966f..c8fe96194e 100644
--- a/src/textual/containers.py
+++ b/src/textual/containers.py
@@ -26,7 +26,7 @@ class Container(Widget):
"""
-class ScrollableContainer(Widget, inherit_bindings=False):
+class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False):
"""A scrollable container with vertical layout, and auto scrollbars on both axis."""
DEFAULT_CSS = """
@@ -76,7 +76,7 @@ class Vertical(Widget, inherit_bindings=False):
"""
-class VerticalScroll(ScrollableContainer, can_focus=True):
+class VerticalScroll(ScrollableContainer):
"""A container with vertical layout and an automatic scrollbar on the Y axis."""
DEFAULT_CSS = """
@@ -101,7 +101,7 @@ class Horizontal(Widget, inherit_bindings=False):
"""
-class HorizontalScroll(ScrollableContainer, can_focus=True):
+class HorizontalScroll(ScrollableContainer):
"""A container with horizontal layout and an automatic scrollbar on the Y axis."""
DEFAULT_CSS = """
diff --git a/src/textual/css/_help_renderables.py b/src/textual/css/_help_renderables.py
index bce6a9a60a..5957162f53 100644
--- a/src/textual/css/_help_renderables.py
+++ b/src/textual/css/_help_renderables.py
@@ -7,7 +7,6 @@
from rich.highlighter import ReprHighlighter
from rich.markup import render
from rich.text import Text
-from rich.tree import Tree
_highlighter = ReprHighlighter()
@@ -89,6 +88,8 @@ def __str__(self) -> str:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
+ from rich.tree import Tree
+
tree = Tree(_markup_and_highlight(f"[b blue]{self.summary}"), guide_style="dim")
if self.bullets is not None:
for bullet in self.bullets:
diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py
index e873bd8a00..fd0788359f 100644
--- a/src/textual/css/_help_text.py
+++ b/src/textual/css/_help_text.py
@@ -14,6 +14,7 @@
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_BORDER,
+ VALID_KEYLINE,
VALID_LAYOUT,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
@@ -149,7 +150,7 @@ def property_invalid_value_help_text(
suggested_property_name = _contextualize_property_name(
suggested_property_name, context
)
- summary += f'. Did you mean "{suggested_property_name}"?'
+ summary += f". Did you mean '{suggested_property_name}'?"
return HelpText(summary)
@@ -323,7 +324,7 @@ def color_property_help_text(
error.suggested_color if error and isinstance(error, ColorParseError) else None
)
if suggested_color:
- summary += f'. Did you mean "{suggested_color}"?'
+ summary += f". Did you mean '{suggested_color}'?"
return HelpText(
summary=summary,
bullets=[
@@ -587,9 +588,7 @@ def scrollbar_size_property_help_text(context: StylingContext) -> HelpText:
),
],
).get_by_context(context),
- Bullet(
- " and must be positive integers, greater than zero"
- ),
+ Bullet(" and must be non-negative integers."),
],
)
@@ -669,6 +668,26 @@ def align_help_text() -> HelpText:
)
+def keyline_help_text() -> HelpText:
+ """Help text to show when the user supplies an invalid value for a `keyline`.
+
+ Returns:
+ Renderable for displaying the help text for this property.
+ """
+ return HelpText(
+ summary="Invalid value for [i]keyline[/] property",
+ bullets=[
+ Bullet(
+ markup="The [i]keyline[/] property expects exactly 2 values",
+ examples=[
+ Example("keyline: "),
+ ],
+ ),
+ Bullet(f"Valid values for are {friendly_list(VALID_KEYLINE)}"),
+ ],
+ )
+
+
def text_align_help_text() -> HelpText:
"""Help text to show when the user supplies an invalid value for the text-align property.
diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py
index 658932165e..efcc309930 100644
--- a/src/textual/css/_style_properties.py
+++ b/src/textual/css/_style_properties.py
@@ -18,7 +18,7 @@
from .._border import normalize_border_value
from ..color import Color, ColorParseError
-from ..geometry import Spacing, SpacingDimensions, clamp
+from ..geometry import NULL_SPACING, Spacing, SpacingDimensions, clamp
from ._error_tools import friendly_list
from ._help_text import (
border_property_help_text,
@@ -31,7 +31,7 @@
string_enum_help_text,
style_flags_property_help_text,
)
-from .constants import NULL_SPACING, VALID_STYLE_FLAGS
+from .constants import VALID_STYLE_FLAGS
from .errors import StyleTypeError, StyleValueError
from .scalar import (
NULL_SCALAR,
@@ -46,8 +46,9 @@
from .transition import Transition
if TYPE_CHECKING:
+ from ..canvas import CanvasLineType
from .._layout import Layout
- from .styles import Styles, StylesBase
+ from .styles import StylesBase
from .types import AlignHorizontal, AlignVertical, DockEdge, EdgeType
@@ -497,6 +498,26 @@ def check_refresh() -> None:
check_refresh()
+class KeylineProperty:
+ """Descriptor for getting and setting keyline information."""
+
+ def __get__(
+ self, obj: StylesBase, objtype: type[StylesBase] | None = None
+ ) -> tuple[CanvasLineType, Color]:
+ return cast(
+ "tuple[CanvasLineType, Color]",
+ obj.get_rule("keyline", ("none", Color.parse("transparent"))),
+ )
+
+ def __set__(self, obj: StylesBase, keyline: tuple[str, Color] | None):
+ if keyline is None:
+ if obj.clear_rule("keyline"):
+ obj.refresh(layout=True)
+ else:
+ if obj.set_rule("keyline", keyline):
+ obj.refresh(layout=True)
+
+
class SpacingProperty:
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
@@ -740,7 +761,7 @@ def __get__(self, obj: StylesBase, objtype: type[StylesBase] | None = None) -> s
Returns:
The string property value.
"""
- return cast(str, obj.get_rule(self.name, self._default))
+ return obj.get_rule(self.name, self._default) # type: ignore
def _before_refresh(self, obj: StylesBase, value: str | None) -> None:
"""Do any housekeeping before asking for a layout refresh after a value change."""
@@ -1063,8 +1084,8 @@ def __set__(self, obj: StylesBase, value: float | str | None) -> None:
obj.refresh(children=self.children)
return
- if isinstance(value, float):
- float_value = value
+ if isinstance(value, (int, float)):
+ float_value = float(value)
elif isinstance(value, str) and value.endswith("%"):
float_value = float(Scalar.parse(value).value) / 100
else:
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
index 641eb36e3c..8b66fec39b 100644
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -20,6 +20,7 @@
dock_property_help_text,
fractional_property_help_text,
integer_help_text,
+ keyline_help_text,
layout_property_help_text,
offset_property_help_text,
offset_single_axis_help_text,
@@ -42,6 +43,7 @@
VALID_CONSTRAIN,
VALID_DISPLAY,
VALID_EDGE,
+ VALID_KEYLINE,
VALID_OVERFLOW,
VALID_OVERLAY,
VALID_SCROLLBAR_GUTTER,
@@ -65,19 +67,6 @@
from .types import BoxSizing, Display, EdgeType, Overflow, Visibility
-def _join_tokens(tokens: Iterable[Token], joiner: str = "") -> str:
- """Convert tokens into a string by joining their values
-
- Args:
- tokens: Tokens to join
- joiner: String to join on.
-
- Returns:
- The tokens, joined together to form a string.
- """
- return joiner.join(token.value for token in tokens)
-
-
class StylesBuilder:
"""
The StylesBuilder object takes tokens parsed from the CSS and converts
@@ -97,9 +86,17 @@ def error(self, name: str, token: Token, message: str | HelpText) -> NoReturn:
raise DeclarationError(name, token, message)
def add_declaration(self, declaration: Declaration) -> None:
- if not declaration.tokens:
+ if not declaration.name:
return
rule_name = declaration.name.replace("-", "_")
+
+ if not declaration.tokens:
+ self.error(
+ rule_name,
+ declaration.token,
+ f"Missing property value for '{declaration.name}:'",
+ )
+
process_method = getattr(self, f"process_{rule_name}", None)
if process_method is None:
@@ -122,6 +119,13 @@ def add_declaration(self, declaration: Declaration) -> None:
if important:
tokens = tokens[:-1]
self.styles.important.add(rule_name)
+
+ # Check for special token(s)
+ if tokens[0].name == "token":
+ value = tokens[0].value
+ if value == "initial":
+ self.styles._rules[rule_name] = None
+ return
try:
process_method(declaration.name, tokens)
except DeclarationError:
@@ -259,7 +263,7 @@ def _distribute_importance(self, prefix: str, suffixes: tuple[str, ...]) -> None
Args:
prefix: The prefix of the style.
- siffixes: The suffixes to distribute amongst.
+ suffixes: The suffixes to distribute amongst.
A number of styles can be set with the 'prefix' of the style,
providing the values as a series of parameters; or they can be set
@@ -543,6 +547,33 @@ def process_outline_bottom(self, name: str, tokens: list[Token]) -> None:
def process_outline_left(self, name: str, tokens: list[Token]) -> None:
self._process_outline("left", name, tokens)
+ def process_keyline(self, name: str, tokens: list[Token]) -> None:
+ if not tokens:
+ return
+ if len(tokens) > 2:
+ self.error(name, tokens[0], keyline_help_text())
+ keyline_style = "none"
+ keyline_color = Color.parse("green")
+ for token in tokens:
+ if token.name == "color":
+ try:
+ keyline_color = Color.parse(token.value)
+ except Exception as error:
+ self.error(
+ name,
+ token,
+ color_property_help_text(name, context="css", error=error),
+ )
+ elif token.name == "token":
+ try:
+ keyline_color = Color.parse(token.value)
+ except Exception as error:
+ keyline_style = token.value
+ if keyline_style not in VALID_KEYLINE:
+ self.error(name, token, keyline_help_text())
+
+ self.styles._rules["keyline"] = (keyline_style, keyline_color)
+
def process_offset(self, name: str, tokens: list[Token]) -> None:
def offset_error(name: str, token: Token) -> None:
self.error(name, token, offset_property_help_text(context="css"))
@@ -659,8 +690,8 @@ def process_color(self, name: str, tokens: list[Token]) -> None:
process_link_color = process_color
process_link_background = process_color
- process_link_hover_color = process_color
- process_link_hover_background = process_color
+ process_link_color_hover = process_color
+ process_link_background_hover = process_color
process_border_title_color = process_color
process_border_title_background = process_color
@@ -681,7 +712,7 @@ def process_text_style(self, name: str, tokens: list[Token]) -> None:
self.styles._rules[name.replace("-", "_")] = style_definition # type: ignore
process_link_style = process_text_style
- process_link_hover_style = process_text_style
+ process_link_style_hover = process_text_style
process_border_title_style = process_text_style
process_border_subtitle_style = process_text_style
@@ -876,11 +907,7 @@ def scrollbar_size_error(name: str, token: Token) -> None:
scrollbar_size_error(name, token2)
horizontal = int(token1.value)
- if horizontal == 0:
- scrollbar_size_error(name, token1)
vertical = int(token2.value)
- if vertical == 0:
- scrollbar_size_error(name, token2)
self.styles._rules["scrollbar_size_horizontal"] = horizontal
self.styles._rules["scrollbar_size_vertical"] = vertical
self._distribute_importance("scrollbar_size", ("horizontal", "vertical"))
@@ -895,8 +922,6 @@ def process_scrollbar_size_vertical(self, name: str, tokens: list[Token]) -> Non
if token.name != "number" or not token.value.isdigit():
self.error(name, token, scrollbar_size_single_axis_help_text(name))
value = int(token.value)
- if value == 0:
- self.error(name, token, scrollbar_size_single_axis_help_text(name))
self.styles._rules["scrollbar_size_vertical"] = value
def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> None:
@@ -909,8 +934,6 @@ def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> N
if token.name != "number" or not token.value.isdigit():
self.error(name, token, scrollbar_size_single_axis_help_text(name))
value = int(token.value)
- if value == 0:
- self.error(name, token, scrollbar_size_single_axis_help_text(name))
self.styles._rules["scrollbar_size_horizontal"] = value
def _process_grid_rows_or_columns(self, name: str, tokens: list[Token]) -> None:
diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py
index 95487e8704..52c13cb067 100644
--- a/src/textual/css/constants.py
+++ b/src/textual/css/constants.py
@@ -2,8 +2,6 @@
import typing
-from ..geometry import Spacing
-
if typing.TYPE_CHECKING:
from typing_extensions import Final
@@ -64,13 +62,14 @@
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"}
-
-NULL_SPACING: Final = Spacing.all(0)
+VALID_KEYLINE: Final = {"none", "thin", "heavy", "double"}
diff --git a/src/textual/css/errors.py b/src/textual/css/errors.py
index 2daa7e3b34..815b11394e 100644
--- a/src/textual/css/errors.py
+++ b/src/textual/css/errors.py
@@ -1,7 +1,6 @@
from __future__ import annotations
from rich.console import Console, ConsoleOptions, RenderResult
-from rich.traceback import Traceback
from ._help_renderables import HelpText
from .tokenizer import Token, TokenError
@@ -38,6 +37,8 @@ def __init__(self, *args: object, help_text: HelpText | None = None):
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
+ from rich.traceback import Traceback
+
yield Traceback.from_exception(type(self), self, self.__traceback__)
if self.help_text is not None:
yield ""
diff --git a/src/textual/css/model.py b/src/textual/css/model.py
index 3766606de1..7bcad93775 100644
--- a/src/textual/css/model.py
+++ b/src/textual/css/model.py
@@ -20,6 +20,7 @@ class SelectorType(Enum):
TYPE = 2
CLASS = 3
ID = 4
+ NESTED = 5
class CombinatorType(Enum):
@@ -106,7 +107,7 @@ def _check_class(self, node: DOMNode) -> bool:
return True
def _check_id(self, node: DOMNode) -> bool:
- if not node.id == self.name:
+ if node.id != self.name:
return False
if self.pseudo_classes and not node.has_pseudo_class(*self.pseudo_classes):
return False
@@ -161,6 +162,7 @@ class RuleSet:
is_default_rules: bool = False
tie_breaker: int = 0
selector_names: set[str] = field(default_factory=set)
+ pseudo_classes: set[str] = field(default_factory=set)
def __hash__(self):
return id(self)
@@ -174,6 +176,7 @@ def _selector_to_css(cls, selectors: list[Selector]) -> str:
elif selector.combinator == CombinatorType.CHILD:
tokens.append(" > ")
tokens.append(selector.css)
+
return "".join(tokens).strip()
@property
@@ -203,31 +206,20 @@ def _post_parse(self) -> None:
type_type = SelectorType.TYPE
universal_type = SelectorType.UNIVERSAL
- update_selectors = self.selector_names.update
+ add_selector = self.selector_names.add
+ add_pseudo_classes = self.pseudo_classes.update
for selector_set in self.selector_set:
- update_selectors(
- "*"
- for selector in selector_set.selectors
- if selector.type == universal_type
- )
- update_selectors(
- selector.name
- for selector in selector_set.selectors
- if selector.type == type_type
- )
- update_selectors(
- f".{selector.name}"
- for selector in selector_set.selectors
- if selector.type == class_type
- )
- update_selectors(
- f"#{selector.name}"
- for selector in selector_set.selectors
- if selector.type == id_type
- )
- update_selectors(
- f":{pseudo_class}"
- for selector in selector_set.selectors
- for pseudo_class in selector.pseudo_classes
- )
+ for selector in selector_set.selectors:
+ add_pseudo_classes(selector.pseudo_classes)
+
+ selector = selector_set.selectors[-1]
+ selector_type = selector.type
+ if selector_type == universal_type:
+ add_selector("*")
+ elif selector_type == type_type:
+ add_selector(selector.name)
+ elif selector_type == class_type:
+ add_selector(f".{selector.name}")
+ elif selector_type == id_type:
+ add_selector(f"#{selector.name}")
diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py
index 1b31e8b66b..d79a12745c 100644
--- a/src/textual/css/parse.py
+++ b/src/textual/css/parse.py
@@ -1,7 +1,7 @@
from __future__ import annotations
+import dataclasses
from functools import lru_cache
-from pathlib import PurePath
from typing import Iterable, Iterator, NoReturn
from ..suggestions import get_suggestion
@@ -19,7 +19,7 @@
from .styles import Styles
from .tokenize import Token, tokenize, tokenize_declarations, tokenize_values
from .tokenizer import EOFError, ReferencedBy
-from .types import Specificity3
+from .types import CSSLocation, Specificity3
SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = {
"selector": (SelectorType.TYPE, (0, 0, 1)),
@@ -30,15 +30,32 @@
"selector_start_id": (SelectorType.ID, (1, 0, 0)),
"selector_universal": (SelectorType.UNIVERSAL, (0, 0, 0)),
"selector_start_universal": (SelectorType.UNIVERSAL, (0, 0, 0)),
+ "nested": (SelectorType.NESTED, (0, 0, 0)),
}
+def _add_specificity(
+ specificity1: Specificity3, specificity2: Specificity3
+) -> Specificity3:
+ """Add specificity tuples together.
+
+ Args:
+ specificity1: Specificity triple.
+ specificity2: Specificity triple.
+
+ Returns:
+ Combined specificity.
+ """
+ a1, b1, c1 = specificity1
+ a2, b2, c2 = specificity2
+ return (a1 + a2, b1 + b2, c1 + c2)
+
+
@lru_cache(maxsize=1024)
def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
if not css_selectors.strip():
return ()
-
- tokens = iter(tokenize(css_selectors, ""))
+ tokens = iter(tokenize(css_selectors, ("", "")))
get_selector = SELECTOR_MAP.get
combinator: CombinatorType | None = CombinatorType.DESCENDENT
@@ -47,10 +64,13 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
while True:
try:
- token = next(tokens)
+ token = next(tokens, None)
except EOFError:
break
+ if token is None:
+ break
token_name = token.name
+
if token_name == "pseudo_class":
selectors[-1]._add_pseudo_class(token.value.lstrip(":"))
elif token_name == "whitespace":
@@ -85,6 +105,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,23 +148,103 @@ 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, "")
-
errors: list[tuple[Token, str | HelpText]] = []
+ nested_rules: list[RuleSet] = []
while True:
token = next(tokens)
+
token_name = token.name
if token_name in ("whitespace", "declaration_end"):
continue
+ if token_name in {
+ "selector_start_id",
+ "selector_start_class",
+ "selector_start_universal",
+ "selector_start",
+ "nested",
+ }:
+ recursive_parse: list[RuleSet] = list(
+ parse_rule_set(
+ "",
+ tokens,
+ token,
+ is_default_rules=is_default_rules,
+ tie_breaker=tie_breaker,
+ )
+ )
+
+ def combine_selectors(
+ selectors1: list[Selector], selectors2: list[Selector]
+ ) -> list[Selector]:
+ """Combine lists of selectors together, processing any nesting.
+
+ Args:
+ selectors1: List of selectors.
+ selectors2: Second list of selectors.
+
+ Returns:
+ Combined selectors.
+ """
+ if selectors2 and selectors2[0].type == SelectorType.NESTED:
+ final_selector = selectors1[-1]
+ nested_selector = selectors2[0]
+ merged_selector = dataclasses.replace(
+ final_selector,
+ pseudo_classes=list(
+ set(
+ final_selector.pseudo_classes
+ + nested_selector.pseudo_classes
+ )
+ ),
+ specificity=_add_specificity(
+ final_selector.specificity, nested_selector.specificity
+ ),
+ )
+ return [*selectors1[:-1], merged_selector, *selectors2[1:]]
+ else:
+ return selectors1 + selectors2
+
+ for rule_selector in rule_selectors:
+ for rule_set in recursive_parse:
+ nested_rule_set = RuleSet(
+ [
+ SelectorSet(
+ combine_selectors(
+ rule_selector, recursive_selectors.selectors
+ ),
+ (recursive_selectors.specificity),
+ )
+ for recursive_selectors in rule_set.selector_set
+ ],
+ rule_set.styles,
+ rule_set.errors,
+ rule_set.is_default_rules,
+ rule_set.tie_breaker + tie_breaker,
+ )
+ nested_rules.append(nested_rule_set)
+ continue
if token_name == "declaration_name":
- if declaration.tokens:
- try:
- styles_builder.add_declaration(declaration)
- except DeclarationError as error:
- errors.append((error.token, error.message))
+ try:
+ styles_builder.add_declaration(declaration)
+ except DeclarationError as error:
+ errors.append((error.token, error.message))
declaration = Declaration(token, "")
declaration.name = token.value.rstrip(":")
elif token_name == "declaration_set_end":
@@ -151,11 +252,10 @@ def parse_rule_set(
else:
declaration.tokens.append(token)
- if declaration.tokens:
- try:
- styles_builder.add_declaration(declaration)
- except DeclarationError as error:
- errors.append((error.token, error.message))
+ try:
+ styles_builder.add_declaration(declaration)
+ except DeclarationError as error:
+ errors.append((error.token, error.message))
rule_set = RuleSet(
list(SelectorSet.from_selectors(rule_selectors)),
@@ -164,27 +264,31 @@ def parse_rule_set(
is_default_rules=is_default_rules,
tie_breaker=tie_breaker,
)
+
rule_set._post_parse()
yield rule_set
+ for nested_rule_set in nested_rules:
+ nested_rule_set._post_parse()
+ yield nested_rule_set
+
-def parse_declarations(css: str, path: str) -> Styles:
+def parse_declarations(css: str, read_from: CSSLocation) -> Styles:
"""Parse declarations and return a Styles object.
Args:
css: String containing CSS.
- path: Path to the CSS, or something else to identify the location.
+ read_from: The location where the CSS was read from.
Returns:
A styles object.
"""
- tokens = iter(tokenize_declarations(css, path))
+ tokens = iter(tokenize_declarations(css, read_from))
styles_builder = StylesBuilder()
declaration: Declaration | None = None
errors: list[tuple[Token, str | HelpText]] = []
-
while True:
token = next(tokens, None)
if token is None:
@@ -193,7 +297,7 @@ def parse_declarations(css: str, path: str) -> Styles:
if token_name in ("whitespace", "declaration_end", "eof"):
continue
if token_name == "declaration_name":
- if declaration and declaration.tokens:
+ if declaration:
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
@@ -207,7 +311,7 @@ def parse_declarations(css: str, path: str) -> Styles:
if declaration:
declaration.tokens.append(token)
- if declaration and declaration.tokens:
+ if declaration:
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
@@ -234,7 +338,7 @@ def _unresolved(variable_name: str, variables: Iterable[str], token: Token) -> N
message += f"; did you mean '${suggested_variable}'?"
raise UnresolvedVariableError(
- token.path,
+ token.read_from,
token.code,
token.start,
message,
@@ -260,7 +364,6 @@ def substitute_references(
attribute populated with information about where the tokens are being substituted to.
"""
variables: dict[str, list[Token]] = css_variables.copy() if css_variables else {}
-
iter_tokens = iter(tokens)
while True:
@@ -328,8 +431,9 @@ def substitute_references(
def parse(
+ scope: str,
css: str,
- path: str | PurePath,
+ read_from: CSSLocation,
variables: dict[str, str] | None = None,
variable_tokens: dict[str, list[Token]] | None = None,
is_default_rules: bool = False,
@@ -339,24 +443,25 @@ def parse(
and generating rule sets from it.
Args:
- css: The input CSS
- path: Path to the CSS
+ scope: CSS type name.
+ css: The input CSS.
+ read_from: The source location of the CSS.
variables: Substitution variables to substitute tokens for.
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.
"""
-
reference_tokens = tokenize_values(variables) if variables is not None else {}
if variable_tokens:
reference_tokens.update(variable_tokens)
- tokens = iter(substitute_references(tokenize(css, path), variable_tokens))
+ tokens = iter(substitute_references(tokenize(css, read_from), variable_tokens))
while True:
token = next(tokens, None)
if token is None:
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/query.py b/src/textual/css/query.py
index ce966d6b18..7df103e7a3 100644
--- a/src/textual/css/query.py
+++ b/src/textual/css/query.py
@@ -49,6 +49,9 @@ class WrongType(QueryError):
QueryType = TypeVar("QueryType", bound="Widget")
+"""Type variable used to type generic queries."""
+ExpectType = TypeVar("ExpectType")
+"""Type variable used to further restrict queries."""
@rich.repr.auto(angular=True)
@@ -187,10 +190,8 @@ def exclude(self, selector: str) -> DOMQuery[QueryType]:
"""
return DOMQuery(self.node, exclude=selector, parent=self)
- ExpectType = TypeVar("ExpectType")
-
@overload
- def first(self) -> Widget:
+ def first(self) -> QueryType:
...
@overload
@@ -226,7 +227,7 @@ def first(
raise NoMatches(f"No nodes match {self!r}")
@overload
- def only_one(self) -> Widget:
+ def only_one(self) -> QueryType:
...
@overload
@@ -235,7 +236,7 @@ def only_one(self, expect_type: type[ExpectType]) -> ExpectType:
def only_one(
self, expect_type: type[ExpectType] | None = None
- ) -> Widget | ExpectType:
+ ) -> QueryType | ExpectType:
"""Get the *only* matching node.
Args:
@@ -253,7 +254,9 @@ def only_one(
_rich_traceback_omit = True
# Call on first to get the first item. Here we'll use all of the
# testing and checking it provides.
- the_one = self.first(expect_type) if expect_type is not None else self.first()
+ the_one: ExpectType | QueryType = (
+ self.first(expect_type) if expect_type is not None else self.first()
+ )
try:
# Now see if we can access a subsequent item in the nodes. There
# should *not* be anything there, so we *should* get an
@@ -268,10 +271,10 @@ def only_one(
# The IndexError was got, that's a good thing in this case. So
# we return what we found.
pass
- return cast("Widget", the_one)
+ return the_one
@overload
- def last(self) -> Widget:
+ def last(self) -> QueryType:
...
@overload
@@ -304,7 +307,7 @@ def last(
return last
@overload
- def results(self) -> Iterator[Widget]:
+ def results(self) -> Iterator[QueryType]:
...
@overload
@@ -313,7 +316,7 @@ def results(self, filter_type: type[ExpectType]) -> Iterator[ExpectType]:
def results(
self, filter_type: type[ExpectType] | None = None
- ) -> Iterator[Widget | ExpectType]:
+ ) -> Iterator[QueryType | ExpectType]:
"""Get query results, optionally filtered by a given type.
Args:
@@ -404,7 +407,7 @@ def set_styles(
node.set_styles(**update_styles)
if css is not None:
try:
- new_styles = parse_declarations(css, path="set_styles")
+ new_styles = parse_declarations(css, read_from=("set_styles", ""))
except DeclarationError as error:
raise DeclarationError(error.name, error.token, error.message) from None
for node in self:
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
index 1cca8364aa..5996918f38 100644
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -23,6 +23,7 @@
DockProperty,
FractionalProperty,
IntegerProperty,
+ KeylineProperty,
LayoutProperty,
NameListProperty,
NameProperty,
@@ -69,6 +70,7 @@
if TYPE_CHECKING:
from .._layout import Layout
from ..dom import DOMNode
+ from .types import CSSLocation
class RulesMap(TypedDict, total=False):
@@ -108,6 +110,8 @@ class RulesMap(TypedDict, total=False):
outline_bottom: tuple[str, Color]
outline_left: tuple[str, Color]
+ keyline: tuple[str, Color]
+
box_sizing: BoxSizing
width: Scalar
height: Scalar
@@ -166,10 +170,10 @@ class RulesMap(TypedDict, total=False):
link_background: Color
link_style: Style
- link_hover_color: Color
- auto_link_hover_color: bool
- link_hover_background: Color
- link_hover_style: Style
+ link_color_hover: Color
+ auto_link_color_hover: bool
+ link_background_hover: Color
+ link_style_hover: Style
auto_border_title_color: bool
border_title_color: Color
@@ -223,8 +227,8 @@ class StylesBase(ABC):
"scrollbar_background_active",
"link_color",
"link_background",
- "link_hover_color",
- "link_hover_background",
+ "link_color_hover",
+ "link_background_hover",
}
node: DOMNode | None = None
@@ -264,6 +268,8 @@ class StylesBase(ABC):
outline_bottom = BoxProperty(Color(0, 255, 0))
outline_left = BoxProperty(Color(0, 255, 0))
+ keyline = KeylineProperty()
+
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True)
width = ScalarProperty(percent_unit=Unit.WIDTH)
height = ScalarProperty(percent_unit=Unit.HEIGHT)
@@ -333,10 +339,10 @@ class StylesBase(ABC):
link_background = ColorProperty("transparent")
link_style = StyleFlagsProperty()
- link_hover_color = ColorProperty("transparent")
- auto_link_hover_color = BooleanProperty(False)
- link_hover_background = ColorProperty("transparent")
- link_hover_style = StyleFlagsProperty()
+ link_color_hover = ColorProperty("transparent")
+ auto_link_color_hover = BooleanProperty(False)
+ link_background_hover = ColorProperty("transparent")
+ link_style_hover = StyleFlagsProperty()
auto_border_title_color = BooleanProperty(default=False)
border_title_color = ColorProperty(Color(255, 255, 255, 0))
@@ -534,12 +540,14 @@ def is_animatable(cls, rule: str) -> bool:
@classmethod
@lru_cache(maxsize=1024)
- def parse(cls, css: str, path: str, *, node: DOMNode | None = None) -> Styles:
+ def parse(
+ cls, css: str, read_from: CSSLocation, *, node: DOMNode | None = None
+ ) -> Styles:
"""Parse CSS and return a Styles object.
Args:
css: Textual CSS.
- path: Path or string indicating source of CSS.
+ read_from: Location where the CSS was read from.
node: Node to associate with the Styles.
Returns:
@@ -547,7 +555,7 @@ def parse(cls, css: str, path: str, *, node: DOMNode | None = None) -> Styles:
"""
from .parse import parse_declarations
- styles = parse_declarations(css, path)
+ styles = parse_declarations(css, read_from)
styles.node = node
return styles
@@ -650,7 +658,11 @@ class Styles(StylesBase):
def copy(self) -> Styles:
"""Get a copy of this Styles object."""
- return Styles(node=self.node, _rules=self.get_rules(), important=self.important)
+ return Styles(
+ node=self.node,
+ _rules=self.get_rules(),
+ important=self.important,
+ )
def has_rule(self, rule: str) -> bool:
assert rule in RULE_NAMES_SET, f"no such rule {rule!r}"
@@ -701,13 +713,15 @@ def get_rule(self, rule: str, default: object = None) -> object:
def refresh(
self, *, layout: bool = False, children: bool = False, parent: bool = False
) -> None:
- if parent and self.node and self.node.parent:
- self.node.parent.refresh()
- if self.node is not None:
- self.node.refresh(layout=layout)
- if children:
- for child in self.node.walk_children(with_self=False, reverse=True):
- child.refresh(layout=layout)
+ node = self.node
+ if node is None or not node._is_mounted:
+ return
+ if parent and node._parent is not None:
+ node._parent.refresh()
+ node.refresh(layout=layout)
+ if children:
+ for child in node.walk_children(with_self=False, reverse=True):
+ child.refresh(layout=layout)
def reset(self) -> None:
"""Reset the rules to initial state."""
@@ -745,11 +759,12 @@ def extract_rules(
A list containing a tuple of , .
"""
is_important = self.important.__contains__
- rules = [
+ default_rules = 0 if is_default_rules else 1
+ rules: list[tuple[str, Specificity6, Any]] = [
(
rule_name,
(
- 0 if is_default_rules else 1,
+ default_rules,
1 if is_important(rule_name) else 0,
*specificity,
tie_breaker,
@@ -758,6 +773,7 @@ def extract_rules(
)
for rule_name, rule_value in self._rules.items()
]
+
return rules
def __rich_repr__(self) -> rich.repr.Result:
@@ -1011,12 +1027,12 @@ def append_declaration(name: str, value: str) -> None:
if "link_style" in rules:
append_declaration("link-style", str(self.link_style))
- if "link_hover_color" in rules:
- append_declaration("link-hover-color", self.link_hover_color.css)
- if "link_hover_background" in rules:
- append_declaration("link-hover-background", self.link_hover_background.css)
- if "link_hover_style" in rules:
- append_declaration("link-hover-style", str(self.link_hover_style))
+ if "link_color_hover" in rules:
+ append_declaration("link-color-hover", self.link_color_hover.css)
+ if "link_background_hover" in rules:
+ append_declaration("link-background-hover", self.link_background_hover.css)
+ if "link_style_hover" in rules:
+ append_declaration("link-style-hover", str(self.link_style_hover))
if "border_title_color" in rules:
append_declaration("title-color", self.border_title_color.css)
@@ -1037,6 +1053,10 @@ def append_declaration(name: str, value: str) -> None:
append_declaration("overlay", str(self.overlay))
if "constrain" in rules:
append_declaration("constrain", str(self.constrain))
+ if "keyline" in rules:
+ keyline_type, keyline_color = self.keyline
+ if keyline_type != "none":
+ append_declaration("keyline", f"{keyline_type}, {keyline_color.css}")
lines.sort()
return lines
diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py
index 22bfccaa3f..587fa166c8 100644
--- a/src/textual/css/stylesheet.py
+++ b/src/textual/css/stylesheet.py
@@ -2,6 +2,7 @@
import os
from collections import defaultdict
+from itertools import chain
from operator import itemgetter
from pathlib import Path, PurePath
from typing import Iterable, NamedTuple, Sequence, cast
@@ -11,10 +12,9 @@
from rich.markup import render
from rich.padding import Padding
from rich.panel import Panel
-from rich.style import Style
-from rich.syntax import Syntax
from rich.text import Text
+from .._cache import LRUCache
from ..dom import DOMNode
from ..widget import Widget
from .errors import StylesheetError
@@ -24,7 +24,9 @@
from .styles import RulesMap, Styles
from .tokenize import Token, tokenize_values
from .tokenizer import TokenError
-from .types import Specificity3, Specificity6
+from .types import CSSLocation, Specificity3, Specificity6
+
+_DEFAULT_STYLES = Styles()
class StylesheetParseError(StylesheetError):
@@ -42,6 +44,8 @@ def __init__(self, rules: list[RuleSet]) -> None:
@classmethod
def _get_snippet(cls, code: str, line_no: int) -> RenderableType:
+ from rich.syntax import Syntax
+
syntax = Syntax(
code,
lexer="scss",
@@ -57,45 +61,52 @@ def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
error_count = 0
- for rule in self.rules:
- for token, message in rule.errors:
- error_count += 1
-
- if token.path:
- path = Path(token.path)
- filename = path.name
- else:
- path = None
- filename = ""
-
- if token.referenced_by:
- line_idx, col_idx = token.referenced_by.location
- else:
- line_idx, col_idx = token.location
- line_no, col_no = line_idx + 1, col_idx + 1
- path_string = (
- f"{path.absolute() if path else filename}:{line_no}:{col_no}"
- )
- link_style = Style(
- link=f"file://{path.absolute()}" if path else None,
- color="red",
- bold=True,
- italic=True,
- )
+ errors = list(
+ dict.fromkeys(chain.from_iterable(_rule.errors for _rule in self.rules))
+ )
- path_text = Text(path_string, style=link_style)
- title = Text.assemble(Text("Error at ", style="bold red"), path_text)
- yield ""
- yield Panel(
- self._get_snippet(
- token.referenced_by.code if token.referenced_by else token.code,
- line_no,
- ),
- title=title,
- title_align="left",
- border_style="red",
- )
- yield Padding(message, pad=(0, 0, 1, 3))
+ for token, message in errors:
+ error_count += 1
+
+ if token.referenced_by:
+ line_idx, col_idx = token.referenced_by.location
+ else:
+ line_idx, col_idx = token.location
+ line_no, col_no = line_idx + 1, col_idx + 1
+
+ display_path, widget_var = token.read_from
+ if display_path:
+ link_path = str(Path(display_path).absolute())
+ filename = Path(link_path).name
+ else:
+ link_path = ""
+ filename = ""
+ # If we have a widget/variable from where the CSS was read, then line/column
+ # numbers are relative to the inline CSS and we'll display them next to the
+ # widget/variable.
+ # Otherwise, they're absolute positions in a TCSS file and we can show them
+ # next to the file path.
+ if widget_var:
+ path_string = link_path or filename
+ widget_string = f" in {widget_var}:{line_no}:{col_no}"
+ else:
+ path_string = f"{link_path or filename}:{line_no}:{col_no}"
+ widget_string = ""
+
+ title = Text.assemble(
+ "Error at ", path_string, widget_string, style="bold red"
+ )
+ yield ""
+ yield Panel(
+ self._get_snippet(
+ token.referenced_by.code if token.referenced_by else token.code,
+ line_no,
+ ),
+ title=title,
+ title_align="left",
+ border_style="red",
+ )
+ yield Padding(message, pad=(0, 0, 1, 3))
yield ""
yield render(
@@ -111,11 +122,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)
@@ -125,9 +139,10 @@ def __init__(self, *, variables: dict[str, str] | None = None) -> None:
self._rules_map: dict[str, list[RuleSet]] | None = None
self._variables = variables or {}
self.__variable_tokens: dict[str, list[Token]] | None = None
- self.source: dict[str, CssSource] = {}
+ self.source: dict[CSSLocation, CssSource] = {}
self._require_parse = False
self._invalid_css: set[str] = set()
+ self._parse_cache: LRUCache[tuple, list[RuleSet]] = LRUCache(64)
def __rich_repr__(self) -> rich.repr.Result:
yield list(self.source.keys())
@@ -189,22 +204,24 @@ def set_variables(self, variables: dict[str, str]) -> None:
self._variables = variables
self.__variable_tokens = None
self._invalid_css = set()
+ self._parse_cache.clear()
def _parse_rules(
self,
css: str,
- path: str | PurePath,
+ read_from: CSSLocation,
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
+ read_from: Original CSS location.
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.
@@ -212,11 +229,17 @@ def _parse_rules(
Returns:
List of RuleSets.
"""
+ cache_key = (css, read_from, is_default_rules, tie_breaker, scope)
+ try:
+ return self._parse_cache[cache_key]
+ except KeyError:
+ pass
try:
rules = list(
parse(
+ scope,
css,
- path,
+ read_from,
variable_tokens=self._variable_tokens,
is_default_rules=is_default_rules,
tie_breaker=tie_breaker,
@@ -225,8 +248,9 @@ def _parse_rules(
except TokenError:
raise
except Exception as error:
- raise StylesheetError(f"failed to parse css; {error}")
+ raise StylesheetError(f"failed to parse css; {error}") from None
+ self._parse_cache[cache_key] = rules
return rules
def read(self, filename: str | PurePath) -> None:
@@ -246,7 +270,7 @@ def read(self, filename: str | PurePath) -> None:
path = os.path.abspath(filename)
except Exception:
raise StylesheetError(f"unable to read CSS file {filename!r}") from None
- self.source[str(path)] = CssSource(css, False, 0)
+ self.source[(str(path), "")] = CssSource(css, False, 0)
self._require_parse = True
def read_all(self, paths: Sequence[PurePath]) -> None:
@@ -262,47 +286,56 @@ def read_all(self, paths: Sequence[PurePath]) -> None:
for path in paths:
self.read(path)
- def has_source(self, path: str | PurePath) -> bool:
+ def has_source(self, path: str, class_var: str = "") -> bool:
"""Check if the stylesheet has this CSS source already.
+ Args:
+ path: The file path of the source in question.
+ class_var: The widget class variable we might be reading the CSS from.
+
Returns:
Whether the stylesheet is aware of this CSS source or not.
"""
- return str(path) in self.source
+ return (path, class_var) in self.source
def add_source(
self,
css: str,
- path: str | PurePath | None = None,
+ read_from: CSSLocation | None = None,
is_default_css: bool = False,
tie_breaker: int = 0,
+ scope: str = "",
) -> None:
"""Parse CSS from a string.
Args:
css: String with CSS source.
+ read_from: The original source location of the CSS.
path: The path of the source if a file, or some other identifier.
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.
StylesheetParseError: If the CSS is invalid.
"""
- if path is None:
- path = str(hash(css))
- elif isinstance(path, PurePath):
- 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]
+ if read_from is None:
+ read_from = ("", str(hash(css)))
+
+ if read_from in self.source and self.source[read_from].content == css:
+ # Location already in source and CSS is identical.
+ content, is_defaults, source_tie_breaker, scope = self.source[read_from]
if source_tie_breaker > tie_breaker:
- self.source[path] = CssSource(content, is_defaults, tie_breaker)
+ self.source[read_from] = CssSource(
+ content, is_defaults, tie_breaker, scope
+ )
return
- self.source[path] = CssSource(css, is_default_css, tie_breaker)
+ self.source[read_from] = CssSource(css, is_default_css, tie_breaker, scope)
self._require_parse = True
+ self._rules_map = None
def parse(self) -> None:
"""Parse the source in the stylesheet.
@@ -313,21 +346,28 @@ def parse(self) -> None:
rules: list[RuleSet] = []
add_rules = rules.extend
- for path, (css, is_default_rules, tie_breaker) in self.source.items():
+ for read_from, (
+ css,
+ is_default_rules,
+ tie_breaker,
+ scope,
+ ) in self.source.items():
if css in self._invalid_css:
continue
try:
css_rules = self._parse_rules(
css,
- path,
+ read_from=read_from,
is_default_rules=is_default_rules,
tie_breaker=tie_breaker,
+ scope=scope,
)
except Exception:
self._invalid_css.add(css)
raise
if any(rule.errors for rule in css_rules):
error_renderable = StylesheetErrors(css_rules)
+ self._invalid_css.add(css)
raise StylesheetParseError(error_renderable)
add_rules(css_rules)
self._rules = rules
@@ -343,14 +383,27 @@ 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 read_from, (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,
+ read_from=read_from,
+ is_default_css=is_defaults,
+ tie_breaker=tie_breaker,
+ scope=scope,
)
- stylesheet.parse()
- self._rules = stylesheet.rules
- self._rules_map = None
- self.source = stylesheet.source
+ try:
+ stylesheet.parse()
+ except Exception:
+ # If we don't update self's invalid CSS, we might end up reparsing this CSS
+ # before Textual quits application mode.
+ # See https://github.com/Textualize/textual/issues/3581.
+ self._invalid_css.update(stylesheet._invalid_css)
+ raise
+ else:
+ self._rules = stylesheet.rules
+ self._rules_map = None
+ self.source = stylesheet.source
+ self._require_parse = False
@classmethod
def _check_rule(
@@ -364,8 +417,8 @@ def apply(
self,
node: DOMNode,
*,
- limit_rules: set[RuleSet] | None = None,
animate: bool = False,
+ cache: dict[tuple, RulesMap] | None = None,
) -> None:
"""Apply the stylesheet to a DOM node.
@@ -376,6 +429,7 @@ def apply(
classes modifying the same CSS property), then only the most specific
rule will be applied.
animate: Animate changed rules.
+ cache: An optional cache when applying a group of nodes.
"""
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
# The tuples contain the rule specificity, and the value for that rule.
@@ -385,44 +439,121 @@ def apply(
rule_attributes: defaultdict[str, list[tuple[Specificity6, object]]]
rule_attributes = defaultdict(list)
+ rules_map = self.rules_map
+
+ # Discard rules which are not applicable early
+ limit_rules = {
+ rule
+ for name in rules_map.keys() & node._selector_names
+ for rule in rules_map[name]
+ }
+ rules = list(filter(limit_rules.__contains__, reversed(self.rules)))
+
+ node._has_hover_style = any("hover" in rule.pseudo_classes for rule in rules)
+ node._has_focus_within = any(
+ "focus-within" in rule.pseudo_classes for rule in rules
+ )
+
+ cache_key: tuple | None
+ if cache is not None:
+ cache_key = (
+ node._parent,
+ (
+ None
+ if node._id is None
+ else (node._id if f"#{node._id}" in rules_map else None)
+ ),
+ node.classes,
+ node.pseudo_classes,
+ node._css_type_name,
+ )
+ cached_result: RulesMap | None = cache.get(cache_key)
+ if cached_result is not None:
+ self.replace_rules(node, cached_result, animate=animate)
+ self._process_component_classes(node)
+ return
+ else:
+ cache_key = None
+
_check_rule = self._check_rule
css_path_nodes = node.css_path_nodes
- rules: Iterable[RuleSet]
- if limit_rules:
- rules = [rule for rule in reversed(self.rules) if rule in limit_rules]
- else:
- rules = reversed(self.rules)
+ # Rules that may be set to the special value `initial`
+ initial: set[str] = set()
+ # Rules in DEFAULT_CSS set to the special value `initial`
+ initial_defaults: set[str] = set()
- # Collect the rules defined in the stylesheet
- node._has_hover_style = False
- node._has_focus_within = False
for rule in rules:
is_default_rules = rule.is_default_rules
tie_breaker = rule.tie_breaker
- if ":hover" in rule.selector_names:
- node._has_hover_style = True
- if ":focus-within" in rule.selector_names:
- node._has_focus_within = True
for base_specificity in _check_rule(rule, css_path_nodes):
for key, rule_specificity, value in rule.styles.extract_rules(
base_specificity, is_default_rules, tie_breaker
):
+ if value is None:
+ if is_default_rules:
+ initial_defaults.add(key)
+ else:
+ initial.add(key)
rule_attributes[key].append((rule_specificity, value))
- if not rule_attributes:
- return
- # For each rule declared for this node, keep only the most specific one
- get_first_item = itemgetter(0)
- node_rules: RulesMap = cast(
- RulesMap,
- {
- name: max(specificity_rules, key=get_first_item)[1]
- for name, specificity_rules in rule_attributes.items()
- },
- )
- self.replace_rules(node, node_rules, animate=animate)
+ if rule_attributes:
+ # For each rule declared for this node, keep only the most specific one
+ get_first_item = itemgetter(0)
+ node_rules: RulesMap = cast(
+ RulesMap,
+ {
+ name: max(specificity_rules, key=get_first_item)[1]
+ for name, specificity_rules in rule_attributes.items()
+ },
+ )
+
+ # Set initial values
+ for initial_rule_name in initial:
+ # Rules with a value of None should be set to the default value
+ if node_rules[initial_rule_name] is None: # type: ignore[literal-required]
+ # Exclude non default values
+ # rule[0] is the specificity, rule[0][0] is 0 for default rules
+ default_rules = [
+ rule
+ for rule in rule_attributes[initial_rule_name]
+ if not rule[0][0]
+ ]
+ if default_rules:
+ # There is a default value
+ new_value = max(default_rules, key=get_first_item)[1]
+ node_rules[initial_rule_name] = new_value # type: ignore[literal-required]
+ else:
+ # No default value
+ initial_defaults.add(initial_rule_name)
+
+ # Rules in DEFAULT_CSS set to initial
+ for initial_rule_name in initial_defaults:
+ if node_rules[initial_rule_name] is None: # type: ignore[literal-required]
+ default_rules = [
+ rule
+ for rule in rule_attributes[initial_rule_name]
+ if rule[0][0]
+ ]
+ if default_rules:
+ # There is a default value
+ rule_value = max(default_rules, key=get_first_item)[1]
+ else:
+ rule_value = getattr(_DEFAULT_STYLES, initial_rule_name)
+ node_rules[initial_rule_name] = rule_value # type: ignore[literal-required]
+
+ if cache is not None:
+ assert cache_key is not None
+ cache[cache_key] = node_rules
+ self.replace_rules(node, node_rules, animate=animate)
+ self._process_component_classes(node)
+
+ def _process_component_classes(self, node: DOMNode) -> None:
+ """Process component classes for the given node.
+ Args:
+ node: A DOM Node.
+ """
component_classes = node._get_component_classes()
if component_classes:
# Create virtual nodes that exist to extract styles
@@ -460,25 +591,18 @@ def replace_rules(
base_styles = styles.base
# Styles currently used on new rules
- modified_rule_keys = base_styles.get_rules().keys() | rules.keys()
- # Current render rules (missing rules are filled with default)
- current_render_rules = styles.get_render_rules()
-
- # Calculate replacement rules (defaults + new rules)
- new_styles = Styles(node, rules)
- if new_styles == base_styles:
- # Nothing to change, return early
- return
-
- # New render rules
- new_render_rules = new_styles.get_render_rules()
-
- # Some aliases
- is_animatable = styles.is_animatable
- get_current_render_rule = current_render_rules.get
- get_new_render_rule = new_render_rules.get
+ modified_rule_keys = base_styles._rules.keys() | rules.keys()
if animate:
+ new_styles = Styles(node, rules)
+ if new_styles == base_styles:
+ # Nothing to animate, return early
+ return
+ current_render_rules = styles.get_render_rules()
+ is_animatable = styles.is_animatable
+ get_current_render_rule = current_render_rules.get
+ new_render_rules = new_styles.get_render_rules()
+ get_new_render_rule = new_render_rules.get
animator = node.app.animator
base = node.styles.base
for key in modified_rule_keys:
@@ -536,21 +660,15 @@ def update_nodes(self, nodes: Iterable[DOMNode], animate: bool = False) -> None:
nodes: Nodes to update.
animate: Enable CSS animation.
"""
- rules_map = self.rules_map
+ cache: dict[tuple, RulesMap] = {}
apply = self.apply
for node in nodes:
- rules = {
- rule
- for name in node._selector_names
- if name in rules_map
- for rule in rules_map[name]
- }
- apply(node, limit_rules=rules, animate=animate)
+ apply(node, animate=animate, cache=cache)
if isinstance(node, Widget) and node.is_scrollable:
if node.show_vertical_scrollbar:
- apply(node.vertical_scrollbar)
+ apply(node.vertical_scrollbar, cache=cache)
if node.show_horizontal_scrollbar:
- apply(node.horizontal_scrollbar)
+ apply(node.horizontal_scrollbar, cache=cache)
if node.show_horizontal_scrollbar and node.show_vertical_scrollbar:
- apply(node.scrollbar_corner)
+ apply(node.scrollbar_corner, cache=cache)
diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py
index 12e7ed6e26..7cf1556613 100644
--- a/src/textual/css/tokenize.py
+++ b/src/textual/css/tokenize.py
@@ -1,10 +1,12 @@
from __future__ import annotations
import re
-from pathlib import PurePath
-from typing import Iterable
+from typing import TYPE_CHECKING, Iterable
-from textual.css.tokenizer import Expect, Token, Tokenizer
+from .tokenizer import Expect, Token, Tokenizer
+
+if TYPE_CHECKING:
+ from .types import CSSLocation
PERCENT = r"-?\d+\.?\d*%"
DECIMAL = r"-?\d+\.?\d*"
@@ -45,6 +47,7 @@
# in the CSS file. At this level we might expect to see selectors, comments,
# variable definitions etc.
expect_root_scope = Expect(
+ "selector or end of file",
whitespace=r"\s+",
comment_start=COMMENT_START,
comment_line=COMMENT_LINE,
@@ -53,11 +56,27 @@
selector_start_universal=r"\*",
selector_start=IDENTIFIER,
variable_name=rf"{VARIABLE_REF}:",
+ declaration_set_end=r"\}",
).expect_eof(True)
+expect_root_nested = Expect(
+ "selector or end of file",
+ whitespace=r"\s+",
+ comment_start=COMMENT_START,
+ comment_line=COMMENT_LINE,
+ selector_start_id=r"\#" + IDENTIFIER,
+ selector_start_class=r"\." + IDENTIFIER,
+ selector_start_universal=r"\*",
+ selector_start=IDENTIFIER,
+ variable_name=rf"{VARIABLE_REF}:",
+ declaration_set_end=r"\}",
+ nested=r"\&",
+)
+
# After a variable declaration e.g. "$warning-text: TOKENS;"
# for tokenizing variable value ------^~~~~~~^
expect_variable_name_continue = Expect(
+ "variable value",
variable_value_end=r"\n|;",
whitespace=r"\s+",
comment_start=COMMENT_START,
@@ -66,12 +85,14 @@
).expect_eof(True)
expect_comment_end = Expect(
+ "comment end",
comment_end=re.escape("*/"),
)
# After we come across a selector in CSS e.g. ".my-class", we may
# find other selectors, pseudo-classes... e.g. ".my-class :hover"
expect_selector_continue = Expect(
+ "selector or {",
whitespace=r"\s+",
comment_start=COMMENT_START,
comment_line=COMMENT_LINE,
@@ -83,19 +104,28 @@
combinator_child=">",
new_selector=r",",
declaration_set_start=r"\{",
-)
+ declaration_set_end=r"\}",
+).expect_eof(True)
# A rule declaration e.g. "text: red;"
# ^---^
expect_declaration = Expect(
+ "rule or selector",
+ nested=r"\&",
whitespace=r"\s+",
comment_start=COMMENT_START,
comment_line=COMMENT_LINE,
declaration_name=r"[a-zA-Z_\-]+\:",
declaration_set_end=r"\}",
+ #
+ selector_start_id=r"\#" + IDENTIFIER,
+ selector_start_class=r"\." + IDENTIFIER,
+ selector_start_universal=r"\*",
+ selector_start=IDENTIFIER,
)
expect_declaration_solo = Expect(
+ "rule declaration",
whitespace=r"\s+",
comment_start=COMMENT_START,
comment_line=COMMENT_LINE,
@@ -106,6 +136,7 @@
# The value(s)/content from a rule declaration e.g. "text: red;"
# ^---^
expect_declaration_content = Expect(
+ "rule value or end of declaration",
declaration_end=r";",
whitespace=r"\s+",
comment_start=COMMENT_START,
@@ -117,6 +148,7 @@
)
expect_declaration_content_solo = Expect(
+ "rule value or end of declaration",
declaration_end=r";",
whitespace=r"\s+",
comment_start=COMMENT_START,
@@ -154,14 +186,16 @@ class TokenizerState:
"declaration_set_start": expect_declaration,
"declaration_name": expect_declaration_content,
"declaration_end": expect_declaration,
- "declaration_set_end": expect_root_scope,
+ "declaration_set_end": expect_root_nested,
+ "nested": expect_selector_continue,
}
- def __call__(self, code: str, path: str | PurePath) -> Iterable[Token]:
- tokenizer = Tokenizer(code, path=path)
+ def __call__(self, code: str, read_from: CSSLocation) -> Iterable[Token]:
+ tokenizer = Tokenizer(code, read_from=read_from)
expect = self.EXPECT
get_token = tokenizer.get_token
get_state = self.STATE_MAP.get
+ nest_level = 0
while True:
token = get_token(expect)
name = token.name
@@ -172,6 +206,13 @@ def __call__(self, code: str, path: str | PurePath) -> Iterable[Token]:
continue
elif name == "eof":
break
+ elif name == "declaration_set_start":
+ nest_level += 1
+ elif name == "declaration_set_end":
+ nest_level -= 1
+ expect = expect_root_nested if nest_level else expect_root_scope
+ yield token
+ continue
expect = get_state(name, expect)
yield token
@@ -194,7 +235,7 @@ class ValueTokenizerState(TokenizerState):
def tokenize_values(values: dict[str, str]) -> dict[str, list[Token]]:
- """Tokens the values in a dict of strings.
+ """Tokenizes the values in a dict of strings.
Args:
values: A mapping of CSS variable name on to a value, to be
@@ -204,6 +245,7 @@ def tokenize_values(values: dict[str, str]) -> dict[str, list[Token]]:
A mapping of name on to a list of tokens,
"""
value_tokens = {
- name: list(tokenize_value(value, "__name__")) for name, value in values.items()
+ name: list(tokenize_value(value, ("__name__", "")))
+ for name, value in values.items()
}
return value_tokens
diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py
index 276b22b54b..cc748aabe0 100644
--- a/src/textual/css/tokenizer.py
+++ b/src/textual/css/tokenizer.py
@@ -1,28 +1,29 @@
from __future__ import annotations
import re
-from pathlib import PurePath
-from typing import NamedTuple
+from typing import TYPE_CHECKING, NamedTuple
import rich.repr
from rich.console import Group, RenderableType
from rich.highlighter import ReprHighlighter
from rich.padding import Padding
from rich.panel import Panel
-from rich.syntax import Syntax
from rich.text import Text
from ..suggestions import get_suggestion
from ._error_tools import friendly_list
from .constants import VALID_PSEUDO_CLASSES
+if TYPE_CHECKING:
+ from .types import CSSLocation
+
class TokenError(Exception):
"""Error raised when the CSS cannot be tokenized (syntax error)."""
def __init__(
self,
- path: str,
+ read_from: CSSLocation,
code: str,
start: tuple[int, int],
message: str,
@@ -30,14 +31,14 @@ def __init__(
) -> None:
"""
Args:
- path: Path to source or "