[Python-modules-commits] [humanfriendly] 01/03: Import humanfriendly_4.4.1.orig.tar.gz

Wolfgang Borgert debacle at moszumanska.debian.org
Mon Aug 28 21:44:31 UTC 2017


This is an automated email from the git hooks/post-receive script.

debacle pushed a commit to branch master
in repository humanfriendly.

commit eccc90433b04dfa7335f7aaf0848f44df549a674
Author: W. Martin Borgert <debacle at debian.org>
Date:   Mon Aug 28 23:19:09 2017 +0200

    Import humanfriendly_4.4.1.orig.tar.gz
---
 MANIFEST.in                                        |   1 +
 PKG-INFO                                           |   2 +-
 docs/conf.py                                       |  79 +++
 docs/images/pretty-table.png                       | Bin 0 -> 44032 bytes
 docs/images/spinner-basic.gif                      | Bin 0 -> 41724 bytes
 docs/images/spinner-with-progress.gif              | Bin 0 -> 57133 bytes
 docs/images/spinner-with-timer.gif                 | Bin 0 -> 57748 bytes
 docs/index.rst                                     |  91 +++
 humanfriendly.egg-info/PKG-INFO                    |   2 +-
 humanfriendly.egg-info/SOURCES.txt                 |   9 +-
 humanfriendly.egg-info/requires.txt                |   1 +
 humanfriendly/__init__.py                          |   4 +-
 humanfriendly/compat.py                            |  11 +-
 humanfriendly/prompts.py                           |  24 +-
 humanfriendly/tables.py                            |   3 +-
 humanfriendly/testing.py                           | 690 +++++++++++++++++++++
 humanfriendly/tests.py                             | 221 ++++---
 humanfriendly/text.py                              | 215 ++++---
 humanfriendly/usage.py                             |   4 +-
 requirements-testing.txt => requirements-tests.txt |   3 +-
 setup.py                                           |   6 +-
 21 files changed, 1176 insertions(+), 190 deletions(-)

diff --git a/MANIFEST.in b/MANIFEST.in
index f218bf2..c33270c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,3 @@
 include *.rst
 include *.txt
