Skip to content

Commit

Permalink
Initial code with software tests
Browse files Browse the repository at this point in the history
  • Loading branch information
amotl committed Oct 30, 2023
1 parent 6861b57 commit 7f65b5b
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pueblo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from pueblo.util.environ import getenvpass
from pueblo.util.logging import setup_logging
25 changes: 25 additions & 0 deletions pueblo/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging

import click
from click_aliases import ClickAliasedGroup

from pueblo.util.cli import boot_click

logger = logging.getLogger(__name__)


def help_pueblo():
"""
Pueblo - a Python toolbox library.
pueblo --version
""" # noqa: E501


@click.group(cls=ClickAliasedGroup)
@click.version_option(package_name="pueblo")
@click.option("--verbose", is_flag=True, required=False, help="Turn on logging")
@click.option("--debug", is_flag=True, required=False, help="Turn on logging with debug level")
@click.pass_context
def cli(ctx: click.Context, verbose: bool, debug: bool):
return boot_click(ctx, verbose, debug)
Empty file added pueblo/testing/__init__.py
Empty file.
49 changes: 49 additions & 0 deletions pueblo/testing/notebook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import typing as t

import pytest


def monkeypatch_pytest_notebook_treat_cell_exit_as_notebook_skip():
"""
Patch `pytest-notebook`, in fact `nbclient.client.NotebookClient`,
to propagate cell-level `pytest.exit()` invocations as signals
to mark the whole notebook as skipped.
In order not to be too intrusive, the feature only skips notebooks
when being explicitly instructed, by adding `[skip-notebook]` at the
end of the `reason` string. Example:
import pytest
if "ACME_API_KEY" not in os.environ:
pytest.exit("ACME_API_KEY not given [skip-notebook]")
https://github.com/chrisjsewell/pytest-notebook/issues/43
"""
from nbclient.client import NotebookClient
from nbclient.exceptions import CellExecutionError
from nbformat import NotebookNode

async_execute_cell_dist = NotebookClient.async_execute_cell

async def async_execute_cell(
self,
cell: NotebookNode,
cell_index: int,
execution_count: t.Optional[int] = None,
store_history: bool = True,
) -> NotebookNode:
try:
return await async_execute_cell_dist(
self,
cell,
cell_index,
execution_count=execution_count,
store_history=store_history,
)
except CellExecutionError as ex:
if ex.ename == "Exit" and ex.evalue.endswith("[skip-notebook]"):
raise pytest.skip(ex.evalue)
else:
raise

NotebookClient.async_execute_cell = async_execute_cell
Empty file added pueblo/util/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions pueblo/util/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import logging
import textwrap
import typing as t

from pueblo.util.logging import setup_logging

logger = logging.getLogger(__name__)


def boot_click(ctx: "click.Context", verbose: bool = False, debug: bool = False):
"""
Bootstrap the CLI application.
"""

# Adjust log level according to `verbose` / `debug` flags.
log_level = logging.INFO
if debug:
log_level = logging.DEBUG

# Setup logging, according to `verbose` / `debug` flags.
setup_logging(level=log_level, verbose=verbose)


def docstring_format_verbatim(text: t.Optional[str]) -> str:
"""
Format docstring to be displayed verbatim as a help text by Click.
- https://click.palletsprojects.com/en/8.1.x/documentation/#preventing-rewrapping
- https://github.com/pallets/click/issues/56
"""
text = text or ""
text = textwrap.dedent(text)
lines = [line if line.strip() else "\b" for line in text.splitlines()]
return "\n".join(lines)


def make_command(cli, name, helpfun, aliases=None):
"""
Convenience shortcut for creating a subcommand.
"""
return cli.command(
name,
help=docstring_format_verbatim(helpfun.__doc__),
context_settings={"max_content_width": 100},
aliases=aliases,
)
21 changes: 21 additions & 0 deletions pueblo/util/environ.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
import getpass
from dotenv import load_dotenv, find_dotenv


def getenvpass(env_var: str, prompt: str, skip_pytest_notebook: bool = True) -> str:
"""
Read variable from environment or `.env` file.
If it is not defined, prompt interactively.
FIXME: Needs a patch to make it work with `pytest-notebook`,
see https://github.com/chrisjsewell/pytest-notebook/issues/43.
"""
load_dotenv(find_dotenv())
if env_var not in os.environ:
if "PYTEST_CURRENT_TEST" in os.environ and skip_pytest_notebook:
import pytest
pytest.exit(f"{env_var} not given [skip-notebook]")
else:
os.environ[env_var] = getpass.getpass(prompt)
return os.environ.get(env_var)
32 changes: 32 additions & 0 deletions pueblo/util/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import importlib
import logging
import sys


def setup_logging(level=logging.INFO, verbose: bool = False):
try:
importlib.import_module("colorlog")
setup_logging_colorlog(level=level, verbose=verbose)
except ImportError:
setup_logging_standard(level=level, verbose=verbose)


def setup_logging_standard(level=logging.INFO, verbose: bool = False):
log_format = "%(asctime)-15s [%(name)-35s] %(levelname)-8s: %(message)s"
logging.basicConfig(format=log_format, stream=sys.stderr, level=level)


def setup_logging_colorlog(level=logging.INFO, verbose: bool = False):
import colorlog
from colorlog.escape_codes import escape_codes
reset = escape_codes["reset"]
log_format = f"%(asctime)-15s [%(name)-36s] %(log_color)s%(levelname)-8s:{reset} %(message)s"

handler = colorlog.StreamHandler()
handler.setFormatter(colorlog.ColoredFormatter(log_format))

logging.basicConfig(format=log_format, level=level, handlers=[handler])


def tweak_log_levels():
logging.getLogger("urllib3.connectionpool").setLevel(level=logging.INFO)
Empty file added tests/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from click.testing import CliRunner

from pueblo.cli import cli


def test_version():
"""
CLI test: Invoke `pueblo --version`
"""
runner = CliRunner()

result = runner.invoke(
cli,
args="--version",
catch_exceptions=False,
)
assert result.exit_code == 0
30 changes: 30 additions & 0 deletions tests/test_environ.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
from unittest import mock

import pytest

from pueblo.util.environ import getenvpass


@pytest.fixture(scope="function", autouse=True)
def init_testcase():
"""
Make sure each test case uses a blank canvas.
"""
if "ACME_API_KEY" in os.environ:
del os.environ["ACME_API_KEY"]


def test_getenvpass_prompt(mocker):
mocker.patch("getpass.getpass", return_value="foobar")
retval = getenvpass("ACME_API_KEY", "really?", skip_pytest_notebook=False)
assert retval == "foobar"


def test_getenvpass_environ():
with \
mock.patch("getpass.getpass"), \
mock.patch("pytest.exit") as exit_mock:
retval = getenvpass("ACME_API_KEY", "really?")
assert retval is None
exit_mock.assert_called_once_with("ACME_API_KEY not given [skip-notebook]",)
16 changes: 16 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import sys

from pueblo.util.logging import setup_logging, tweak_log_levels


def test_setup_logging_default():
setup_logging()


def test_setup_logging_no_colorlog(mocker):
mocker.patch.dict(sys.modules, {"colorlog": None})
setup_logging()


def test_tweak_log_levels():
tweak_log_levels()
5 changes: 5 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pueblo.testing.notebook import monkeypatch_pytest_notebook_treat_cell_exit_as_notebook_skip


def test_monkeypatch_pytest_notebook_treat_cell_exit_as_notebook_skip():
monkeypatch_pytest_notebook_treat_cell_exit_as_notebook_skip()

0 comments on commit 7f65b5b

Please sign in to comment.