[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