+graft docs
diff --git a/PKG-INFO b/PKG-INFO
index 20d5571..841699e 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: humanfriendly
-Version: 3.2
+Version: 4.4.1
 Summary: Human friendly output for text interfaces using Python
 Home-page: https://humanfriendly.readthedocs.io
 Author: Peter Odding
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..aaabd80
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+
+"""Documentation build configuration file for the `humanfriendly` package."""
+
+import os
+import sys
+
+# Add the 'humanfriendly' source distribution's root directory to the module path.
+sys.path.insert(0, os.path.abspath('..'))
+
+# -- General configuration -----------------------------------------------------
+
+# Sphinx extension module names.
+extensions = [
+    'sphinx.ext.doctest',
+    'sphinx.ext.autodoc',
+    'sphinx.ext.intersphinx',
+    'humanfriendly.sphinx',
+]
+
+# Configuration for the `autodoc' extension.
+autodoc_member_order = 'bysource'
+
+# Paths that contain templates, relative to this directory.
+templates_path = ['templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'humanfriendly'
+copyright = u'2017, Peter Odding'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+
+# Find the package version and make it the release.
+from humanfriendly import __version__ as humanfriendly_version  # noqa
+
+# The short X.Y version.
+version = '.'.join(humanfriendly_version.split('.')[:2])
+
+# The full version, including alpha/beta/rc tags.
+release = humanfriendly_version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+language = 'en'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['build']
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# Refer to the Python standard library.
+# From: http://twistedmatrix.com/trac/ticket/4582.
+intersphinx_mapping = dict(
+    python2=('https://docs.python.org/2', None),
+    python3=('https://docs.python.org/3', None),
+    coloredlogs=('https://coloredlogs.readthedocs.io/en/latest/', None),
+)
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'humanfriendlydoc'
diff --git a/docs/images/pretty-table.png b/docs/images/pretty-table.png
new file mode 100644
index 0000000..b0c0c45
Binary files /dev/null and b/docs/images/pretty-table.png differ
diff --git a/docs/images/spinner-basic.gif b/docs/images/spinner-basic.gif
new file mode 100644
index 0000000..cf14929
Binary files /dev/null and b/docs/images/spinner-basic.gif differ
diff --git a/docs/images/spinner-with-progress.gif b/docs/images/spinner-with-progress.gif
new file mode 100644
index 0000000..3495518
Binary files /dev/null and b/docs/images/spinner-with-progress.gif differ
diff --git a/docs/images/spinner-with-timer.gif b/docs/images/spinner-with-timer.gif
new file mode 100644
index 0000000..a0427c7
Binary files /dev/null and b/docs/images/spinner-with-timer.gif differ
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..3e37809
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,91 @@
+.. include:: ../README.rst
+
+API documentation
+=================
+
+The following documentation is based on the source code of version |release| of
+the `humanfriendly` package.
+
+.. contents::
+   :local:
+
+A note about backwards compatibility
+------------------------------------
+
+The `humanfriendly` package started out as a single :mod:`humanfriendly`
+module. Eventually this module grew to a size that necessitated splitting up
+the code into multiple modules (see e.g. :mod:`~humanfriendly.tables`,
+:mod:`~humanfriendly.terminal`, :mod:`~humanfriendly.text` and
+:mod:`~humanfriendly.usage`). Most of the functionality that remains in the
+:mod:`humanfriendly` module will eventually be moved to submodules as well (as
+time permits and a logical subdivision of functionality presents itself to me).
+
+While moving functionality around like this my goal is to always preserve
+backwards compatibility. For example if a function is moved to a submodule an
+import of that function is added in the main module so that backwards
+compatibility with previously written import statements is preserved.
+
+If backwards compatibility of documented functionality has to be broken then
+the major version number will be bumped. So if you're using the `humanfriendly`
+package in your project, make sure to at least pin the major version number in
+order to avoid unexpected surprises.
+
+The :mod:`humanfriendly` module
+-------------------------------
+
+.. automodule:: humanfriendly
+   :members:
+
+The :mod:`humanfriendly.cli` module
+-----------------------------------
+
+.. automodule:: humanfriendly.cli
+   :members:
+
+The :mod:`humanfriendly.compat` module
+--------------------------------------
+
+.. automodule:: humanfriendly.compat
+   :members:
+
+The :mod:`humanfriendly.prompts` module
+---------------------------------------
+
+.. automodule:: humanfriendly.prompts
+   :members:
+
+The :mod:`humanfriendly.sphinx` module
+--------------------------------------
+
+.. automodule:: humanfriendly.sphinx
+   :members:
+
+The :mod:`humanfriendly.tables` module
+--------------------------------------
+
+.. automodule:: humanfriendly.tables
+   :members:
+
+The :mod:`humanfriendly.terminal` module
+----------------------------------------
+
+.. automodule:: humanfriendly.terminal
+   :members:
+
+The :mod:`humanfriendly.testing` module
+---------------------------------------
+
+.. automodule:: humanfriendly.testing
+   :members:
+
+The :mod:`humanfriendly.text` module
+------------------------------------
+
+.. automodule:: humanfriendly.text
+   :members:
+
+The :mod:`humanfriendly.usage` module
+-------------------------------------
+
+.. automodule:: humanfriendly.usage
+   :members:
diff --git a/humanfriendly.egg-info/PKG-INFO b/humanfriendly.egg-info/PKG-INFO
index 20d5571..841699e 100644
--- a/humanfriendly.egg-info/PKG-INFO
+++ b/humanfriendly.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: humanfriendly
-Version: 3.2
+Version: 4.4.1
 Summary: Human friendly output for text interfaces using Python
 Home-page: https://humanfriendly.readthedocs.io
 Author: Peter Odding
diff --git a/humanfriendly.egg-info/SOURCES.txt b/humanfriendly.egg-info/SOURCES.txt
index 6f96b0b..ae88d61 100644
--- a/humanfriendly.egg-info/SOURCES.txt
+++ b/humanfriendly.egg-info/SOURCES.txt
@@ -2,9 +2,15 @@ LICENSE.txt
 MANIFEST.in
 README.rst
 requirements-checks.txt
-requirements-testing.txt
+requirements-tests.txt
 setup.cfg
 setup.py
+docs/conf.py
+docs/index.rst
+docs/images/pretty-table.png
+docs/images/spinner-basic.gif
+docs/images/spinner-with-progress.gif
+docs/images/spinner-with-timer.gif
 humanfriendly/__init__.py
 humanfriendly/cli.py
 humanfriendly/compat.py
@@ -12,6 +18,7 @@ humanfriendly/prompts.py
 humanfriendly/sphinx.py
 humanfriendly/tables.py
 humanfriendly/terminal.py
+humanfriendly/testing.py
 humanfriendly/tests.py
 humanfriendly/text.py
 humanfriendly/usage.py
diff --git a/humanfriendly.egg-info/requires.txt b/humanfriendly.egg-info/requires.txt
index 0b619b0..9c5b149 100644
--- a/humanfriendly.egg-info/requires.txt
+++ b/humanfriendly.egg-info/requires.txt
@@ -4,3 +4,4 @@ monotonic
 
 [:python_version == "2.6" or python_version == "3.0"]
 importlib
+unittest2
diff --git a/humanfriendly/__init__.py b/humanfriendly/__init__.py
index f60a3b0..90a0b28 100644
--- a/humanfriendly/__init__.py
+++ b/humanfriendly/__init__.py
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <peter at peterodding.com>
-# Last Change: May 18, 2017
+# Last Change: August 7, 2017
 # URL: https://humanfriendly.readthedocs.io
 
 """The main module of the `humanfriendly` package."""
@@ -39,7 +39,7 @@ from humanfriendly.prompts import prompt_for_choice  # NOQA
 from humanfriendly.compat import is_string, monotonic
 
 # Semi-standard module versioning.
-__version__ = '3.2'
+__version__ = '4.4.1'
 
 # Spinners are redrawn at most this many seconds.
 minimum_spinner_interval = 0.2
diff --git a/humanfriendly/compat.py b/humanfriendly/compat.py
index 766a4a3..fddf590 100644
--- a/humanfriendly/compat.py
+++ b/humanfriendly/compat.py
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <peter at peterodding.com>
-# Last Change: January 16, 2017
+# Last Change: July 1, 2017
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -46,6 +46,7 @@ __all__ = (
     'is_unicode',
     'monotonic',
     'unicode',
+    'unittest',
 )
 
 try:
@@ -74,6 +75,14 @@ except ImportError:
         # failing when {time,monotonic}.monotonic() are both missing.
         from time import time as monotonic
 
+try:
+    # A replacement for Python 2.6:
+    # https://pypi.python.org/pypi/unittest2/
+    import unittest2 as unittest
+except ImportError:
+    # The standard library module (on other Python versions).
+    import unittest
+
 
 def coerce_string(value):
     """
diff --git a/humanfriendly/prompts.py b/humanfriendly/prompts.py
index 5df4489..3edfebc 100644
--- a/humanfriendly/prompts.py
+++ b/humanfriendly/prompts.py
@@ -3,7 +3,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <peter at peterodding.com>
-# Last Change: May 4, 2017
+# Last Change: June 24, 2017
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -29,7 +29,7 @@ from humanfriendly.terminal import (
     terminal_supports_colors,
     warning,
 )
-from humanfriendly.text import compact, format, concatenate
+from humanfriendly.text import format, concatenate
 
 MAX_ATTEMPTS = 10
 """The number of times an interactive prompt is shown on invalid input (an integer)."""
@@ -328,23 +328,27 @@ def prepare_friendly_prompts():
     import readline  # NOQA
 
 
-def retry_limit():
+def retry_limit(limit=MAX_ATTEMPTS):
     """
-    Allow the user to provide valid input up to :data:`MAX_ATTEMPTS` times.
+    Allow the user to provide valid input up to `limit` times.
 
+    :param limit: The maximum number of attempts (a number,
+                  defaults to :data:`MAX_ATTEMPTS`).
+    :returns: A generator of numbers starting from one.
     :raises: :exc:`TooManyInvalidReplies` when an interactive prompt
              receives repeated invalid input (:data:`MAX_ATTEMPTS`).
 
     This function returns a generator for interactive prompts that want to
     repeat on invalid input without getting stuck in infinite loops.
     """
-    for i in range(MAX_ATTEMPTS):
+    for i in range(limit):
         yield i + 1
-    logger.warning("Too many invalid replies on interactive prompt, aborting! (after %i attempts)", MAX_ATTEMPTS)
-    raise TooManyInvalidReplies(compact("""
-        Received too many invalid replies on interactive prompt,
-        giving up! (tried %i times)
-    """, MAX_ATTEMPTS))
+    msg = "Received too many invalid replies on interactive prompt, giving up! (tried %i times)"
+    formatted_msg = msg % limit
+    # Make sure the event is logged.
+    logger.warning(formatted_msg)
+    # Force the caller to decide what to do now.
+    raise TooManyInvalidReplies(formatted_msg)
 
 
 class TooManyInvalidReplies(Exception):
diff --git a/humanfriendly/tables.py b/humanfriendly/tables.py
index b85ecf9..88508cb 100644
--- a/humanfriendly/tables.py
+++ b/humanfriendly/tables.py
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <peter at peterodding.com>
-# Last Change: January 29, 2016
+# Last Change: June 24, 2017
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -38,6 +38,7 @@ from humanfriendly.terminal import (
     HIGHLIGHT_COLOR,
 )
 
+# Public identifiers that require documentation.
 __all__ = (
     'format_pretty_table',
     'format_robust_table',
diff --git a/humanfriendly/testing.py b/humanfriendly/testing.py
new file mode 100644
index 0000000..c73aaa9
--- /dev/null
+++ b/humanfriendly/testing.py
@@ -0,0 +1,690 @@
+# Human friendly input/output in Python.
+#
+# Author: Peter Odding <peter at peterodding.com>
+# Last Change: July 16, 2017
+# URL: https://humanfriendly.readthedocs.io
+
+"""
+Utility classes and functions that make it easy to write :mod:`unittest` compatible test suites.
+
+Over the years I've developed the habit of writing test suites for Python
+projects using the :mod:`unittest` module. During those years I've come to know
+pytest_ and in fact I use pytest to run my test suites (due to its much better
+error reporting) but I've yet to publish a test suite that *requires* pytest.
+I have several reasons for doing so:
+
+- It's nice to keep my test suites as simple and accessible as possible and
+  not requiring a specific test runner is part of that attitude.
+
+- Whereas :mod:`unittest` is quite explicit, pytest contains a lot of magic,
+  which kind of contradicts the Python mantra "explicit is better than
+  implicit" (IMHO).
+
+.. _pytest: https://docs.pytest.org
+"""
+
+# Standard library module
+import functools
+import logging
+import os
+import pipes
+import shutil
+import sys
+import tempfile
+import time
+
+# Modules included in our package.
+from humanfriendly.compat import StringIO, unicode, unittest
+from humanfriendly.text import compact, random_string
+
+# Initialize a logger for this module.
+logger = logging.getLogger(__name__)
+
+# A unique object reference used to detect missing attributes.
+NOTHING = object()
+
+# Public identifiers that require documentation.
+__all__ = (
+    'CallableTimedOut',
+    'CaptureOutput',
+    'ContextManager',
+    'CustomSearchPath',
+    'MockedProgram',
+    'PatchedAttribute',
+    'PatchedItem',
+    'TemporaryDirectory',
+    'TestCase',
+    'configure_logging',
+    'make_dirs',
+    'retry',
+    'run_cli',
+    'touch',
+)
+
+
+def configure_logging(log_level=logging.DEBUG):
+    """configure_logging(log_level=logging.DEBUG)
+    Automatically configure logging to the terminal.
+
+    :param log_level: The log verbosity (a number, defaults to
+                      :data:`logging.DEBUG`).
+
+    When :mod:`coloredlogs` is installed :func:`coloredlogs.install()` will be
+    used to configure logging to the terminal. When this fails with an
+    :exc:`~exceptions.ImportError` then :func:`logging.basicConfig()` is used
+    as a fall back.
+    """
+    try:
+        import coloredlogs
+        coloredlogs.install(level=log_level)
+    except ImportError:
+        logging.basicConfig(
+            level=log_level,
+            format='%(asctime)s %(name)s[%(process)d] %(levelname)s %(message)s',
+            datefmt='%Y-%m-%d %H:%M:%S')
+
+
+def make_dirs(pathname):
+    """
+    Create missing directories.
+
+    :param pathname: The pathname of a directory (a string).
+    """
+    if not os.path.isdir(pathname):
+        os.makedirs(pathname)
+
+
+def retry(func, timeout=60, exc_type=AssertionError):
+    """retry(func, timeout=60, exc_type=AssertionError)
+    Retry a function until assertions no longer fail.
+
+    :param func: A callable. When the callable returns
+                 :data:`False` it will also be retried.
+    :param timeout: The number of seconds after which to abort (a number,
+                    defaults to 60).
+    :param exc_type: The type of exceptions to retry (defaults
+                     to :exc:`~exceptions.AssertionError`).
+    :returns: The value returned by `func`.
+    :raises: Once the timeout has expired :func:`retry()` will raise the
+             previously retried assertion error. When `func` keeps returning
+             :data:`False` until `timeout` expires :exc:`CallableTimedOut`
+             will be raised.
+
+    This function sleeps between retries to avoid claiming CPU cycles we don't
+    need. It starts by sleeping for 0.1 second but adjusts this to one second
+    as the number of retries grows.
+    """
+    pause = 0.1
+    timeout += time.time()
+    while True:
+        try:
+            result = func()
+            if result is not False:
+                return result
+        except exc_type:
+            if time.time() > timeout:
+                raise
+        else:
+            if time.time() > timeout:
+                raise CallableTimedOut()
+        time.sleep(pause)
+        if pause < 1:
+            pause *= 2
+
+
+def run_cli(entry_point, *arguments, **options):
+    """
+    Test a command line entry point.
+
+    :param entry_point: The function that implements the command line interface
+                        (a callable).
+    :param arguments: Any positional arguments (strings) become the command
+                      line arguments (:data:`sys.argv` items 1-N).
+    :param options: The following keyword arguments are supported:
+
+                    **input**
+                     Refer to :class:`CaptureOutput`.
+                    **merged**
+                     Refer to :class:`CaptureOutput`.
+                    **program_name**
+                     Used to set :data:`sys.argv` item 0.
+    :returns: A tuple with two values:
+
+              1. The return code (an integer).
+              2. The captured output (a string).
+    """
+    merged = options.get('merged', False)
+    # Add the `program_name' option to the arguments.
+    arguments = list(arguments)
+    arguments.insert(0, options.pop('program_name', sys.executable))
+    # Log the command line arguments (and the fact that we're about to call the
+    # command line entry point function).
+    logger.debug("Calling command line entry point with arguments: %s", arguments)
+    # Prepare to capture the return code and output even if the command line
+    # interface raises an exception (whether the exception type is SystemExit
+    # or something else).
+    returncode = 0
+    stdout = None
+    stderr = None
+    try:
+        # Temporarily override sys.argv.
+        with PatchedAttribute(sys, 'argv', arguments):
+            # Manipulate the standard input/output/error streams.
+            with CaptureOutput(**options) as capturer:
+                try:
+                    # Call the command line interface.
+                    entry_point()
+                finally:
+                    # Get the output even if an exception is raised.
+                    stdout = capturer.stdout.getvalue()
+                    stderr = capturer.stderr.getvalue()
+                    # Reconfigure logging to the terminal because it is very
+                    # likely that the entry point function has changed the
+                    # configured log level.
+                    configure_logging()
+    except BaseException as e:
+        if isinstance(e, SystemExit):
+            logger.debug("Intercepting return code %s from SystemExit exception.", e.code)
+            returncode = e.code
+        else:
+            logger.warning("Defaulting return code to 1 due to raised exception.", exc_info=True)
+            returncode = 1
+    else:
+        logger.debug("Command line entry point returned successfully!")
+    # Always log the output captured on stdout/stderr, to make it easier to
+    # diagnose test failures (but avoid duplicate logging when merged=True).
+    merged_streams = [('merged streams', stdout)]
+    separate_streams = [('stdout', stdout), ('stderr', stderr)]
+    streams = merged_streams if merged else separate_streams
+    for name, value in streams:
+        if value:
+            logger.debug("Output on %s:\n%s", name, value)
+        else:
+            logger.debug("No output on %s.", name)
+    return returncode, stdout
+
+
+def touch(filename):
+    """
+    The equivalent of the UNIX ``touch`` program in Python.
+
+    :param filename: The pathname of the file to touch (a string).
+
+    Note that missing directories are automatically created using
+    :func:`make_dirs()`.
+    """
+    make_dirs(os.path.dirname(filename))
+    with open(filename, 'a'):
+        os.utime(filename, None)
+
+
+class CallableTimedOut(Exception):
+
+    """Raised by :func:`retry()` when the timeout expires."""
+
+
+class ContextManager(object):
+
+    """Base class to enable composition of context managers."""
+
+    def __enter__(self):
+        """Enable use as context managers."""
+        return self
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Enable use as context managers."""
+
+
+class PatchedAttribute(ContextManager):
+
+    """Context manager that temporary replaces an object attribute using :func:`setattr()`."""
+
+    def __init__(self, obj, name, value):
+        """
+        Initialize a :class:`PatchedAttribute` object.
+
+        :param obj: The object to patch.
+        :param name: An attribute name.
+        :param value: The value to set.
+        """
+        self.object_to_patch = obj
+        self.attribute_to_patch = name
+        self.patched_value = value
+        self.original_value = NOTHING
+
+    def __enter__(self):
+        """
+        Replace (patch) the attribute.
+
+        :returns: The object whose attribute was patched.
+        """
+        # Enable composition of context managers.
+        super(PatchedAttribute, self).__enter__()
+        # Patch the object's attribute.
+        self.original_value = getattr(self.object_to_patch, self.attribute_to_patch, NOTHING)
+        setattr(self.object_to_patch, self.attribute_to_patch, self.patched_value)
+        return self.object_to_patch
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Restore the attribute to its original value."""
+        # Enable composition of context managers.
+        super(PatchedAttribute, self).__exit__(exc_type, exc_value, traceback)
+        # Restore the object's attribute.
+        if self.original_value is NOTHING:
+            delattr(self.object_to_patch, self.attribute_to_patch)
+        else:
+            setattr(self.object_to_patch, self.attribute_to_patch, self.original_value)
+
+
+class PatchedItem(ContextManager):
+
+    """Context manager that temporary replaces an object item using :func:`~object.__setitem__()`."""
+
+    def __init__(self, obj, item, value):
+        """
+        Initialize a :class:`PatchedItem` object.
+
+        :param obj: The object to patch.
+        :param item: The item to patch.
+        :param value: The value to set.
+        """
+        self.object_to_patch = obj
+        self.item_to_patch = item
+        self.patched_value = value
+        self.original_value = NOTHING
+
+    def __enter__(self):
+        """
+        Replace (patch) the item.
+
+        :returns: The object whose item was patched.
+        """
+        # Enable composition of context managers.
+        super(PatchedItem, self).__enter__()
+        # Patch the object's item.
+        try:
+            self.original_value = self.object_to_patch[self.item_to_patch]
+        except KeyError:
+            self.original_value = NOTHING
+        self.object_to_patch[self.item_to_patch] = self.patched_value
+        return self.object_to_patch
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Restore the item to its original value."""
+        # Enable composition of context managers.
+        super(PatchedItem, self).__exit__(exc_type, exc_value, traceback)
+        # Restore the object's item.
+        if self.original_value is NOTHING:
+            del self.object_to_patch[self.item_to_patch]
+        else:
+            self.object_to_patch[self.item_to_patch] = self.original_value
+
+
+class TemporaryDirectory(ContextManager):
+
+    """
+    Easy temporary directory creation & cleanup using the :keyword:`with` statement.
+
+    Here's an example of how to use this:
+
+    .. code-block:: python
+
+       with TemporaryDirectory() as directory:
+           # Do something useful here.
+           assert os.path.isdir(directory)
+    """
+
+    def __init__(self, **options):
+        """
+        Initialize a :class:`TemporaryDirectory` object.
+
+        :param options: Any keyword arguments are passed on to
+                        :func:`tempfile.mkdtemp()`.
+        """
+        self.mkdtemp_options = options
+        self.temporary_directory = None
+
+    def __enter__(self):
+        """
+        Create the temporary directory using :func:`tempfile.mkdtemp()`.
+
+        :returns: The pathname of the directory (a string).
+        """
+        # Enable composition of context managers.
+        super(TemporaryDirectory, self).__enter__()
+        # Create the temporary directory.
+        self.temporary_directory = tempfile.mkdtemp(**self.mkdtemp_options)
+        return self.temporary_directory
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Cleanup the temporary directory using :func:`shutil.rmtree()`."""
+        # Enable composition of context managers.
+        super(TemporaryDirectory, self).__exit__(exc_type, exc_value, traceback)
+        # Cleanup the temporary directory.
+        if self.temporary_directory is not None:
+            shutil.rmtree(self.temporary_directory)
+            self.temporary_directory = None
+
+
+class MockedHomeDirectory(PatchedItem, TemporaryDirectory):
+
+    """
+    Context manager to temporarily change ``$HOME`` (the current user's profile directory).
+
+    This class is a composition of the :class:`PatchedItem` and
+    :class:`TemporaryDirectory` context managers.
+    """
+
+    def __init__(self):
+        """Initialize a :class:`MockedHomeDirectory` object."""
+        PatchedItem.__init__(self, os.environ, 'HOME', os.environ.get('HOME'))
+        TemporaryDirectory.__init__(self)
+
+    def __enter__(self):
+        """
+        Activate the custom ``$PATH``.
+
+        :returns: The pathname of the directory that has
+                  been added to ``$PATH`` (a string).
+        """
+        # Get the temporary directory.
+        directory = TemporaryDirectory.__enter__(self)
+        # Override the value to patch now that we have
+        # the pathname of the temporary directory.
+        self.patched_value = directory
+        # Temporary patch $HOME.
+        PatchedItem.__enter__(self)
+        # Pass the pathname of the temporary directory to the caller.
+        return directory
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Deactivate the custom ``$HOME``."""
+        super(MockedHomeDirectory, self).__exit__(exc_type, exc_value, traceback)
+
+
+class CustomSearchPath(PatchedItem, TemporaryDirectory):
+
+    """
+    Context manager to temporarily customize ``$PATH`` (the executable search path).
+
+    This class is a composition of the :class:`PatchedItem` and
+    :class:`TemporaryDirectory` context managers.
+    """
+
+    def __init__(self, isolated=False):
+        """
+        Initialize a :class:`CustomSearchPath` object.
+
+        :param isolated: :data:`True` to clear the original search path,
+                         :data:`False` to add the temporary directory to the
+                         start of the search path.
+        """
+        # Initialize our own instance variables.
+        self.isolated_search_path = isolated
+        # Selectively initialize our superclasses.
+        PatchedItem.__init__(self, os.environ, 'PATH', self.current_search_path)
+        TemporaryDirectory.__init__(self)
+
+    def __enter__(self):
+        """
+        Activate the custom ``$PATH``.
+
+        :returns: The pathname of the directory that has
+                  been added to ``$PATH`` (a string).
+        """
+        # Get the temporary directory.
+        directory = TemporaryDirectory.__enter__(self)
+        # Override the value to patch now that we have
+        # the pathname of the temporary directory.
+        self.patched_value = (
+            directory if self.isolated_search_path
+            else os.pathsep.join([directory] + self.current_search_path.split(os.pathsep))
+        )
+        # Temporary patch the $PATH.
+        PatchedItem.__enter__(self)
+        # Pass the pathname of the temporary directory to the caller
+        # because they may want to `install' custom executables.
+        return directory
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Deactivate the custom ``$PATH``."""
+        super(CustomSearchPath, self).__exit__(exc_type, exc_value, traceback)
+
+    @property
+    def current_search_path(self):
+        """The value of ``$PATH`` or :data:`os.defpath` (a string)."""
+        return os.environ.get('PATH', os.defpath)
+
+
+class MockedProgram(CustomSearchPath):
+
+    """
+    Context manager to mock the existence of a program (executable).
+
+    This class extends the functionality of :class:`CustomSearchPath`.
+    """
+
+    def __init__(self, name, returncode=0):
+        """
+        Initialize a :class:`MockedProgram` object.
+
+        :param name: The name of the program (a string).
+        :param returncode: The return code that the program should emit (a
+                           number, defaults to zero).
+        """
+        # Initialize our own instance variables.
+        self.program_name = name
+        self.program_returncode = returncode
+        self.program_signal_file = None
+        # Initialize our superclasses.
+        super(MockedProgram, self).__init__()
+
+    def __enter__(self):
+        """
+        Create the mock program.
+
+        :returns: The pathname of the directory that has
+                  been added to ``$PATH`` (a string).
+        """
+        directory = super(MockedProgram, self).__enter__()
+        self.program_signal_file = os.path.join(directory, 'program-was-run-%s' % random_string(10))
+        pathname = os.path.join(directory, self.program_name)
+        with open(pathname, 'w') as handle:
+            handle.write('#!/bin/sh\n')
+            handle.write('echo > %s\n' % pipes.quote(self.program_signal_file))
+            handle.write('exit %i\n' % self.program_returncode)
+        os.chmod(pathname, 0o755)
+        return directory
+
+    def __exit__(self, *args, **kw):
+        """
+        Ensure that the mock program was run.
+
+        :raises: :exc:`~exceptions.AssertionError` when
+                 the mock program hasn't been run.
+        """
+        try:
+            assert self.program_signal_file and os.path.isfile(self.program_signal_file), \
+                ("It looks like %r was never run!" % self.program_name)
+        finally:
+            return super(MockedProgram, self).__exit__(*args, **kw)
+
+
+class CaptureOutput(ContextManager):
+
+    """Context manager that captures what's written to :data:`sys.stdout` and :data:`sys.stderr`."""
+
+    def __init__(self, merged=False, input=''):
+        """
+        Initialize a :class:`CaptureOutput` object.
+
+        :param merged: :data:`True` to merge the streams,
+                       :data:`False` to capture them separately.
+        :param input: The data that reads from :data:`sys.stdin`
+                      should return (a string).
+        """
+        self.stdin = StringIO(input)
+        self.stdout = StringIO()
+        self.stderr = self.stdout if merged else StringIO()
+        self.patched_attributes = [
+            PatchedAttribute(sys, name, getattr(self, name))
+            for name in ('stdin', 'stdout', 'stderr')
+        ]
+
+    stdin = None
+    """The :class:`~humanfriendly.compat.StringIO` object used to feed the standard input stream."""
+
+    stdout = None
+    """The :class:`~humanfriendly.compat.StringIO` object used to capture the standard output stream."""
+
+    stderr = None
+    """The :class:`~humanfriendly.compat.StringIO` object used to capture the standard error stream."""
+
+    def __enter__(self):
+        """Start capturing what's written to :data:`sys.stdout` and :data:`sys.stderr`."""
+        super(CaptureOutput, self).__enter__()
+        for context in self.patched_attributes:
+            context.__enter__()
+        return self
+
+    def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+        """Stop capturing what's written to :data:`sys.stdout` and :data:`sys.stderr`."""
+        super(CaptureOutput, self).__exit__(exc_type, exc_value, traceback)
+        for context in self.patched_attributes:
+            context.__exit__(exc_type, exc_value, traceback)
+
+    def getvalue(self):
+        """Get the text written to :data:`sys.stdout`."""
+        return self.stdout.getvalue()
+
+
... 778 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/humanfriendly.git



More information about the Python-modules-commits mailing list