diff --git a/CHANGELOG.md b/CHANGELOG.md index c293278149..9d4fa45441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed -- Fixed `DataTable.update_cell` not raising an error with an invalid column key. +- Fixed `DataTable` not updating component styles on hot-reloading https://github.com/Textualize/textual/issues/3312 +- Fixed `DataTable.update_cell` not raising an error with an invalid column key https://github.com/Textualize/textual/issues/3335 ## [0.37.1] - 2023-09-16 @@ -18,7 +19,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 @@ -46,6 +47,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 @@ -63,6 +65,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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41b440244b..b7d3488111 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,120 +1,103 @@ -# Contributing Guidelines +# Contributing to Textual -๐ŸŽ‰ **First of all, thanks for taking the time to contribute!** ๐ŸŽ‰ +First of all, thanks for taking the time to contribute to Textual! -## ๐Ÿค” How can I contribute? +## How can I contribute? -**1.** Fix issue +You can contribute to Textual in many ways: -**2.** Report bug + 1. [Report a bug](https://github.com/textualize/textual/issues/new?title=%5BBUG%5D%20short%20bug%20description&template=bug_report.md) + 2. Add a new feature + 3. Fix a bug + 4. Improve the documentation -**3.** Improve Documentation +## Setup -## Setup ๐Ÿš€ -You need to set up Textualize to make your contribution. Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows, and probably any OS where Python also runs. +To make a code or documentation contribution you will need to set up Textual locally. +You can follow these steps: -### Installation + 1. Make sure you have Poetry installed ([see instructions here](https://python-poetry.org)) + 2. Clone the Textual repository + 3. Run `poetry shell` to create a virtual environment for the dependencies + 4. Run `poetry install` to install all dependencies + 5. Make sure the latest version of Textual was installed by running the command `textual --version` + 6. Install the pre-commit hooks with the command `pre-commit install` -**Install Texualize via pip:** -```bash -pip install textual -``` -**Install [Poetry](https://python-poetry.org/)** -```bash -curl -sSL https://install.python-poetry.org | python3 - -``` -**To install all dependencies, run:** -```bash -poetry install --all -``` -**Make sure everything works fine:** -```bash -textual --version -``` -### Demo +## Demo -Once you have Textual installed, run the following to get an impression of what it can do: +Once you have Textual installed, run the Textual demo to get an impression of what Textual can do and to double check that everything was installed correctly: ```bash python -m textual ``` -If Texualize is installed, you should see this: -demo -## Make contribution -**1.** Fork [this](repo) repository. +## Guidelines -**2.** Clone the forked repository. +- Read any issue instructions carefully. Feel free to ask for clarification if any details are missing. -```bash -git clone https://github.com//textual.git -``` +- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from. -**3.** Navigate to the project directory. +- Write tests for your code. + - If you are fixing a bug, make sure to add regression tests that link to the original issue. + - If you are implementing a visual element, make sure to add _snapshot tests_. [See below](#snapshot-testing) for more details. -```bash -cd textual -``` +## Before opening a PR + +Before you open your PR, please go through this checklist and make sure you've checked all the items that apply: -**4.** Create a new [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) + - [ ] Update the `CHANGELOG.md` + - [ ] Format your code with black (`make format`) + - [ ] All your code has docstrings in the style of the rest of the codebase + - [ ] Your code passes all tests (`make test`) +([Read this](#makefile-commands) if the command `make` doesn't work for you.) -### ๐Ÿ“ฃ Pull Requests(PRs) +## Updating and building the documentation -The process described here should check off these goals: +If you change the documentation, you will want to build the documentation to make sure everything looks like it should. +The command `make docs-serve-offline` should start a server that will let you preview the documentation locally and that should reload whenever you save changes to the documentation or the code files. -- [x] Maintain the project's quality. -- [x] Fix problems that are important to users. -- [x] The CHANGELOG.md was updated; -- [x] Your code was formatted with black (make format); -- [x] All of your code has docstrings in the style of the rest of the codebase; -- [x] your code passes all tests (make test); and -- [x] You added documentation when needed. +([Read this](#makefile-commands) if the command `make` doesn't work for you.) + +## After opening a PR -### After the PR ๐Ÿฅณ When you open a PR, your code will be reviewed by one of the Textual maintainers. In that review process, -- We will take a look at all of the changes you are making; -- We might ask for clarifications (why did you do X or Y?); -- We might ask for more tests/more documentation; and -- We might ask for some code changes. +- We will take a look at all of the changes you are making +- We might ask for clarifications (why did you do X or Y?) +- We might ask for more tests/more documentation +- We might ask for some code changes The sole purpose of those interactions is to make sure that, in the long run, everyone has the best experience possible with Textual and with the feature you are implementing/fixing. Don't be discouraged if a reviewer asks for code changes. If you go through our history of pull requests, you will see that every single one of the maintainers has had to make changes following a review. +## Snapshot testing +Snapshot tests ensure that visual things (like widgets) look like they are supposed to. +PR [#1969](https://github.com/Textualize/textual/pull/1969) is a good example of what adding snapshot tests looks like: it amounts to a change in the file `tests/snapshot_tests/test_snapshots.py` that should run an app that you write and compare it against a historic snapshot of what that app should look like. -## ๐Ÿ›‘ Important - -- Make sure to read the issue instructions carefully. If you are a newbie you should look out for some good first issues because they should be clear enough and sometimes even provide some hints. If something isn't clear, ask for clarification! - -- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from. - -- Write tests for your code. - -- If you are fixing a bug, make sure to add regression tests that link to the original issue. - -- If you are implementing a visual element, make sure to add snapshot tests. See below for more details. - - -### Snapshot Testing -Snapshot tests ensure that things like widgets look like they are supposed to. -PR [#1969](https://github.com/Textualize/textual/pull/1969) is a good example of what adding snapshot tests means: it amounts to a change in the file ```tests/snapshot_tests/test_snapshots.py```, that should run an app that you write and compare it against a historic snapshot of what that app should look like. - -When you create a new snapshot test, run it with ```pytest -vv tests/snapshot_tests/test_snapshots.py.``` -Because you just created this snapshot test, there is no history to compare against and the test will fail automatically. +When you create a new snapshot test, run it with `pytest -vv tests/snapshot_tests/test_snapshots.py`. +Because you just created this snapshot test, there is no history to compare against and the test will fail. After running the snapshot tests, you should see a link that opens an interface in your browser. -This interface should show all failing snapshot tests and a side-by-side diff between what the app looked like when it ran VS the historic snapshot. +This interface should show all failing snapshot tests and a side-by-side diff between what the app looked like when the test ran versus the historic snapshot. Make sure your snapshot app looks like it is supposed to and that you didn't break any other snapshot tests. -If that's the case, you can run ```make test-snapshot-update``` to update the snapshot history with your new snapshot. -This will write to the file ```tests/snapshot_tests/__snapshots__/test_snapshots.ambr```, that you should NOT modify by hand +If everything looks fine, you can run `make test-snapshot-update` to update the snapshot history with your new snapshot. +This will write to the file `tests/snapshot_tests/__snapshots__/test_snapshots.ambr`, which you should NOT modify by hand. + +([Read this](#makefile-commands) if the command `make` doesn't work for you.) + +## Join the community +Seems a little overwhelming? +Join our community on [Discord](https://discord.gg/uNRPEGCV) to get help! -### ๐Ÿ“ˆJoin the community +## Makefile commands -- ๐Ÿ˜• Seems a little overwhelming? Join our community on [Discord](https://discord.gg/uNRPEGCV) to get help. +Textual has a `Makefile` file that contains the most common commands used when developing Textual. +([Read about Make and makefiles on Wikipedia.](https://en.wikipedia.org/wiki/Make_(software))) +If you don't have Make and you're on Windows, you may want to [install Make](https://stackoverflow.com/q/32127524/2828287). diff --git a/docs/api/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/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/index.md b/docs/index.md index c8e48c5748..4720ff4bcf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,11 @@ -# Introduction +--- +hide: + - toc + - navigation +--- + + +# Welcome Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. @@ -16,7 +23,7 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework docume Textual is a *Rapid Application Development* framework for Python, built by [Textualize.io](https://www.textualize.io). -Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (*coming soon*) a web browser. +Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal *or* a web browser (with [textual-web](https://github.com/Textualize/textual-web))! diff --git a/docs/roadmap.md b/docs/roadmap.md index 90e05d1e1d..c4b881bceb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -19,9 +19,8 @@ High-level features we plan on implementing. * [x] Monochrome mode * [ ] High contrast theme * [ ] Color-blind themes -- [ ] Command interface - * [ ] Command menu - * [ ] Fuzzy search +- [X] Command palette + * [X] Fuzzy search - [ ] Configuration (.toml based extensible configuration format) - [x] Console - [ ] Devtools diff --git a/docs/widgets/_template.md b/docs/widgets/_template.md index c4e83c06aa..ecedff151c 100644 --- a/docs/widgets/_template.md +++ b/docs/widgets/_template.md @@ -30,7 +30,7 @@ Example app showing the widget: ``` -## Reactive attributes +## Reactive Attributes ## Bindings diff --git a/docs/widgets/button.md b/docs/widgets/button.md index 290895d374..55a4120f9d 100644 --- a/docs/widgets/button.md +++ b/docs/widgets/button.md @@ -41,6 +41,14 @@ Clicking any of the non-disabled buttons in the example app below will result in - [Button.Pressed][textual.widgets.Button.Pressed] +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## Additional Notes - The spacing between the text and the edges of a button are _not_ due to padding. The default styling for a `Button` has the `height` set to 3 lines and a `min-width` of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with `border: none;`. diff --git a/docs/widgets/checkbox.md b/docs/widgets/checkbox.md index a8d6520c2f..0b227ba311 100644 --- a/docs/widgets/checkbox.md +++ b/docs/widgets/checkbox.md @@ -34,6 +34,10 @@ The example below shows check boxes in various states. | ------- | ------ | ------- | -------------------------- | | `value` | `bool` | `False` | The value of the checkbox. | +## Messages + +- [Checkbox.Changed][textual.widgets.Checkbox.Changed] + ## Bindings The checkbox widget defines the following bindings: @@ -45,17 +49,13 @@ The checkbox widget defines the following bindings: ## Component Classes -The checkbox widget provides the following component classes: +The checkbox widget inherits the following component classes: ::: textual.widgets._toggle_button.ToggleButton.COMPONENT_CLASSES options: show_root_heading: false show_root_toc_entry: false -## Messages - -- [Checkbox.Changed][textual.widgets.Checkbox.Changed] - --- diff --git a/docs/widgets/collapsible.md b/docs/widgets/collapsible.md index 6ff479582d..009f7f760b 100644 --- a/docs/widgets/collapsible.md +++ b/docs/widgets/collapsible.md @@ -120,12 +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. | +## Messages + +This widget posts no messages. + +## Bindings + +The collapsible widget defines the following binding on its title: + +::: textual.widgets._collapsible.CollapsibleTitle.BINDINGS + options: + show_root_heading: false + show_root_toc_entry: false + +## Component Classes + +This widget has no component classes. + ::: textual.widgets.Collapsible options: diff --git a/docs/widgets/content_switcher.md b/docs/widgets/content_switcher.md index dc8f06bf22..126213c94b 100644 --- a/docs/widgets/content_switcher.md +++ b/docs/widgets/content_switcher.md @@ -50,6 +50,18 @@ When the user presses the "Markdown" button the view is switched: | --------- | --------------- | ------- | ----------------------------------------------------------------------- | | `current` | `str` \| `None` | `None` | The ID of the currently-visible child. `None` means nothing is visible. | +## Messages + +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/digits.md b/docs/widgets/digits.md index 6dd33044ce..4fb919f762 100644 --- a/docs/widgets/digits.md +++ b/docs/widgets/digits.md @@ -44,15 +44,19 @@ Here's another example which uses `Digits` to display the current time: --8<-- "docs/examples/widgets/clock.py" ``` -## Reactive attributes +## Reactive Attributes This widget has no reactive attributes. +## Messages + +This widget posts no messages. + ## Bindings This widget has no bindings. -## Component classes +## Component Classes This widget has no component classes. diff --git a/docs/widgets/directory_tree.md b/docs/widgets/directory_tree.md index 56f1a00375..992a9fc127 100644 --- a/docs/widgets/directory_tree.md +++ b/docs/widgets/directory_tree.md @@ -34,10 +34,6 @@ and directories: --8<-- "docs/examples/widgets/directory_tree_filtered.py" ~~~ -## Messages - -- [DirectoryTree.FileSelected][textual.widgets.DirectoryTree.FileSelected] - ## Reactive Attributes | Name | Type | Default | Description | @@ -46,6 +42,14 @@ and directories: | `show_guides` | `bool` | `True` | Show guide lines between levels. | | `guide_depth` | `int` | `4` | Amount of indentation between parent and child. | +## Messages + +- [DirectoryTree.FileSelected][textual.widgets.DirectoryTree.FileSelected] + +## Bindings + +The directory tree widget inherits [the bindings from the tree widget][textual.widgets.Tree.BINDINGS]. + ## Component Classes The directory tree widget provides the following component classes: diff --git a/docs/widgets/footer.md b/docs/widgets/footer.md index 4affbe2191..fcb25cf836 100644 --- a/docs/widgets/footer.md +++ b/docs/widgets/footer.md @@ -30,7 +30,11 @@ widget. Notice how the `Footer` automatically displays the keybinding. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. ## Component Classes diff --git a/docs/widgets/header.md b/docs/widgets/header.md index c589ddcf00..1ffdf70dd1 100644 --- a/docs/widgets/header.md +++ b/docs/widgets/header.md @@ -45,7 +45,15 @@ This example shows how to set the text in the `Header` using `App.title` and `Ap ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/input.md b/docs/widgets/input.md index 455861a397..cd861a79b5 100644 --- a/docs/widgets/input.md +++ b/docs/widgets/input.md @@ -88,7 +88,7 @@ as seen for `Palindrome` in the example above. ## Bindings -The Input widget defines the following bindings: +The input widget defines the following bindings: ::: textual.widgets.Input.BINDINGS options: diff --git a/docs/widgets/label.md b/docs/widgets/label.md index ae1216d0a2..2a0c1819a7 100644 --- a/docs/widgets/label.md +++ b/docs/widgets/label.md @@ -28,7 +28,15 @@ This widget has no reactive attributes. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/list_item.md b/docs/widgets/list_item.md index 309079ea87..c4d306cb78 100644 --- a/docs/widgets/list_item.md +++ b/docs/widgets/list_item.md @@ -29,12 +29,17 @@ of multiple `ListItem`s. The arrow keys can be used to navigate the list. | ------------- | ------ | ------- | ------------------------------------ | | `highlighted` | `bool` | `False` | True if this ListItem is highlighted | +## Messages -#### Attributes +This widget posts no messages. -| attribute | type | purpose | -| --------- | ---------- | --------------------------- | -| `item` | `ListItem` | The item that was selected. | +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/list_view.md b/docs/widgets/list_view.md index cc403f2c8c..d5c85cdbc6 100644 --- a/docs/widgets/list_view.md +++ b/docs/widgets/list_view.md @@ -31,9 +31,9 @@ The example below shows an app with a simple `ListView`. ## Reactive Attributes -| Name | Type | Default | Description | -| ------- | ----- | ------- | ------------------------------- | -| `index` | `int` | `0` | The currently highlighted index | +| Name | Type | Default | Description | +| ------- | ----- | ------- | -------------------------------- | +| `index` | `int` | `0` | The currently highlighted index. | ## Messages @@ -49,6 +49,10 @@ The list view widget defines the following bindings: show_root_heading: false show_root_toc_entry: false +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/loading_indicator.md b/docs/widgets/loading_indicator.md index 1936115522..2a5235d12e 100644 --- a/docs/widgets/loading_indicator.md +++ b/docs/widgets/loading_indicator.md @@ -7,6 +7,23 @@ Displays pulsating dots to indicate when data is being loaded. - [ ] Focusable - [ ] Container +## Example + +Simple usage example: + +=== "Output" + + ```{.textual path="docs/examples/widgets/loading_indicator.py"} + ``` + +=== "loading_indicator.py" + + ```python + --8<-- "docs/examples/widgets/loading_indicator.py" + ``` + +## Changing Indicator Color + You can set the color of the loading indicator by setting its `color` style. Here's how you would do that with CSS: @@ -17,17 +34,22 @@ LoadingIndicator { } ``` +## Reactive Attributes -=== "Output" +This widget has no reactive attributes. - ```{.textual path="docs/examples/widgets/loading_indicator.py"} - ``` +## Messages -=== "loading_indicator.py" +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. - ```python - --8<-- "docs/examples/widgets/loading_indicator.py" - ``` --- diff --git a/docs/widgets/log.md b/docs/widgets/log.md index 04e54f0f00..72509313a6 100644 --- a/docs/widgets/log.md +++ b/docs/widgets/log.md @@ -37,10 +37,17 @@ The example below shows how to write text to a `Log` widget: | `max_lines` | `int` | `None` | Maximum number of lines in the log or `None` for no maximum. | | `auto_scroll` | `bool` | `False` | Scroll to end of log when new lines are added. | - ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/markdown.md b/docs/widgets/markdown.md index 6897c4c713..1382d1a5aa 100644 --- a/docs/widgets/markdown.md +++ b/docs/widgets/markdown.md @@ -27,12 +27,29 @@ The following example displays Markdown from a string. --8<-- "docs/examples/widgets/markdown.py" ~~~ +## Reactive Attributes + +This widget has no reactive attributes. + ## Messages - [Markdown.TableOfContentsUpdated][textual.widgets.Markdown.TableOfContentsUpdated] - [Markdown.TableOfContentsSelected][textual.widgets.Markdown.TableOfContentsSelected] - [Markdown.LinkClicked][textual.widgets.Markdown.LinkClicked] +## Bindings + +This widget has no bindings. + +## Component Classes + +The markdown widget provides the following component classes: + +::: textual.widgets.Markdown.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false + ## See Also diff --git a/docs/widgets/markdown_viewer.md b/docs/widgets/markdown_viewer.md index 6a4e3f47df..d830281fd4 100644 --- a/docs/widgets/markdown_viewer.md +++ b/docs/widgets/markdown_viewer.md @@ -33,6 +33,18 @@ The following example displays Markdown from a string and a Table of Contents. | ------------------------ | ---- | ------- | ----------------------------------------------------------------- | | `show_table_of_contents` | bool | True | Wether a Table of Contents should be displayed with the Markdown. | +## Messages + +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## See Also * [Markdown][textual.widgets.Markdown] code reference diff --git a/docs/widgets/placeholder.md b/docs/widgets/placeholder.md index c566b871dd..c8006d780a 100644 --- a/docs/widgets/placeholder.md +++ b/docs/widgets/placeholder.md @@ -41,7 +41,15 @@ The example below shows each placeholder variant. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/progress_bar.md b/docs/widgets/progress_bar.md index ab02516c98..ab927aa763 100644 --- a/docs/widgets/progress_bar.md +++ b/docs/widgets/progress_bar.md @@ -104,15 +104,6 @@ Refer to the [section below](#styling-the-progress-bar) for more information. --8<-- "docs/examples/widgets/progress_bar_styled.tcss" ``` -## Reactive Attributes - -| Name | Type | Default | Description | -| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- | -| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. | -| `progress` | `float` | `0` | The number of steps of progress already made. | -| `total` | `float | None` | The total number of steps that we are keeping track of. | - - ## Styling the Progress Bar The progress bar is composed of three sub-widgets that can be styled independently: @@ -130,8 +121,27 @@ The progress bar is composed of three sub-widgets that can be styled independent show_root_heading: false show_root_toc_entry: false ---- +## Reactive Attributes + +| Name | Type | Default | Description | +| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- | +| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. | +| `progress` | `float` | `0` | The number of steps of progress already made. | +| `total` | `float | None` | The total number of steps that we are keeping track of. | + +## Messages +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + +--- ::: textual.widgets.ProgressBar options: diff --git a/docs/widgets/radiobutton.md b/docs/widgets/radiobutton.md index 36df3a3c0a..8161ceaf17 100644 --- a/docs/widgets/radiobutton.md +++ b/docs/widgets/radiobutton.md @@ -36,6 +36,10 @@ The example below shows radio buttons, used within a [`RadioSet`](./radioset.md) | ------- | ------ | ------- | ------------------------------ | | `value` | `bool` | `False` | The value of the radio button. | +## Messages + +- [RadioButton.Changed][textual.widgets.RadioButton.Changed] + ## Bindings The radio button widget defines the following bindings: @@ -47,17 +51,13 @@ The radio button widget defines the following bindings: ## Component Classes -The radio button widget provides the following component classes: +The checkbox widget inherits the following component classes: ::: textual.widgets._toggle_button.ToggleButton.COMPONENT_CLASSES options: show_root_heading: false show_root_toc_entry: false -## Messages - -- [RadioButton.Changed][textual.widgets.RadioButton.Changed] - ## See Also - [RadioSet](./radioset.md) diff --git a/docs/widgets/radioset.md b/docs/widgets/radioset.md index e51e56b784..f947dbc2ff 100644 --- a/docs/widgets/radioset.md +++ b/docs/widgets/radioset.md @@ -9,6 +9,8 @@ A container widget that groups [`RadioButton`](./radiobutton.md)s together. ## Example +### Simple example + The example below shows two radio sets, one built using a collection of [radio buttons](./radiobutton.md), the other a collection of simple strings. @@ -29,11 +31,7 @@ The example below shows two radio sets, one built using a collection of --8<-- "docs/examples/widgets/radio_set.tcss" ``` -## Messages - -- [RadioSet.Changed][textual.widgets.RadioSet.Changed] - -#### Example +### Reacting to Changes in a Radio Set Here is an example of using the message to react to changes in a `RadioSet`: @@ -54,6 +52,18 @@ Here is an example of using the message to react to changes in a `RadioSet`: --8<-- "docs/examples/widgets/radio_set_changed.tcss" ``` +## Messages + +- [RadioSet.Changed][textual.widgets.RadioSet.Changed] + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## See Also diff --git a/docs/widgets/rich_log.md b/docs/widgets/rich_log.md index 2778db7ea3..5f373218fd 100644 --- a/docs/widgets/rich_log.md +++ b/docs/widgets/rich_log.md @@ -42,6 +42,14 @@ The example below shows an application showing a `RichLog` with different kinds This widget sends no messages. +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/rule.md b/docs/widgets/rule.md index bc7a2ec1de..5740b42376 100644 --- a/docs/widgets/rule.md +++ b/docs/widgets/rule.md @@ -62,6 +62,14 @@ The example below shows vertical rules with all the available line styles. This widget sends no messages. +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/select.md b/docs/widgets/select.md index 7687e2e584..6f9690cb24 100644 --- a/docs/widgets/select.md +++ b/docs/widgets/select.md @@ -58,12 +58,8 @@ The following example presents a `Select` with a number of options. --8<-- "docs/examples/widgets/select.tcss" ``` -## Messages - -- [Select.Changed][textual.widgets.Select.Changed] - -## Reactive attributes +## Reactive Attributes | Name | Type | Default | Description | @@ -71,6 +67,9 @@ The following example presents a `Select` with a number of options. | `expanded` | `bool` | `False` | True to expand the options overlay. | | `value` | `SelectType` \| `None` | `None` | Current value of the Select. | +## Messages + +- [Select.Changed][textual.widgets.Select.Changed] ## Bindings @@ -81,6 +80,9 @@ The Select widget defines the following bindings: show_root_heading: false show_root_toc_entry: false +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/sparkline.md b/docs/widgets/sparkline.md index 98790f9c65..454e674c42 100644 --- a/docs/widgets/sparkline.md +++ b/docs/widgets/sparkline.md @@ -102,7 +102,15 @@ The example below shows how to use component classes to change the colors of the ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/static.md b/docs/widgets/static.md index 561f053431..9df032994b 100644 --- a/docs/widgets/static.md +++ b/docs/widgets/static.md @@ -27,7 +27,15 @@ This widget has no reactive attributes. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. ## See Also diff --git a/docs/widgets/switch.md b/docs/widgets/switch.md index 4cd8b61825..1482c08a8d 100644 --- a/docs/widgets/switch.md +++ b/docs/widgets/switch.md @@ -32,6 +32,10 @@ The example below shows switches in various states. | ------- | ------ | ------- | ------------------------ | | `value` | `bool` | `False` | The value of the switch. | +## Messages + +- [Switch.Changed][textual.widgets.Switch.Changed] + ## Bindings The switch widget defines the following bindings: @@ -50,10 +54,6 @@ The switch widget provides the following component classes: show_root_heading: false show_root_toc_entry: false -## Messages - -- [Switch.Changed][textual.widgets.Switch.Changed] - ## Additional Notes - To remove the spacing around a `Switch`, set `border: none;` and `padding: 0;`. diff --git a/docs/widgets/tabbed_content.md b/docs/widgets/tabbed_content.md index 7a61318dfc..f121e314e8 100644 --- a/docs/widgets/tabbed_content.md +++ b/docs/widgets/tabbed_content.md @@ -94,7 +94,7 @@ The following example contains a `TabbedContent` with three tabs. --8<-- "docs/examples/widgets/tabbed_content.py" ``` -## Reactive attributes +## Reactive Attributes | Name | Type | Default | Description | | -------- | ----- | ------- | -------------------------------------------------------------- | @@ -105,6 +105,14 @@ The following example contains a `TabbedContent` with three tabs. - [TabbedContent.TabActivated][textual.widgets.TabbedContent.TabActivated] +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## See also diff --git a/docs/widgets/tabs.md b/docs/widgets/tabs.md index b7d7130d74..a076fb715b 100644 --- a/docs/widgets/tabs.md +++ b/docs/widgets/tabs.md @@ -73,6 +73,9 @@ The Tabs widget defines the following bindings: show_root_heading: false show_root_toc_entry: false +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/toast.md b/docs/widgets/toast.md index 647f730369..9f54c0f47b 100644 --- a/docs/widgets/toast.md +++ b/docs/widgets/toast.md @@ -71,6 +71,27 @@ Toast.-information .toast--title { --8<-- "docs/examples/widgets/toast.py" ``` +## Reactive Attributes + +This widget has no reactive attributes. + +## Messages + +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +The toast widget provides the following component classes: + +::: textual.widgets._toast.Toast.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false + --- ::: textual.widgets._toast diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 3e7c060583..66e3f2480b 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -1,6 +1,6 @@ nav: + - "index.md" - Introduction: - - "index.md" - "getting_started.md" - "help.md" - "tutorial.md" diff --git a/poetry.lock b/poetry.lock index 85f0779436..1d1ce00cba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# 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.4.0 and should not be changed by hand. [[package]] name = "aiohttp" version = "3.8.5" description = "Async http client/server framework (asyncio)" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -114,6 +115,7 @@ speedups = ["Brotli", "aiodns", "cchardet"] name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -128,6 +130,7 @@ frozenlist = ">=1.1.0" name = "anyio" version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -150,6 +153,7 @@ trio = ["trio (<0.22)"] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -164,6 +168,7 @@ typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} name = "asynctest" version = "0.13.0" description = "Enhance the standard unittest package with features for testing asyncio libraries" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -175,6 +180,7 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -196,6 +202,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "babel" version = "2.12.1" description = "Internationalization utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -210,6 +217,7 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} name = "black" version = "23.3.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -260,6 +268,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "cached-property" version = "1.5.2" description = "A decorator for caching properties in classes." +category = "dev" optional = false python-versions = "*" files = [ @@ -271,6 +280,7 @@ files = [ name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -282,6 +292,7 @@ files = [ name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -293,6 +304,7 @@ files = [ name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -377,6 +389,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -392,6 +405,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -403,6 +417,7 @@ files = [ name = "colored" version = "1.4.4" description = "Simple library for color and formatting to terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -413,6 +428,7 @@ files = [ name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -485,6 +501,7 @@ toml = ["tomli"] name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -496,6 +513,7 @@ files = [ name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -510,6 +528,7 @@ test = ["pytest (>=6)"] name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -525,6 +544,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "frozenlist" version = "1.3.3" description = "A list-like structure which implements collections.abc.MutableSequence" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -608,6 +628,7 @@ files = [ name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." +category = "dev" optional = false python-versions = "*" files = [ @@ -625,6 +646,7 @@ dev = ["flake8", "markdown", "twine", "wheel"] name = "gitdb" version = "4.0.10" description = "Git Object Database" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -637,23 +659,28 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.34" +version = "3.1.36" description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.34-py3-none-any.whl", hash = "sha256:5d3802b98a3bae1c2b8ae0e1ff2e4aa16bcdf02c145da34d092324f599f01395"}, - {file = "GitPython-3.1.34.tar.gz", hash = "sha256:85f7d365d1f6bf677ae51039c1ef67ca59091c7ebd5a3509aa399d4eda02d6dd"}, + {file = "GitPython-3.1.36-py3-none-any.whl", hash = "sha256:8d22b5cfefd17c79914226982bb7851d6ade47545b1735a9d010a2a4c26d8388"}, + {file = "GitPython-3.1.36.tar.gz", hash = "sha256:4bb0c2a6995e85064140d31a33289aa5dce80133a23d36fcd372d716c54d3ebf"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar", "virtualenv"] + [[package]] name = "griffe" version = "0.30.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -669,6 +696,7 @@ colorama = ">=0.4" name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -683,6 +711,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} name = "httpcore" version = "0.16.3" description = "A minimal low-level HTTP client." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -694,16 +723,17 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" +sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" version = "0.23.3" description = "The next generation HTTP client." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -719,14 +749,15 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" version = "2.5.24" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -741,6 +772,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -752,6 +784,7 @@ files = [ name = "importlib-metadata" version = "6.7.0" description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -772,6 +805,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -783,6 +817,7 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -800,6 +835,7 @@ i18n = ["Babel (>=2.7)"] name = "linkify-it-py" version = "2.0.2" description = "Links recognition library with FULL unicode support." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -820,6 +856,7 @@ test = ["coverage", "pytest", "pytest-cov"] name = "markdown" version = "3.4.4" description = "Python implementation of John Gruber's Markdown." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -838,6 +875,7 @@ testing = ["coverage", "pyyaml"] name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -865,6 +903,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -924,6 +963,7 @@ files = [ name = "mdit-py-plugins" version = "0.3.5" description = "Collection of plugins for markdown-it-py" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -943,6 +983,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -954,6 +995,7 @@ files = [ name = "mergedeep" version = "1.3.4" description = "A deep merge function for ๐Ÿ." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -965,6 +1007,7 @@ files = [ name = "mkdocs" version = "1.5.2" description = "Project documentation with Markdown." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -997,6 +1040,7 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp name = "mkdocs-autorefs" version = "0.4.1" description = "Automatically link across pages in MkDocs." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1012,6 +1056,7 @@ mkdocs = ">=1.1" name = "mkdocs-exclude" version = "1.0.2" description = "A mkdocs plugin that lets you exclude files or trees." +category = "dev" optional = false python-versions = "*" files = [ @@ -1025,6 +1070,7 @@ mkdocs = "*" name = "mkdocs-material" version = "9.2.7" description = "Documentation that simply works" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1049,6 +1095,7 @@ requests = ">=2.26,<3.0" name = "mkdocs-material-extensions" version = "1.1.1" description = "Extension pack for Python Markdown and MkDocs Material." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1060,6 +1107,7 @@ files = [ name = "mkdocs-rss-plugin" version = "1.5.0" description = "MkDocs plugin which generates a static RSS feed using git log and page.meta." +category = "dev" optional = false python-versions = ">=3.7, <4" files = [ @@ -1070,17 +1118,18 @@ files = [ [package.dependencies] GitPython = ">=3.1,<3.2" mkdocs = ">=1.1,<2" -pytz = {version = "==2022.*", markers = "python_version < \"3.9\""} -tzdata = {version = "==2022.*", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} +pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""} +tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""} [package.extras] -dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (==4.0.*)", "validator-collection (>=1.5,<1.6)"] -doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (==0.5.*)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] +dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"] +doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"] [[package]] name = "mkdocstrings" version = "0.20.0" description = "Automatic documentation from sources, for MkDocs." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1106,6 +1155,7 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] name = "mkdocstrings-python" version = "0.10.1" description = "A Python handler for mkdocstrings." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1121,6 +1171,7 @@ mkdocstrings = ">=0.20" name = "msgpack" version = "1.0.5" description = "MessagePack serializer" +category = "dev" optional = false python-versions = "*" files = [ @@ -1193,6 +1244,7 @@ files = [ name = "multidict" version = "6.0.4" description = "multidict implementation" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1276,6 +1328,7 @@ files = [ name = "mypy" version = "1.4.1" description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1323,6 +1376,7 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1334,6 +1388,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1348,6 +1403,7 @@ setuptools = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1359,6 +1415,7 @@ files = [ name = "paginate" version = "0.5.6" description = "Divides large result sets into pages for easier browsing" +category = "dev" optional = false python-versions = "*" files = [ @@ -1369,6 +1426,7 @@ files = [ name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1380,6 +1438,7 @@ files = [ name = "platformdirs" version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1398,6 +1457,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1416,6 +1476,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1435,6 +1496,7 @@ virtualenv = ">=20.10.0" name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1449,6 +1511,7 @@ plugins = ["importlib-metadata"] name = "pymdown-extensions" version = "10.2.1" description = "Extension pack for Python Markdown." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1465,13 +1528,14 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "7.4.1" +version = "7.4.2" description = "pytest: simple powerful testing with Python" +category = "dev" 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.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] @@ -1488,13 +1552,14 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-aiohttp" -version = "1.0.4" +version = "1.0.5" description = "Pytest plugin for aiohttp support" +category = "dev" 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"}, + {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"}, + {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"}, ] [package.dependencies] @@ -1509,6 +1574,7 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"] name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1528,6 +1594,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1547,6 +1614,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-textual-snapshot" version = "0.4.0" description = "Snapshot testing for Textual apps" +category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -1565,6 +1633,7 @@ textual = ">=0.28.0" name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1579,6 +1648,7 @@ six = ">=1.5" name = "pytz" version = "2022.7.1" description = "World timezone definitions, modern and historical" +category = "dev" optional = false python-versions = "*" files = [ @@ -1590,6 +1660,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1639,6 +1710,7 @@ files = [ name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1653,6 +1725,7 @@ pyyaml = "*" name = "regex" version = "2022.10.31" description = "Alternative regular expression module, to replace re." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1750,6 +1823,7 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1771,6 +1845,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" +category = "dev" optional = false python-versions = "*" files = [ @@ -1786,13 +1861,14 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.5.2" +version = "13.5.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" optional = false python-versions = ">=3.7.0" 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.5.3-py3-none-any.whl", hash = "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9"}, + {file = "rich-13.5.3.tar.gz", hash = "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6"}, ] [package.dependencies] @@ -1807,6 +1883,7 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "setuptools" version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1823,6 +1900,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1832,19 +1910,21 @@ files = [ [[package]] name = "smmap" -version = "5.0.0" +version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" 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]] name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1856,6 +1936,7 @@ files = [ name = "syrupy" version = "3.0.6" description = "Pytest Snapshot Test Utility" +category = "dev" optional = false python-versions = ">=3.7,<4" files = [ @@ -1871,6 +1952,7 @@ pytest = ">=5.1.0,<8.0.0" name = "textual-dev" version = "1.1.0" description = "Development tools for working with Textual" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -1889,6 +1971,7 @@ typing-extensions = ">=4.4.0,<5.0.0" name = "time-machine" version = "2.10.0" description = "Travel through time in your tests." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1955,6 +2038,7 @@ python-dateutil = "*" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1966,6 +2050,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1977,6 +2062,7 @@ files = [ name = "typed-ast" version = "1.5.5" description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2027,6 +2113,7 @@ files = [ name = "types-setuptools" version = "67.8.0.0" description = "Typing stubs for setuptools" +category = "dev" optional = false python-versions = "*" files = [ @@ -2038,6 +2125,7 @@ files = [ name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2049,6 +2137,7 @@ files = [ name = "tzdata" version = "2022.7" description = "Provider of IANA time zone data" +category = "dev" optional = false python-versions = ">=2" files = [ @@ -2060,6 +2149,7 @@ files = [ name = "uc-micro-py" version = "1.0.2" description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2074,6 +2164,7 @@ test = ["coverage", "pytest", "pytest-cov"] name = "urllib3" version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2089,13 +2180,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.4" +version = "20.24.5" description = "Virtual Python Environment builder" +category = "dev" 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.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, ] [package.dependencies] @@ -2112,6 +2204,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "watchdog" version = "3.0.0" description = "Filesystem events monitoring" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2151,6 +2244,7 @@ watchmedo = ["PyYAML (>=3.10)"] name = "yarl" version = "1.9.2" description = "Yet another URL library" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2239,6 +2333,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.7" files = [ 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/app.py b/src/textual/app.py index e89a74cc80..8ebf04c34b 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -329,7 +329,7 @@ class MyApp(App[None]): """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). + """Command providers used by the [command palette](/guide/command_palette). Should be a set of [command.Provider][textual.command.Provider] classes. """ @@ -1198,6 +1198,10 @@ async def run_test( ) -> AsyncGenerator[Pilot, None]: """An asynchronous context manager for testing apps. + !!! 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: diff --git a/src/textual/dom.py b/src/textual/dom.py index a65b8beeea..c9a04de072 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -399,9 +399,12 @@ def _post_register(self, app: App) -> None: """ def __rich_repr__(self) -> rich.repr.Result: - yield "name", self._name, None - yield "id", self._id, None - if self._classes: + # Being a bit defensive here to guard against errors when calling repr before initialization + if hasattr(self, "_name"): + yield "name", self._name, None + if hasattr(self, "_id"): + yield "id", self._id, None + if hasattr(self, "_classes") and self._classes: yield "classes", " ".join(self._classes) def _get_default_css(self) -> list[tuple[str, str, int]]: diff --git a/src/textual/errors.py b/src/textual/errors.py index 021bcff0fa..034139e204 100644 --- a/src/textual/errors.py +++ b/src/textual/errors.py @@ -1,3 +1,8 @@ +""" +General exception classes. + +""" + from __future__ import annotations diff --git a/src/textual/filter.py b/src/textual/filter.py index 65378818eb..7494d9a52a 100644 --- a/src/textual/filter.py +++ b/src/textual/filter.py @@ -1,3 +1,16 @@ +"""Filter classes. + +!!! note + + Filters are used internally, and not recommended for use by Textual app developers. + +Filters are used internally to process terminal output after it has been rendered. +Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set. + +In the future, this system will be used to implement accessibility features. + +""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/textual/fuzzy.py b/src/textual/fuzzy.py index 3fa4b0094f..f2c46259d2 100644 --- a/src/textual/fuzzy.py +++ b/src/textual/fuzzy.py @@ -1,3 +1,10 @@ +""" +Fuzzy matcher. + +This class is used by the [command palette](guide/command_palette) to match search terms. + +""" + from __future__ import annotations from re import IGNORECASE, compile, escape diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7ed468dca2..3d49080a6a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -1,6 +1,11 @@ """ -A message pump is a base class for any object which processes messages, which includes Widget, Screen, and App. +A `MessagePump` is a base class for any object which processes messages, which includes Widget, Screen, and App. + +!!! tip + + Most of the method here are useful in general app development. + """ from __future__ import annotations diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 685186d5eb..c3c64d2e9a 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -1,6 +1,9 @@ """ The pilot object is used by [App.run_test][textual.app.App.run_test] to programmatically operate an app. + +See the guide on how to [test Textual apps](/guide/testing). + """ from __future__ import annotations @@ -42,7 +45,10 @@ def _get_mouse_message_arguments( class WaitForScreenTimeout(Exception): - pass + """Exception raised if messages aren't being processed quickly enough. + + If this occurs, the most likely explanation is some kind of deadlock in the app code. + """ @rich.repr.auto(angular=True) diff --git a/src/textual/screen.py b/src/textual/screen.py index d0bb45cf0c..b93e9dc46e 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -158,7 +158,7 @@ class Screen(Generic[ScreenResultType], Widget): """Screen title to override [the app title][textual.app.App.title].""" COMMANDS: ClassVar[set[type[Provider]]] = set() - """Command providers used by the [command palette](/guide/command), associated with the screen. + """Command providers used by the [command palette](/guide/command_palette), associated with the screen. Should be a set of [`command.Provider`][textual.command.Provider] classes. """ diff --git a/src/textual/suggester.py b/src/textual/suggester.py index 362fe89f6d..505993b43a 100644 --- a/src/textual/suggester.py +++ b/src/textual/suggester.py @@ -1,3 +1,9 @@ +""" + +The `Suggester` class is used by the [Input](/widgets/input) widget. + +""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/textual/types.py b/src/textual/types.py index b768c424c4..024d388f24 100644 --- a/src/textual/types.py +++ b/src/textual/types.py @@ -9,6 +9,7 @@ CallbackType, IgnoreReturnCallbackType, MessageTarget, + UnusedParameter, WatchCallbackType, ) from .actions import ActionParseResult @@ -29,5 +30,6 @@ "MessageTarget", "NoActiveAppError", "RenderStyles", + "UnusedParameter", "WatchCallbackType", ] diff --git a/src/textual/widgets/_collapsible.py b/src/textual/widgets/_collapsible.py index d673f3de50..5901cbc9de 100644 --- a/src/textual/widgets/_collapsible.py +++ b/src/textual/widgets/_collapsible.py @@ -37,6 +37,11 @@ class CollapsibleTitle(Widget, can_focus=True): """ BINDINGS = [Binding("enter", "toggle", "Toggle collapsible", show=False)] + """ + | Key(s) | Description | + | :- | :- | + | enter | Toggle the collapsible. | + """ collapsed = reactive(True) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 8df64e29d9..f9a328e9c3 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -33,7 +33,7 @@ from ..widget import PseudoClasses CellCacheKey: TypeAlias = ( - "tuple[RowKey, ColumnKey, Style, bool, bool, int, PseudoClasses]" + "tuple[RowKey, ColumnKey, Style, bool, bool, bool, int, PseudoClasses]" ) LineCacheKey: TypeAlias = "tuple[int, int, int, int, Coordinate, Coordinate, Style, CursorType, bool, int, PseudoClasses]" RowCacheKey: TypeAlias = "tuple[RowKey, int, Style, Coordinate, Coordinate, CursorType, bool, bool, int, PseudoClasses]" @@ -187,6 +187,7 @@ class Row: key: RowKey height: int label: Text | None = None + auto_height: bool = False class RowRenderables(NamedTuple): @@ -954,6 +955,7 @@ def _clear_caches(self) -> None: self._styles_cache.clear() self._offset_cache.clear() self._ordered_row_cache.clear() + self._get_styles_to_render_cell.cache_clear() def get_row_height(self, row_key: RowKey) -> int: """Given a row key, return the height of that row in terminal cells. @@ -968,7 +970,7 @@ def get_row_height(self, row_key: RowKey) -> int: return self.header_height return self.rows[row_key].height - async def _on_styles_updated(self) -> None: + def notify_style_update(self) -> None: self._clear_caches() self.refresh() @@ -1193,8 +1195,16 @@ def _update_column_widths(self, updated_cells: set[CellKey]) -> None: self._require_update_dimensions = True def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None: - """Called to recalculate the virtual (scrollable) size.""" + """Called to recalculate the virtual (scrollable) size. + + This recomputes column widths and then checks if any of the new rows need + to have their height computed. + + Args: + new_rows: The new rows that will affect the `DataTable` dimensions. + """ console = self.app.console + auto_height_rows: list[tuple[int, Row, list[RenderableType]]] = [] for row_key in new_rows: row_index = self._row_locations.get(row_key) @@ -1204,6 +1214,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None: continue row = self.rows.get(row_key) + assert row is not None if row.label is not None: self._labelled_row_exists = True @@ -1218,7 +1229,65 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None: content_width = measure(console, renderable, 1) column.content_width = max(column.content_width, content_width) - self._clear_caches() + if row.auto_height: + auto_height_rows.append((row_index, row, cells_in_row)) + + # If there are rows that need to have their height computed, render them correctly + # so that we can cache this rendering for later. + if auto_height_rows: + render_cell = self._render_cell # This method renders & caches. + should_highlight = self._should_highlight + cursor_type = self.cursor_type + cursor_location = self.cursor_coordinate + hover_location = self.hover_coordinate + base_style = self.rich_style + fixed_style = self.get_component_styles( + "datatable--fixed" + ).rich_style + Style.from_meta({"fixed": True}) + ordered_columns = self.ordered_columns + fixed_columns = self.fixed_columns + + for row_index, row, cells_in_row in auto_height_rows: + height = 0 + row_style = self._get_row_style(row_index, base_style) + + # As we go through the cells, save their rendering, height, and + # column width. After we compute the height of the row, go over the cells + # that were rendered with the wrong height and append the missing padding. + rendered_cells: list[tuple[SegmentLines, int, int]] = [] + for column_index, column in enumerate(ordered_columns): + style = fixed_style if column_index < fixed_columns else row_style + cell_location = Coordinate(row_index, column_index) + rendered_cell = render_cell( + row_index, + column_index, + style, + column.render_width, + cursor=should_highlight( + cursor_location, cell_location, cursor_type + ), + hover=should_highlight( + hover_location, cell_location, cursor_type + ), + ) + cell_height = len(rendered_cell) + rendered_cells.append( + (rendered_cell, cell_height, column.render_width) + ) + height = max(height, cell_height) + + row.height = height + # Do surgery on the cache for cells that were rendered with the incorrect + # height during the first pass. + for cell_renderable, cell_height, column_width in rendered_cells: + if cell_height < height: + first_line_space_style = cell_renderable[0][0].style + cell_renderable.extend( + [ + [Segment(" " * column_width, first_line_space_style)] + for _ in range(height - cell_height) + ] + ) data_cells_width = sum(column.render_width for column in self.columns.values()) total_width = data_cells_width + self._row_label_column_width @@ -1376,7 +1445,7 @@ def add_column( def add_row( self, *cells: CellType, - height: int = 1, + height: int | None = 1, key: str | None = None, label: TextType | None = None, ) -> RowKey: @@ -1384,13 +1453,14 @@ def add_row( Args: *cells: Positional arguments should contain cell data. - height: The height of a row (in lines). + height: The height of a row (in lines). Use `None` to auto-detect the optimal + height. key: A key which uniquely identifies this row. If None, it will be generated for you and returned. label: The label for the row. Will be displayed to the left if supplied. Returns: - Uniquely identifies this row. Can be used to retrieve this row regardless + Unique identifier for this row. Can be used to retrieve this row regardless of its current location in the DataTable (it could have moved after being added due to sorting or insertion/deletion of other rows). """ @@ -1410,7 +1480,15 @@ def add_row( for column, cell in zip_longest(self.ordered_columns, cells) } label = Text.from_markup(label) if isinstance(label, str) else label - self.rows[row_key] = Row(row_key, height, label) + # Rows with auto-height get a height of 0 because 1) we need an integer height + # to do some intermediate computations and 2) because 0 doesn't impact the data + # table while we don't figure out how tall this row is. + self.rows[row_key] = Row( + row_key, + height or 0, + label, + height is None, + ) self._new_rows.add(row_key) self._require_update_dimensions = True self.cursor_coordinate = self.cursor_coordinate @@ -1549,7 +1627,8 @@ async def _on_idle(self, _: events.Idle) -> None: if self._require_update_dimensions: # Add the new rows *before* updating the column widths, since - # cells in a new row may influence the final width of a column + # cells in a new row may influence the final width of a column. + # Only then can we compute optimal height of rows with "auto" height. self._require_update_dimensions = False new_rows = self._new_rows.copy() self._new_rows.clear() @@ -1757,7 +1836,7 @@ def _render_cell( row_key = self._row_locations.get_key(row_index) column_key = self._column_locations.get_key(column_index) - cell_cache_key = ( + cell_cache_key: CellCacheKey = ( row_key, column_key, base_style, @@ -1770,7 +1849,6 @@ def _render_cell( if cell_cache_key not in self._cell_render_cache: base_style += Style.from_meta({"row": row_index, "column": column_index}) - height = self.header_height if is_header_cell else self.rows[row_key].height row_label, row_cells = self._get_row_renderables(row_index) if is_row_label_cell: @@ -1778,50 +1856,104 @@ def _render_cell( else: cell = row_cells[column_index] - get_component = self.get_component_rich_style - show_cursor = self.show_cursor - component_style = Style() - - if hover and show_cursor and self._show_hover_cursor: - component_style += get_component("datatable--hover") - if is_header_cell or is_row_label_cell: - # Apply subtle variation in style for the header/label (blue background by - # default) rows and columns affected by the cursor, to ensure we can - # still differentiate between the labels and the data. - component_style += get_component("datatable--header-hover") - - if cursor and show_cursor: - cursor_style = get_component("datatable--cursor") - component_style += cursor_style - if is_header_cell or is_row_label_cell: - component_style += get_component("datatable--header-cursor") - elif is_fixed_style_cell: - component_style += get_component("datatable--fixed-cursor") - - post_foreground = ( - Style.from_color(color=component_style.color) - if self.cursor_foreground_priority == "css" - else Style.null() - ) - post_background = ( - Style.from_color(bgcolor=component_style.bgcolor) - if self.cursor_background_priority == "css" - else Style.null() + component_style, post_style = self._get_styles_to_render_cell( + is_header_cell, + is_row_label_cell, + is_fixed_style_cell, + hover, + cursor, + self.show_cursor, + self._show_hover_cursor, + self.cursor_foreground_priority == "css", + self.cursor_background_priority == "css", ) + if is_header_cell: + options = self.app.console.options.update_dimensions( + width, self.header_height + ) + else: + row = self.rows[row_key] + # If an auto-height row hasn't had its height calculated, we don't fix + # the value for `height` so that we can measure the height of the cell. + if row.auto_height and row.height == 0: + options = self.app.console.options.update_width(width) + else: + options = self.app.console.options.update_dimensions( + width, row.height + ) lines = self.app.console.render_lines( Styled( Padding(cell, (0, 1)), pre_style=base_style + component_style, - post_style=post_foreground + post_background, + post_style=post_style, ), - self.app.console.options.update_dimensions(width, height), + options, ) self._cell_render_cache[cell_cache_key] = lines return self._cell_render_cache[cell_cache_key] + @functools.lru_cache(maxsize=32) + def _get_styles_to_render_cell( + self, + is_header_cell: bool, + is_row_label_cell: bool, + is_fixed_style_cell: bool, + hover: bool, + cursor: bool, + show_cursor: bool, + show_hover_cursor: bool, + has_css_foreground_priority: bool, + has_css_background_priority: bool, + ) -> tuple[Style, Style]: + """Auxiliary method to compute styles used to render a given cell. + + Args: + is_header_cell: Is this a cell from a header? + is_row_label_cell: Is this the label of any given row? + is_fixed_style_cell: Should this cell be styled like a fixed cell? + hover: Does this cell have the hover pseudo class? + cursor: Is this cell covered by the cursor? + show_cursor: Do we want to show the cursor in the data table? + show_hover_cursor: Do we want to show the mouse hover when using the keyboard + to move the cursor? + has_css_foreground_priority: `self.cursor_foreground_priority == "css"`? + has_css_background_priority: `self.cursor_background_priority == "css"`? + """ + get_component = self.get_component_rich_style + component_style = Style() + + if hover and show_cursor and show_hover_cursor: + component_style += get_component("datatable--hover") + if is_header_cell or is_row_label_cell: + # Apply subtle variation in style for the header/label (blue background by + # default) rows and columns affected by the cursor, to ensure we can + # still differentiate between the labels and the data. + component_style += get_component("datatable--header-hover") + + if cursor and show_cursor: + cursor_style = get_component("datatable--cursor") + component_style += cursor_style + if is_header_cell or is_row_label_cell: + component_style += get_component("datatable--header-cursor") + elif is_fixed_style_cell: + component_style += get_component("datatable--fixed-cursor") + + post_foreground = ( + Style.from_color(color=component_style.color) + if has_css_foreground_priority + else Style.null() + ) + post_background = ( + Style.from_color(bgcolor=component_style.bgcolor) + if has_css_background_priority + else Style.null() + ) + + return component_style, post_foreground + post_background + def _render_line_in_row( self, row_key: RowKey, @@ -1862,29 +1994,9 @@ def _render_line_in_row( if cache_key in self._row_render_cache: return self._row_render_cache[cache_key] - def _should_highlight( - cursor: Coordinate, - target_cell: Coordinate, - type_of_cursor: CursorType, - ) -> bool: - """Determine whether we should highlight a cell given the location - of the cursor, the location of the cell, and the type of cursor that - is currently active.""" - if type_of_cursor == "cell": - return cursor == target_cell - elif type_of_cursor == "row": - cursor_row, _ = cursor - cell_row, _ = target_cell - return cursor_row == cell_row - elif type_of_cursor == "column": - _, cursor_column = cursor - _, cell_column = target_cell - return cursor_column == cell_column - else: - return False - - is_header_row = row_key is self._header_row_key + should_highlight = self._should_highlight render_cell = self._render_cell + header_style = self.get_component_styles("datatable--header").rich_style if row_key in self._row_locations: row_index = self._row_locations.get(row_key) @@ -1893,7 +2005,6 @@ def _should_highlight( # If the row has a label, add it to fixed_row here with correct style. fixed_row = [] - header_style = self.get_component_styles("datatable--header").rich_style if self._labelled_row_exists and self.show_row_labels: # The width of the row label is updated again on idle @@ -1903,14 +2014,17 @@ def _should_highlight( -1, header_style, width=self._row_label_column_width, - cursor=_should_highlight(cursor_location, cell_location, cursor_type), - hover=_should_highlight(hover_location, cell_location, cursor_type), + cursor=should_highlight(cursor_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), )[line_no] fixed_row.append(label_cell_lines) if self.fixed_columns: - fixed_style = self.get_component_styles("datatable--fixed").rich_style - fixed_style += Style.from_meta({"fixed": True}) + if row_key is self._header_row_key: + fixed_style = header_style # We use the header style either way. + else: + fixed_style = self.get_component_styles("datatable--fixed").rich_style + fixed_style += Style.from_meta({"fixed": True}) for column_index, column in enumerate( self.ordered_columns[: self.fixed_columns] ): @@ -1918,28 +2032,16 @@ def _should_highlight( fixed_cell_lines = render_cell( row_index, column_index, - header_style if is_header_row else fixed_style, + fixed_style, column.render_width, - cursor=_should_highlight( + cursor=should_highlight( cursor_location, cell_location, cursor_type ), - hover=_should_highlight(hover_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), )[line_no] fixed_row.append(fixed_cell_lines) - is_header_row = row_key is self._header_row_key - if is_header_row: - row_style = self.get_component_styles("datatable--header").rich_style - elif row_index < self.fixed_rows: - row_style = self.get_component_styles("datatable--fixed").rich_style - else: - if self.zebra_stripes: - component_row_style = ( - "datatable--odd-row" if row_index % 2 else "datatable--even-row" - ) - row_style = self.get_component_styles(component_row_style).rich_style - else: - row_style = base_style + row_style = self._get_row_style(row_index, base_style) scrollable_row = [] for column_index, column in enumerate(self.ordered_columns): @@ -1949,8 +2051,8 @@ def _should_highlight( column_index, row_style, column.render_width, - cursor=_should_highlight(cursor_location, cell_location, cursor_type), - hover=_should_highlight(hover_location, cell_location, cursor_type), + cursor=should_highlight(cursor_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), )[line_no] scrollable_row.append(cell_lines) @@ -2078,6 +2180,63 @@ def render_line(self, y: int) -> Strip: return self._render_line(y, scroll_x, scroll_x + width, self.rich_style) + def _should_highlight( + self, + cursor: Coordinate, + target_cell: Coordinate, + type_of_cursor: CursorType, + ) -> bool: + """Determine if the given cell should be highlighted because of the cursor. + + This auxiliary method takes the cursor position and type into account when + determining whether the cell should be highlighted. + + Args: + cursor: The current position of the cursor. + target_cell: The cell we're checking for the need to highlight. + type_of_cursor: The type of cursor that is currently active. + + Returns: + Whether or not the given cell should be highlighted. + """ + if type_of_cursor == "cell": + return cursor == target_cell + elif type_of_cursor == "row": + cursor_row, _ = cursor + cell_row, _ = target_cell + return cursor_row == cell_row + elif type_of_cursor == "column": + _, cursor_column = cursor + _, cell_column = target_cell + return cursor_column == cell_column + else: + return False + + def _get_row_style(self, row_index: int, base_style: Style) -> Style: + """Gets the Style that should be applied to the row at the given index. + + Args: + row_index: The index of the row to style. + base_style: The base style to use by default. + + Returns: + The appropriate style. + """ + + if row_index == -1: + row_style = self.get_component_styles("datatable--header").rich_style + elif row_index < self.fixed_rows: + row_style = self.get_component_styles("datatable--fixed").rich_style + else: + if self.zebra_stripes: + component_row_style = ( + "datatable--odd-row" if row_index % 2 else "datatable--even-row" + ) + row_style = self.get_component_styles(component_row_style).rich_style + else: + row_style = base_style + return row_style + def _on_mouse_move(self, event: events.MouseMove): """If the hover cursor is visible, display it by extracting the row and column metadata from the segments present in the cells.""" diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 7c15be6534..af125c4fa3 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -544,7 +544,19 @@ class Markdown(Widget): text-style: bold dim; } """ + COMPONENT_CLASSES = {"em", "strong", "s", "code_inline"} + """ + These component classes target standard inline markdown styles. + Changing these will potentially break the standard markdown formatting. + + | Class | Description | + | :- | :- | + | `code_inline` | Target text that is styled as inline code. | + | `em` | Target text that is emphasized inline. | + | `s` | Target text that is styled inline with strykethrough. | + | `strong` | Target text that is styled inline with strong. | + """ BULLETS = ["\u25CF ", "โ–ช ", "โ€ฃ ", "โ€ข ", "โญ‘ "] diff --git a/src/textual/widgets/_progress_bar.py b/src/textual/widgets/_progress_bar.py index 617d390892..ec8c1b22cb 100644 --- a/src/textual/widgets/_progress_bar.py +++ b/src/textual/widgets/_progress_bar.py @@ -8,16 +8,19 @@ from rich.style import Style -from textual.geometry import clamp - +from .._types import UnusedParameter from ..app import ComposeResult, RenderResult from ..containers import Horizontal +from ..geometry import clamp from ..reactive import reactive from ..renderables.bar import Bar as BarRenderable from ..timer import Timer from ..widget import Widget from ..widgets import Label +UNUSED = UnusedParameter() +"""Sentinel for method signatures.""" + class Bar(Widget, can_focus=False): """The bar portion of the progress bar.""" @@ -276,7 +279,6 @@ class ProgressBar(Widget, can_focus=False): """The total number of steps associated with this progress bar, when known. The value `None` will render an indeterminate progress bar. - Once `total` is set to a numerical value, it cannot be set back to `None`. """ percentage: reactive[float | None] = reactive[Optional[float]](None) """The percentage of progress that has been completed. @@ -398,6 +400,7 @@ def advance(self, advance: float = 1) -> None: ```py progress_bar.advance(10) # Advance 10 steps. ``` + Args: advance: Number of steps to advance progress by. """ @@ -406,30 +409,28 @@ def advance(self, advance: float = 1) -> None: def update( self, *, - total: float | None = None, - progress: float | None = None, - advance: float | None = None, + total: None | float | UnusedParameter = UNUSED, + progress: float | UnusedParameter = UNUSED, + advance: float | UnusedParameter = UNUSED, ) -> None: """Update the progress bar with the given options. - Options only affect the progress bar if they are not `None`. - Example: ```py progress_bar.update( total=200, # Set new total to 200 steps. - progress=None, # This has no effect. + progress=50, # Set the progress to 50 (out of 200). ) ``` Args: - total: New total number of steps (if not `None`). - progress: Set the progress to the given number of steps (if not `None`). - advance: Advance the progress by this number of steps (if not `None`). + total: New total number of steps. + progress: Set the progress to the given number of steps. + advance: Advance the progress by this number of steps. """ - if total is not None: + if not isinstance(total, UnusedParameter): self.total = total - if progress is not None: + if not isinstance(progress, UnusedParameter): self.progress = progress - if advance is not None: + if not isinstance(advance, UnusedParameter): self.progress += advance diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index e95c8e6a89..543f624d23 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -13685,6 +13685,322 @@ ''' # --- +# name: test_datatable_add_row_auto_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AutoHeightRowsApp + + + + + + + + + +  N  Column      +  3  hey there   +  1  hey there   +  5  long        +  string      +  2  โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ   +  โ”‚ Hello โ”‚   +  โ”‚ world โ”‚   +  โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ   +  4  1           +  2           +  3           +  4           +  5           +  6           +  7           + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_add_row_auto_height_sorted + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AutoHeightRowsApp + + + + + + + + + +  N  Column      +  1  hey there   +  2  โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ   +  โ”‚ Hello โ”‚   +  โ”‚ world โ”‚   +  โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ   +  3  hey there   +  4  1           +  2           +  3           +  4           +  5           +  6           +  7           +  5  long        +  string      + + + + + + + + + + + + + ''' +# --- # name: test_datatable_column_cursor_render ''' @@ -13846,6 +14162,167 @@ ''' # --- +# name: test_datatable_hot_reloading + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DataTableHotReloadingApp + + + + + + + + + +  A           B     +  one         two   +  three       four  +  five        six   + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_datatable_labels_and_fixed_data ''' diff --git a/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py new file mode 100644 index 0000000000..23a224b4ff --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py @@ -0,0 +1,25 @@ +from rich.panel import Panel +from rich.text import Text + +from textual.app import App +from textual.widgets import DataTable + + +class AutoHeightRowsApp(App[None]): + def compose(self): + table = DataTable() + self.column = table.add_column("N") + table.add_column("Column", width=10) + table.add_row(3, "hey there", height=None) + table.add_row(1, Text("hey there"), height=None) + table.add_row(5, Text("long string", overflow="fold"), height=None) + table.add_row(2, Panel.fit("Hello\nworld"), height=None) + table.add_row(4, "1\n2\n3\n4\n5\n6\n7", height=None) + yield table + + def key_s(self): + self.query_one(DataTable).sort(self.column) + + +if __name__ == "__main__": + AutoHeightRowsApp().run() diff --git a/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py new file mode 100644 index 0000000000..7e3a803155 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py @@ -0,0 +1,58 @@ +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.widgets import DataTable + +CSS_PATH = (Path(__file__) / "../datatable_hot_reloading.tcss").resolve() + +# Write some CSS to the file before the app loads. +# Then, the test will clear all the CSS to see if the +# hot reloading applies the changes correctly. +CSS_PATH.write_text( + """\ +DataTable > .datatable--cursor { + background: purple; +} + +DataTable > .datatable--fixed { + background: red; +} + +DataTable > .datatable--fixed-cursor { + background: blue; +} + +DataTable > .datatable--header { + background: yellow; +} + +DataTable > .datatable--odd-row { + background: pink; +} + +DataTable > .datatable--even-row { + background: brown; +} +""" +) + + +class DataTableHotReloadingApp(App[None]): + CSS_PATH = CSS_PATH + + def compose(self) -> ComposeResult: + yield DataTable(zebra_stripes=True, cursor_type="row") + + def on_mount(self) -> None: + dt = self.query_one(DataTable) + dt.add_column("A", width=10) + self.c = dt.add_column("B") + dt.fixed_columns = 1 + dt.add_row("one", "two") + dt.add_row("three", "four") + dt.add_row("five", "six") + + +if __name__ == "__main__": + app = DataTableHotReloadingApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss new file mode 100644 index 0000000000..5e9ee82eb7 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss @@ -0,0 +1 @@ +/* This file is purposefully empty. */ diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 68a731fdbb..f8d8a67b83 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -148,6 +148,18 @@ def test_datatable_add_column(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_column.py") +def test_datatable_add_row_auto_height(snap_compare): + # Check that rows added with auto height computation look right. + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py") + + +def test_datatable_add_row_auto_height_sorted(snap_compare): + # Check that rows added with auto height computation look right. + assert snap_compare( + SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py", press=["s"] + ) + + def test_footer_render(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py") @@ -511,6 +523,20 @@ async def run_before(pilot): ) +def test_datatable_hot_reloading(snap_compare): + """Regression test for https://github.com/Textualize/textual/issues/3312.""" + + async def run_before(pilot): + css_file = pilot.app.CSS_PATH + with open(css_file, "w") as f: + f.write("/* This file is purposefully empty. */\n") # Clear all the CSS. + await pilot.app._on_css_change() + + assert snap_compare( + SNAPSHOT_APPS_DIR / "datatable_hot_reloading.py", run_before=run_before + ) + + def test_layer_fix(snap_compare): # Check https://github.com/Textualize/textual/issues/1358 assert snap_compare(SNAPSHOT_APPS_DIR / "layer_fix.py", press=["d"]) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 96e9ca7b31..7d162dc16b 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -1,11 +1,12 @@ from __future__ import annotations import pytest +from rich.panel import Panel from rich.text import Text from textual._wait import wait_for_idle from textual.actions import SkipAction -from textual.app import App +from textual.app import App, RenderableType from textual.coordinate import Coordinate from textual.geometry import Offset from textual.message import Message @@ -419,11 +420,11 @@ async def test_get_cell_coordinate_returns_coordinate(): table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2") table.add_row("ValR3C1", "ValR3C2", "ValR3C3", key="R3") - assert table.get_cell_coordinate('R1', 'C1') == Coordinate(0, 0) - assert table.get_cell_coordinate('R2', 'C2') == Coordinate(1, 1) - assert table.get_cell_coordinate('R1', 'C3') == Coordinate(0, 2) - assert table.get_cell_coordinate('R3', 'C1') == Coordinate(2, 0) - assert table.get_cell_coordinate('R3', 'C2') == Coordinate(2, 1) + assert table.get_cell_coordinate("R1", "C1") == Coordinate(0, 0) + assert table.get_cell_coordinate("R2", "C2") == Coordinate(1, 1) + assert table.get_cell_coordinate("R1", "C3") == Coordinate(0, 2) + assert table.get_cell_coordinate("R3", "C1") == Coordinate(2, 0) + assert table.get_cell_coordinate("R3", "C2") == Coordinate(2, 1) async def test_get_cell_coordinate_invalid_row_key(): @@ -434,7 +435,7 @@ async def test_get_cell_coordinate_invalid_row_key(): table.add_row("TargetValue", key="R1") with pytest.raises(CellDoesNotExist): - coordinate = table.get_cell_coordinate('INVALID_ROW', 'C1') + coordinate = table.get_cell_coordinate("INVALID_ROW", "C1") async def test_get_cell_coordinate_invalid_column_key(): @@ -445,7 +446,7 @@ async def test_get_cell_coordinate_invalid_column_key(): table.add_row("TargetValue", key="R1") with pytest.raises(CellDoesNotExist): - coordinate = table.get_cell_coordinate('R1', 'INVALID_COLUMN') + coordinate = table.get_cell_coordinate("R1", "INVALID_COLUMN") async def test_get_cell_at_returns_value_at_cell(): @@ -531,9 +532,9 @@ async def test_get_row_index_returns_index(): table.add_row("ValR2C1", "ValR2C2", key="R2") table.add_row("ValR3C1", "ValR3C2", key="R3") - assert table.get_row_index('R1') == 0 - assert table.get_row_index('R2') == 1 - assert table.get_row_index('R3') == 2 + assert table.get_row_index("R1") == 0 + assert table.get_row_index("R2") == 1 + assert table.get_row_index("R3") == 2 async def test_get_row_index_invalid_row_key(): @@ -544,7 +545,7 @@ async def test_get_row_index_invalid_row_key(): table.add_row("TargetValue", key="R1") with pytest.raises(RowDoesNotExist): - index = table.get_row_index('InvalidRow') + index = table.get_row_index("InvalidRow") async def test_get_column(): @@ -591,6 +592,7 @@ async def test_get_column_at_invalid_index(index): with pytest.raises(ColumnDoesNotExist): list(table.get_column_at(index)) + async def test_get_column_index_returns_index(): app = DataTableApp() async with app.run_test(): @@ -598,12 +600,12 @@ async def test_get_column_index_returns_index(): table.add_column("Column1", key="C1") table.add_column("Column2", key="C2") table.add_column("Column3", key="C3") - table.add_row("ValR1C1", "ValR1C2", "ValR1C3", key="R1") - table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2") + table.add_row("ValR1C1", "ValR1C2", "ValR1C3", key="R1") + table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2") - assert table.get_column_index('C1') == 0 - assert table.get_column_index('C2') == 1 - assert table.get_column_index('C3') == 2 + assert table.get_column_index("C1") == 0 + assert table.get_column_index("C2") == 1 + assert table.get_column_index("C3") == 2 async def test_get_column_index_invalid_column_key(): @@ -613,11 +615,10 @@ async def test_get_column_index_invalid_column_key(): table.add_column("Column1", key="C1") table.add_column("Column2", key="C2") table.add_column("Column3", key="C3") - table.add_row("TargetValue1", "TargetValue2", "TargetValue3", key="R1") + table.add_row("TargetValue1", "TargetValue2", "TargetValue3", key="R1") with pytest.raises(ColumnDoesNotExist): - index = table.get_column_index('InvalidCol') - + index = table.get_column_index("InvalidCol") async def test_update_cell_cell_exists(): @@ -1172,3 +1173,43 @@ async def test_unset_hover_highlight_when_no_table_cell_under_mouse(): # the widget, and the hover cursor is hidden await pilot.hover(DataTable, offset=Offset(42, 1)) assert not table._show_hover_cursor + + +@pytest.mark.parametrize( + ["cell", "height"], + [ + ("hey there", 1), + (Text("hey there"), 1), + (Text("long string", overflow="fold"), 2), + (Panel.fit("Hello\nworld"), 4), + ("1\n2\n3\n4\n5\n6\n7", 7), + ], +) +async def test_add_row_auto_height(cell: RenderableType, height: int): + app = DataTableApp() + async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_column("C", width=10) + row_key = table.add_row(cell, height=None) + row = table.rows.get(row_key) + await pilot.pause() + assert row.height == height + + +async def test_add_row_expands_column_widths(): + """Regression test for https://github.com/Textualize/textual/issues/1026.""" + app = DataTableApp() + from textual.widgets._data_table import CELL_X_PADDING + + async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_column("First") + table.add_column("Second", width=10) + await pilot.pause() + assert table.ordered_columns[0].render_width == 5 + CELL_X_PADDING + assert table.ordered_columns[1].render_width == 10 + CELL_X_PADDING + + table.add_row("a" * 20, "a" * 20) + await pilot.pause() + assert table.ordered_columns[0].render_width == 20 + CELL_X_PADDING + assert table.ordered_columns[1].render_width == 10 + CELL_X_PADDING diff --git a/tests/test_progress_bar.py b/tests/test_progress_bar.py index 64b034817d..bc7f799196 100644 --- a/tests/test_progress_bar.py +++ b/tests/test_progress_bar.py @@ -79,7 +79,7 @@ def test_update_total(): assert pb.total == 1000 pb.update(total=None) - assert pb.total == 1000 + assert pb.total is None pb.update(total=100) assert pb.total == 100 @@ -119,6 +119,15 @@ def test_update(): assert pb.progress == 50 +def test_go_back_to_indeterminate(): + pb = ProgressBar() + + pb.total = 100 + assert pb.percentage == 0 + pb.total = None + assert pb.percentage is None + + @pytest.mark.parametrize( ["show_bar", "show_percentage", "show_eta"], [