diff --git a/cli_test_helpers/__init__.py b/cli_test_helpers/__init__.py index d3af124..d4ad522 100644 --- a/cli_test_helpers/__init__.py +++ b/cli_test_helpers/__init__.py @@ -5,8 +5,9 @@ __all__ = [ "ArgvContext", "EnvironContext", + "RandomDirectoryContext", "shell", ] from .commands import shell -from .decorators import ArgvContext, EnvironContext +from .decorators import ArgvContext, EnvironContext, RandomDirectoryContext diff --git a/cli_test_helpers/decorators.py b/cli_test_helpers/decorators.py index 919a141..0cda18e 100644 --- a/cli_test_helpers/decorators.py +++ b/cli_test_helpers/decorators.py @@ -3,7 +3,9 @@ """ import contextlib +import os import sys +from tempfile import TemporaryDirectory from unittest.mock import patch __all__ = [] @@ -12,6 +14,10 @@ class ArgvContext: """ A simple context manager allowing to temporarily override ``sys.argv``. + + Use it to mimic the command line arguments of the CLI application. + Note that the first argument (index ``0``) is always the script or + application name. """ def __init__(self, *new_args): @@ -50,3 +56,27 @@ def __enter__(self): for key in self.clear_variables: with contextlib.suppress(KeyError): self.in_dict.pop(key) + + +class RandomDirectoryContext(TemporaryDirectory): + """ + Change the execution directory to a random location, temporarily. + + Keyword arguments are optional and identical to the ones of + `tempfile.TemporaryDirectory`_ of the Python standard library. + + .. _tempfile.TemporaryDirectory: + https://docs.python.org/3/library/tempfile.html#tempfile.TemporaryDirectory + """ + + def __enter__(self): + """Create a temporary directory and ``cd`` into it.""" + self.__prev_dir = os.getcwd() + super().__enter__() + os.chdir(self.name) + return os.getcwd() + + def __exit__(self, exc_type, exc_value, traceback): + """Return to the original directory before execution.""" + os.chdir(self.__prev_dir) + return super().__exit__(exc_type, exc_value, traceback) diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..f3c6499 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,21 @@ +API Documentation +================= + +This section describes the context managers and helper functions provided +by the CLI test helpers package. + +.. module:: cli_test_helpers + +Context Managers +---------------- + +.. autoclass:: ArgvContext + +.. autoclass:: EnvironContext + +.. autoclass:: RandomDirectoryContext + +Utilities +--------- + +.. autofunction:: shell diff --git a/docs/conf.py b/docs/conf.py index 1f06592..0568457 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,9 +10,9 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- @@ -28,6 +28,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.viewcode', ] diff --git a/docs/index.rst b/docs/index.rst index 49fe44c..160ab2f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Contents installation tutorial + api other techniques contributing diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 8048c63..f27eb17 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -36,8 +36,8 @@ Start with a simple set of functional tests: installed) - Is command XYZ available? etc. Cover your entire CLI usage here! -This is almost a stupid exercise: Run the command as a shell command -and inspect the exit code of the exiting process, e.g. +This is almost a stupid exercise: Run the command as a :func:`~cli_test_helpers.shell` +command and inspect the exit code of the exiting process, e.g. .. code-block:: python @@ -67,7 +67,8 @@ Then you're ready to take advantage of our helpers. ``ArgvContext`` +++++++++++++++ -``ArgvContext`` allows you to mimic the use of specific CLI arguments: +:class:`~cli_test_helpers.ArgvContext` allows you to mimic the use of +specific CLI arguments: .. code-block:: python @@ -96,8 +97,8 @@ See more |example code (argparse-cli)|_. ``EnvironContext`` ++++++++++++++++++ -``EnvironContext`` allows you to mimic the presence (or absence) of -environment variables: +:class:`~cli_test_helpers.EnvironContext` allows you to mimic the presence +(or absence) of environment variables: .. code-block:: python @@ -112,6 +113,20 @@ environment variables: See more |example code (click-command)|_. +``RandomDirectoryContext`` +++++++++++++++++++++++++++ + +:class:`~cli_test_helpers.RandomDirectoryContext` allows you to verify that +your CLI program logic is independent of where it is executed in the file +system: + +.. code-block:: python + + def test_load_configfile(): + """Must not fail when executed anywhere in the filesystem.""" + with ArgvContext('foobar', 'load'), RandomDirectoryContext(): + foobar.cli.main() + .. |example code (argparse-cli)| replace:: example code .. |example code (click-cli)| replace:: example code diff --git a/pyproject.toml b/pyproject.toml index f155f42..ff66327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ extend-ignore = [ "D203", "D205", "D212", + "PTH109", "Q000", ] diff --git a/tests/test_decorators.py b/tests/test_decorators.py index d904e8b..025ae7b 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -3,7 +3,7 @@ import os import sys -from cli_test_helpers import ArgvContext, EnvironContext +from cli_test_helpers import ArgvContext, EnvironContext, RandomDirectoryContext def test_argv_context(): @@ -45,3 +45,20 @@ def test_environ_context(): assert os.environ == old_environ, "object os.environ was not restored" assert os.getenv("PATH") == old_path, "env var PATH was not restored" assert os.getenv("FOO") is None, "env var FOO was not cleared" + + +def test_random_directory_context(): + """ + In a directory context, are we effectively in a different location? + """ + before_dir = os.getcwd() + + with RandomDirectoryContext() as random_dir: + new_dir = os.getcwd() + + assert new_dir == random_dir, "Doesn't behave like TemporaryDirectory" + assert new_dir != before_dir, "Context not in a different file system location" + + after_dir = os.getcwd() + + assert after_dir == before_dir, "Execution directory not restored to original"