Skip to content

Commit

Permalink
adding basic test coverage; updating README; updating pyproject.toml
Browse files Browse the repository at this point in the history
  • Loading branch information
travishathaway committed Sep 30, 2024
1 parent c123c3e commit 80fcfa5
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 17 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# conda-rich
# Conda rich

Demonstration project utilizing the "rich" library for conda
This is a plugin for overriding certain elements of the conda CLI with components
from the rich library. To install and configure run the following commands:

```
conda install -c conda-forge conda-rich
conda config --set console rich
```

> [!TIP]
> You must be using conda at version 24.11 or higher to access this feature.
This plugin currently overrides the following display components:

- Loading spinner
- Download progress bars
- Confirmation dialogs
- The `conda info` display

This plugin also serves as a demonstration of how to use the "reporter backend"
plugin hook.
8 changes: 5 additions & 3 deletions conda_rich/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def envs_list(self, data, **kwargs) -> str:
console = Console(file=sys.stdout)

with console.capture() as capture:
console.print("Enviroments")
console.print("Environments")
console.print(data)

return capture.get()
Expand Down Expand Up @@ -112,11 +112,13 @@ def __init__(
self,
description: str,
context_manager=None,
visible_when_finished=False,
**kwargs,
) -> None:
super().__init__(description)

self.progress: Progress | None = None
self.visible_when_finished = visible_when_finished

if isinstance(context_manager, Progress):
self.progress = context_manager
Expand All @@ -134,7 +136,7 @@ def update_to(self, fraction) -> None:
self.progress.update(self.task, completed=fraction)

if fraction == 1:
self.progress.update(self.task, visible=False)
self.progress.update(self.task, visible=self.visible_when_finished)

def close(self) -> None:
if self.progress is not None:
Expand Down Expand Up @@ -178,7 +180,7 @@ def __enter__(self):

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type or exc_val:
sys.stdout.write(self.fail_message)
sys.stdout.write(f"{self.fail_message}\n")
else:
sys.stdout.write("done\n")
sys.stdout.flush()
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ channels:
dependencies:
- python>=3.8
- conda-canary/label/dev::conda
- rich
25 changes: 17 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm"]
build-backend = "setuptools.build_meta"
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
name = "conda-rich"
version = "0.1.0"
description = "Demonstration project utilizing the \"rich\" library for conda"
dynamic = ["version"]
description = "Conda plugin which uses rich components for its display"
requires-python = ">=3.8"
license = {file = "LICENSE"}
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy"
]
dependencies = [
"conda",
"rich"
]

[project.entry-points.conda]
conda-rich = "conda_rich.hooks"
conda-rich = "conda_rich.plugin"

[tool.setuptools]
packages = ["conda_rich"]
[tool.setuptools.packages]
find = {}

[tool.hatch.version]
source = "vcs"

[tool.pixi.project]
channels = ["conda-forge"]
platforms = ["osx-arm64"]
platforms = ["osx-arm64", "osx-64", "linux-64", "win-64"]

[tool.pixi.dependencies]
conda = {channel = "conda-canary/label/dev"}
Expand Down
236 changes: 236 additions & 0 deletions tests/test_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import time
from io import StringIO

import pytest

from conda.exceptions import CondaError
from conda.plugins import CondaReporterBackend
from rich.progress import Progress

from conda_rich.hooks import (
QuietProgressBar,
QuietSpinner,
RichSpinner,
RichProgressBar,
conda_reporter_backends,
RichReporterRenderer,
)


def test_conda_reporter_backends():
"""
Ensure that a ``CondaReporterBackend`` object is yielded from the ``conda_reporter_backends``
function
"""
hook_obj = next(conda_reporter_backends())

assert hook_obj is not None
assert isinstance(hook_obj, CondaReporterBackend)
assert hook_obj.name == "rich"
assert hook_obj.description == "Rich implementation for console reporting in conda"
assert hook_obj.renderer is RichReporterRenderer


def test_quiet_progress_bar(capsys):
"""
Basic test to ensure the quiet progress bar works as expected
"""
description = "Test"
quiet_progress = QuietProgressBar(description)

capture = capsys.readouterr()

assert capture.out == f"...downloading {description}...\n"

# Test individual methods; these should do nothing and return None
assert quiet_progress.refresh() is None
assert quiet_progress.close() is None
assert quiet_progress.update_to(1) is None


def test_rich_progress_bar(capsys):
"""
Basic test to ensure the rich progress bar works as expected
"""
description = "Test"
units = 0

with Progress() as progress:
rich_progress = RichProgressBar(
description, context_manager=progress, visible_when_finished=True
)

while units < 5:
units += 1
rich_progress.update_to(units / 4)
rich_progress.refresh()
time.sleep(0.01)

