diff --git a/.github/workflows/check-code-coverage.yaml b/.github/workflows/check-code-coverage.yaml new file mode 100644 index 0000000..e541e0b --- /dev/null +++ b/.github/workflows/check-code-coverage.yaml @@ -0,0 +1,41 @@ +# This workflow will install Progress Table and test whether basic importing works correctly +# For more information see: +# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: check code coverage + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + strategy: + matrix: + python-version: [ + "3.10", + ] + os: [ + "ubuntu-22.04", + ] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install . + pip install pytest pytest-cov numpy pandas scikit-learn + + - name: Test with pytest + run: pytest . --cov=progress_table --cov-report=xml + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v5 diff --git a/.github/workflows/check-code-quality.yaml b/.github/workflows/check-code-quality.yaml index 77be224..6469821 100644 --- a/.github/workflows/check-code-quality.yaml +++ b/.github/workflows/check-code-quality.yaml @@ -2,7 +2,7 @@ # For more information see: # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: check code quality with python 3.10 +name: check code quality on: push: diff --git a/.github/workflows/check-with-pytest.yaml b/.github/workflows/check-with-pytest.yaml index 837714c..4c3819b 100644 --- a/.github/workflows/check-with-pytest.yaml +++ b/.github/workflows/check-with-pytest.yaml @@ -2,7 +2,7 @@ # For more information see: # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: install locally and run pytest +name: run tests on: push: @@ -25,6 +25,7 @@ jobs: ] os: [ "ubuntu-22.04", + "windows-latest", ] runs-on: ${{ matrix.os }} @@ -40,13 +41,5 @@ jobs: pip install . pip install pytest numpy pandas scikit-learn - # Fix failing importlib - - name: Set PYTHONPATH - run: echo "PYTHONPATH=$(pwd)" >> $GITHUB_ENV - - # For some reason it is necessary to run pytest separately for each test. - # Otherwise tests might fail. - - name: Test with pytest 1 - run: pytest tests/test_docs_auto.py --log-cli-level=WARNING - - name: Test with pytest 2 - run: pytest tests/test_examples_auto.py --log-cli-level=WARNING + - name: Test with pytest + run: pytest . diff --git a/.github/workflows/install-from-pypi-run.yaml b/.github/workflows/install-from-pypi-run.yaml index 55d6242..3003eb0 100644 --- a/.github/workflows/install-from-pypi-run.yaml +++ b/.github/workflows/install-from-pypi-run.yaml @@ -2,7 +2,7 @@ # For more information see: # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: install from pypi and run basic tests +name: install from pypi on: push: diff --git a/.github/workflows/install-locally-and-run.yaml b/.github/workflows/install-locally-and-run.yaml index 6e37adb..699825f 100644 --- a/.github/workflows/install-locally-and-run.yaml +++ b/.github/workflows/install-locally-and-run.yaml @@ -2,7 +2,7 @@ # For more information see: # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: install locally and run basic tests +name: install locally on: push: diff --git a/.gitignore b/.gitignore index 235b356..2fb78ba 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ *.ipynb __pycache__ *.egg-info +.coverage +coverage.xml devel/ dist diff --git a/README.md b/README.md index 8f9d10b..893b0d4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ # Progress Table [![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table) -[![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table) +[![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://github.com/sjmikler/progress-table/blob/main/LICENSE.txt) +[![codecov](https://codecov.io/gh/sjmikler/progress-table/graph/badge.svg?token=CDJKF0FFAQ)](https://codecov.io/gh/sjmikler/progress-table) Lightweight utility to display the progress of your process as a pretty table in the command line. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..42a8475 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,132 @@ +> Version 2.x.x introduces new features and new interactive modes. +> +> Version 3.x.x improves compatibility and stability. +> +> New features allow for previously impossible applications, see examples below. + +# Progress Table + +[![PyPi version](https://img.shields.io/badge/dynamic/json?label=latest&query=info.version&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table) +[![PyPI license](https://img.shields.io/badge/dynamic/json?label=license&query=info.license&url=https%3A%2F%2Fpypi.org%2Fpypi%2Fprogress-table%2Fjson)](https://pypi.org/project/progress-table) + +Lightweight utility to display the progress of your process as a pretty table in the command line. + +* Alternative to TQDM whenever you want to track metrics produced by your process +* Designed to monitor ML experiments, but works for any metrics-producing process +* Allows you to see at a glance what's going on with your process +* Increases readability and simplifies your command line logging + +### Change this: + +![example](https://raw.githubusercontent.com/sjmikler/progress-table/main/images/progress-before3.gif) + +### Into this: + +![example](https://raw.githubusercontent.com/sjmikler/progress-table/main/images/progress-after4.gif) + +## Examples + +From `examples/` directory: + +* Neural network training + +![example-training](https://raw.githubusercontent.com/sjmikler/progress-table/main/images/examples-training.gif) + +* Progress of multi-threaded downloads + +![example-download](https://raw.githubusercontent.com/sjmikler/progress-table/main/images/examples-download.gif) + +* Simulation and interactive display of Brownian motion + +![example-brown2d](https://raw.githubusercontent.com/sjmikler/progress-table/main/images/examples-brown2d.gif) + +* Display of a game board + +![example-tictactoe](https://raw.githubusercontent.com/sjmikler/progress-table/main/images/examples-tictactoe.gif) + +## Quick start code + +```python +import random +import time + +from progress_table import ProgressTable + +# Create table object: +table = ProgressTable(num_decimal_places=1) + +# You can (optionally) define the columns at the beginning +table.add_column("x", color="bold red") + +for step in range(10): + x = random.randint(0, 200) + + # You can add entries in a compact way + table["x"] = x + + # Or you can use the update method + table.update("x", value=x, weight=1.0) + + # Display the progress bar by wrapping an iterator or an integer + for _ in table(10): # -> Equivalent to `table(range(10))` + # Set and get values from the table + table["y"] = random.randint(0, 200) + table["x-y"] = table["x"] - table["y"] + table.update("average x-y", value=table["x-y"], weight=1.0, aggregate="mean") + time.sleep(0.1) + + # Go to the next row when you're ready + table.next_row() + +# Close the table when it's finished +table.close() + +``` + +> Go to [integrations](https://github.com/sjmikler/progress-table/blob/main/docs//integrations.md) +> page to see examples of integration with deep learning libraries. + +## Advanced usage + +Go to [advanced usage](https://github.com/sjmikler/progress-table/blob/main/docs//advanced-usage.md) page for more information. + +## Troubleshooting + +### Exceesive output + +Progress Table works correctly in most consoles, but there are some exceptions: + +* Some cloud logging consoles (e.g. kubernetes) don't support `\r` properly. You can still use ProgressTable, but with `interactive=0` option. This mode will not display progress bars. + +* Some consoles like 'PyCharm Python Console' or 'IDLE' don't support cursor movement. You can still use ProgressTable, but with `interactive=1` option. In this mode, you can display only a single progress bar. + +> By default `interactive=2`. You can change the default 'interactive' with an argument when creating the table object or by setting 'PTABLE_INTERACTIVE' environment variable, e.g. `PTABLE_INTERACTIVE=1`. + +### Other problems + +If you encounter different messy outputs or other unexpected behavior: please create an issue! + +## Installation + +Install Progress Table easily with pip: + +``` +pip install progress-table +``` + +## Links + +* [See on GitHub](https://github.com/gahaalt/progress-table) +* [See on PyPI](https://pypi.org/project/progress-table) + +## Alternatives + +* Progress bars: great for tracking progress, but they don't provide ways to display data in clear and compact way + * `tqdm` + * `rich.progress` + * `keras.utils.Progbar` + +* Libraries displaying data: great for presenting tabular data, but they lack the progress tracking aspect + * `rich.table` + * `tabulate` + * `texttable` diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 48cdd17..0032a28 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -30,7 +30,7 @@ table.close() Which might give you the following: -``` +```output ╭──────────╮ │ Value │ ├──────────┤ @@ -56,10 +56,12 @@ table.add_rows(4) # Adding empty rows table.at[:] = 0.0 # Initialize all values to 0.0 table.at[0, :] = 2.0 # Set all values in the first row to 2.0 table.at[:, 1] = 2.0 # Set all values in the second column to 2.0 -table.at[-2, 0] = 3.0 # Set the first column in the second-to-last row to 3.0 +table.at[2, 0] = 3.0 # Set the first column in the second-to-last row to 3.0 + +table.close() ``` -Which might give you the following: +Which should give you the following: ``` ╭──────────┬──────────┬──────────┬──────────╮ @@ -69,6 +71,7 @@ Which might give you the following: │ 0.0000 │ 2.0000 │ 0.0000 │ 0.0000 │ │ 3.0000 │ 2.0000 │ 0.0000 │ 0.0000 │ │ 0.0000 │ 2.0000 │ 0.0000 │ 0.0000 │ +╰──────────┴──────────┴──────────┴──────────╯ ``` ## Progress Bars @@ -90,20 +93,27 @@ It is possible to customize the looks of the embedded progress bar by specyfing ```python from progress_table import ProgressTable -table = ProgressTable(pbar_style_embed="cdots") +table = ProgressTable(pbar_style_embed="dash") ``` -Below we show a sample of available styles for embedded progress bars +Below we show a sample of available styles for embedded progress bars: ---- +`dash` + +``` +| Name | Value | Number | +|-----------------------------------| +| test1 | 1.0 | 42 | +|---test2--|----2.0-> | 37 | +``` -`cdots` +`rich` ``` | Name | Value | Number | |-----------------------------------| | test1 | 1.0 | 42 | -|ꞏꞏꞏtest2ꞏꞏ|ꞏꞏꞏꞏ2.0ꞏ> | 37 | +|━━━test2━━|━━━━2.0━━ | 37 | ``` `under` @@ -253,20 +263,28 @@ The available keywords are: Additionaly, you can specify the color of the progress bar using `color` and `color_empty` arguments when creating a progress bar object. This will override whatever color is set in `style` or `style_embed`. +We can combine this option with `colorama.Back` to modify colors +of the background instead of the foreground symbols. ```python from progress_table import ProgressTable import colorama +import time -table = ProgressTable() +table = ProgressTable("a", "b", "c") +table.add_rows(1) pbar = table.pbar( - range(1000), + range(100), style_embed="hidden", color=colorama.Back.RED, color_empty=colorama.Back.BLUE, ) + +for _ in pbar: + time.sleep(0.1) ``` -In the example above, we use a very specific embedded progress bar. -The typical progress bar symbols will be hidden, but the background color will show us the progress of the process. +Try the example above. It contains a different type of embedded progress bar. +Here the typical progress bar symbols will are hidden, +but the background color will show us the progress of the process. diff --git a/examples/brown2d.py b/examples/brown2d.py index 049c743..3483d60 100644 --- a/examples/brown2d.py +++ b/examples/brown2d.py @@ -58,7 +58,12 @@ def main(random_seed=None, sleep_duration=0.001, **overrides): MAX_ROWS = 20 STEP_SIZE = 100 - distance_pbar = table.pbar(TARGET_DISTANCE, description="Distance", show_throughput=False, show_progress=True) + distance_pbar = table.pbar( + TARGET_DISTANCE, + description="Distance", + show_throughput=False, + show_progress=True, + ) current_position = (0, 0) current_velocity = PARTICLE_VELOCITY @@ -72,8 +77,14 @@ def main(random_seed=None, sleep_duration=0.001, **overrides): random_direction = random.uniform(0, 2 * 3.1415) new_velocity = random.uniform(0, PARTICLE_VELOCITY * 2) current_velocity = current_velocity * PARTICLE_MOMENTUM + new_velocity * (1 - PARTICLE_MOMENTUM) - move_vector = (current_velocity * math.cos(random_direction), current_velocity * math.sin(random_direction)) - current_position = (current_position[0] + move_vector[0], current_position[1] + move_vector[1]) + move_vector = ( + current_velocity * math.cos(random_direction), + current_velocity * math.sin(random_direction), + ) + current_position = ( + current_position[0] + move_vector[0], + current_position[1] + move_vector[1], + ) distance_from_center = calc_distance(current_position) tick += 1 diff --git a/examples/training.py b/examples/training.py index e7fbe87..8b2d115 100644 --- a/examples/training.py +++ b/examples/training.py @@ -80,7 +80,7 @@ def main(random_seed=None, sleep_duration=SLEEP_DURATION, **overrides): for epoch in table(NUM_EPOCHS, show_throughput=False, show_eta=True): table["epoch"] = epoch # Shuffling training dataset each epoch - X_train, Y_train = shuffle(X_train, Y_train) + X_train, Y_train = shuffle(X_train, Y_train) # type: ignore NUM_BATCHES = 16 X_batches = np.array_split(X_train, NUM_BATCHES) @@ -116,8 +116,20 @@ def main(random_seed=None, sleep_duration=SLEEP_DURATION, **overrides): # Use aggregation weight equal to batch size to get real mean over the validation dataset batch_size = x.shape[0] - table.update("valid loss", loss_value, weight=batch_size, aggregate="mean", color="red") - table.update("valid accuracy", accuracy, weight=batch_size, aggregate="mean", color="red bold") + table.update( + "valid loss", + loss_value, + weight=batch_size, + aggregate="mean", + color="red", + ) + table.update( + "valid accuracy", + accuracy, + weight=batch_size, + aggregate="mean", + color="red bold", + ) table.next_row(split=run_validation) table.close() diff --git a/hooks.py b/hooks.py index 2260201..ff310d9 100644 --- a/hooks.py +++ b/hooks.py @@ -10,7 +10,7 @@ """ from pathlib import Path -from hatchling.plugin import hookimpl + from hatchling.metadata.plugin.interface import MetadataHookInterface @@ -22,13 +22,13 @@ def with_direct_github_urls(text): return text.replace("(docs", "(" + docs_github_link) -class CustomBuildHook(MetadataHookInterface): +class ReadmeHook(MetadataHookInterface): def update(self, metadata: dict): readme_path = Path("README.md") original_text = readme_path.read_text(encoding="utf-8") updated_text = with_direct_github_urls(original_text) - with open("README_pypi.md", "w", encoding="utf-8") as f: + with open("docs/README.md", "w", encoding="utf-8") as f: f.write(updated_text) # Tell Hatch to use this modified README - metadata["readme"] = f.name + print("Generated README for Pypi!") diff --git a/progress_table/__init__.py b/progress_table/__init__.py index 4c1c46b..fe64fed 100644 --- a/progress_table/__init__.py +++ b/progress_table/__init__.py @@ -10,7 +10,7 @@ """ __license__ = "MIT" -__version__ = "3.0.2" +__version__ = "3.1.0" __author__ = "Szymon Mikler" from progress_table.progress_table import ProgressTable, styles diff --git a/progress_table/progress_table.py b/progress_table/progress_table.py index a9e7173..98fcb69 100644 --- a/progress_table/progress_table.py +++ b/progress_table/progress_table.py @@ -15,12 +15,17 @@ from collections.abc import Callable, Iterable, Iterator, Sized from dataclasses import dataclass from threading import Thread -from typing import TextIO +from typing import Any, TextIO from colorama import Style from progress_table import styles -from progress_table.common import CURSOR_UP, ColorFormat, ColorFormatTuple, maybe_convert_to_colorama +from progress_table.common import ( + CURSOR_UP, + ColorFormat, + ColorFormatTuple, + maybe_convert_to_colorama, +) ###################### ## HELPER FUNCTIONS ## @@ -83,8 +88,8 @@ def get_aggregate_fn(aggregate: None | str | Callable) -> Callable: raise ValueError(msg) -def get_default_format_fn(decimal_places: int) -> Callable[[object], str]: - def fmt(x: object) -> str: +def get_default_format_fn(decimal_places: int) -> Callable[[Any], str]: + def fmt(x: Any) -> str: if isinstance(x, int): return str(x) try: @@ -104,13 +109,14 @@ def fmt(x: object) -> str: class DataRow: """Basic unit of data storage for the table.""" - values: dict[str, object] + values: dict[str, Any] weights: dict[str, float] colors: dict[str, str] + user: bool = False def is_empty(self) -> bool: """Check if the row is empty.""" - return not any(self.values) + return not any(self.values) and not self.user class ProgressTable: @@ -144,7 +150,7 @@ def __init__( pbar_style_embed: str | styles.PbarStyleBase = "cdots", print_header_on_top: bool = True, print_header_every_n_rows: int = 30, - custom_cell_format: Callable[[object], str] | None = None, + custom_cell_format: Callable[[Any], str] | None = None, table_style: str | styles.TableStyleBase = "round", file: TextIO | list[TextIO] | tuple[TextIO] | None = None, # DEPRECATED ARGUMENTS @@ -387,7 +393,7 @@ def reorder_columns(self, *column_names) -> None: def update( self, name: str, - value: object, + value: Any, *, row: int = -1, weight: float = 1.0, @@ -429,8 +435,12 @@ def update( if self.interactive > 0: self._append_or_update_display_row(data_row_index) - def __setitem__(self, key: str | tuple[str, int], value: object) -> None: + def __setitem__(self, key: str | tuple[str, int], value: Any) -> None: """Update value in the current row. Calls 'update'.""" + if isinstance(key, slice): + msg = "slicing not supported! Did you want to use 'table.at[:]' indexer?" + raise IndexError(msg) + if isinstance(key, tuple): name, row = key if isinstance(name, int) and isinstance(row, str): @@ -441,8 +451,12 @@ def __setitem__(self, key: str | tuple[str, int], value: object) -> None: assert isinstance(row, int), f"Row {row} has to be an integer, not {type(row)}!" self.update(name, value, row=row, weight=1) - def __getitem__(self, key: str | tuple[str, int]) -> object | None: + def __getitem__(self, key: str | tuple[str, int]) -> Any: """Get the value from the current row in table.""" + if isinstance(key, slice): + msg = "slicing not supported! Did you want to use 'table.at[:]' indexer?" + raise IndexError(msg) + if isinstance(key, tuple): name, row = key if isinstance(name, int) and isinstance(row, int): @@ -454,7 +468,7 @@ def __getitem__(self, key: str | tuple[str, int]) -> object | None: assert isinstance(row, int), f"Row {row} has to be an integer, not {type(row)}!" return self._data_rows[row].values.get(name, None) - def update_from_dict(self, dictionary: dict[str, object]) -> None: + def update_from_dict(self, dictionary: dict[str, Any]) -> None: """Update multiple values in the current row.""" for key, value in dictionary.items(): self.update(key, value) @@ -500,7 +514,7 @@ def next_row( row.colors = {**self._resolve_row_color_dict(color), **row.colors} # Refreshing the existing row is necessary to apply colors - # Or - if row is empty, this will cause the first addition to display rows + # Or - if row is new - this will add it to display rows self._append_or_update_display_row(data_row_index) self._append_new_empty_data_row() @@ -513,9 +527,18 @@ def next_row( def add_row(self, *values, **kwds) -> None: """Mimicking rich.table behavior for adding full rows in one call.""" + if not self._data_rows[-1].is_empty(): + self.next_row(**kwds) + for key, value in zip(self.column_names, values): self.update(key, value) - self.next_row(**kwds) + + # The row was explicitly added, make sure it is displayed + data_row_index = len(self._data_rows) - 1 + self._append_or_update_display_row(data_row_index) + + # Mark row as explicitly added by the user + self._data_rows[data_row_index].user = True def add_rows(self, *rows, **kwds) -> None: """Like `add_row` but adds multiple rows at once. @@ -574,14 +597,14 @@ def write(self, *args, sep: str = " ") -> None: self._append_or_update_display_row("USER WRITE " + line) - def to_list(self) -> list[list[object]]: + def to_list(self) -> list[list[Any]]: """Convert to Python nested list.""" values = [[row.values.get(col, None) for col in self.column_names] for row in self._data_rows] if self._data_rows[-1].is_empty(): values.pop(-1) return values - def to_numpy(self) -> object: + def to_numpy(self) -> Any: """Convert to numpy array. Numpy library is required. @@ -590,7 +613,7 @@ def to_numpy(self) -> object: return np.array(self.to_list()) - def to_df(self) -> object: + def to_df(self) -> Any: """Convert to pandas DataFrame. Pandas library is required. @@ -809,7 +832,7 @@ def _resolve_row_color_dict(self, color: ColorFormat | dict[str, ColorFormat] = color_colorama = {column: maybe_convert_to_colorama(color) for column, color in color.items()} return {col: self.column_colors[col] + color_colorama[col] for col in color} - def _apply_cell_formatting(self, value: object, column_name: str, color: str) -> str: + def _apply_cell_formatting(self, value: Any, column_name: str, color: str) -> str: str_value = self.custom_cell_format(value) width = self.column_widths[column_name] alignment = self.column_alignments[column_name] @@ -1284,7 +1307,7 @@ def _parse_index(self, key: slice | tuple) -> tuple: column_names = self.table.column_names[cols] if isinstance(cols, slice) else [self.table.column_names[cols]] return row_indices, column_names, mode - def __setitem__(self, key: slice | tuple, value: object) -> None: + def __setitem__(self, key: slice | tuple, value: Any) -> None: """Set the values, colors, or weights of a slice in the table.""" row_indices, column_names, edit_mode = self._parse_index(key) if edit_mode == "colors": diff --git a/progress_table/styles.py b/progress_table/styles.py index 39df3e0..c8d0c18 100644 --- a/progress_table/styles.py +++ b/progress_table/styles.py @@ -32,8 +32,6 @@ def _parse_colors_from_description(description: str) -> tuple[str, str, str]: class UnknownStyleError(ValueError): """Raised when style description is not recognized.""" - pass - def parse_pbar_style(description: str | PbarStyleBase) -> PbarStyleBase: """Parse progress bar style description and return a style object. diff --git a/pyproject.toml b/pyproject.toml index 6a3db9a..7a5a886 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -readme = "README.md" +readme = "docs/README.md" [project.urls] Home = "https://github.com/gahaalt/progress-table" @@ -36,7 +36,7 @@ dev = ["black", "isort", "build", "twine"] path = "progress_table/__init__.py" [tool.hatch.build.targets.sdist] -include = ["progress_table", "README.md"] +include = ["progress_table", "examples", "docs", "README.md"] [tool.hatch.build.targets.wheel] packages = ["progress_table"] @@ -44,8 +44,7 @@ packages = ["progress_table"] [tool.hatch.metadata.hooks.custom] path = "hooks.py" - -# %% +# %% TOOLS [tool.pyright] typeCheckingMode = "standard" @@ -53,8 +52,23 @@ exclude = ["devel", "build", "dist"] [tool.ruff] line-length = 120 -fix = true target-version = "py37" [tool.ruff.lint] select = ["E", "F", "I", "B"] + +# %% + +[tool.pytest.ini_options] +pythonpath = ["."] + +[tool.mypy] +ignore_missing_imports = true +exclude = ["devel", "build", "dist"] + +[tool.isort] +profile = "black" +line_length = 120 + +[tool.black] +line_length = 120 \ No newline at end of file diff --git a/tests/test_docs_auto.py b/tests/test_auto_docs.py similarity index 100% rename from tests/test_docs_auto.py rename to tests/test_auto_docs.py diff --git a/tests/test_examples_auto.py b/tests/test_auto_examples.py similarity index 96% rename from tests/test_examples_auto.py rename to tests/test_auto_examples.py index 9362b04..c8b4549 100644 --- a/tests/test_examples_auto.py +++ b/tests/test_auto_examples.py @@ -9,7 +9,7 @@ EXPECTED_OUTPUTS = { "examples.brown2d": "e85fcc33e982cb783059c09c090fca4e", "examples.training": "91ca0321e3776d5f2ac45add37e0db27", - "examples.tictactoe": "b71d814bc517e3aa6d2477dd72e55e8f", + "examples.tictactoe": "261c337ff04ae63e84857f9fcaf1d276", } diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py new file mode 100644 index 0000000..1e1fb8a --- /dev/null +++ b/tests/test_end_to_end.py @@ -0,0 +1,102 @@ +# Copyright (c) 2022-2025 Szymon Mikler +# Licensed under the MIT License + +from io import StringIO + + +def test_simple_example_1(): + from progress_table import ProgressTable + + file = StringIO() + table = ProgressTable(interactive=0, file=file) + table.add_column("Value") + table.add_rows(3) # Adding empty rows + + table.update(name="Value", value=1.0, row=1) + table.update(name="Value", value=2.0, row=0) + table.update(name="Value", value=3.0, row=2) + table.close() + + results = file.getvalue() + expected = """ +╭──────────╮ +│ Value │ +├──────────┤ +│ 2.0000 │ +│ 1.0000 │ +│ 3.0000 │ +╰──────────╯ +""" + assert results.strip() == expected.strip() + + +def test_simple_example_2(): + from progress_table import ProgressTable + + file = StringIO() + table = ProgressTable(interactive=0, file=file) + table.add_columns(4) # Add more columns with automatic names + table.add_rows(4) # Adding empty rows + + table.at[:] = 0.0 # Initialize all values to 0.0 + table.at[0, :] = 2.0 # Set all values in the first row to 2.0 + table.at[:, 1] = 2.0 # Set all values in the second column to 2.0 + table.at[2, 0] = 3.0 # Set the first column in the second-to-last row to 3.0 + table.close() + + results = file.getvalue() + expected = """ +╭──────────┬──────────┬──────────┬──────────╮ +│ 0 │ 1 │ 2 │ 3 │ +├──────────┼──────────┼──────────┼──────────┤ +│ 2.0000 │ 2.0000 │ 2.0000 │ 2.0000 │ +│ 0.0000 │ 2.0000 │ 0.0000 │ 0.0000 │ +│ 3.0000 │ 2.0000 │ 0.0000 │ 0.0000 │ +│ 0.0000 │ 2.0000 │ 0.0000 │ 0.0000 │ +╰──────────┴──────────┴──────────┴──────────╯ +""" + assert results.strip() == expected.strip() + + +def test_example_3(): + import random + + from progress_table import ProgressTable + + random.seed(42) + + file = StringIO() + table = ProgressTable(interactive=0, file=file) + table.add_column("x") + + for _step in range(9): + x = random.randint(0, 200) + + table["x"] = x + table.update("x", value=x, weight=1.0) + + for _ in table(13): + table["y"] = random.randint(0, 200) + table["x-y"] = table["x"] - table["y"] + table.update("average x-y", value=table["x-y"], weight=1.0, aggregate="mean") + + table.next_row() + + table.close() + results = file.getvalue() + expected = """ +╭──────────┬──────────┬──────────┬─────────────╮ +│ x │ y │ x-y │ average x-y │ +├──────────┼──────────┼──────────┼─────────────┤ +│ 163 │ 22 │ 141 │ 71.9231 │ +│ 151 │ 166 │ -15 │ 67.0769 │ +│ 179 │ 71 │ 108 │ 77.7692 │ +│ 39 │ 186 │ -147 │ -45.8462 │ +│ 117 │ 17 │ 100 │ 16.7692 │ +│ 11 │ 41 │ -30 │ -79.9231 │ +│ 94 │ 186 │ -92 │ -29.0769 │ +│ 62 │ 14 │ 48 │ -55.5385 │ +│ 58 │ 101 │ -43 │ -33.1538 │ +╰──────────┴──────────┴──────────┴─────────────╯ +""" + assert results.strip() == expected.strip() diff --git a/tests/test_unit.py b/tests/test_unit.py new file mode 100644 index 0000000..9823244 --- /dev/null +++ b/tests/test_unit.py @@ -0,0 +1,45 @@ +from progress_table.progress_table import ProgressTable + + +def test_aggregate_mean(): + table = ProgressTable() + table.add_column("value", aggregate="mean") + assert table.column_aggregates["value"].__name__ == "aggregate_mean" + + for i in range(10): + table["value"] = i + + assert table["value"] == 4.5 + + +def test_aggregate_sum(): + table = ProgressTable() + table.add_column("value", aggregate="sum") + assert table.column_aggregates["value"].__name__ == "aggregate_sum" + + for i in range(10): + table["value"] = i + + assert table["value"] == 45 + + +def test_aggregate_min(): + table = ProgressTable() + table.add_column("value", aggregate="min") + assert table.column_aggregates["value"].__name__ == "aggregate_min" + + for i in range(10): + table["value"] = i + + assert table["value"] == 0 + + +def test_aggregate_max(): + table = ProgressTable() + table.add_column("value", aggregate="max") + assert table.column_aggregates["value"].__name__ == "aggregate_max" + + for i in range(10): + table["value"] = i + + assert table["value"] == 9 diff --git a/tests/test_from_claude.py b/tests/test_unit_from_claude.py similarity index 100% rename from tests/test_from_claude.py rename to tests/test_unit_from_claude.py diff --git a/tests/test_from_gemini.py b/tests/test_unit_from_gemini.py similarity index 88% rename from tests/test_from_gemini.py rename to tests/test_unit_from_gemini.py index 223ba34..c4b3de2 100644 --- a/tests/test_from_gemini.py +++ b/tests/test_unit_from_gemini.py @@ -110,19 +110,20 @@ def test_add_row(): table.add_row(1, "abc") assert table[("col1", 0)] == 1 assert table[("col2", 0)] == "abc" - assert len(table._data_rows) == 2 + assert len(table._data_rows) == 1 def test_add_rows_with_integer_argument(): table = ProgressTable("col1") table.add_rows(2) - assert len(table._data_rows) == 3 + assert len(table._data_rows) == 2 + assert table.to_list() == [[None], [None]] def test_num_rows(): table = ProgressTable() table.add_rows(5) - assert table.num_rows() == 6 + assert table.num_rows() == 5 def test_num_columns(): @@ -156,7 +157,15 @@ def test_table_at_setitem_slice_rows_cols(): table = ProgressTable("col1", "col2") table.add_rows(2) table.at[:2, :] = 5 - assert table.at[:2, :] == [[5, 5], [5, 5]] + assert table.at[:2, :2] == [[5, 5], [5, 5]] + assert table.at[:] == [[5, 5], [5, 5]] + + +def test_table_at_setitem_slice_rows_cols2(): + table = ProgressTable("col1", "col2") + table.add_rows(2) + table.at[:] = 5 + assert table.at[:] == [[5, 5], [5, 5]] def test_table_at_setitem_slice_rows(): @@ -165,6 +174,7 @@ def test_table_at_setitem_slice_rows(): table.at[0, :] = 5 assert table.at[0, :] == [5, 5] assert table.at[1, :] == [None, None] + assert table.at[:] == [[5, 5], [None, None]] def test_table_at_setitem_slice_cols(): @@ -173,6 +183,7 @@ def test_table_at_setitem_slice_cols(): table.at[:2, 0] = 5 assert table.at[:2, 0] == [5, 5] assert table.at[:2, 1] == [None, None] + assert table.at[:] == [[5, None], [5, None]] def test_table_at_setitem_int_rows_cols(): @@ -186,8 +197,8 @@ def test_table_at_setitem_int_rows_cols(): def test_table_at_setitem_slice_rows_cols_mode(): table = ProgressTable("col1", "col2") table.add_rows(2) - table.at[:2, :, "weights"] = 1 - assert table.at[:2, :, "weights"] == [[1, 1], [1, 1]] + table.at[:, :, "weights"] = 1 + assert table.at[:, :, "weights"] == [[1, 1], [1, 1]] def test_table_at_setitem_slice_rows_cols_mode_colors(): @@ -205,6 +216,7 @@ def test_table_at_getitem_slice_rows_cols(): table.add_rows(2) table.at[0, 0] = 5 table.at[1, 1] = 10 + assert table.at[:, :] == [[5, None], [None, 10]] assert table.at[:2, :] == [[5, None], [None, 10]] @@ -212,8 +224,9 @@ def test_table_at_getitem_slice_rows(): table = ProgressTable("col1", "col2") table.add_rows(2) table.at[0, 0] = 5 - table.at[1, 1] = 10 + table.at[-1, -1] = 10 assert table.at[0, :] == [5, None] + assert table.at[:] == [[5, None], [None, 10]] def test_table_at_getitem_slice_cols(): @@ -221,6 +234,7 @@ def test_table_at_getitem_slice_cols(): table.add_rows(2) table.at[0, 0] = 5 table.at[1, 1] = 10 + assert table.at[:, 1] == [None, 10] assert table.at[:2, 1] == [None, 10] @@ -236,6 +250,7 @@ def test_table_at_getitem_slice_rows_cols_mode(): table = ProgressTable("col1", "col2") table.add_rows(2) table.at[:2, :, "weights"] = 1 + assert table.at[:, :, "weights"] == [[1, 1], [1, 1]] assert table.at[:2, :, "weights"] == [[1, 1], [1, 1]] @@ -243,7 +258,26 @@ def test_table_at_getitem_slice_rows_cols_mode_colors(): table = ProgressTable("col1", "col2") table.add_rows(2) table.at[:2, :, "colors"] = "red" - assert table.at[:2, :, "colors"] == [["\x1b[31m", "\x1b[31m"], ["\x1b[31m", "\x1b[31m"]] + assert table.at[:2, :, "colors"] == [ + ["\x1b[31m", "\x1b[31m"], + ["\x1b[31m", "\x1b[31m"], + ] + + +def test_table_adding_rows(): + table = ProgressTable("col1", "col2") + for _i in range(3): + table.add_row() + table.at[:] = 0 + assert table.at[:] == [[0, 0], [0, 0], [0, 0]] + + +def test_table_adding_rows2(): + table = ProgressTable("col1", "col2") + for i in range(3): + table.add_row() + table.at[-1, :] = i + assert table.at[:] == [[0, 0], [1, 1], [2, 2]] def test_aggregate_dont(): @@ -293,7 +327,7 @@ def test_get_aggregate_fn_invalid_string(): def test_get_aggregate_fn_invalid_type(): with pytest.raises(ValueError): - progress_table.get_aggregate_fn(123) + progress_table.get_aggregate_fn(123) # type: ignore def test_get_default_format_fn_int(): diff --git a/tests/test_from_gpt4o.py b/tests/test_unit_from_gpt4o.py similarity index 100% rename from tests/test_from_gpt4o.py rename to tests/test_unit_from_gpt4o.py