rich_progress.close()

capture = capsys.readouterr()

assert capture.out == "Test ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00\n"


def test_rich_progress_bar_misconfigured():
"""
Ensure an exception is raised when we fail to pass a ``rich.progress.Progress``
bar object to ``RichProgressBar``.
"""
with pytest.raises(
CondaError, match="Rich is configured, but there is no progress bar available"
):
RichProgressBar("Test description", None)


def test_quiet_spinner(capsys):
"""
Basic test to ensure the quiet spinner works as expected
"""
message = "test"
with QuietSpinner(message):
pass

capture = capsys.readouterr()

assert capture.out == f"{message}: ...working... done\n"


def test_quiet_spinner_failed_state(capsys):
"""
Basic test to ensure the quiet spinner works as expected
"""
message = "test"
fail_message = "testing fail message"

try:
with QuietSpinner(message, fail_message=fail_message):
raise Exception("Test fail")

except Exception:
capture = capsys.readouterr()
assert capture.out == f"{message}: ...working... {fail_message}\n"


def test_rich_spinner(capsys):
"""
Basic test to ensure the rich spinner works as expected
"""
message = "test"
with RichSpinner(message):
pass

capture = capsys.readouterr()

assert capture.out == "test (done)\n"


def test_rich_reporter_renderer_detail_view():
"""
Basic test to ensure that the ``detail_view`` method on the ``RichReporterRenderer`` class
works as expected
"""
renderer = RichReporterRenderer()

data = {
"field_one": "one",
"field_two": "two",
}

render_str = renderer.detail_view(data)

assert render_str == "\n field_one : one\n field_two : two\n\n"


def test_rich_reporter_renderer_env_list():
"""
Basic test to ensure that the ``env_list`` method on the ``RichReporterRenderer`` class
works as expected
"""
renderer = RichReporterRenderer()
environments = ["one", "two", "three"]

render_str = renderer.envs_list(environments)

assert render_str == "Environments\n['one', 'two', 'three']\n"


def test_rich_reporter_renderer_progress_bar(mocker):
"""
Basic test to ensure that the ``progress_bar`` method on the ``RichReporterRenderer`` class
works as expected
"""
mock_context = mocker.patch("conda_rich.hooks.context")
mock_context.quiet = False

renderer = RichReporterRenderer()
progress = Progress()
progress_bar = renderer.progress_bar("test", context_manager=progress)

assert isinstance(progress_bar, RichProgressBar)


def test_rich_reporter_renderer_progress_bar_with_quiet(mocker):
"""
Basic test to ensure that the ``progress_bar`` method on the ``RichReporterRenderer`` class
works as expected when ``context.quiet`` is ``True``
"""
mock_context = mocker.patch("conda_rich.hooks.context")
mock_context.quiet = True

renderer = RichReporterRenderer()
progress_bar = renderer.progress_bar("test")

assert isinstance(progress_bar, QuietProgressBar)


def test_rich_reporter_renderer_progress_bar_context_manager():
"""
Basic test to ensure that the ``progress_bar_context_manager`` method on the
``RichReporterRenderer`` class works as expected
"""
renderer = RichReporterRenderer()

# Make sure that the item the context manager returns is a ``rich.progress.Progress`` instance
with renderer.progress_bar_context_manager() as ctx:
assert isinstance(ctx, Progress)


def test_rich_reporter_renderer_spinner(mocker):
"""
Basic test to ensure that the ``spinner`` method on the ``RichReporterRenderer`` class
works as expected
"""
mock_context = mocker.patch("conda_rich.hooks.context")
mock_context.quiet = False

renderer = RichReporterRenderer()
spinner = renderer.spinner("test message")

assert isinstance(spinner, RichSpinner)


def test_rich_reporter_renderer_spinner_with_quiet(mocker):
"""
Basic test to ensure that the ``spinner`` method on the ``RichReporterRenderer`` class
works as expected when ``context.quiet`` is ``True``
"""
mock_context = mocker.patch("conda_rich.hooks.context")
mock_context.quiet = True

renderer = RichReporterRenderer()
spinner = renderer.spinner("test")

assert isinstance(spinner, QuietSpinner)


def test_rich_reporter_renderer_prompt(monkeypatch, capsys):
"""
Basic test to ensure that the ``prompt`` method on the ``RichReporterRenderer`` class
works as expected
"""
renderer = RichReporterRenderer()
monkeypatch.setattr("sys.stdin", StringIO("yes\n"))

assert renderer.prompt() == "yes"
4 changes: 0 additions & 4 deletions tests/test_placeholder.py

This file was deleted.

0 comments on commit 80fcfa5

Please sign in to comment.