diff -Nru python-daemon-1.5.5/ChangeLog python-daemon-2.0.5/ChangeLog --- python-daemon-1.5.5/ChangeLog 2010-03-02 09:26:36.000000000 +0000 +++ python-daemon-2.0.5/ChangeLog 2015-02-02 04:43:28.000000000 +0000 @@ -1,187 +1,380 @@ -2010-03-02 Ben Finney +Version 2.0.5 +============= - Version 1.5.5 released. +:Released: 2015-02-02 +:Maintainer: Ben Finney - * Stop using ‘pkg_resources’ and revert to pre-1.5.3 version-string - handling, until a better way that doesn't break everyone else's - installation can be found. +* Refine compatibility of exceptions for file operations. +* Specify the text encoding when opening the changelog file. -2010-02-27 Ben Finney - Version 1.5.4 released. +Version 2.0.4 +============= - * MANIFEST.in: Explicitly include version data file, otherwise - everything breaks for users of the sdist. +:Released: 2015-01-23 +:Maintainer: Ben Finney -2010-02-26 Ben Finney +* Record version info via Setuptools commands. +* Remove the custom Setuptools entry points. + This closes Alioth bug#314948. - Version 1.5.3 released. - * daemon/daemon.py: Invoke the pidfile context manager's ‘__exit__’ - method with the correct arguments (as per - ). - Thanks to Ludvig Ericson for the bug report. - * version: New plain-text data file to store project version string. - * setup.py: Read version string from data file. - * daemon/version/__init__.py: Query version string with ‘pkg_resources’. +Version 2.0.3 +============= -2010-01-20 Ben Finney +:Released: 2015-01-14 +:Maintainer: Ben Finney - * Add ‘pylint’ configuration for this project. - * Update copyright notices. +* Break circular import dependency for ‘setup.py’. +* Refactor all initial metadata functionality to ‘daemon._metadata’. +* Distribute ‘version’ (and its tests) only in source, not install. +* Build a “universal” (Python 2 and Python 3) wheel. -2009-10-24 Ben Finney - Version 1.5.2 released. +Version 2.0.2 +============= -2009-10-19 Ben Finney +:Released: 2015-01-13 +:Maintainer: Ben Finney - * Ensure we only prevent core dumps if ‘prevent_core’ is true. - Thanks to Denis Bilenko for reporting the lacking implementation of - this documented option. +* Declare test-time dependency on recent ‘unittest2’. +* Declare packaging-time dependency on ‘docutils’ library. +* Include unit tests for ‘version’ module with source distribution. +* Record version info consistent with distribution metadata. -2009-09-28 Ben Finney - * Add initial Frequently Asked Questions document. +Version 2.0.1 +============= -2009-09-26 Ben Finney +:Released: 2015-01-11 +:Maintainer: Ben Finney - Version 1.5.1 released. +* Include the ‘version’ module with source distribution. - * Make a separate collection of DaemonRunner test scenarios. - * Handle a start request with a timeout on the PID file lock acquire. -2009-09-24 Ben Finney +Version 2.0 +=========== - * Implement ‘TimeoutPIDLockFile’ to specify a timeout in advance of - lock acquisition. - * Use lock with timeout for ‘DaemonRunner’. +:Released: 2015-01-10 +:Maintainer: Ben Finney -2009-09-24 Ben Finney +* Support both Python 3 (version 3.2 or later) and Python 2 (version + 2.7 or later). +* Document the API of all functions comprehensively in docstrings. +* Add a hacking guide for developers. +* Add explicit credit for contributors. +* Document the security impact of the default umask. - Version 1.5 released. +* Specify explicit text or binary mode when opening files. +* Preserve exception context in custom exceptions. - * Make a separate collection of PIDLockFile test scenarios. +* Declare compatibility with current Python versions. +* Depend on Python 3 compatible libraries. +* Update package homepage to Alioth hosted project page. +* Use ‘pydoc.splitdoc’ to get package description text. +* Remove ASCII translation of package description, not needed now the + docstring is a proper Unicode text value. +* Include test suite with source distribution. +* Move package metadata to ‘daemon/_metadata.py’. +* Migrate to JSON (instead of Python) for serialised version info. +* Add unit tests for metadata. +* Store and retrieve version info in Setuptools metadata. -2009-09-23 Ben Finney +* Migrate to ‘str.format’ for interpolation of values into text. +* Migrate to ‘mock’ library for mock objects in tests. +* Migrate to ‘testscenarios’ library for unit test scenarios. +* Migrate to ‘unittest2’ library for back-ported improvements. + Remove custom test suite creation. +* Discriminate Python 2-and-3 compatible usage of dict methods. +* Discriminate Python 2-and-3 compatible bytes versus text. +* Declare explicit absolute and relative imports. +* Discriminate between different ‘fileno’ method behaviours. + In Python 3, ‘StringIO.fileno’ is callable but raises an exception. +* Migrate to built-in ‘next’ function. +* Wrap the ‘fromlist’ parameter of ‘__import__’ for Python 3 + compatibility. +* Wrap function introspection for Python 3 compatibility. +* Wrap standard library imports where names changed in Python 3. - * Raise specific errors on ‘DaemonRunner’ failures. - * Distinguish different conditions on reading and parsing PID file. - * Refactor code to ‘_terminate_daemon_process’ method. - * Improve explanations in comments and docstrings. - * Don't set pidfile at all if no path specified to constructor. - * Write the PID file using correct OS locking and permissions. - * Close the PID file after writing. - * Implement ‘PIDLockFile’ as subclass of ‘lockfile.LinkFileLock’. - * Remove redundant checks for file existence. -2009-09-18 Ben Finney +Version 1.6.1 +============= - * Manage the excluded file descriptors as a set (not a list). - * Only inspect the file descriptor of streams if they actually have - one (via a ‘fileno’ method) when determining which file descriptors - to close. Thanks to Ask Solem for revealing this bug. +:Released: 2014-08-04 +:Maintainer: Ben Finney -2009-09-17 Ben Finney +* Use unambiguous “except FooType as foo” syntax. + This is to ease the port to Python 3, where the ambiguous comma + usage is an error. +* Ensure a ‘basestring’ name bound to the base type for strings. + This is to allow checks to work on Python 2 and 3. +* Specify versions of Python supported, as trove classifiers. - Version 1.4.8 released. +* Update copyright notices. +* Add editor hints for most files. +* Distinguish continuation-line indentation versus block indentation. - * Remove child-exit signal (‘SIGCLD’, ‘SIGCHLD’) from default signal - map. Thanks to Joel Martin for pinpointing this issue. - * Document requirement for ensuring any operating-system specific - signal handlers are considered. - * Refactor ‘fork_then_exit_parent’ functionality to avoid duplicate - code. - * Remove redundant imports. - * Remove unused code from unit test suite scaffold. - * Add specific license terms for unit test suite scaffold. +* Use unicode literals by default, specifying bytes where necessary. + This is to ease the port to Python 3, where the default string type + is unicode. +* Update copyright notices. +* Update the GPL license file to version 3, as declared in our + copyright notices. -2009-09-03 Ben Finney +* Change license of library code to Apache License 2.0. Rationale at + . - Version 1.4.7 released. -2009-09-02 Ben Finney +Version 1.6 +=========== - * Fix keywords argument for distribution setup. - * Exclude ‘test’ package from distribution installation. +:Released: 2010-05-10 +:Maintainer: Ben Finney -2009-06-21 Ben Finney +* Use absolute imports to disambiguate provenance of names. +* setup.py: Require ‘lockfile >=0.9’. +* daemon/pidfile.py: Renamed from ‘daemon/pidlockfile.py’. Change + references elsewhere to use this new name. +* test/test_pidfile.py: Renamed from ‘test/test_pidlockfile.py’. + Change references elsewhere to use this new name. +* daemon/pidfile.py: Remove functionality now migrated to ‘lockfile’ + library. - Version 1.4.6 released. +* FAQ: Add some entries and re-structure the document. - * Update documentation for changes from latest PEP 3143 revision. - * Implement DaemonContext.is_open method. +* Use ‘unicode’ data type for all text values. +* Prepare for Python 3 upgrade by tweaking some names and imports. -2009-05-17 Ben Finney +* MANIFEST.in: Include the documentation in the distribution. - Version 1.4.5 released. - * Register DaemonContext.close method for atexit processing. - * Move PID file cleanup to close method. - * Improve docstrings by reference to, and copy from, PEP 3143. - * Use mock checking capabilities of newer ‘MiniMock’ library. - * Automate building a versioned distribution tarball. - * Include developer documentation files in source distribution. +Version 1.5.5 +============= -2009-03-26 Ben Finney +:Released: 2010-03-02 +:Maintainer: Ben Finney - Version 1.4.4 released. +* Stop using ‘pkg_resources’ and revert to pre-1.5.3 version-string + handling, until a better way that doesn't break everyone else's + installation can be found. - * Conform to current PEP version, now released as PEP 3143 “Standard - daemon process library”. - * Ensure UID and GID are set in correct order. - * Delay closing all open files until just before re-binding standard - streams. - * Redirect standard streams to null device by default. -2009-03-19 Ben Finney +Version 1.5.4 +============= - Version 1.4.3 released. +:Released: 2010-02-27 +:Maintainer: Ben Finney - * Close the PID file context on exit. +* MANIFEST.in: Explicitly include version data file, otherwise + everything breaks for users of the sdist. -2009-03-18 Ben Finney - Version 1.4.2 released. +Version 1.5.3 +============= - * Context manager methods for DaemonContext. +:Released: 2010-02-26 +:Maintainer: Ben Finney -2009-03-18 Ben Finney +* daemon/daemon.py: Invoke the pidfile context manager's ‘__exit__’ + method with the correct arguments (as per + ). + Thanks to Ludvig Ericson for the bug report. +* version: New plain-text data file to store project version string. +* setup.py: Read version string from data file. +* daemon/version/__init__.py: Query version string with ‘pkg_resources’. - Version 1.4.1 released. +* Add ‘pylint’ configuration for this project. +* Update copyright notices. - * Improvements to docstrings. - * Further conformance with draft PEP. -2009-03-17 Ben Finney +Version 1.5.2 +============= - Version 1.4 released. +:Released: 2009-10-24 +:Maintainer: Ben Finney - * Implement the interface from a draft PEP for process daemonisation. - * Complete statement coverage from unit test suite. +* Ensure we only prevent core dumps if ‘prevent_core’ is true. + Thanks to Denis Bilenko for reporting the lacking implementation of + this documented option. -2009-03-12 Ben Finney +* Add initial Frequently Asked Questions document. - Version 1.3 released. - * Separate controller (now ‘DaemonRunner’) from daemon process - context (now ‘DaemonContext’). - * Fix many corner cases and bugs. - * Huge increase in unit test suite. +Version 1.5.1 +============= -2009-01-27 Ben Finney +:Released: 2009-09-26 +:Maintainer: Ben Finney - Version 1.2 released. +* Make a separate collection of DaemonRunner test scenarios. +* Handle a start request with a timeout on the PID file lock acquire. - * Initial release of this project forked from ‘bda.daemon’. Thanks, - Robert Niederreiter. - * Refactor some functionality out to helper functions. - * Begin unit test suite. +* Implement ‘TimeoutPIDLockFile’ to specify a timeout in advance of + lock acquisition. +* Use lock with timeout for ‘DaemonRunner’. + + +Version 1.5 +=========== + +:Released: 2009-09-24 +:Maintainer: Ben Finney + +* Make a separate collection of PIDLockFile test scenarios. + +* Raise specific errors on ‘DaemonRunner’ failures. +* Distinguish different conditions on reading and parsing PID file. +* Refactor code to ‘_terminate_daemon_process’ method. +* Improve explanations in comments and docstrings. +* Don't set pidfile at all if no path specified to constructor. +* Write the PID file using correct OS locking and permissions. +* Close the PID file after writing. +* Implement ‘PIDLockFile’ as subclass of ‘lockfile.LinkFileLock’. +* Remove redundant checks for file existence. + +* Manage the excluded file descriptors as a set (not a list). +* Only inspect the file descriptor of streams if they actually have + one (via a ‘fileno’ method) when determining which file descriptors + to close. Thanks to Ask Solem for revealing this bug. + + +Version 1.4.8 +============= + +:Released: 2009-09-17 +:Maintainer: Ben Finney + +* Remove child-exit signal (‘SIGCLD’, ‘SIGCHLD’) from default signal + map. Thanks to Joel Martin for pinpointing this issue. +* Document requirement for ensuring any operating-system specific + signal handlers are considered. +* Refactor ‘fork_then_exit_parent’ functionality to avoid duplicate + code. +* Remove redundant imports. +* Remove unused code from unit test suite scaffold. +* Add specific license terms for unit test suite scaffold. + + +Version 1.4.7 +============= + +:Released: 2009-09-03 +:Maintainer: Ben Finney + +* Fix keywords argument for distribution setup. +* Exclude ‘test’ package from distribution installation. + + +Version 1.4.6 +============= + +:Released: 2009-06-21 +:Maintainer: Ben Finney + +* Update documentation for changes from latest PEP 3143 revision. +* Implement DaemonContext.is_open method. + + +Version 1.4.5 +============= + +:Released: 2009-05-17 +:Maintainer: Ben Finney + +* Register DaemonContext.close method for atexit processing. +* Move PID file cleanup to close method. +* Improve docstrings by reference to, and copy from, PEP 3143. +* Use mock checking capabilities of newer ‘MiniMock’ library. +* Automate building a versioned distribution tarball. +* Include developer documentation files in source distribution. + + +Version 1.4.4 +============= + +:Released: 2009-03-26 +:Maintainer: Ben Finney + +* Conform to current PEP version, now released as PEP 3143 “Standard + daemon process library”. +* Ensure UID and GID are set in correct order. +* Delay closing all open files until just before re-binding standard + streams. +* Redirect standard streams to null device by default. + + +Version 1.4.3 +============= + +:Released: 2009-03-19 +:Maintainer: Ben Finney + +* Close the PID file context on exit. + + +Version 1.4.2 +============= + +:Released: 2009-03-18 +:Maintainer: Ben Finney + +* Context manager methods for DaemonContext. + + +Version 1.4.1 +============= + +:Released: 2009-03-18 +:Maintainer: Ben Finney + +* Improvements to docstrings. +* Further conformance with draft PEP. + + +Version 1.4 +=========== + +:Released: 2009-03-17 +:Maintainer: Ben Finney + +* Implement the interface from a draft PEP for process daemonisation. +* Complete statement coverage from unit test suite. + + +Version 1.3 +=========== + +:Released: 2009-03-12 +:Maintainer: Ben Finney + +* Separate controller (now ‘DaemonRunner’) from daemon process + context (now ‘DaemonContext’). +* Fix many corner cases and bugs. +* Huge increase in unit test suite. + + +Version 1.2 +=========== + +:Released: 2009-01-27 +:Maintainer: Ben Finney + +* Initial release of this project forked from ‘bda.daemon’. Thanks, + Robert Niederreiter. +* Refactor some functionality out to helper functions. +* Begin unit test suite. -Local variables: -mode: change-log -coding: utf-8 -left-margin: 4 -indent-tabs-mode: nil -End: +.. + This is free software: you may copy, modify, and/or distribute this work + under the terms of the Apache License version 2.0 as published by the + Apache Software Foundation. + No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + End: + vim: fileencoding=utf-8 filetype=rst : diff -Nru python-daemon-1.5.5/daemon/daemon.py python-daemon-2.0.5/daemon/daemon.py --- python-daemon-1.5.5/daemon/daemon.py 2010-02-26 03:09:27.000000000 +0000 +++ python-daemon-2.0.5/daemon/daemon.py 2015-02-02 04:43:28.000000000 +0000 @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # daemon/daemon.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2008–2010 Ben Finney +# Copyright © 2008–2015 Ben Finney # Copyright © 2007–2008 Robert Niederreiter, Jens Klein # Copyright © 2004–2005 Chad J. Schroeder # Copyright © 2003 Clark Evans @@ -11,13 +11,15 @@ # Copyright © 2001 Jürgen Hermann # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. """ Daemon process behaviour. """ +from __future__ import (absolute_import, unicode_literals) + import os import sys import resource @@ -25,11 +27,27 @@ import signal import socket import atexit +try: + # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text). + basestring = basestring + unicode = unicode +except NameError: + # Python 3 names the Unicode data type ‘str’. + basestring = str + unicode = str class DaemonError(Exception): """ Base exception class for errors from this module. """ + def __init__(self, *args, **kwargs): + self._chain_from_context() + + super(DaemonError, self).__init__(*args, **kwargs) + + def _chain_from_context(self): + _chain_exception_from_existing_exception_context(self, as_cause=True) + class DaemonOSEnvironmentError(DaemonError, OSError): """ Exception raised when daemon OS environment setup receives error. """ @@ -39,7 +57,7 @@ """ Exception raised when process detach fails. """ -class DaemonContext(object): +class DaemonContext: """ Context for turning the current program into a daemon process. A `DaemonContext` instance represents the behaviour settings and @@ -98,9 +116,15 @@ File access creation mask (“umask”) to set for the process on daemon start. - Since a process inherits its umask from its parent process, - starting the daemon will reset the umask to this value so that - files are created by the daemon with access modes as it expects. + A daemon should not rely on the parent process's umask value, + which is beyond its control and may prevent creating a file with + the required access mode. So when the daemon context opens, the + umask is set to an explicit known value. + + If the conventional value of 0 is too open, consider setting a + value such as 0o022, 0o027, 0o077, or another specific value. + Otherwise, ensure the daemon creates every file with an + explicit access mode for the purpose. `pidfile` :Default: ``None`` @@ -191,7 +215,8 @@ which will be used as the new file for the standard I/O stream `sys.stdin`, `sys.stdout`, and `sys.stderr` respectively. The file should therefore be open, with a minimum of mode 'r' in the case - of `stdin`, and mode 'w+' in the case of `stdout` and `stderr`. + of `stdin`, and mimimum of mode 'w+' in the case of `stdout` and + `stderr`. If the object has a `fileno()` method that returns a file descriptor, the corresponding file will be excluded from being @@ -203,22 +228,24 @@ """ + __metaclass__ = type + def __init__( - self, - chroot_directory=None, - working_directory='/', - umask=0, - uid=None, - gid=None, - prevent_core=True, - detach_process=None, - files_preserve=None, - pidfile=None, - stdin=None, - stdout=None, - stderr=None, - signal_map=None, - ): + self, + chroot_directory=None, + working_directory="/", + umask=0, + uid=None, + gid=None, + prevent_core=True, + detach_process=None, + files_preserve=None, + pidfile=None, + stdin=None, + stdout=None, + stderr=None, + signal_map=None, + ): """ Set up a new instance. """ self.chroot_directory = chroot_directory self.working_directory = working_directory @@ -254,7 +281,8 @@ def open(self): """ Become a daemon process. - :Return: ``None`` + + :return: ``None``. Open the daemon context, turning the current program into a daemon process. This performs the following steps: @@ -354,7 +382,8 @@ def close(self): """ Exit the daemon process context. - :Return: ``None`` + + :return: ``None``. Close the daemon context. This performs the following steps: @@ -385,7 +414,11 @@ def terminate(self, signal_number, stack_frame): """ Signal handler for end-process signals. - :Return: ``None`` + + :param signal_number: The OS signal number received. + :param stack_frame: The frame object at the point the + signal was received. + :return: ``None``. Signal handler for the ``signal.SIGTERM`` signal. Performs the following step: @@ -394,22 +427,25 @@ """ exception = SystemExit( - "Terminating on signal %(signal_number)r" - % vars()) + "Terminating on signal {signal_number!r}".format( + signal_number=signal_number)) raise exception def _get_exclude_file_descriptors(self): - """ Return the set of file descriptors to exclude closing. + """ Get the set of file descriptors to exclude closing. + + :return: A set containing the file descriptors for the + files to be preserved. - Returns a set containing the file descriptors for the + The file descriptors to be preserved are those from the items in `files_preserve`, and also each of `stdin`, - `stdout`, and `stderr`: + `stdout`, and `stderr`. For each item: * If the item is ``None``, it is omitted from the return set. - * If the item has a ``fileno()`` method, that method's - return value is in the return set. + * If the item's ``fileno()`` method returns a value, that + value is in the return set. * Otherwise, the item is in the return set verbatim. @@ -418,30 +454,36 @@ if files_preserve is None: files_preserve = [] files_preserve.extend( - item for item in [self.stdin, self.stdout, self.stderr] - if hasattr(item, 'fileno')) + item for item in [self.stdin, self.stdout, self.stderr] + if hasattr(item, 'fileno')) + exclude_descriptors = set() for item in files_preserve: if item is None: continue - if hasattr(item, 'fileno'): - exclude_descriptors.add(item.fileno()) + file_descriptor = _get_file_descriptor(item) + if file_descriptor is not None: + exclude_descriptors.add(file_descriptor) else: exclude_descriptors.add(item) + return exclude_descriptors def _make_signal_handler(self, target): """ Make the signal handler for a specified target object. - If `target` is ``None``, returns ``signal.SIG_IGN``. If - `target` is a string, returns the attribute of this - instance named by that string. Otherwise, returns `target` - itself. + :param target: A specification of the target for the + handler; see below. + :return: The value for use by `signal.signal()`. + + If `target` is ``None``, return ``signal.SIG_IGN``. If `target` + is a text string, return the attribute of this instance named + by that string. Otherwise, return `target` itself. """ if target is None: result = signal.SIG_IGN - elif isinstance(target, basestring): + elif isinstance(target, unicode): name = target result = getattr(self, name) else: @@ -452,98 +494,135 @@ def _make_signal_handler_map(self): """ Make the map from signals to handlers for this instance. - Constructs a map from signal numbers to handlers for this + :return: The constructed signal map for this instance. + + Construct a map from signal numbers to handlers for this context instance, suitable for passing to `set_signal_handlers`. """ signal_handler_map = dict( - (signal_number, self._make_signal_handler(target)) - for (signal_number, target) in self.signal_map.items()) + (signal_number, self._make_signal_handler(target)) + for (signal_number, target) in self.signal_map.items()) return signal_handler_map + +def _get_file_descriptor(obj): + """ Get the file descriptor, if the object has one. + + :param obj: The object expected to be a file-like object. + :return: The file descriptor iff the file supports it; otherwise + ``None``. + + The object may be a non-file object. It may also be a + file-like object with no support for a file descriptor. In + either case, return ``None``. + + """ + file_descriptor = None + if hasattr(obj, 'fileno'): + try: + file_descriptor = obj.fileno() + except ValueError: + # The item doesn't support a file descriptor. + pass + + return file_descriptor + def change_working_directory(directory): """ Change the working directory of this process. + + :param directory: The target directory path. + :return: ``None``. + """ try: os.chdir(directory) - except Exception, exc: + except Exception as exc: error = DaemonOSEnvironmentError( - "Unable to change working directory (%(exc)s)" - % vars()) + "Unable to change working directory ({exc})".format(exc=exc)) raise error def change_root_directory(directory): """ Change the root directory of this process. - Sets the current working directory, then the process root - directory, to the specified `directory`. Requires appropriate - OS privileges for this process. + :param directory: The target directory path. + :return: ``None``. + + Set the current working directory, then the process root directory, + to the specified `directory`. Requires appropriate OS privileges + for this process. """ try: os.chdir(directory) os.chroot(directory) - except Exception, exc: + except Exception as exc: error = DaemonOSEnvironmentError( - "Unable to change root directory (%(exc)s)" - % vars()) + "Unable to change root directory ({exc})".format(exc=exc)) raise error def change_file_creation_mask(mask): """ Change the file creation mask for this process. + + :param mask: The numeric file creation mask to set. + :return: ``None``. + """ try: os.umask(mask) - except Exception, exc: + except Exception as exc: error = DaemonOSEnvironmentError( - "Unable to change file creation mask (%(exc)s)" - % vars()) + "Unable to change file creation mask ({exc})".format(exc=exc)) raise error def change_process_owner(uid, gid): """ Change the owning UID and GID of this process. - Sets the GID then the UID of the process (in that order, to - avoid permission errors) to the specified `gid` and `uid` - values. Requires appropriate OS privileges for this process. + :param uid: The target UID for the daemon process. + :param gid: The target GID for the daemon process. + :return: ``None``. + + Set the GID then the UID of the process (in that order, to avoid + permission errors) to the specified `gid` and `uid` values. + Requires appropriate OS privileges for this process. """ try: os.setgid(gid) os.setuid(uid) - except Exception, exc: + except Exception as exc: error = DaemonOSEnvironmentError( - "Unable to change file creation mask (%(exc)s)" - % vars()) + "Unable to change process owner ({exc})".format(exc=exc)) raise error def prevent_core_dump(): """ Prevent this process from generating a core dump. - Sets the soft and hard limits for core dump size to zero. On - Unix, this prevents the process from creating core dump - altogether. + :return: ``None``. + + Set the soft and hard limits for core dump size to zero. On Unix, + this entirely prevents the process from creating core dump. """ core_resource = resource.RLIMIT_CORE try: # Ensure the resource limit exists on this platform, by requesting - # its current value + # its current value. core_limit_prev = resource.getrlimit(core_resource) - except ValueError, exc: + except ValueError as exc: error = DaemonOSEnvironmentError( - "System does not support RLIMIT_CORE resource limit (%(exc)s)" - % vars()) + "System does not support RLIMIT_CORE resource limit" + " ({exc})".format(exc=exc)) raise error - # Set hard and soft limits to zero, i.e. no core dump at all + # Set hard and soft limits to zero, i.e. no core dump at all. core_limit = (0, 0) resource.setrlimit(core_resource, core_limit) @@ -551,31 +630,34 @@ def detach_process_context(): """ Detach the process context from parent and session. + :return: ``None``. + Detach from the parent process and session group, allowing the parent to exit while this process continues running. Reference: “Advanced Programming in the Unix Environment”, section 13.3, by W. Richard Stevens, published 1993 by Addison-Wesley. - + """ def fork_then_exit_parent(error_message): """ Fork a child process, then exit the parent process. - If the fork fails, raise a ``DaemonProcessDetachError`` - with ``error_message``. + :param error_message: Message for the exception in case of a + detach failure. + :return: ``None``. + :raise DaemonProcessDetachError: If the fork fails. """ try: pid = os.fork() if pid > 0: os._exit(0) - except OSError, exc: - exc_errno = exc.errno - exc_strerror = exc.strerror + except OSError as exc: error = DaemonProcessDetachError( - "%(error_message)s: [%(exc_errno)d] %(exc_strerror)s" % vars()) + "{message}: [{exc.errno:d}] {exc.strerror}".format( + message=error_message, exc=exc)) raise error fork_then_exit_parent(error_message="Failed first fork") @@ -584,11 +666,13 @@ def is_process_started_by_init(): - """ Determine if the current process is started by `init`. + """ Determine whether the current process is started by `init`. + + :return: ``True`` iff the parent process is `init`; otherwise + ``False``. + + The `init` process is the one with process ID of 1. - The `init` process has the process ID of 1; if that is our - parent process ID, return ``True``, otherwise ``False``. - """ result = False @@ -600,10 +684,14 @@ def is_socket(fd): - """ Determine if the file descriptor is a socket. + """ Determine whether the file descriptor is a socket. - Return ``False`` if querying the socket type of `fd` raises an - error; otherwise return ``True``. + :param fd: The file descriptor to interrogate. + :return: ``True`` iff the file descriptor is a socket; otherwise + ``False``. + + Query the socket type of `fd`. If there is no error, the file is a + socket. """ result = False @@ -612,30 +700,33 @@ try: socket_type = file_socket.getsockopt( - socket.SOL_SOCKET, socket.SO_TYPE) - except socket.error, exc: + socket.SOL_SOCKET, socket.SO_TYPE) + except socket.error as exc: exc_errno = exc.args[0] if exc_errno == errno.ENOTSOCK: - # Socket operation on non-socket + # Socket operation on non-socket. pass else: - # Some other socket error + # Some other socket error. result = True else: - # No error getting socket type + # No error getting socket type. result = True return result def is_process_started_by_superserver(): - """ Determine if the current process is started by the superserver. + """ Determine whether the current process is started by the superserver. + + :return: ``True`` if this process was started by the internet + superserver; otherwise ``False``. The internet superserver creates a network socket, and attaches it to the standard streams of the child process. If that is the case for this process, return ``True``, otherwise ``False``. - + """ result = False @@ -647,15 +738,20 @@ def is_detach_process_context_required(): - """ Determine whether detaching process context is required. + """ Determine whether detaching the process context is required. - Return ``True`` if the process environment indicates the - process is already detached: + :return: ``True`` iff the process is already detached; otherwise + ``False``. + + The process environment is interrogated for the following: * Process was started by `init`; or * Process was started by `inetd`. + If any of the above are true, the process is deemed to be already + detached. + """ result = True if is_process_started_by_init() or is_process_started_by_superserver(): @@ -667,32 +763,37 @@ def close_file_descriptor_if_open(fd): """ Close a file descriptor if already open. + :param fd: The file descriptor to close. + :return: ``None``. + Close the file descriptor `fd`, suppressing an error in the case the file was not open. """ try: os.close(fd) - except OSError, exc: + except EnvironmentError as exc: if exc.errno == errno.EBADF: - # File descriptor was not open + # File descriptor was not open. pass else: error = DaemonOSEnvironmentError( - "Failed to close file descriptor %(fd)d" - " (%(exc)s)" - % vars()) + "Failed to close file descriptor {fd:d} ({exc})".format( + fd=fd, exc=exc)) raise error MAXFD = 2048 def get_maximum_file_descriptors(): - """ Return the maximum number of open file descriptors for this process. + """ Get the maximum number of open file descriptors for this process. - Return the process hard resource limit of maximum number of - open file descriptors. If the limit is “infinity”, a default - value of ``MAXFD`` is returned. + :return: The number (integer) to use as the maximum number of open + files for this process. + + The maximum is the process hard resource limit of maximum number of + open file descriptors. If the limit is “infinity”, a default value + of ``MAXFD`` is returned. """ limits = resource.getrlimit(resource.RLIMIT_NOFILE) @@ -705,6 +806,10 @@ def close_all_open_files(exclude=set()): """ Close all open file descriptors. + :param exclude: Collection of file descriptors to skip when closing + files. + :return: ``None``. + Closes every file descriptor (if open) of this process. If specified, `exclude` is a set of file descriptors to *not* close. @@ -719,6 +824,12 @@ def redirect_stream(system_stream, target_stream): """ Redirect a system stream to a specified file. + :param standard_stream: A file object representing a standard I/O + stream. + :param target_stream: The target file object for the redirected + stream, or ``None`` to specify the null device. + :return: ``None``. + `system_stream` is a standard system stream such as ``sys.stdout``. `target_stream` is an open file object that should replace the corresponding system stream object. @@ -737,20 +848,22 @@ def make_default_signal_map(): """ Make the default signal map for this system. - The signals available differ by system. The map will not - contain any signals not defined on the running system. + :return: A mapping from signal number to handler object. + + The signals available differ by system. The map will not contain + any signals not defined on the running system. """ name_map = { - 'SIGTSTP': None, - 'SIGTTIN': None, - 'SIGTTOU': None, - 'SIGTERM': 'terminate', - } + 'SIGTSTP': None, + 'SIGTTIN': None, + 'SIGTTOU': None, + 'SIGTERM': 'terminate', + } signal_map = dict( - (getattr(signal, name), target) - for (name, target) in name_map.items() - if hasattr(signal, name)) + (getattr(signal, name), target) + for (name, target) in name_map.items() + if hasattr(signal, name)) return signal_map @@ -758,8 +871,12 @@ def set_signal_handlers(signal_handler_map): """ Set the signal handlers as specified. - The `signal_handler_map` argument is a map from signal number - to signal handler. See the `signal` module for details. + :param signal_handler_map: A map from signal number to handler + object. + :return: ``None``. + + See the `signal` module for details on signal numbers and signal + handlers. """ for (signal_number, handler) in signal_handler_map.items(): @@ -769,8 +886,41 @@ def register_atexit_function(func): """ Register a function for processing at program exit. + :param func: A callable function expecting no arguments. + :return: ``None``. + The function `func` is registered for a call with no arguments at program exit. """ atexit.register(func) + + +def _chain_exception_from_existing_exception_context(exc, as_cause=False): + """ Decorate the specified exception with the existing exception context. + + :param exc: The exception instance to decorate. + :param as_cause: If true, the existing context is declared to be + the cause of the exception. + :return: ``None``. + + :PEP:`344` describes syntax and attributes (`__traceback__`, + `__context__`, `__cause__`) for use in exception chaining. + + Python 2 does not have that syntax, so this function decorates + the exception with values from the current exception context. + + """ + (existing_exc_type, existing_exc, existing_traceback) = sys.exc_info() + if as_cause: + exc.__cause__ = existing_exc + else: + exc.__context__ = existing_exc + exc.__traceback__ = existing_traceback + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/daemon/__init__.py python-daemon-2.0.5/daemon/__init__.py --- python-daemon-1.5.5/daemon/__init__.py 2010-01-20 11:33:10.000000000 +0000 +++ python-daemon-2.0.5/daemon/__init__.py 2015-01-10 11:29:24.000000000 +0000 @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- # daemon/__init__.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2009–2010 Ben Finney +# Copyright © 2009–2015 Ben Finney # Copyright © 2006 Robert Niederreiter # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. """ Library to implement a well-behaved Unix daemon process. This library implements the well-behaved daemon specification of - :pep:`3143`, "Standard daemon process library". + :pep:`3143`, “Standard daemon process library”. A well-behaved Unix daemon process is tricky to get right, but the required steps are much the same for every daemon program. A @@ -37,11 +37,13 @@ """ -import version -from daemon import DaemonContext +from __future__ import (absolute_import, unicode_literals) + +from .daemon import DaemonContext -_version = version.version -_copyright = version.copyright -_license = version.license -_url = "http://pypi.python.org/pypi/python-daemon/" +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/daemon/_metadata.py python-daemon-2.0.5/daemon/_metadata.py --- python-daemon-1.5.5/daemon/_metadata.py 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/daemon/_metadata.py 2015-01-23 01:54:21.000000000 +0000 @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +# daemon/_metadata.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Package metadata for the ‘python-daemon’ distribution. """ + +from __future__ import (absolute_import, unicode_literals) + +import json +import re +import collections +import datetime + +import pkg_resources + + +distribution_name = "python-daemon" +version_info_filename = "version_info.json" + +def get_distribution_version_info(filename=version_info_filename): + """ Get the version info from the installed distribution. + + :param filename: Base filename of the version info resource. + :return: The version info as a mapping of fields. If the + distribution is not available, the mapping is empty. + + The version info is stored as a metadata file in the + distribution. + + """ + version_info = { + 'release_date': "UNKNOWN", + 'version': "UNKNOWN", + 'maintainer': "UNKNOWN", + } + + try: + distribution = pkg_resources.get_distribution(distribution_name) + except pkg_resources.DistributionNotFound: + distribution = None + + if distribution is not None: + if distribution.has_metadata(version_info_filename): + content = distribution.get_metadata(version_info_filename) + version_info = json.loads(content) + + return version_info + +version_info = get_distribution_version_info() + +version_installed = version_info['version'] + + +rfc822_person_regex = re.compile( + "^(?P[^<]+) <(?P[^>]+)>$") + +ParsedPerson = collections.namedtuple('ParsedPerson', ['name', 'email']) + +def parse_person_field(value): + """ Parse a person field into name and email address. + + :param value: The text value specifying a person. + :return: A 2-tuple (name, email) for the person's details. + + If the `value` does not match a standard person with email + address, the `email` item is ``None``. + + """ + result = (None, None) + + match = rfc822_person_regex.match(value) + if len(value): + if match is not None: + result = ParsedPerson( + name=match.group('name'), + email=match.group('email')) + else: + result = ParsedPerson(name=value, email=None) + + return result + +author_name = "Ben Finney" +author_email = "ben+python@benfinney.id.au" +author = "{name} <{email}>".format(name=author_name, email=author_email) + + +class YearRange: + """ A range of years spanning a period. """ + + def __init__(self, begin, end=None): + self.begin = begin + self.end = end + + def __unicode__(self): + text = "{range.begin:04d}".format(range=self) + if self.end is not None: + if self.end > self.begin: + text = "{range.begin:04d}–{range.end:04d}".format(range=self) + return text + + __str__ = __unicode__ + + +def make_year_range(begin_year, end_date=None): + """ Construct the year range given a start and possible end date. + + :param begin_date: The beginning year (text) for the range. + :param end_date: The end date (text, ISO-8601 format) for the + range, or a non-date token string. + :return: The range of years as a `YearRange` instance. + + If the `end_date` is not a valid ISO-8601 date string, the + range has ``None`` for the end year. + + """ + begin_year = int(begin_year) + + try: + end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d") + except (TypeError, ValueError): + # Specified end_date value is not a valid date. + end_year = None + else: + end_year = end_date.year + + year_range = YearRange(begin=begin_year, end=end_year) + + return year_range + +copyright_year_begin = "2001" +build_date = version_info['release_date'] +copyright_year_range = make_year_range(copyright_year_begin, build_date) + +copyright = "Copyright © {year_range} {author} and others".format( + year_range=copyright_year_range, author=author) +license = "Apache-2" +url = "https://alioth.debian.org/projects/python-daemon/" + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/daemon/pidfile.py python-daemon-2.0.5/daemon/pidfile.py --- python-daemon-1.5.5/daemon/pidfile.py 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/daemon/pidfile.py 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# daemon/pidfile.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Lockfile behaviour implemented via Unix PID files. + """ + +from __future__ import (absolute_import, unicode_literals) + +from lockfile.pidlockfile import PIDLockFile + + +class TimeoutPIDLockFile(PIDLockFile, object): + """ Lockfile with default timeout, implemented as a Unix PID file. + + This uses the ``PIDLockFile`` implementation, with the + following changes: + + * The `acquire_timeout` parameter to the initialiser will be + used as the default `timeout` parameter for the `acquire` + method. + + """ + + def __init__(self, path, acquire_timeout=None, *args, **kwargs): + """ Set up the parameters of a TimeoutPIDLockFile. + + :param path: Filesystem path to the PID file. + :param acquire_timeout: Value to use by default for the + `acquire` call. + :return: ``None``. + + """ + self.acquire_timeout = acquire_timeout + super(TimeoutPIDLockFile, self).__init__(path, *args, **kwargs) + + def acquire(self, timeout=None, *args, **kwargs): + """ Acquire the lock. + + :param timeout: Specifies the timeout; see below for valid + values. + :return: ``None``. + + The `timeout` defaults to the value set during + initialisation with the `acquire_timeout` parameter. It is + passed to `PIDLockFile.acquire`; see that method for + details. + + """ + if timeout is None: + timeout = self.acquire_timeout + super(TimeoutPIDLockFile, self).acquire(timeout, *args, **kwargs) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/daemon/pidlockfile.py python-daemon-2.0.5/daemon/pidlockfile.py --- python-daemon-1.5.5/daemon/pidlockfile.py 2010-01-20 11:33:10.000000000 +0000 +++ python-daemon-2.0.5/daemon/pidlockfile.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- - -# daemon/pidlockfile.py -# Part of python-daemon, an implementation of PEP 3143. -# -# Copyright © 2008–2010 Ben Finney -# -# This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. - -""" Lockfile behaviour implemented via Unix PID files. - """ - -import os -import errno - -from lockfile import ( - LinkFileLock, - AlreadyLocked, LockFailed, - NotLocked, NotMyLock, - ) - - -class PIDFileError(Exception): - """ Abstract base class for errors specific to PID files. """ - -class PIDFileParseError(ValueError, PIDFileError): - """ Raised when parsing contents of PID file fails. """ - - -class PIDLockFile(LinkFileLock, object): - """ Lockfile implemented as a Unix PID file. - - The PID file is named by the attribute `path`. When locked, - the file will be created with a single line of text, - containing the process ID (PID) of the process that acquired - the lock. - - The lock is acquired and maintained as per `LinkFileLock`. - - """ - - def read_pid(self): - """ Get the PID from the lock file. - """ - result = read_pid_from_pidfile(self.path) - return result - - def acquire(self, *args, **kwargs): - """ Acquire the lock. - - Locks the PID file then creates the PID file for this - lock. The `timeout` parameter is used as for the - `LinkFileLock` class. - - """ - super(PIDLockFile, self).acquire(*args, **kwargs) - try: - write_pid_to_pidfile(self.path) - except OSError, exc: - error = LockFailed("%(exc)s" % vars()) - raise error - - def release(self): - """ Release the lock. - - Removes the PID file then releases the lock, or raises an - error if the current process does not hold the lock. - - """ - if self.i_am_locking(): - remove_existing_pidfile(self.path) - super(PIDLockFile, self).release() - - def break_lock(self): - """ Break an existing lock. - - If the lock is held, breaks the lock and removes the PID - file. - - """ - super(PIDLockFile, self).break_lock() - remove_existing_pidfile(self.path) - - -class TimeoutPIDLockFile(PIDLockFile): - """ Lockfile with default timeout, implemented as a Unix PID file. - - This uses the ``PIDLockFile`` implementation, with the - following changes: - - * The `acquire_timeout` parameter to the initialiser will be - used as the default `timeout` parameter for the `acquire` - method. - - """ - - def __init__(self, path, acquire_timeout=None, *args, **kwargs): - """ Set up the parameters of a DaemonRunnerLock. """ - self.acquire_timeout = acquire_timeout - super(TimeoutPIDLockFile, self).__init__(path, *args, **kwargs) - - def acquire(self, timeout=None, *args, **kwargs): - """ Acquire the lock. """ - if timeout is None: - timeout = self.acquire_timeout - super(TimeoutPIDLockFile, self).acquire(timeout, *args, **kwargs) - - -def read_pid_from_pidfile(pidfile_path): - """ Read the PID recorded in the named PID file. - - Read and return the numeric PID recorded as text in the named - PID file. If the PID file does not exist, return ``None``. If - the content is not a valid PID, raise ``PIDFileParseError``. - - """ - pid = None - pidfile = None - try: - pidfile = open(pidfile_path, 'r') - except IOError, exc: - if exc.errno == errno.ENOENT: - pass - else: - raise - - if pidfile: - # According to the FHS 2.3 section on PID files in ‘/var/run’: - # - # The file must consist of the process identifier in - # ASCII-encoded decimal, followed by a newline character. … - # - # Programs that read PID files should be somewhat flexible - # in what they accept; i.e., they should ignore extra - # whitespace, leading zeroes, absence of the trailing - # newline, or additional lines in the PID file. - - line = pidfile.readline().strip() - try: - pid = int(line) - except ValueError: - raise PIDFileParseError( - "PID file %(pidfile_path)r contents invalid" % vars()) - pidfile.close() - - return pid - - -def write_pid_to_pidfile(pidfile_path): - """ Write the PID in the named PID file. - - Get the numeric process ID (“PID”) of the current process - and write it to the named file as a line of text. - - """ - open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY) - open_mode = ( - ((os.R_OK | os.W_OK) << 6) | - ((os.R_OK) << 3) | - ((os.R_OK))) - pidfile_fd = os.open(pidfile_path, open_flags, open_mode) - pidfile = os.fdopen(pidfile_fd, 'w') - - # According to the FHS 2.3 section on PID files in ‘/var/run’: - # - # The file must consist of the process identifier in - # ASCII-encoded decimal, followed by a newline character. For - # example, if crond was process number 25, /var/run/crond.pid - # would contain three characters: two, five, and newline. - - pid = os.getpid() - line = "%(pid)d\n" % vars() - pidfile.write(line) - pidfile.close() - - -def remove_existing_pidfile(pidfile_path): - """ Remove the named PID file if it exists. - - Remove the named PID file. Ignore the condition if the file - does not exist, since that only means we are already in the - desired state. - - """ - try: - os.remove(pidfile_path) - except OSError, exc: - if exc.errno == errno.ENOENT: - pass - else: - raise diff -Nru python-daemon-1.5.5/daemon/runner.py python-daemon-2.0.5/daemon/runner.py --- python-daemon-1.5.5/daemon/runner.py 2010-01-20 11:33:10.000000000 +0000 +++ python-daemon-2.0.5/daemon/runner.py 2015-01-10 11:29:24.000000000 +0000 @@ -1,46 +1,72 @@ # -*- coding: utf-8 -*- # daemon/runner.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2009–2010 Ben Finney +# Copyright © 2009–2015 Ben Finney # Copyright © 2007–2008 Robert Niederreiter, Jens Klein # Copyright © 2003 Clark Evans # Copyright © 2002 Noah Spurrier # Copyright © 2001 Jürgen Hermann # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. """ Daemon runner library. """ +from __future__ import (absolute_import, unicode_literals) + import sys import os import signal import errno - -import pidlockfile - -from daemon import DaemonContext +try: + # Python 3 standard library. + ProcessLookupError +except NameError: + # No such class in Python 2. + ProcessLookupError = NotImplemented + +import lockfile + +from . import pidfile +from .daemon import (basestring, unicode) +from .daemon import DaemonContext +from .daemon import _chain_exception_from_existing_exception_context class DaemonRunnerError(Exception): """ Abstract base class for errors from DaemonRunner. """ -class DaemonRunnerInvalidActionError(ValueError, DaemonRunnerError): + def __init__(self, *args, **kwargs): + self._chain_from_context() + + super(DaemonRunnerError, self).__init__(*args, **kwargs) + + def _chain_from_context(self): + _chain_exception_from_existing_exception_context(self, as_cause=True) + + +class DaemonRunnerInvalidActionError(DaemonRunnerError, ValueError): """ Raised when specified action for DaemonRunner is invalid. """ -class DaemonRunnerStartFailureError(RuntimeError, DaemonRunnerError): + def _chain_from_context(self): + # This exception is normally not caused by another. + _chain_exception_from_existing_exception_context(self, as_cause=False) + + +class DaemonRunnerStartFailureError(DaemonRunnerError, RuntimeError): """ Raised when failure starting DaemonRunner. """ -class DaemonRunnerStopFailureError(RuntimeError, DaemonRunnerError): + +class DaemonRunnerStopFailureError(DaemonRunnerError, RuntimeError): """ Raised when failure stopping DaemonRunner. """ -class DaemonRunner(object): +class DaemonRunner: """ Controller for a callable running in a separate background process. The first command-line argument is the action to take: @@ -51,54 +77,77 @@ """ - start_message = "started with pid %(pid)d" + __metaclass__ = type + + start_message = "started with pid {pid:d}" def __init__(self, app): """ Set up the parameters of a new runner. + :param app: The application instance; see below. + :return: ``None``. + The `app` argument must have the following attributes: - * `stdin_path`, `stdout_path`, `stderr_path`: Filesystem - paths to open and replace the existing `sys.stdin`, - `sys.stdout`, `sys.stderr`. - - * `pidfile_path`: Absolute filesystem path to a file that - will be used as the PID file for the daemon. If - ``None``, no PID file will be used. + * `stdin_path`, `stdout_path`, `stderr_path`: Filesystem paths + to open and replace the existing `sys.stdin`, `sys.stdout`, + `sys.stderr`. + + * `pidfile_path`: Absolute filesystem path to a file that will + be used as the PID file for the daemon. If ``None``, no PID + file will be used. - * `pidfile_timeout`: Used as the default acquisition - timeout value supplied to the runner's PID lock file. + * `pidfile_timeout`: Used as the default acquisition timeout + value supplied to the runner's PID lock file. * `run`: Callable that will be invoked when the daemon is started. - + """ self.parse_args() self.app = app self.daemon_context = DaemonContext() - self.daemon_context.stdin = open(app.stdin_path, 'r') - self.daemon_context.stdout = open(app.stdout_path, 'w+') + self.daemon_context.stdin = open(app.stdin_path, 'rt') + self.daemon_context.stdout = open(app.stdout_path, 'w+t') self.daemon_context.stderr = open( - app.stderr_path, 'w+', buffering=0) + app.stderr_path, 'w+t', buffering=0) self.pidfile = None if app.pidfile_path is not None: self.pidfile = make_pidlockfile( - app.pidfile_path, app.pidfile_timeout) + app.pidfile_path, app.pidfile_timeout) self.daemon_context.pidfile = self.pidfile def _usage_exit(self, argv): """ Emit a usage message, then exit. + + :param argv: The command-line arguments used to invoke the + program, as a sequence of strings. + :return: ``None``. + """ progname = os.path.basename(argv[0]) usage_exit_code = 2 action_usage = "|".join(self.action_funcs.keys()) - message = "usage: %(progname)s %(action_usage)s" % vars() + message = "usage: {progname} {usage}".format( + progname=progname, usage=action_usage) emit_message(message) sys.exit(usage_exit_code) def parse_args(self, argv=None): """ Parse command-line arguments. + + :param argv: The command-line arguments used to invoke the + program, as a sequence of strings. + + :return: ``None``. + + The parser expects the first argument as the program name, the + second argument as the action to perform. + + If the parser fails to parse the arguments, emit a usage + message and exit the program. + """ if argv is None: argv = sys.argv @@ -107,46 +156,65 @@ if len(argv) < min_args: self._usage_exit(argv) - self.action = argv[1] + self.action = unicode(argv[1]) if self.action not in self.action_funcs: self._usage_exit(argv) def _start(self): """ Open the daemon context and run the application. + + :return: ``None``. + :raises DaemonRunnerStartFailureError: If the PID file cannot + be locked by this process. + """ if is_pidfile_stale(self.pidfile): self.pidfile.break_lock() try: self.daemon_context.open() - except pidlockfile.AlreadyLocked: - pidfile_path = self.pidfile.path - raise DaemonRunnerStartFailureError( - "PID file %(pidfile_path)r already locked" % vars()) + except lockfile.AlreadyLocked: + error = DaemonRunnerStartFailureError( + "PID file {pidfile.path!r} already locked".format( + pidfile=self.pidfile)) + raise error pid = os.getpid() - message = self.start_message % vars() + message = self.start_message.format(pid=pid) emit_message(message) self.app.run() def _terminate_daemon_process(self): """ Terminate the daemon process specified in the current PID file. + + :return: ``None``. + :raises DaemonRunnerStopFailureError: If terminating the daemon + fails with an OS error. + """ pid = self.pidfile.read_pid() try: os.kill(pid, signal.SIGTERM) - except OSError, exc: - raise DaemonRunnerStopFailureError( - "Failed to terminate %(pid)d: %(exc)s" % vars()) + except OSError as exc: + error = DaemonRunnerStopFailureError( + "Failed to terminate {pid:d}: {exc}".format( + pid=pid, exc=exc)) + raise error def _stop(self): """ Exit the daemon process specified in the current PID file. + + :return: ``None``. + :raises DaemonRunnerStopFailureError: If the PID file is not + already locked. + """ if not self.pidfile.is_locked(): - pidfile_path = self.pidfile.path - raise DaemonRunnerStopFailureError( - "PID file %(pidfile_path)r not locked" % vars()) + error = DaemonRunnerStopFailureError( + "PID file {pidfile.path!r} not locked".format( + pidfile=self.pidfile)) + raise error if is_pidfile_stale(self.pidfile): self.pidfile.break_lock() @@ -160,27 +228,40 @@ self._start() action_funcs = { - 'start': _start, - 'stop': _stop, - 'restart': _restart, - } + 'start': _start, + 'stop': _stop, + 'restart': _restart, + } def _get_action_func(self): - """ Return the function for the specified action. + """ Get the function for the specified action. + + :return: The function object corresponding to the specified + action. + :raises DaemonRunnerInvalidActionError: if the action is + unknown. - Raises ``DaemonRunnerInvalidActionError`` if the action is - unknown. + The action is specified by the `action` attribute, which is set + during `parse_args`. """ try: func = self.action_funcs[self.action] except KeyError: - raise DaemonRunnerInvalidActionError( - "Unknown action: %(action)r" % vars(self)) + error = DaemonRunnerInvalidActionError( + "Unknown action: {action!r}".format( + action=self.action)) + raise error return func def do_action(self): """ Perform the requested action. + + :return: ``None``. + + The action is specified by the `action` attribute, which is set + during `parse_args`. + """ func = self._get_action_func() func(self) @@ -190,19 +271,21 @@ """ Emit a message to the specified stream (default `sys.stderr`). """ if stream is None: stream = sys.stderr - stream.write("%(message)s\n" % vars()) + stream.write("{message}\n".format(message=message)) stream.flush() def make_pidlockfile(path, acquire_timeout): """ Make a PIDLockFile instance with the given filesystem path. """ if not isinstance(path, basestring): - error = ValueError("Not a filesystem path: %(path)r" % vars()) + error = ValueError("Not a filesystem path: {path!r}".format( + path=path)) raise error if not os.path.isabs(path): - error = ValueError("Not an absolute path: %(path)r" % vars()) + error = ValueError("Not an absolute path: {path!r}".format( + path=path)) raise error - lockfile = pidlockfile.TimeoutPIDLockFile(path, acquire_timeout) + lockfile = pidfile.TimeoutPIDLockFile(path, acquire_timeout) return lockfile @@ -210,9 +293,10 @@ def is_pidfile_stale(pidfile): """ Determine whether a PID file is stale. - Return ``True`` (“stale”) if the contents of the PID file are - valid but do not match the PID of a currently-running process; - otherwise return ``False``. + :return: ``True`` iff the PID file is stale; otherwise ``False``. + + The PID file is “stale” if its contents are valid but do not + match the PID of a currently-running process. """ result = False @@ -221,9 +305,20 @@ if pidfile_pid is not None: try: os.kill(pidfile_pid, signal.SIG_DFL) - except OSError, exc: + except ProcessLookupError: + # The specified PID does not exist. + result = True + except OSError as exc: if exc.errno == errno.ESRCH: - # The specified PID does not exist + # Under Python 2, process lookup error is an OSError. + # The specified PID does not exist. result = True return result + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/daemon/version/__init__.py python-daemon-2.0.5/daemon/version/__init__.py --- python-daemon-1.5.5/daemon/version/__init__.py 2010-03-02 09:20:42.000000000 +0000 +++ python-daemon-2.0.5/daemon/version/__init__.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- - -# daemon/version/__init__.py -# Part of python-daemon, an implementation of PEP 3143. -# -# Copyright © 2008–2010 Ben Finney -# This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. - -""" Version information for the python-daemon distribution. """ - -from version_info import version_info - -version_info['version_string'] = u"1.5.5" - -version_short = u"%(version_string)s" % version_info -version_full = u"%(version_string)s.r%(revno)s" % version_info -version = version_short - -author_name = u"Ben Finney" -author_email = u"ben+python@benfinney.id.au" -author = u"%(author_name)s <%(author_email)s>" % vars() - -copyright_year_begin = u"2001" -date = version_info['date'].split(' ', 1)[0] -copyright_year = date.split('-')[0] -copyright_year_range = copyright_year_begin -if copyright_year > copyright_year_begin: - copyright_year_range += u"–%(copyright_year)s" % vars() - -copyright = ( - u"Copyright © %(copyright_year_range)s %(author)s and others" - ) % vars() -license = u"PSF-2+" diff -Nru python-daemon-1.5.5/daemon/version/version_info.py python-daemon-2.0.5/daemon/version/version_info.py --- python-daemon-1.5.5/daemon/version/version_info.py 2009-05-22 09:50:21.000000000 +0000 +++ python-daemon-2.0.5/daemon/version/version_info.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,23 +0,0 @@ -#!/usr/bin/env python -"""This file is automatically generated by generate_version_info -It uses the current working tree to determine the revision. -So don't edit it. :) -""" - -version_info = {'branch_nick': u'python-daemon.devel', - 'build_date': '2009-05-22 19:50:06 +1000', - 'clean': None, - 'date': '2009-05-22 19:47:30 +1000', - 'revision_id': 'ben+python@benfinney.id.au-20090522094730-p4vsa0reh7ktt4e1', - 'revno': 145} - -revisions = {} - -file_revisions = {} - - - -if __name__ == '__main__': - print 'revision: %(revno)d' % version_info - print 'nick: %(branch_nick)s' % version_info - print 'revision id: %(revision_id)s' % version_info diff -Nru python-daemon-1.5.5/debian/bzr-builddeb.conf python-daemon-2.0.5/debian/bzr-builddeb.conf --- python-daemon-1.5.5/debian/bzr-builddeb.conf 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/debian/bzr-builddeb.conf 2015-06-08 20:19:45.000000000 +0000 @@ -0,0 +1,4 @@ +[builddeb] + +# Build source package by merging this working tree with upstream source. +merge = True diff -Nru python-daemon-1.5.5/debian/changelog python-daemon-2.0.5/debian/changelog --- python-daemon-1.5.5/debian/changelog 2011-12-17 14:09:14.000000000 +0000 +++ python-daemon-2.0.5/debian/changelog 2015-06-08 20:19:45.000000000 +0000 @@ -1,8 +1,53 @@ -python-daemon (1.5.5-1ubuntu1) precise; urgency=low +python-daemon (2.0.5-1) unstable; urgency=high - * Build using dh_python2 + * The “Ghazi Beji” release. + * Urgency high now that our dependency ‘python-lockfile’ broke backward + compatibility. + * New upstream version. Highlights since previous release: + * Supports Python 2 and Python 3 with the same code base. + * Now uses ‘lockfile’ >= 0.9, with its implementation of + ‘PIDLockFile’. + (Closes: bug#787497) + * Licensed under terms of Apache License 2.0. + * debian/control: + * Declare “Standards-Version: 3.9.6”. + * Use canonical Alioth VCS anonymous-access URLs. + * Update build dependencies for upstream changes: + * Removed ‘python-minimock’. + * Versioned dependency on ‘lockfile’ library. + * Add Python ‘unittest2’, ‘mock’, ‘testtools’, ‘testscenarios’ build + dependencies. + * Add Python ‘docutils’ dependency. + * Remove support for Python versions earlier than 2.7. + * debian/copyright: + * Conform to official copyright format version 1.0. + * Update copyright information. + * debian/rules: + * Use a catch-all target for all Policy-required targets. + * Add ‘get-orig-source’ target (as recommended by Policy §4.9) and + ‘get-packaged-orig-source’ target. + * Upstream test suite currently not working; don't run it. + * Discard HTTP traffic during packaging actions. + * debian/rules, debian/compat, debian/control, debian/pyversions: + * Upgrade to Debhelper 9. + * Convert Python packaging system to Pybuild. + (Closes: bug#706190) Thanks to Dave Steele for the bug report. + * Convert to ‘dh_python{2,3}’ build system. (Closes: bug#785997) + Thanks to Luca Falavigna for the bug report. + * Specify range of Python versions supported. + Remove obsolete ‘pyversions’ file. + (Closes: bug#581180) + * Build packages targeting both Python 2 and Python 3. + * debian/bzr-builddeb.conf: + * Add this package's configuration for ‘bzr builddeb’. + * debian/upstream/signing-key.asc: + * Add public keyring for keys used to sign upstream source. + * debian/watch: + * Update comments, add editor hints. + * Update patterns for Debian redirector for PyPI. + The PyPI directory-listing API is no longer supported. - -- Matthias Klose Sat, 17 Dec 2011 14:09:14 +0000 + -- Ben Finney Tue, 09 Jun 2015 05:53:51 +1000 python-daemon (1.5.5-1) unstable; urgency=low diff -Nru python-daemon-1.5.5/debian/compat python-daemon-2.0.5/debian/compat --- python-daemon-1.5.5/debian/compat 2010-04-10 05:45:30.000000000 +0000 +++ python-daemon-2.0.5/debian/compat 2015-06-08 20:19:45.000000000 +0000 @@ -1 +1 @@ -7 +9 diff -Nru python-daemon-1.5.5/debian/control python-daemon-2.0.5/debian/control --- python-daemon-1.5.5/debian/control 2011-12-17 14:09:32.000000000 +0000 +++ python-daemon-2.0.5/debian/control 2015-06-08 20:19:45.000000000 +0000 @@ -3,21 +3,37 @@ Maintainer: Ben Finney Section: python Homepage: http://pypi.python.org/pypi/python-daemon -VCS-bzr: http://bzr.debian.org/bzr/collab-maint/python-daemon/python-daemon.debian/ -VCS-Browser: http://bzr.debian.org/loggerhead/collab-maint/python-daemon/python-daemon.debian/ -Build-Depends: debhelper (>= 7.0.14), +VCS-bzr: http://anonscm.debian.org/bzr/collab-maint/pkg-python-daemon/trunk/ +VCS-Browser: http://anonscm.debian.org/loggerhead/collab-maint/pkg-python-daemon/trunk/ +Build-Depends: + python3-setuptools, + python3-docutils, + python3-mock (>= 1.0), + python3-testtools, + python3-testscenarios (>= 0.4), + python3-all, + python3-lockfile, python-setuptools, - python-minimock (>= 1.2.2), - python-lockfile, - python (>= 2.6.6-3~) -Standards-Version: 3.8.4 + python-docutils, + python-unittest2, + python-mock (>= 1.0), + python-testtools, + python-testscenarios (>= 0.4), + python-lockfile (>= 1:0.9), + python-all, + dh-python, + debhelper (>= 9~) +Standards-Version: 3.9.6 +X-Python-Version: >= 2.7 +X-Python3-Version: >= 3.2 Package: python-daemon Architecture: all Depends: - python-lockfile, + python-pkg-resources, + python-lockfile (>= 1:0.9), ${python:Depends}, ${misc:Depends} -Description: library for making a Unix daemon process +Description: library for making a Unix daemon process — Python 2 ‘daemon’ is a library that assists a Python program to turn itself into a well-behaved Unix daemon process, as specified in PEP 3143. . @@ -33,3 +49,30 @@ * Open new file descriptors for stdin, stdout, and stderr. * Manage a specified PID lock file. * Register cleanup functions for at-exit processing. + . + This package installs the library for Python 2. + +Package: python3-daemon +Architecture: all +Depends: + python3-pkg-resources, + python3-lockfile, + ${python3:Depends}, ${misc:Depends} +Description: library for making a Unix daemon process — Python 3 + ‘daemon’ is a library that assists a Python program to turn itself + into a well-behaved Unix daemon process, as specified in PEP 3143. + . + This library provides a ‘DaemonContext’ class that manages the + following important tasks for becoming a daemon process: + . + * Detach the process into its own process group. + * Set process environment appropriate for running inside a chroot. + * Renounce suid and sgid privileges. + * Close all open file descriptors. + * Change the working directory, uid, gid, and umask. + * Set appropriate signal handlers. + * Open new file descriptors for stdin, stdout, and stderr. + * Manage a specified PID lock file. + * Register cleanup functions for at-exit processing. + . + This package installs the library for Python 3. diff -Nru python-daemon-1.5.5/debian/copyright python-daemon-2.0.5/debian/copyright --- python-daemon-1.5.5/debian/copyright 2010-04-10 05:45:30.000000000 +0000 +++ python-daemon-2.0.5/debian/copyright 2015-06-08 20:19:45.000000000 +0000 @@ -1,17 +1,39 @@ -Format-Specification: - http://wiki.debian.org/Proposals/CopyrightFormat?action=recall&rev=233 +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: python-daemon -Upstream-Maintainer: Ben Finney -Upstream-Source: http://pypi.python.org/packages/source/p/python-daemon/ +Upstream-Contact: Ben Finney +Source: http://pypi.python.org/packages/source/p/python-daemon/ Files: * -License: PSF-2+ +Copyright: + © 2008–2014 Ben Finney + © 2007–2008 Robert Niederreiter, Jens Klein + © 2004–2005 Chad J. Schroeder + © 2003 Clark Evans + © 2002 Noah Spurrier + © 2001 Jürgen Hermann +License: Apache-2 This is free software: you may copy, modify, and/or distribute this work - under the terms of the Python Software Foundation License, version 2 or - later as published by the Python Software Foundation. - No warranty expressed or implied. + under the terms of the Apache License, version 2.0 as published by the + Apache Software Foundation. + . + On Debian systems, the complete text of the Apache License version 2.0 + can be found in the file ‘/usr/share/common-licenses/Apache-2.0’. + +Files: setup.py +Copyright: + © 2008–2014 Ben Finney + © 2008 Robert Niederreiter, Jens Klein +License: GPL-3+ + This is free software: you may copy, modify, and/or distribute this work + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3 of that license or any later version. + . + On Debian systems, the complete text of the GNU General Public License + version 3 can be found in the file ‘/usr/share/common-licenses/GPL-3’. Files: debian/* +Copyright: + © 2008–2014 Ben Finney License: GPL-2+ This is free software; you may copy, modify, and/or distribute this work under the terms of the GNU General Public License, version 2 or later. @@ -19,53 +41,3 @@ . On Debian systems, the complete text of the GNU General Public License version 2 can be found in the file ‘/usr/share/common-licenses/GPL-2’. - -License: PSF-2 - PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 - -------------------------------------------- - . - 1. This LICENSE AGREEMENT is between the Python Software Foundation - ("PSF"), and the Individual or Organization ("Licensee") accessing and - otherwise using this software ("Python") in source or binary form and - its associated documentation. - . - 2. Subject to the terms and conditions of this License Agreement, PSF - hereby grants Licensee a nonexclusive, royalty-free, world-wide - license to reproduce, analyze, test, perform and/or display publicly, - prepare derivative works, distribute, and otherwise use Python - alone or in any derivative version, provided, however, that PSF's - License Agreement and PSF's notice of copyright, i.e., "Copyright (c) - 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation; - All Rights Reserved" are retained in Python alone or in any derivative - version prepared by Licensee. - . - 3. In the event Licensee prepares a derivative work that is based on - or incorporates Python or any part thereof, and wants to make - the derivative work available to others as provided herein, then - Licensee hereby agrees to include in any such work a brief summary of - the changes made to Python. - . - 4. PSF is making Python available to Licensee on an "AS IS" - basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR - IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND - DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS - FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT - INFRINGE ANY THIRD PARTY RIGHTS. - . - 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON - FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS - A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, - OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - . - 6. This License Agreement will automatically terminate upon a material - breach of its terms and conditions. - . - 7. Nothing in this License Agreement shall be deemed to create any - relationship of agency, partnership, or joint venture between PSF and - Licensee. This License Agreement does not grant permission to use PSF - trademarks or trade name in a trademark sense to endorse or promote - products or services of Licensee, or any third party. - . - 8. By copying, installing or otherwise using Python, Licensee - agrees to be bound by the terms and conditions of this License - Agreement. diff -Nru python-daemon-1.5.5/debian/pyversions python-daemon-2.0.5/debian/pyversions --- python-daemon-1.5.5/debian/pyversions 2010-04-10 05:45:30.000000000 +0000 +++ python-daemon-2.0.5/debian/pyversions 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -2.5- diff -Nru python-daemon-1.5.5/debian/rules python-daemon-2.0.5/debian/rules --- python-daemon-1.5.5/debian/rules 2011-12-17 14:10:18.000000000 +0000 +++ python-daemon-2.0.5/debian/rules 2015-06-08 20:19:45.000000000 +0000 @@ -1,36 +1,63 @@ #! /usr/bin/make -f -# -*- mode: makefile; coding: utf-8 -*- # # debian/rules # -# Copyright © 2008–2009 Ben Finney +# Copyright © 2008–2015 Ben Finney # This is free software; you may copy, modify, and/or distribute this work # under the terms of the GNU General Public License, version 2 or later. # No warranty expressed or implied. # See the file '/usr/share/common-licenses/GPL-2' for details. -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 +export PYBUILD_NAME = daemon + +# Send HTTP traffic to the “discard” service during packaging actions. +export http_proxy = http://127.0.1.1:9/ +export https_proxy = ${http_proxy} -.PHONY: build -build: - dh build --with=python2 - -.PHONY: clean -clean: - dh clean --with=python2 - -.PHONY: install -install: build - dh install --with=python2 - -.PHONY: binary-indep -binary-indep: build install - dh binary-indep --with=python2 +ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) + +# The test suite doesn't yet run on a build instance. +.PHONY: override_dh_auto_test +override_dh_auto_test: + +endif -.PHONY: binary-arch -binary-arch: build install -.PHONY: binary -binary: build binary-indep binary-arch +%: + dh $@ --with python2,python3 --buildsystem=pybuild + + +.PHONY: get-packaged-orig-source +get-packaged-orig-source: + $(uscan-download-source) --download-current-version + +# Policy §4.9 strongly recommends the ‘get-orig-source’ target: +# “This target is optional, but providing it if possible is a good idea.” +# +# This target is an anomaly: per Policy §4.9, it fetches the *latest* +# upstream source, regardless of this package's version. To fetch the +# upstream source corresponding to this package's *declared* version +# in ‘debian/changelog’, use ‘get-packaged-orig-source’ instead. +.PHONY: get-orig-source +get-orig-source: + $(uscan-download-source) + +get-orig-source get-packaged-orig-source: http_proxy = +get-orig-source get-packaged-orig-source: makefile_dir = $(abspath $(dir $(firstword ${MAKEFILE_LIST}))) +get-orig-source get-packaged-orig-source: package_dir = $(abspath $(dir ${makefile_dir})) + +define uscan-download-source + uscan --noconf --verbose \ + --force-download \ + --rename \ + --destdir=$(CURDIR) \ + --check-dirname-level=0 ${package_dir} +endef # uscan-download-source + + +# Local variables: +# mode: makefile +# coding: utf-8 +# End: +# vim: filetype=make fileencoding=utf-8 : diff -Nru python-daemon-1.5.5/debian/upstream/signing-key.asc python-daemon-2.0.5/debian/upstream/signing-key.asc --- python-daemon-1.5.5/debian/upstream/signing-key.asc 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/debian/upstream/signing-key.asc 2015-06-08 20:19:45.000000000 +0000 @@ -0,0 +1,1669 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +sr49S3AZP+TpM0vg76grW0Rhmxywj1POppGGZcGSMzziin0qT5S8HKkw6dNJ9djz +jrD8UppSES+26L3PLSAdvK+nro2rNMuQuvDVIIl400K73Qj3DA5oCZU0L1QfdXMJ +UG04bwVLey/1vaXkwDGCe+BP5Ilxdm9r5TEgzuibBFSa5fX4ewF2nfVuBk+BMZAe +QuWXzqo15WyZlcz/LcGdDP0epi8gd7wog7psg6K8fkpOmciQmcJ23YDm8K43sP2V +TaZg+mD21d95lY2Y01ydSVlrbZl+dOHB9HmEhNKEhz/VxJeV0aXIdEZWdpjvjkGs +aXS+JhBPDlyki+++TaKrCPUxwIAl4SLa9kTX2mXcPSZgOwBX0A8H2JN3CwOpoVfc +vg91xTkMaliArsxs5IE6v/dYo7VyRXjynFAcyHAVPtIgsoLDZQqjCXZFfrpgumQW +bZ7fHtjybaiqF82UzyYjHJUFkhCwJyHfqJqb6ec5HqnN/rD5lkTizf/V4SHSmVdw +iTHew6Zft+p7Lri6JnVcysXJptFrPKw86+y87kIW+7pl09VcpABb4Drruz1khAo5 +BTTTZdq6GRvgf16irLPKi3dj1XX/UbjEPQO75XUrJrsJCQVkYkFNhnirVrBJDnut +HX1upEO9VxmvOlrmgL+IRgQQEQgABgUCTO22RwAKCRC3rC5RvUFxSxg5AJ40ltkz +CM0u+IUgIqEJxePkf/xv3ACffPXYSQr+T9fgzEqeyTOK0vgCC0CIRgQQEQIABgUC +TP3naQAKCRA3GsW/oErjEyWHAKCn5DdfVJowgLJkiWaqiWnkF3kLkwCg0u5ntpcn +eOBs8jRmTOmfApL7WwSJAhwEEAEIAAYFAk1M8dQACgkQ6S1oVS5vu6n/9Q/9Ed7F +NkzZbNCBw3UmUTNLb8vz55niWku4yvEAhs774RkYL8aYTZ7VZRekGrFWxq5EA1lP +jfLTtwNqEJhgrEo2MdAtB9GyHT7ime3AjkBx9w2+CiZPWVXPo7PcvBrbt9PfC6bb +SOPu/z2IL1wdugsKrvVOpBzdwnf9yJoy0rDZ2sUxeiBDykpRbJNDqKZGHA2C4AUM +t4gmWJ4Aiix0O6lYSjkOWqxHkLkXHqvcvbTY8R2bMhM3/Po3PmfQPhpq802eKXIq +DelIlIjrMgErCJcnk04s5vUfz1Ys1lYqh+jzIzwWPjvG+J/CJ5wUBFFZAGf5WYiG +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 +x3eo2+qQpw== +=KX2Z +-----END PGP PUBLIC KEY BLOCK----- diff -Nru python-daemon-1.5.5/debian/watch python-daemon-2.0.5/debian/watch --- python-daemon-1.5.5/debian/watch 2010-04-10 05:45:30.000000000 +0000 +++ python-daemon-2.0.5/debian/watch 2015-06-08 20:19:45.000000000 +0000 @@ -1,9 +1,18 @@ # debian/watch -# Debian uscan file for python-daemon package. -# Manpage: uscan(1) +# Debian uscan file for ‘python-daemon’ package. +# Manpage: ‘uscan(1)’ # Compulsory line, this is a version 3 file. version=3 -# Current version from Python cheeseshop. -http://pypi.python.org/packages/source/p/python-daemon/python-daemon-(.+).tar.gz +# Current version from Debian redirector for Python Package Index. +opts="uversionmangle=s/(rc|a|b|c)/~$1/" \ + http://pypi.debian.net/python-daemon/python-daemon-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) \ + debian + + +# Local variables: +# coding: utf-8 +# mode: conf +# End: +# vim: fileencoding=utf-8 filetype=conf : diff -Nru python-daemon-1.5.5/doc/CREDITS python-daemon-2.0.5/doc/CREDITS --- python-daemon-1.5.5/doc/CREDITS 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/doc/CREDITS 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,53 @@ +Credits for contributors to ‘python-daemon’ +########################################### + +:Updated: 2014-12-23 + +The ‘python-daemon’ library is the work of many contributors. + + +Primary developers +================== + +The library has been maintained over the years by: + +* Ben Finney +* Robert Niederreiter +* Jens Klein + + +Precursors +========== + +The library code base is inherited from prior work by: + +* Chad J. Schroeder +* Clark Evans +* Noah Spurrier +* Jürgen Hermann + + +Additional contributors +======================= + +People who have also contributed substantial improvements: + + + +.. + This is free software: you may copy, modify, and/or distribute this work + under the terms of the Apache License version 2.0 as published by the + Apache Software Foundation. + No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + time-stamp-format: "%:y-%02m-%02d" + time-stamp-start: "^:Updated:[ ]+" + time-stamp-end: "$" + time-stamp-line-limit: 20 + End: + vim: fileencoding=utf-8 filetype=rst : diff -Nru python-daemon-1.5.5/doc/FAQ python-daemon-2.0.5/doc/FAQ --- python-daemon-1.5.5/doc/FAQ 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/doc/FAQ 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,156 @@ +‘python-daemon’ Frequently Asked Questions +########################################## + +:Author: Ben Finney +:Updated: 2015-01-10 + +.. contents:: +.. + 1 General + 1.1 What is the purpose of the ‘python-daemon’ library? + 1.2 How can I run a service communicating with a separate daemon process? + 2 Security + 2.1 Why is the umask set to 0 by default? + 3 File descriptors + 3.1 Why does the output stop after opening the daemon context? + 3.2 How can I preserve a ‘logging’ handler's file descriptor? + +General +======= + +What is the purpose of the ‘python-daemon’ library? +--------------------------------------------------- + +The ‘python-daemon’ library has a deliberately narrow focus: that of +being a reference implementation for `PEP 3143`_, “Standard daemon +process library”. + +.. _`PEP 3143`: http://www.python.org/dev/peps/pep-3143 + +How can I run a service communicating with a separate daemon process? +--------------------------------------------------------------------- + +As specified in `PEP 3143`_, the ‘python-daemon’ library is +specifically focussed on the goal of having the *current running +program* become a well-behaved Unix daemon process. This leaves open +the question of how this program is started, or about multiple +programs interacting. As detailed in PEP 3143: + + A daemon is not a service + + There is a related concept in many systems, called a “service”. A + service differs from the model in this PEP, in that rather than + having the *current* program continue to run as a daemon process, + a service starts an *additional* process to run in the background, + and the current process communicates with that additional process + via some defined channels. + + The Unix-style daemon model in this PEP can be used, among other + things, to implement the background-process part of a service; but + this PEP does not address the other aspects of setting up and + managing a service. + +A possible starting point for such a “service” model of execution is +in a `message from 2009-01-30`_ to the ``python-ideas`` forum. + +.. _`message from 2009-01-30`: http://mail.python.org/pipermail/python-ideas/2009-January/002606.html + + +Security +======== + +Why is the umask set to 0 by default? +------------------------------------- + +A daemon should not rely on the parent process's umask value, which is +beyond its control and may prevent creating a file with the required +access mode. So when the daemon context opens, the umask is set to an +explicit known value. + +If the conventional value of 0 is too open, consider setting a value +such as 0o022, 0o027, 0o077, or another specific value. Otherwise, +ensure the daemon creates every file with an explicit access mode for +the purpose. + + +File descriptors +================ + +Why does the output stop after opening the daemon context? +---------------------------------------------------------- + +The specified behaviour in `PEP 3143`_ includes the requirement to +detach the process from the controlling terminal (to allow the process +to continue to run as a daemon), and to close all file descriptors not +known to be safe once detached (to ensure any files that continue to +be used are under the control of the daemon process). + +If you want the process to generate output via the system streams +‘sys.stdout’ and ‘sys.stderr’, set the ‘DaemonContext’'s ‘stdout’ +and/or ‘stderr’ options to a file-like object (e.g. the ‘stream’ +attribute of a ‘logging.Handler’ instance). If these objects have file +descriptors, they will be preserved when the daemon context opens. + +How can I preserve a ‘logging’ handler's file descriptor? +--------------------------------------------------------- + +The ‘DaemonContext.open’ method conforms to `PEP 3143`_ by closing all +open file descriptors, but excluding those files specified in the +‘files_preserve’ option. This option is a list of files or file +descriptors. + +The Python standard library ‘logging’ module provides log handlers +that write to streams, including to files via the ‘StreamHandler’ +class and its sub-classes. The documentation (both the online `logging +module documentation`_ and the docstrings for the code) makes no +mention of a way to get at the stream associated with a handler +object. + +However, looking at the source code for ‘StreamHandler’, in Python 2.5 +as ``/usr/lib/python2.5/logging/__init__.py``, shows a ‘stream’ +attribute that is bound to the stream object. The attribute is not +marked private (i.e. it is not named with a leading underscore), so we +can presume it is part of the public API. + +That attribute can then be used to specify that a logging handler's +file descriptor should, when the ‘DaemonContext’ opens, be excluded +from closure:: + + import logging + import daemon + + # any subclass of StreamHandler should provide the ‘stream’ attribute. + lh = logging.handlers.TimedRotatingFileHandler( + "/var/log/foo.log", + # … + ) + + # … do some logging and other activity … + + daemon_context = daemon.DaemonContext() + daemon_context.files_preserve = [lh.stream] + + daemon_context.open() + + # … continue as a daemon process … + +.. _`logging module documentation`: http://docs.python.org/library/logging + + +.. + This is free software: you may copy, modify, and/or distribute this work + under the terms of the Apache License version 2.0 as published by the + Apache Software Foundation. + No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + time-stamp-format: "%:y-%02m-%02d" + time-stamp-start: "^:Updated:[ ]+" + time-stamp-end: "$" + time-stamp-line-limit: 20 + End: + vim: fileencoding=utf-8 filetype=rst : diff -Nru python-daemon-1.5.5/doc/hacking.txt python-daemon-2.0.5/doc/hacking.txt --- python-daemon-1.5.5/doc/hacking.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/doc/hacking.txt 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,180 @@ +Developer's guide +################# + +:Author: Ben Finney +:Updated: 2014-11-28 + + +Project layout +============== + +:: + + ./ Top level of source tree + doc/ Project documentation + bin/ Executable programs + daemon/ Main ‘daemon’ library + test/ Unit tests + + +Code style +========== + +Python +------ + +All Python code should conform to the guidelines in PEP8_. In +particular: + +* Indent each level using 4 spaces (``U+0020 SPACE``), and no TABs + (``U+0008 CHARACTER TABULATION``). + +* Name modules in lower case, ``multiplewordslikethis``. + +* Name classes in title case, ``MultipleWordsLikeThis``. + +* Name functions, instances and other variables in lower case, + ``multiple_words_like_this``. + +* Every module, class, and function has a Python doc string explaining + its purpose and API. + + *Exception*: Functions whose purpose and API are mandated by Python + itself (dunder-named methods) do not need a doc string. + +* Doc strings are written as triple-quoted strings. + + * The text of the doc string is marked up with reStructuredText. + + * The first line is a one-line synopsis of the object. This summary + line appears on the same line as the opening triple-quote, + separated by a single space. + + * Further lines, if needed, are separated from the first by one + blank line. + + * The synopsis is separated by one space from the opening + triple-quote; this causes it to appear four columns past the + beginning of the line. All subsequent lines are indented at least + four columns also. + + * The synopsis is followed by a reStructuredText field list. The + field names are: “param foo” for each parameter (where “foo” is + the parameter name), and “return” for the return value. The field + values describe the purpose of each. + + * The closing triple-quote appears on a separate line. + + Example:: + + def frobnicate(spam, algorithm="dv"): + """ Perform frobnication on ``spam``. + + :param spam: A travortionate (as a sequence of strings). + :param algorithm: The name of the algorithm to use for + frobnicating the travortionate. + :return: The frobnicated travortionate, if it is + non-empty; otherwise None. + + The frobnication is done by the Dietzel-Venkman algorithm, + and optimises for the case where ``spam`` is freebled and + agglutinative. + + """ + spagnify(spam) + # … + +* All ``import`` statements appear at the top of the module. + +* Each ``import`` statement imports a single module, or multiple names + from a single module. + + Example:: + + import sys + import os + from spam import foo, bar, baz + +.. _PEP8: http://www.python.org/dev/peps/pep-0008/ + +Additional style guidelines: + +* All text files (including program code) are encoded in UTF-8. + +* A page break (``U+000C FORM FEED``) whitespace character is used + within a module to break up semantically separate areas of the + module. + +* Editor hints for Emacs and Vim appear in a comment block at the + file's end:: + + + # Local variables: + # coding: utf-8 + # mode: python + # End: + # vim: fileencoding=utf-8 filetype=python : + + +Unit tests +========== + +All code should aim for 100% coverage by unit tests. New code, or +changes to existing code, will only be considered for inclusion in the +development tree when accompanied by corresponding additions or +changes to the unit tests. + +Test-driven development +----------------------- + +Where possible, practice test-driven development to implement program +code. + +* During a development session, maintain a separate window or terminal + with the unit test suite for the project running continuously, or + automatically every few seconds. + +* Any time a test is failing, the only valid change is to make all + tests pass. + +* Develop new interface features (changes to the program unit's + behaviour) only when all current tests pass. + +* Refactor as needed, but only when all tests pass. + + * Refactoring is any change to the code which does not alter its + interface or expected behaviour, such as performance + optimisations, readability improvements, modularisation + improvements etc. + +* Develop new or changed program behaviour by: + + * *First* write a single, specific test case for that new behaviour, + then watch the test fail in the absence of the desired behaviour. + + * Implement the minimum necessary change to satisfy the failing + test. Continue until all tests pass again, then stop making + functional changes. + + * Once all tests (including the new test) pass, consider refactoring + the code and the tests immediately, then ensure all the tests pass + again after any changes. + + * Iterate for each incremental change in interface or behaviour. + +Test-driven development is not absolutely necessary, but is the +simplest, most direct way to generate the kind of program changes +accompanied by unit tests that are necessary for inclusion in the +project. + + +.. + Local variables: + coding: utf-8 + mode: rst + time-stamp-format: "%:y-%02m-%02d" + time-stamp-start: "^:Updated:[ ]+" + time-stamp-end: "$" + time-stamp-line-limit: 20 + End: + vim: fileencoding=utf-8 filetype=rst : diff -Nru python-daemon-1.5.5/doc/TODO python-daemon-2.0.5/doc/TODO --- python-daemon-1.5.5/doc/TODO 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/doc/TODO 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,95 @@ +TODO for ‘python-daemon’ library +################################ + +:Updated: 2015-01-10 + +======= +PENDING +======= + +Tests +===== + +Libraries +========= + +* Evaluate switching to ‘flufl.lock’ library for PID lockfile behaviour + _. + +Features +======== + +Important +--------- + +Wishlist +-------- + +* Allow specification of a syslog service name to log as (default: + output to stdout and stderr, not syslog). + +Documentation +============= + +Standard library inclusion +========================== + + +==== +DONE +==== + +* Convert to Python 2 and Python 3 compatible code base. + +* Work correctly with current ‘lockfile’ library (0.10 or later). + +* Write full unit tests for every new or changed behaviour at time of + commit. + +* Detect whether started by another process that handles + daemonisation, such as ‘inetd’, and behave appropriately. + +* Detach to new process and session group. + +* Allow specification of working directory (default: '/'). + +* Allow specification of umask (default: 0o000). + +* Drop ‘suid’ and ‘sgid’ privileges if set. + +* Close all open file handles. + +* Re-open stdin, stdout, stderr to user-specified files. + +* Default re-open stdin, stdout, stderr to ‘/dev/null’. + +* Allow specification of a non-root user and group to drop to, if + started as ‘root’ (default: no change of user or group). + +* Implement context manager protocol for daemon context. + +* Allow specification of PID file with its own context manager + (default: no PID file). + +* Full docstrings for functions, classes, and modules. + +* PEP 3143 for adding this library to the Python standard library. + + +.. + This is free software: you may copy, modify, and/or distribute this work + under the terms of the Apache License version 2.0 as published by the + Apache Software Foundation. + No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + time-stamp-format: "%:y-%02m-%02d" + time-stamp-start: "^:Updated:[ ]+" + time-stamp-end: "$" + time-stamp-line-limit: 20 + End: + vim: fileencoding=utf-8 filetype=rst : diff -Nru python-daemon-1.5.5/LICENSE.ASF-2 python-daemon-2.0.5/LICENSE.ASF-2 --- python-daemon-1.5.5/LICENSE.ASF-2 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/LICENSE.ASF-2 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff -Nru python-daemon-1.5.5/LICENSE.GPL-2 python-daemon-2.0.5/LICENSE.GPL-2 --- python-daemon-1.5.5/LICENSE.GPL-2 2009-09-17 06:09:57.000000000 +0000 +++ python-daemon-2.0.5/LICENSE.GPL-2 1970-01-01 00:00:00.000000000 +0000 @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff -Nru python-daemon-1.5.5/LICENSE.GPL-3 python-daemon-2.0.5/LICENSE.GPL-3 --- python-daemon-1.5.5/LICENSE.GPL-3 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/LICENSE.GPL-3 2015-01-10 11:29:24.000000000 +0000 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff -Nru python-daemon-1.5.5/LICENSE.PSF-2 python-daemon-2.0.5/LICENSE.PSF-2 --- python-daemon-1.5.5/LICENSE.PSF-2 2009-03-27 04:12:48.000000000 +0000 +++ python-daemon-2.0.5/LICENSE.PSF-2 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python -alone or in any derivative version, provided, however, that PSF's -License Agreement and PSF's notice of copyright, i.e., "Copyright (c) -2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative -version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. diff -Nru python-daemon-1.5.5/MANIFEST.in python-daemon-2.0.5/MANIFEST.in --- python-daemon-1.5.5/MANIFEST.in 2010-03-02 09:13:02.000000000 +0000 +++ python-daemon-2.0.5/MANIFEST.in 2015-01-14 03:37:58.000000000 +0000 @@ -1,4 +1,7 @@ include MANIFEST.in include LICENSE.* include ChangeLog -include TODO +recursive-include doc * +include version.py +include test_version.py +recursive-include test *.py diff -Nru python-daemon-1.5.5/PKG-INFO python-daemon-2.0.5/PKG-INFO --- python-daemon-1.5.5/PKG-INFO 2010-03-02 09:35:54.000000000 +0000 +++ python-daemon-2.0.5/PKG-INFO 2015-02-02 04:44:42.000000000 +0000 @@ -1,13 +1,13 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: python-daemon -Version: 1.5.5 +Version: 2.0.5 Summary: Library to implement a well-behaved Unix daemon process. -Home-page: http://pypi.python.org/pypi/python-daemon/ +Home-page: https://alioth.debian.org/projects/python-daemon/ Author: Ben Finney Author-email: ben+python@benfinney.id.au -License: PSF-2+ +License: Apache-2 Description: This library implements the well-behaved daemon specification of - :pep:`3143`, "Standard daemon process library". + :pep:`3143`, “Standard daemon process library”. A well-behaved Unix daemon process is tricky to get right, but the required steps are much the same for every daemon program. A @@ -17,12 +17,12 @@ Simple example of usage:: - import daemon + import daemon - from spam import do_main_program + from spam import do_main_program - with daemon.DaemonContext(): - do_main_program() + with daemon.DaemonContext(): + do_main_program() Customisation of the steps to become a daemon is available by setting options on the `DaemonContext` instance; see the @@ -30,8 +30,9 @@ Keywords: daemon,fork,unix Platform: UNKNOWN Classifier: Development Status :: 4 - Beta -Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -Nru python-daemon-1.5.5/python_daemon.egg-info/PKG-INFO python-daemon-2.0.5/python_daemon.egg-info/PKG-INFO --- python-daemon-1.5.5/python_daemon.egg-info/PKG-INFO 2010-03-02 09:35:54.000000000 +0000 +++ python-daemon-2.0.5/python_daemon.egg-info/PKG-INFO 2015-02-02 04:44:32.000000000 +0000 @@ -1,13 +1,13 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: python-daemon -Version: 1.5.5 +Version: 2.0.5 Summary: Library to implement a well-behaved Unix daemon process. -Home-page: http://pypi.python.org/pypi/python-daemon/ +Home-page: https://alioth.debian.org/projects/python-daemon/ Author: Ben Finney Author-email: ben+python@benfinney.id.au -License: PSF-2+ +License: Apache-2 Description: This library implements the well-behaved daemon specification of - :pep:`3143`, "Standard daemon process library". + :pep:`3143`, “Standard daemon process library”. A well-behaved Unix daemon process is tricky to get right, but the required steps are much the same for every daemon program. A @@ -17,12 +17,12 @@ Simple example of usage:: - import daemon + import daemon - from spam import do_main_program + from spam import do_main_program - with daemon.DaemonContext(): - do_main_program() + with daemon.DaemonContext(): + do_main_program() Customisation of the steps to become a daemon is available by setting options on the `DaemonContext` instance; see the @@ -30,8 +30,9 @@ Keywords: daemon,fork,unix Platform: UNKNOWN Classifier: Development Status :: 4 - Beta -Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -Nru python-daemon-1.5.5/python_daemon.egg-info/requires.txt python-daemon-2.0.5/python_daemon.egg-info/requires.txt --- python-daemon-1.5.5/python_daemon.egg-info/requires.txt 2010-03-02 09:35:54.000000000 +0000 +++ python-daemon-2.0.5/python_daemon.egg-info/requires.txt 2015-02-02 04:44:32.000000000 +0000 @@ -1,2 +1,3 @@ setuptools -lockfile >=0.7 \ No newline at end of file +docutils +lockfile >=0.10 diff -Nru python-daemon-1.5.5/python_daemon.egg-info/SOURCES.txt python-daemon-2.0.5/python_daemon.egg-info/SOURCES.txt --- python-daemon-1.5.5/python_daemon.egg-info/SOURCES.txt 2010-03-02 09:35:54.000000000 +0000 +++ python-daemon-2.0.5/python_daemon.egg-info/SOURCES.txt 2015-02-02 04:44:33.000000000 +0000 @@ -1,22 +1,30 @@ ChangeLog -LICENSE.GPL-2 -LICENSE.PSF-2 +LICENSE.ASF-2 +LICENSE.GPL-3 MANIFEST.in +setup.cfg setup.py +test_version.py +version.py daemon/__init__.py +daemon/_metadata.py daemon/daemon.py -daemon/pidlockfile.py +daemon/pidfile.py daemon/runner.py -daemon/version/__init__.py -daemon/version/version_info.py +doc/CREDITS +doc/FAQ +doc/TODO +doc/hacking.txt python_daemon.egg-info/PKG-INFO python_daemon.egg-info/SOURCES.txt python_daemon.egg-info/dependency_links.txt python_daemon.egg-info/not-zip-safe python_daemon.egg-info/requires.txt python_daemon.egg-info/top_level.txt +python_daemon.egg-info/version_info.json test/__init__.py test/scaffold.py test/test_daemon.py -test/test_pidlockfile.py +test/test_metadata.py +test/test_pidfile.py test/test_runner.py \ No newline at end of file diff -Nru python-daemon-1.5.5/python_daemon.egg-info/version_info.json python-daemon-2.0.5/python_daemon.egg-info/version_info.json --- python-daemon-1.5.5/python_daemon.egg-info/version_info.json 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/python_daemon.egg-info/version_info.json 2015-02-02 04:44:33.000000000 +0000 @@ -0,0 +1,6 @@ +{ + "release_date": "2015-02-02", + "version": "2.0.5", + "maintainer": "Ben Finney ", + "body": "* Refine compatibility of exceptions for file operations.\n* Specify the text encoding when opening the changelog file.\n" +} \ No newline at end of file diff -Nru python-daemon-1.5.5/setup.cfg python-daemon-2.0.5/setup.cfg --- python-daemon-1.5.5/setup.cfg 2010-03-02 09:35:54.000000000 +0000 +++ python-daemon-2.0.5/setup.cfg 2015-02-02 04:44:42.000000000 +0000 @@ -1,5 +1,11 @@ +[aliases] +distribute = register sdist bdist_wheel upload + +[bdist_wheel] +universal = true + [egg_info] -tag_build = -tag_date = 0 tag_svn_revision = 0 +tag_date = 0 +tag_build = diff -Nru python-daemon-1.5.5/setup.py python-daemon-2.0.5/setup.py --- python-daemon-1.5.5/setup.py 2010-03-02 09:16:34.000000000 +0000 +++ python-daemon-2.0.5/setup.py 2015-02-02 04:43:28.000000000 +0000 @@ -1,64 +1,106 @@ # -*- coding: utf-8 -*- # setup.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2008–2010 Ben Finney +# Copyright © 2008–2015 Ben Finney # Copyright © 2008 Robert Niederreiter, Jens Klein # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; version 3 of that license or any later version. +# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details. -""" Distribution setup for python-daemon library. - """ +""" Distribution setup for ‘python-daemon’ library. """ -import textwrap -from setuptools import setup, find_packages +from __future__ import (absolute_import, unicode_literals) -distribution_name = "python-daemon" +import sys +import os +import os.path +import pydoc +import distutils.util + +from setuptools import (setup, find_packages) + +import version + + +fromlist_expects_type = str +if sys.version_info < (3, 0): + fromlist_expects_type = bytes + + main_module_name = 'daemon' -main_module = __import__(main_module_name, fromlist=['version']) -version = main_module.version +main_module_fromlist = list(map(fromlist_expects_type, [ + '_metadata'])) +main_module = __import__( + main_module_name, + level=0, fromlist=main_module_fromlist) +metadata = main_module._metadata + +(synopsis, long_description) = pydoc.splitdoc(pydoc.getdoc(main_module)) + +version_info = metadata.get_distribution_version_info() +version_string = version_info['version'] -short_description, long_description = ( - textwrap.dedent(d).strip() - for d in main_module.__doc__.split(u'\n\n', 1) - ) +(maintainer_name, maintainer_email) = metadata.parse_person_field( + version_info['maintainer']) setup( - name=distribution_name, - version=version.version, - packages=find_packages(exclude=["test"]), - - # setuptools metadata - zip_safe=False, - test_suite="test.suite", - tests_require=[ - "MiniMock >=1.2.2", - ], - install_requires=[ - "setuptools", - "lockfile >=0.7", - ], - - # PyPI metadata - author=version.author_name, - author_email=version.author_email, - description=short_description, - license=version.license, - keywords=u"daemon fork unix".split(), - url=main_module._url, - long_description=long_description, - classifiers=[ - # Reference: http://pypi.python.org/pypi?%3Aaction=list_classifiers - "Development Status :: 4 - Beta", - "License :: OSI Approved :: Python Software Foundation License", - "Operating System :: POSIX", - "Programming Language :: Python", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - ) + name=metadata.distribution_name, + version=version_string, + packages=find_packages(exclude=["test"]), + cmdclass={ + "write_version_info": version.WriteVersionInfoCommand, + "egg_info": version.EggInfoCommand, + }, + + # Setuptools metadata. + maintainer=maintainer_name, + maintainer_email=maintainer_email, + zip_safe=False, + setup_requires=[ + "docutils", + ], + test_suite="unittest2.collector", + tests_require=[ + "unittest2 >=0.6", + "testtools", + "testscenarios >=0.4", + "mock >=1.0", + "docutils", + ], + install_requires=[ + "setuptools", + "docutils", + "lockfile >=0.10", + ], + + # PyPI metadata. + author=metadata.author_name, + author_email=metadata.author_email, + description=synopsis, + license=metadata.license, + keywords="daemon fork unix".split(), + url=metadata.url, + long_description=long_description, + classifiers=[ + # Reference: http://pypi.python.org/pypi?%3Aaction=list_classifiers + "Development Status :: 4 - Beta", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + ) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test/__init__.py python-daemon-2.0.5/test/__init__.py --- python-daemon-1.5.5/test/__init__.py 2010-01-20 11:33:10.000000000 +0000 +++ python-daemon-2.0.5/test/__init__.py 2015-01-10 11:29:24.000000000 +0000 @@ -1,19 +1,23 @@ # -*- coding: utf-8 -*- # # test/__init__.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2008–2010 Ben Finney +# Copyright © 2008–2015 Ben Finney # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. -""" Unit test suite for daemon package. +""" Unit test suite for ‘daemon’ package. """ -import scaffold +from __future__ import (absolute_import, unicode_literals) -suite = scaffold.make_suite() +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test/scaffold.py python-daemon-2.0.5/test/scaffold.py --- python-daemon-1.5.5/test/scaffold.py 2010-01-20 11:33:10.000000000 +0000 +++ python-daemon-2.0.5/test/scaffold.py 2015-01-10 11:29:24.000000000 +0000 @@ -1,16 +1,20 @@ # -*- coding: utf-8 -*- # test/scaffold.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2007–2010 Ben Finney -# This is free software; you may copy, modify and/or distribute this work -# under the terms of the GNU General Public License, version 2 or later. -# No warranty expressed or implied. See the file LICENSE.GPL-2 for details. +# Copyright © 2007–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. """ Scaffolding for unit test modules. """ +from __future__ import (absolute_import, unicode_literals) + import unittest import doctest import logging @@ -18,13 +22,22 @@ import sys import operator import textwrap -from minimock import ( - Mock, - TraceTracker as MockTracker, - mock, - restore as mock_restore, - ) +from copy import deepcopy +import functools + +try: + # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text). + basestring = basestring + unicode = unicode +except NameError: + # Python 3 names the Unicode data type ‘str’. + basestring = str + unicode = str +import testscenarios +import testtools.testcase + + test_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(test_dir) if not test_dir in sys.path: @@ -32,173 +45,114 @@ if not parent_dir in sys.path: sys.path.insert(1, parent_dir) -# Disable all but the most critical logging messages +# Disable all but the most critical logging messages. logging.disable(logging.CRITICAL) -def get_python_module_names(file_list, file_suffix='.py'): - """ Return a list of module names from a filename list. """ - module_names = [m[:m.rfind(file_suffix)] for m in file_list - if m.endswith(file_suffix)] - return module_names - - -def get_test_module_names(module_list, module_prefix='test_'): - """ Return the list of module names that qualify as test modules. """ - module_names = [m for m in module_list - if m.startswith(module_prefix)] - return module_names - - -def make_suite(path=test_dir): - """ Create the test suite for the given path. """ - loader = unittest.TestLoader() - python_module_names = get_python_module_names(os.listdir(path)) - test_module_names = get_test_module_names(python_module_names) - suite = loader.loadTestsFromNames(test_module_names) +def get_function_signature(func): + """ Get the function signature as a mapping of attributes. - return suite + :param func: The function object to interrogate. + :return: A mapping of the components of a function signature. - -def get_function_signature(func): - """ Get the function signature as a mapping of attributes. """ - arg_count = func.func_code.co_argcount - arg_names = func.func_code.co_varnames[:arg_count] + The signature is constructed as a mapping: - arg_defaults = {} - func_defaults = () - if func.func_defaults is not None: + * 'name': The function's defined name. + * 'arg_count': The number of arguments expected by the function. + * 'arg_names': A sequence of the argument names, as strings. + * 'arg_defaults': A sequence of the default values for the arguments. + * 'va_args': The name bound to remaining positional arguments. + * 'va_kw_args': The name bound to remaining keyword arguments. + + """ + try: + # Python 3 function attributes. + func_code = func.__code__ + func_defaults = func.__defaults__ + except AttributeError: + # Python 2 function attributes. + func_code = func.func_code func_defaults = func.func_defaults - for (name, value) in zip(arg_names[::-1], func_defaults[::-1]): - arg_defaults[name] = value + + arg_count = func_code.co_argcount + arg_names = func_code.co_varnames[:arg_count] + + arg_defaults = {} + if func_defaults is not None: + arg_defaults = dict( + (name, value) + for (name, value) in + zip(arg_names[::-1], func_defaults[::-1])) signature = { - 'name': func.__name__, - 'arg_count': arg_count, - 'arg_names': arg_names, - 'arg_defaults': arg_defaults, - } + 'name': func.__name__, + 'arg_count': arg_count, + 'arg_names': arg_names, + 'arg_defaults': arg_defaults, + } - non_pos_names = list(func.func_code.co_varnames[arg_count:]) + non_pos_names = list(func_code.co_varnames[arg_count:]) COLLECTS_ARBITRARY_POSITIONAL_ARGS = 0x04 - if func.func_code.co_flags & COLLECTS_ARBITRARY_POSITIONAL_ARGS: + if func_code.co_flags & COLLECTS_ARBITRARY_POSITIONAL_ARGS: signature['var_args'] = non_pos_names.pop(0) COLLECTS_ARBITRARY_KEYWORD_ARGS = 0x08 - if func.func_code.co_flags & COLLECTS_ARBITRARY_KEYWORD_ARGS: + if func_code.co_flags & COLLECTS_ARBITRARY_KEYWORD_ARGS: signature['var_kw_args'] = non_pos_names.pop(0) return signature def format_function_signature(func): - """ Format the function signature as printable text. """ + """ Format the function signature as printable text. + + :param func: The function object to interrogate. + :return: A formatted text representation of the function signature. + + The signature is rendered a text; for example:: + + foo(spam, eggs, ham=True, beans=None, *args, **kwargs) + + """ signature = get_function_signature(func) args_text = [] for arg_name in signature['arg_names']: if arg_name in signature['arg_defaults']: - arg_default = signature['arg_defaults'][arg_name] - arg_text_template = "%(arg_name)s=%(arg_default)r" + arg_text = "{name}={value!r}".format( + name=arg_name, value=signature['arg_defaults'][arg_name]) else: - arg_text_template = "%(arg_name)s" - args_text.append(arg_text_template % vars()) + arg_text = "{name}".format( + name=arg_name) + args_text.append(arg_text) if 'var_args' in signature: - args_text.append("*%(var_args)s" % signature) + args_text.append("*{var_args}".format(signature)) if 'var_kw_args' in signature: - args_text.append("**%(var_kw_args)s" % signature) + args_text.append("**{var_kw_args}".format(signature)) signature_args_text = ", ".join(args_text) func_name = signature['name'] - signature_text = ( - "%(func_name)s(%(signature_args_text)s)" % vars()) + signature_text = "{name}({args})".format( + name=func_name, args=signature_args_text) return signature_text -class TestCase(unittest.TestCase): +class TestCase(testtools.testcase.TestCase): """ Test case behaviour. """ - def failUnlessRaises(self, exc_class, func, *args, **kwargs): - """ Fail unless the function call raises the expected exception. - - Fail the test if an instance of the exception class - ``exc_class`` is not raised when calling ``func`` with the - arguments ``*args`` and ``**kwargs``. - - """ - try: - super(TestCase, self).failUnlessRaises( - exc_class, func, *args, **kwargs) - except self.failureException: - exc_class_name = exc_class.__name__ - msg = ( - "Exception %(exc_class_name)s not raised" - " for function call:" - " func=%(func)r args=%(args)r kwargs=%(kwargs)r" - ) % vars() - raise self.failureException(msg) - - def failIfIs(self, first, second, msg=None): - """ Fail if the two objects are identical. - - Fail the test if ``first`` and ``second`` are identical, - as determined by the ``is`` operator. - - """ - if first is second: - if msg is None: - msg = "%(first)r is %(second)r" % vars() - raise self.failureException(msg) - - def failUnlessIs(self, first, second, msg=None): - """ Fail unless the two objects are identical. - - Fail the test unless ``first`` and ``second`` are - identical, as determined by the ``is`` operator. - - """ - if first is not second: - if msg is None: - msg = "%(first)r is not %(second)r" % vars() - raise self.failureException(msg) - - assertIs = failUnlessIs - assertNotIs = failIfIs - - def failIfIn(self, first, second, msg=None): - """ Fail if the second object is in the first. - - Fail the test if ``first`` contains ``second``, as - determined by the ``in`` operator. - - """ - if second in first: - if msg is None: - msg = "%(second)r is in %(first)r" % vars() - raise self.failureException(msg) - - def failUnlessIn(self, first, second, msg=None): - """ Fail unless the second object is in the first. - - Fail the test unless ``first`` contains ``second``, as - determined by the ``in`` operator. - - """ - if second not in first: - if msg is None: - msg = "%(second)r is not in %(first)r" % vars() - raise self.failureException(msg) - - assertIn = failUnlessIn - assertNotIn = failIfIn - def failUnlessOutputCheckerMatch(self, want, got, msg=None): """ Fail unless the specified string matches the expected. - Fail the test unless ``want`` matches ``got``, as - determined by a ``doctest.OutputChecker`` instance. This - is not an equality check, but a pattern match according to - the ``OutputChecker`` rules. + :param want: The desired output pattern. + :param got: The actual text to match. + :param msg: A message to prefix on the failure message. + :return: ``None``. + :raises self.failureException: If the text does not match. + + Fail the test unless ``want`` matches ``got``, as determined by + a ``doctest.OutputChecker`` instance. This is not an equality + check, but a pattern match according to the ``OutputChecker`` + rules. """ checker = doctest.OutputChecker() @@ -206,107 +160,42 @@ source = "" example = doctest.Example(source, want) got = textwrap.dedent(got) - checker_optionflags = reduce(operator.or_, [ - doctest.ELLIPSIS, - ]) + checker_optionflags = functools.reduce(operator.or_, [ + doctest.ELLIPSIS, + ]) if not checker.check_output(want, got, checker_optionflags): if msg is None: diff = checker.output_difference( - example, got, checker_optionflags) + example, got, checker_optionflags) msg = "\n".join([ - "Output received did not match expected output", - "%(diff)s", - ]) % vars() + "Output received did not match expected output", + "{diff}", + ]).format( + diff=diff) raise self.failureException(msg) assertOutputCheckerMatch = failUnlessOutputCheckerMatch - def failUnlessMockCheckerMatch(self, want, tracker=None, msg=None): - """ Fail unless the mock tracker matches the wanted output. - - Fail the test unless `want` matches the output tracked by - `tracker` (defaults to ``self.mock_tracker``. This is not - an equality check, but a pattern match according to the - ``minimock.MinimockOutputChecker`` rules. - - """ - if tracker is None: - tracker = self.mock_tracker - if not tracker.check(want): - if msg is None: - diff = tracker.diff(want) - msg = "\n".join([ - "Output received did not match expected output", - "%(diff)s", - ]) % vars() - raise self.failureException(msg) - - def failIfMockCheckerMatch(self, want, tracker=None, msg=None): - """ Fail if the mock tracker matches the specified output. - - Fail the test if `want` matches the output tracked by - `tracker` (defaults to ``self.mock_tracker``. This is not - an equality check, but a pattern match according to the - ``minimock.MinimockOutputChecker`` rules. - - """ - if tracker is None: - tracker = self.mock_tracker - if tracker.check(want): - if msg is None: - diff = tracker.diff(want) - msg = "\n".join([ - "Output received matched specified undesired output", - "%(diff)s", - ]) % vars() - raise self.failureException(msg) - - assertMockCheckerMatch = failUnlessMockCheckerMatch - assertNotMockCheckerMatch = failIfMockCheckerMatch - - def failIfIsInstance(self, obj, classes, msg=None): - """ Fail if the object is an instance of the specified classes. - - Fail the test if the object ``obj`` is an instance of any - of ``classes``. - - """ - if isinstance(obj, classes): - if msg is None: - msg = ( - "%(obj)r is an instance of one of %(classes)r" - ) % vars() - raise self.failureException(msg) - - def failUnlessIsInstance(self, obj, classes, msg=None): - """ Fail unless the object is an instance of the specified classes. - - Fail the test unless the object ``obj`` is an instance of - any of ``classes``. - - """ - if not isinstance(obj, classes): - if msg is None: - msg = ( - "%(obj)r is not an instance of any of %(classes)r" - ) % vars() - raise self.failureException(msg) - - assertIsInstance = failUnlessIsInstance - assertNotIsInstance = failIfIsInstance - def failUnlessFunctionInTraceback(self, traceback, function, msg=None): """ Fail if the function is not in the traceback. - Fail the test if the function ``function`` is not at any - of the levels in the traceback object ``traceback``. + :param traceback: The traceback object to interrogate. + :param function: The function object to match. + :param msg: A message to prefix on the failure message. + :return: ``None``. + + :raises self.failureException: If the function is not in the + traceback. + + Fail the test if the function ``function`` is not at any of the + levels in the traceback object ``traceback``. """ func_in_traceback = False - expect_code = function.func_code + expected_code = function.func_code current_traceback = traceback while current_traceback is not None: - if expect_code is current_traceback.tb_frame.f_code: + if expected_code is current_traceback.tb_frame.f_code: func_in_traceback = True break current_traceback = current_traceback.tb_next @@ -314,9 +203,10 @@ if not func_in_traceback: if msg is None: msg = ( - "Traceback did not lead to original function" - " %(function)s" - ) % vars() + "Traceback did not lead to original function" + " {function}" + ).format( + function=function) raise self.failureException(msg) assertFunctionInTraceback = failUnlessFunctionInTraceback @@ -324,9 +214,16 @@ def failUnlessFunctionSignatureMatch(self, first, second, msg=None): """ Fail if the function signatures do not match. - Fail the test if the function signature does not match - between the ``first`` function and the ``second`` - function. + :param first: The first function to compare. + :param second: The second function to compare. + :param msg: A message to prefix to the failure message. + :return: ``None``. + + :raises self.failureException: If the function signatures do + not match. + + Fail the test if the function signature does not match between + the ``first`` function and the ``second`` function. The function signature includes: @@ -351,52 +248,75 @@ first_signature_text = format_function_signature(first) second_signature_text = format_function_signature(second) msg = (textwrap.dedent("""\ - Function signatures do not match: - %(first_signature)r != %(second_signature)r - Expected: - %(first_signature_text)s - Got: - %(second_signature_text)s""") - ) % vars() + Function signatures do not match: + {first!r} != {second!r} + Expected: + {first_text} + Got: + {second_text}""") + ).format( + first=first_signature, + first_text=first_signature_text, + second=second_signature, + second_text=second_signature_text, + ) raise self.failureException(msg) assertFunctionSignatureMatch = failUnlessFunctionSignatureMatch - -class Exception_TestCase(TestCase): - """ Test cases for exception classes. """ - def __init__(self, *args, **kwargs): - """ Set up a new instance """ - self.valid_exceptions = NotImplemented - super(Exception_TestCase, self).__init__(*args, **kwargs) - - def setUp(self): - """ Set up test fixtures. """ - for exc_type, params in self.valid_exceptions.items(): - args = (None, ) * params['min_args'] - params['args'] = args - instance = exc_type(*args) - params['instance'] = instance +class TestCaseWithScenarios(testscenarios.WithScenarios, TestCase): + """ Test cases run per scenario. """ - super(Exception_TestCase, self).setUp() + +class Exception_TestCase(TestCaseWithScenarios): + """ Test cases for exception classes. """ def test_exception_instance(self): """ Exception instance should be created. """ - for params in self.valid_exceptions.values(): - instance = params['instance'] - self.failIfIs(None, instance) + self.assertIsNot(self.instance, None) def test_exception_types(self): - """ Exception instances should match expected types. """ - for params in self.valid_exceptions.values(): - instance = params['instance'] - for match_type in params['types']: - match_type_name = match_type.__name__ - fail_msg = ( - "%(instance)r is not an instance of" - " %(match_type_name)s" - ) % vars() - self.failUnless( - isinstance(instance, match_type), - msg=fail_msg) + """ Exception instance should match expected types. """ + for match_type in self.types: + self.assertIsInstance(self.instance, match_type) + + +def make_exception_scenarios(scenarios): + """ Make test scenarios for exception classes. + + :param scenarios: Sequence of scenarios. + :return: List of scenarios with additional mapping entries. + + Use this with `testscenarios` to adapt `Exception_TestCase`_ for + any exceptions that need testing. + + Each scenario is a tuple (`name`, `map`) where `map` is a mapping + of attributes to be applied to each test case. Attributes map must + contain items for: + + :key exc_type: + The exception type to be tested. + :key min_args: + The minimum argument count for the exception instance + initialiser. + :key types: + Sequence of types that should be superclasses of each + instance of the exception type. + + """ + updated_scenarios = deepcopy(scenarios) + for (name, scenario) in updated_scenarios: + args = (None,) * scenario['min_args'] + scenario['args'] = args + instance = scenario['exc_type'](*args) + scenario['instance'] = instance + + return updated_scenarios + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test/test_daemon.py python-daemon-2.0.5/test/test_daemon.py --- python-daemon-1.5.5/test/test_daemon.py 2010-02-26 03:09:27.000000000 +0000 +++ python-daemon-2.0.5/test/test_daemon.py 2015-02-02 04:43:28.000000000 +0000 @@ -1,18 +1,20 @@ # -*- coding: utf-8 -*- # # test/test_daemon.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2008–2010 Ben Finney +# Copyright © 2008–2015 Ben Finney # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. -""" Unit test for daemon module. +""" Unit test for ‘daemon’ module. """ +from __future__ import (absolute_import, unicode_literals) + import os import sys import tempfile @@ -21,346 +23,328 @@ import signal import socket from types import ModuleType -import atexit -from StringIO import StringIO - -import scaffold -from test_pidlockfile import ( - FakeFileDescriptorStringIO, - setup_pidfile_fixtures, - ) +import collections +import functools +try: + # Standard library of Python 2.7 and later. + from io import StringIO +except ImportError: + # Standard library of Python 2.6 and earlier. + from StringIO import StringIO + +import mock + +from . import scaffold +from .scaffold import (basestring, unicode) +from .test_pidfile import ( + FakeFileDescriptorStringIO, + setup_pidfile_fixtures, + ) -from daemon import pidlockfile import daemon -class Exception_TestCase(scaffold.Exception_TestCase): +class ModuleExceptions_TestCase(scaffold.Exception_TestCase): """ Test cases for module exception classes. """ - def __init__(self, *args, **kwargs): - """ Set up a new instance. """ - super(Exception_TestCase, self).__init__(*args, **kwargs) - - self.valid_exceptions = { - daemon.daemon.DaemonError: dict( + scenarios = scaffold.make_exception_scenarios([ + ('daemon.daemon.DaemonError', dict( + exc_type = daemon.daemon.DaemonError, min_args = 1, - types = (Exception,), - ), - daemon.daemon.DaemonOSEnvironmentError: dict( + types = [Exception], + )), + ('daemon.daemon.DaemonOSEnvironmentError', dict( + exc_type = daemon.daemon.DaemonOSEnvironmentError, min_args = 1, - types = (daemon.daemon.DaemonError, OSError), - ), - daemon.daemon.DaemonProcessDetachError: dict( + types = [daemon.daemon.DaemonError, OSError], + )), + ('daemon.daemon.DaemonProcessDetachError', dict( + exc_type = daemon.daemon.DaemonProcessDetachError, min_args = 1, - types = (daemon.daemon.DaemonError, OSError), - ), - } + types = [daemon.daemon.DaemonError, OSError], + )), + ]) def setup_daemon_context_fixtures(testcase): - """ Set up common test fixtures for DaemonContext test case. """ - testcase.mock_tracker = scaffold.MockTracker() + """ Set up common test fixtures for DaemonContext test case. + + :param testcase: A ``TestCase`` instance to decorate. + :return: ``None``. + Decorate the `testcase` with fixtures for tests involving + `DaemonContext`. + + """ setup_streams_fixtures(testcase) setup_pidfile_fixtures(testcase) - testcase.mock_pidfile_path = tempfile.mktemp() - testcase.mock_pidlockfile = scaffold.Mock( - "pidlockfile.PIDLockFile", - tracker=testcase.mock_tracker) - testcase.mock_pidlockfile.path = testcase.mock_pidfile_path - - scaffold.mock( - "daemon.daemon.is_detach_process_context_required", - returns=True, - tracker=testcase.mock_tracker) - scaffold.mock( - "daemon.daemon.make_default_signal_map", - returns=object(), - tracker=testcase.mock_tracker) - - scaffold.mock( - "os.getuid", - returns=object(), - tracker=testcase.mock_tracker) - scaffold.mock( - "os.getgid", - returns=object(), - tracker=testcase.mock_tracker) + testcase.fake_pidfile_path = tempfile.mktemp() + testcase.mock_pidlockfile = mock.MagicMock() + testcase.mock_pidlockfile.path = testcase.fake_pidfile_path testcase.daemon_context_args = dict( - stdin = testcase.stream_files_by_name['stdin'], - stdout = testcase.stream_files_by_name['stdout'], - stderr = testcase.stream_files_by_name['stderr'], - ) + stdin=testcase.stream_files_by_name['stdin'], + stdout=testcase.stream_files_by_name['stdout'], + stderr=testcase.stream_files_by_name['stderr'], + ) testcase.test_instance = daemon.DaemonContext( - **testcase.daemon_context_args) + **testcase.daemon_context_args) +fake_default_signal_map = object() -class DaemonContext_TestCase(scaffold.TestCase): - """ Test cases for DaemonContext class. """ +@mock.patch.object( + daemon.daemon, "is_detach_process_context_required", + new=(lambda: True)) +@mock.patch.object( + daemon.daemon, "make_default_signal_map", + new=(lambda: fake_default_signal_map)) +@mock.patch.object(os, "setgid", new=(lambda x: object())) +@mock.patch.object(os, "setuid", new=(lambda x: object())) +class DaemonContext_BaseTestCase(scaffold.TestCase): + """ Base class for DaemonContext test case classes. """ def setUp(self): """ Set up test fixtures. """ + super(DaemonContext_BaseTestCase, self).setUp() + setup_daemon_context_fixtures(self) - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + +class DaemonContext_TestCase(DaemonContext_BaseTestCase): + """ Test cases for DaemonContext class. """ def test_instantiate(self): """ New instance of DaemonContext should be created. """ - self.failUnlessIsInstance( - self.test_instance, daemon.daemon.DaemonContext) + self.assertIsInstance( + self.test_instance, daemon.daemon.DaemonContext) def test_minimum_zero_arguments(self): """ Initialiser should not require any arguments. """ instance = daemon.daemon.DaemonContext() - self.failIfIs(None, instance) + self.assertIsNot(instance, None) def test_has_specified_chroot_directory(self): """ Should have specified chroot_directory option. """ args = dict( - chroot_directory = object(), - ) - expect_directory = args['chroot_directory'] + chroot_directory=object(), + ) + expected_directory = args['chroot_directory'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_directory, instance.chroot_directory) + self.assertEqual(expected_directory, instance.chroot_directory) def test_has_specified_working_directory(self): """ Should have specified working_directory option. """ args = dict( - working_directory = object(), - ) - expect_directory = args['working_directory'] + working_directory=object(), + ) + expected_directory = args['working_directory'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_directory, instance.working_directory) + self.assertEqual(expected_directory, instance.working_directory) def test_has_default_working_directory(self): """ Should have default working_directory option. """ args = dict() - expect_directory = '/' + expected_directory = "/" instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_directory, instance.working_directory) + self.assertEqual(expected_directory, instance.working_directory) def test_has_specified_creation_mask(self): """ Should have specified umask option. """ args = dict( - umask = object(), - ) - expect_mask = args['umask'] + umask=object(), + ) + expected_mask = args['umask'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_mask, instance.umask) + self.assertEqual(expected_mask, instance.umask) def test_has_default_creation_mask(self): """ Should have default umask option. """ args = dict() - expect_mask = 0 + expected_mask = 0 instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_mask, instance.umask) + self.assertEqual(expected_mask, instance.umask) def test_has_specified_uid(self): """ Should have specified uid option. """ args = dict( - uid = object(), - ) - expect_id = args['uid'] + uid=object(), + ) + expected_id = args['uid'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_id, instance.uid) + self.assertEqual(expected_id, instance.uid) def test_has_derived_uid(self): """ Should have uid option derived from process. """ args = dict() - expect_id = os.getuid() + expected_id = os.getuid() instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_id, instance.uid) + self.assertEqual(expected_id, instance.uid) def test_has_specified_gid(self): """ Should have specified gid option. """ args = dict( - gid = object(), - ) - expect_id = args['gid'] + gid=object(), + ) + expected_id = args['gid'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_id, instance.gid) + self.assertEqual(expected_id, instance.gid) def test_has_derived_gid(self): """ Should have gid option derived from process. """ args = dict() - expect_id = os.getgid() + expected_id = os.getgid() instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_id, instance.gid) + self.assertEqual(expected_id, instance.gid) def test_has_specified_detach_process(self): """ Should have specified detach_process option. """ args = dict( - detach_process = object(), - ) - expect_value = args['detach_process'] + detach_process=object(), + ) + expected_value = args['detach_process'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_value, instance.detach_process) + self.assertEqual(expected_value, instance.detach_process) def test_has_derived_detach_process(self): """ Should have detach_process option derived from environment. """ args = dict() func = daemon.daemon.is_detach_process_context_required - expect_value = func() + expected_value = func() instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_value, instance.detach_process) + self.assertEqual(expected_value, instance.detach_process) def test_has_specified_files_preserve(self): """ Should have specified files_preserve option. """ args = dict( - files_preserve = object(), - ) - expect_files_preserve = args['files_preserve'] + files_preserve=object(), + ) + expected_files_preserve = args['files_preserve'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_files_preserve, instance.files_preserve) + self.assertEqual(expected_files_preserve, instance.files_preserve) def test_has_specified_pidfile(self): """ Should have the specified pidfile. """ args = dict( - pidfile = object(), - ) - expect_pidfile = args['pidfile'] + pidfile=object(), + ) + expected_pidfile = args['pidfile'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_pidfile, instance.pidfile) + self.assertEqual(expected_pidfile, instance.pidfile) def test_has_specified_stdin(self): """ Should have specified stdin option. """ args = dict( - stdin = object(), - ) - expect_file = args['stdin'] + stdin=object(), + ) + expected_file = args['stdin'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_file, instance.stdin) + self.assertEqual(expected_file, instance.stdin) def test_has_specified_stdout(self): """ Should have specified stdout option. """ args = dict( - stdout = object(), - ) - expect_file = args['stdout'] + stdout=object(), + ) + expected_file = args['stdout'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_file, instance.stdout) + self.assertEqual(expected_file, instance.stdout) def test_has_specified_stderr(self): """ Should have specified stderr option. """ args = dict( - stderr = object(), - ) - expect_file = args['stderr'] + stderr=object(), + ) + expected_file = args['stderr'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_file, instance.stderr) + self.assertEqual(expected_file, instance.stderr) def test_has_specified_signal_map(self): """ Should have specified signal_map option. """ args = dict( - signal_map = object(), - ) - expect_signal_map = args['signal_map'] + signal_map=object(), + ) + expected_signal_map = args['signal_map'] instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_signal_map, instance.signal_map) + self.assertEqual(expected_signal_map, instance.signal_map) def test_has_derived_signal_map(self): """ Should have signal_map option derived from system. """ args = dict() - expect_signal_map = daemon.daemon.make_default_signal_map() + expected_signal_map = daemon.daemon.make_default_signal_map() instance = daemon.daemon.DaemonContext(**args) - self.failUnlessEqual(expect_signal_map, instance.signal_map) + self.assertEqual(expected_signal_map, instance.signal_map) -class DaemonContext_is_open_TestCase(scaffold.TestCase): +class DaemonContext_is_open_TestCase(DaemonContext_BaseTestCase): """ Test cases for DaemonContext.is_open property. """ - def setUp(self): - """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - def test_begin_false(self): """ Initial value of is_open should be False. """ instance = self.test_instance - self.failUnlessEqual(False, instance.is_open) + self.assertEqual(False, instance.is_open) def test_write_fails(self): """ Writing to is_open should fail. """ instance = self.test_instance - self.failUnlessRaises( - AttributeError, - setattr, instance, 'is_open', object()) + self.assertRaises( + AttributeError, + setattr, instance, 'is_open', object()) -class DaemonContext_open_TestCase(scaffold.TestCase): +class DaemonContext_open_TestCase(DaemonContext_BaseTestCase): """ Test cases for DaemonContext.open method. """ def setUp(self): """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) - self.mock_tracker.clear() + super(DaemonContext_open_TestCase, self).setUp() self.test_instance._is_open = False - scaffold.mock( - "daemon.daemon.detach_process_context", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.change_working_directory", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.change_root_directory", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.change_file_creation_mask", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.change_process_owner", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.prevent_core_dump", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.close_all_open_files", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.redirect_stream", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.set_signal_handlers", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.register_atexit_function", - tracker=self.mock_tracker) + self.mock_module_daemon = mock.MagicMock() + daemon_func_patchers = dict( + (func_name, mock.patch.object( + daemon.daemon, func_name)) + for func_name in [ + "detach_process_context", + "change_working_directory", + "change_root_directory", + "change_file_creation_mask", + "change_process_owner", + "prevent_core_dump", + "close_all_open_files", + "redirect_stream", + "set_signal_handlers", + "register_atexit_function", + ]) + for (func_name, patcher) in daemon_func_patchers.items(): + mock_func = patcher.start() + self.addCleanup(patcher.stop) + self.mock_module_daemon.attach_mock(mock_func, func_name) - self.test_files_preserve_fds = object() - scaffold.mock( - "daemon.daemon.DaemonContext._get_exclude_file_descriptors", - returns=self.test_files_preserve_fds, - tracker=self.mock_tracker) + self.mock_module_daemon.attach_mock(mock.Mock(), 'DaemonContext') + self.test_files_preserve_fds = object() self.test_signal_handler_map = object() - scaffold.mock( - "daemon.daemon.DaemonContext._make_signal_handler_map", - returns=self.test_signal_handler_map, - tracker=self.mock_tracker) - - scaffold.mock( - "sys.stdin", - tracker=self.mock_tracker) - scaffold.mock( - "sys.stdout", - tracker=self.mock_tracker) - scaffold.mock( - "sys.stderr", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + daemoncontext_method_return_values = { + '_get_exclude_file_descriptors': + self.test_files_preserve_fds, + '_make_signal_handler_map': + self.test_signal_handler_map, + } + daemoncontext_func_patchers = dict( + (func_name, mock.patch.object( + daemon.daemon.DaemonContext, + func_name, + return_value=return_value)) + for (func_name, return_value) in + daemoncontext_method_return_values.items()) + for (func_name, patcher) in daemoncontext_func_patchers.items(): + mock_func = patcher.start() + self.addCleanup(patcher.stop) + self.mock_module_daemon.DaemonContext.attach_mock( + mock_func, func_name) def test_performs_steps_in_expected_sequence(self): """ Should perform daemonisation steps in expected sequence. """ @@ -368,117 +352,89 @@ instance.chroot_directory = object() instance.detach_process = True instance.pidfile = self.mock_pidlockfile - expect_mock_output = """\ - Called daemon.daemon.change_root_directory(...) - Called daemon.daemon.prevent_core_dump() - Called daemon.daemon.change_file_creation_mask(...) - Called daemon.daemon.change_working_directory(...) - Called daemon.daemon.change_process_owner(...) - Called daemon.daemon.detach_process_context() - Called daemon.daemon.DaemonContext._make_signal_handler_map() - Called daemon.daemon.set_signal_handlers(...) - Called daemon.daemon.DaemonContext._get_exclude_file_descriptors() - Called daemon.daemon.close_all_open_files(...) - Called daemon.daemon.redirect_stream(...) - Called daemon.daemon.redirect_stream(...) - Called daemon.daemon.redirect_stream(...) - Called pidlockfile.PIDLockFile.__enter__() - Called daemon.daemon.register_atexit_function(...) - """ % vars() - self.mock_tracker.clear() + self.mock_module_daemon.attach_mock( + self.mock_pidlockfile, 'pidlockfile') + expected_calls = [ + mock.call.change_root_directory(mock.ANY), + mock.call.prevent_core_dump(), + mock.call.change_file_creation_mask(mock.ANY), + mock.call.change_working_directory(mock.ANY), + mock.call.change_process_owner(mock.ANY, mock.ANY), + mock.call.detach_process_context(), + mock.call.DaemonContext._make_signal_handler_map(), + mock.call.set_signal_handlers(mock.ANY), + mock.call.DaemonContext._get_exclude_file_descriptors(), + mock.call.close_all_open_files(exclude=mock.ANY), + mock.call.redirect_stream(mock.ANY, mock.ANY), + mock.call.redirect_stream(mock.ANY, mock.ANY), + mock.call.redirect_stream(mock.ANY, mock.ANY), + mock.call.pidlockfile.__enter__(), + mock.call.register_atexit_function(mock.ANY), + ] instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.assert_has_calls(expected_calls) def test_returns_immediately_if_is_open(self): """ Should return immediately if is_open property is true. """ instance = self.test_instance instance._is_open = True - expect_mock_output = """\ - """ - self.mock_tracker.clear() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.assertEqual(0, len(self.mock_module_daemon.mock_calls)) def test_changes_root_directory_to_chroot_directory(self): """ Should change root directory to `chroot_directory` option. """ instance = self.test_instance chroot_directory = object() instance.chroot_directory = chroot_directory - expect_mock_output = """\ - Called daemon.daemon.change_root_directory( - %(chroot_directory)r) - ... - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.change_root_directory.assert_called_with( + chroot_directory) def test_omits_chroot_if_no_chroot_directory(self): """ Should omit changing root directory if no `chroot_directory`. """ instance = self.test_instance instance.chroot_directory = None - unwanted_output = """\ - ...Called daemon.daemon.change_root_directory(...)...""" instance.open() - self.failIfMockCheckerMatch(unwanted_output) + self.assertFalse(self.mock_module_daemon.change_root_directory.called) def test_prevents_core_dump(self): """ Should request prevention of core dumps. """ instance = self.test_instance - expect_mock_output = """\ - Called daemon.daemon.prevent_core_dump() - ... - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.prevent_core_dump.assert_called_with() def test_omits_prevent_core_dump_if_prevent_core_false(self): """ Should omit preventing core dumps if `prevent_core` is false. """ instance = self.test_instance instance.prevent_core = False - unwanted_output = """\ - ...Called daemon.daemon.prevent_core_dump()...""" instance.open() - self.failIfMockCheckerMatch(unwanted_output) + self.assertFalse(self.mock_module_daemon.prevent_core_dump.called) def test_closes_open_files(self): """ Should close all open files, excluding `files_preserve`. """ instance = self.test_instance - expect_exclude = self.test_files_preserve_fds - expect_mock_output = """\ - ... - Called daemon.daemon.close_all_open_files( - exclude=%(expect_exclude)r) - ... - """ % vars() + expected_exclude = self.test_files_preserve_fds instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.close_all_open_files.assert_called_with( + exclude=expected_exclude) def test_changes_directory_to_working_directory(self): """ Should change current directory to `working_directory` option. """ instance = self.test_instance working_directory = object() instance.working_directory = working_directory - expect_mock_output = """\ - ... - Called daemon.daemon.change_working_directory( - %(working_directory)r) - ... - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.change_working_directory.assert_called_with( + working_directory) def test_changes_creation_mask_to_umask(self): """ Should change file creation mask to `umask` option. """ instance = self.test_instance umask = object() instance.umask = umask - expect_mock_output = """\ - ... - Called daemon.daemon.change_file_creation_mask(%(umask)r) - ... - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.change_file_creation_mask.assert_called_with( + umask) def test_changes_owner_to_specified_uid_and_gid(self): """ Should change process UID and GID to `uid` and `gid` options. """ @@ -487,626 +443,566 @@ gid = object() instance.uid = uid instance.gid = gid - expect_mock_output = """\ - ... - Called daemon.daemon.change_process_owner(%(uid)r, %(gid)r) - ... - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.change_process_owner.assert_called_with( + uid, gid) def test_detaches_process_context(self): """ Should request detach of process context. """ instance = self.test_instance - expect_mock_output = """\ - ... - Called daemon.daemon.detach_process_context() - ... - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.detach_process_context.assert_called_with() def test_omits_process_detach_if_not_required(self): """ Should omit detach of process context if not required. """ instance = self.test_instance instance.detach_process = False - unwanted_output = """\ - ...Called daemon.daemon.detach_process_context(...)...""" instance.open() - self.failIfMockCheckerMatch(unwanted_output) + self.assertFalse(self.mock_module_daemon.detach_process_context.called) def test_sets_signal_handlers_from_signal_map(self): """ Should set signal handlers according to `signal_map`. """ instance = self.test_instance instance.signal_map = object() - expect_signal_handler_map = self.test_signal_handler_map - expect_mock_output = """\ - ... - Called daemon.daemon.set_signal_handlers( - %(expect_signal_handler_map)r) - ... - """ % vars() + expected_signal_handler_map = self.test_signal_handler_map instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.set_signal_handlers.assert_called_with( + expected_signal_handler_map) def test_redirects_standard_streams(self): """ Should request redirection of standard stream files. """ instance = self.test_instance (system_stdin, system_stdout, system_stderr) = ( - sys.stdin, sys.stdout, sys.stderr) + sys.stdin, sys.stdout, sys.stderr) (target_stdin, target_stdout, target_stderr) = ( - self.stream_files_by_name[name] - for name in ['stdin', 'stdout', 'stderr']) - expect_mock_output = """\ - ... - Called daemon.daemon.redirect_stream( - %(system_stdin)r, %(target_stdin)r) - Called daemon.daemon.redirect_stream( - %(system_stdout)r, %(target_stdout)r) - Called daemon.daemon.redirect_stream( - %(system_stderr)r, %(target_stderr)r) - ... - """ % vars() + self.stream_files_by_name[name] + for name in ['stdin', 'stdout', 'stderr']) + expected_calls = [ + mock.call(system_stdin, target_stdin), + mock.call(system_stdout, target_stdout), + mock.call(system_stderr, target_stderr), + ] instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.redirect_stream.assert_has_calls( + expected_calls, any_order=True) def test_enters_pidfile_context(self): """ Should enter the PID file context manager. """ instance = self.test_instance instance.pidfile = self.mock_pidlockfile - expect_mock_output = """\ - ... - Called pidlockfile.PIDLockFile.__enter__() - ... - """ instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_pidlockfile.__enter__.assert_called_with() def test_sets_is_open_true(self): """ Should set the `is_open` property to True. """ instance = self.test_instance instance.open() - self.failUnlessEqual(True, instance.is_open) + self.assertEqual(True, instance.is_open) def test_registers_close_method_for_atexit(self): """ Should register the `close` method for atexit processing. """ instance = self.test_instance close_method = instance.close - expect_mock_output = """\ - ... - Called daemon.daemon.register_atexit_function(%(close_method)r) - """ % vars() instance.open() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_daemon.register_atexit_function.assert_called_with( + close_method) -class DaemonContext_close_TestCase(scaffold.TestCase): +class DaemonContext_close_TestCase(DaemonContext_BaseTestCase): """ Test cases for DaemonContext.close method. """ def setUp(self): """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) - self.mock_tracker.clear() + super(DaemonContext_close_TestCase, self).setUp() self.test_instance._is_open = True - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - def test_returns_immediately_if_not_is_open(self): """ Should return immediately if is_open property is false. """ instance = self.test_instance instance._is_open = False instance.pidfile = object() - expect_mock_output = """\ - """ - self.mock_tracker.clear() instance.close() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.assertFalse(self.mock_pidlockfile.__exit__.called) def test_exits_pidfile_context(self): """ Should exit the PID file context manager. """ instance = self.test_instance instance.pidfile = self.mock_pidlockfile - expect_mock_output = """\ - Called pidlockfile.PIDLockFile.__exit__(None, None, None) - """ instance.close() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_pidlockfile.__exit__.assert_called_with(None, None, None) def test_returns_none(self): """ Should return None. """ instance = self.test_instance - expect_result = None + expected_result = None result = instance.close() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) def test_sets_is_open_false(self): """ Should set the `is_open` property to False. """ instance = self.test_instance instance.close() - self.failUnlessEqual(False, instance.is_open) + self.assertEqual(False, instance.is_open) -class DaemonContext_context_manager_enter_TestCase(scaffold.TestCase): +@mock.patch.object(daemon.daemon.DaemonContext, "open") +class DaemonContext_context_manager_enter_TestCase(DaemonContext_BaseTestCase): """ Test cases for DaemonContext.__enter__ method. """ - def setUp(self): - """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) - self.mock_tracker.clear() - - scaffold.mock( - "daemon.daemon.DaemonContext.open", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_opens_daemon_context(self): + def test_opens_daemon_context(self, mock_func_daemoncontext_open): """ Should open the DaemonContext. """ instance = self.test_instance - expect_mock_output = """\ - Called daemon.daemon.DaemonContext.open() - """ instance.__enter__() - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_daemoncontext_open.assert_called_with() - def test_returns_self_instance(self): + def test_returns_self_instance(self, mock_func_daemoncontext_open): """ Should return DaemonContext instance. """ instance = self.test_instance - expect_result = instance + expected_result = instance result = instance.__enter__() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) -class DaemonContext_context_manager_exit_TestCase(scaffold.TestCase): +@mock.patch.object(daemon.daemon.DaemonContext, "close") +class DaemonContext_context_manager_exit_TestCase(DaemonContext_BaseTestCase): """ Test cases for DaemonContext.__exit__ method. """ def setUp(self): """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) - self.mock_tracker.clear() + super(DaemonContext_context_manager_exit_TestCase, self).setUp() self.test_args = dict( - exc_type = object(), - exc_value = object(), - traceback = object(), - ) - - scaffold.mock( - "daemon.daemon.DaemonContext.close", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + exc_type=object(), + exc_value=object(), + traceback=object(), + ) - def test_closes_daemon_context(self): + def test_closes_daemon_context(self, mock_func_daemoncontext_close): """ Should close the DaemonContext. """ instance = self.test_instance args = self.test_args - expect_mock_output = """\ - Called daemon.daemon.DaemonContext.close() - """ instance.__exit__(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_daemoncontext_close.assert_called_with() - def test_returns_none(self): + def test_returns_none(self, mock_func_daemoncontext_close): """ Should return None, indicating exception was not handled. """ instance = self.test_instance args = self.test_args - expect_result = None + expected_result = None result = instance.__exit__(**args) - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) -class DaemonContext_terminate_TestCase(scaffold.TestCase): +class DaemonContext_terminate_TestCase(DaemonContext_BaseTestCase): """ Test cases for DaemonContext.terminate method. """ def setUp(self): """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) + super(DaemonContext_terminate_TestCase, self).setUp() self.test_signal = signal.SIGTERM self.test_frame = None self.test_args = (self.test_signal, self.test_frame) - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - def test_raises_system_exit(self): """ Should raise SystemExit. """ instance = self.test_instance args = self.test_args - expect_exception = SystemExit - self.failUnlessRaises( - expect_exception, - instance.terminate, *args) + expected_exception = SystemExit + self.assertRaises( + expected_exception, + instance.terminate, *args) def test_exception_message_contains_signal_number(self): """ Should raise exception with a message containing signal number. """ instance = self.test_instance args = self.test_args signal_number = self.test_signal - expect_exception = SystemExit - try: - instance.terminate(*args) - except expect_exception, exc: - pass - self.failUnlessIn(str(exc), str(signal_number)) + expected_exception = SystemExit + exc = self.assertRaises( + expected_exception, + instance.terminate, *args) + self.assertIn(unicode(signal_number), unicode(exc)) -class DaemonContext_get_exclude_file_descriptors_TestCase(scaffold.TestCase): +class DaemonContext_get_exclude_file_descriptors_TestCase( + DaemonContext_BaseTestCase): """ Test cases for DaemonContext._get_exclude_file_descriptors function. """ def setUp(self): """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) + super( + DaemonContext_get_exclude_file_descriptors_TestCase, + self).setUp() self.test_files = { - 2: FakeFileDescriptorStringIO(), - 5: 5, - 11: FakeFileDescriptorStringIO(), - 17: None, - 23: FakeFileDescriptorStringIO(), - 37: 37, - 42: FakeFileDescriptorStringIO(), - } + 2: FakeFileDescriptorStringIO(), + 5: 5, + 11: FakeFileDescriptorStringIO(), + 17: None, + 23: FakeFileDescriptorStringIO(), + 37: 37, + 42: FakeFileDescriptorStringIO(), + } for (fileno, item) in self.test_files.items(): if hasattr(item, '_fileno'): item._fileno = fileno self.test_file_descriptors = set( - fd for (fd, item) in self.test_files.items() - if item is not None) + fd for (fd, item) in self.test_files.items() + if item is not None) self.test_file_descriptors.update( - self.stream_files_by_name[name].fileno() - for name in ['stdin', 'stdout', 'stderr'] - ) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + self.stream_files_by_name[name].fileno() + for name in ['stdin', 'stdout', 'stderr'] + ) def test_returns_expected_file_descriptors(self): """ Should return expected set of file descriptors. """ instance = self.test_instance - instance.files_preserve = self.test_files.values() - expect_result = self.test_file_descriptors + instance.files_preserve = list(self.test_files.values()) + expected_result = self.test_file_descriptors result = instance._get_exclude_file_descriptors() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) def test_returns_stream_redirects_if_no_files_preserve(self): """ Should return only stream redirects if no files_preserve. """ instance = self.test_instance instance.files_preserve = None - expect_result = set( - stream.fileno() - for stream in self.stream_files_by_name.values()) + expected_result = set( + stream.fileno() + for stream in self.stream_files_by_name.values()) result = instance._get_exclude_file_descriptors() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) def test_returns_empty_set_if_no_files(self): """ Should return empty set if no file options. """ instance = self.test_instance for name in ['files_preserve', 'stdin', 'stdout', 'stderr']: setattr(instance, name, None) - expect_result = set() + expected_result = set() result = instance._get_exclude_file_descriptors() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) - def test_return_set_omits_streams_without_file_descriptors(self): - """ Should omit any stream without a file descriptor. """ + def test_omits_non_file_streams(self): + """ Should omit non-file stream attributes. """ instance = self.test_instance - instance.files_preserve = self.test_files.values() + instance.files_preserve = list(self.test_files.values()) stream_files = self.stream_files_by_name - stream_names = stream_files.keys() - expect_result = self.test_file_descriptors.copy() + expected_result = self.test_file_descriptors.copy() for (pseudo_stream_name, pseudo_stream) in stream_files.items(): - setattr(instance, pseudo_stream_name, StringIO()) + test_non_file_object = object() + setattr(instance, pseudo_stream_name, test_non_file_object) stream_fd = pseudo_stream.fileno() - expect_result.discard(stream_fd) + expected_result.discard(stream_fd) result = instance._get_exclude_file_descriptors() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) - -class DaemonContext_make_signal_handler_TestCase(scaffold.TestCase): - """ Test cases for DaemonContext._make_signal_handler function. """ + def test_includes_verbatim_streams_without_file_descriptor(self): + """ Should include verbatim any stream without a file descriptor. """ + instance = self.test_instance + instance.files_preserve = list(self.test_files.values()) + stream_files = self.stream_files_by_name + mock_fileno_method = mock.MagicMock( + spec=sys.__stdin__.fileno, + side_effect=ValueError) + expected_result = self.test_file_descriptors.copy() + for (pseudo_stream_name, pseudo_stream) in stream_files.items(): + test_non_fd_stream = StringIO() + if not hasattr(test_non_fd_stream, 'fileno'): + # Python < 3 StringIO doesn't have ‘fileno’ at all. + # Add a method which raises an exception. + test_non_fd_stream.fileno = mock_fileno_method + setattr(instance, pseudo_stream_name, test_non_fd_stream) + stream_fd = pseudo_stream.fileno() + expected_result.discard(stream_fd) + expected_result.add(test_non_fd_stream) + result = instance._get_exclude_file_descriptors() + self.assertEqual(expected_result, result) - def setUp(self): - """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) + def test_omits_none_streams(self): + """ Should omit any stream attribute which is None. """ + instance = self.test_instance + instance.files_preserve = list(self.test_files.values()) + stream_files = self.stream_files_by_name + expected_result = self.test_file_descriptors.copy() + for (pseudo_stream_name, pseudo_stream) in stream_files.items(): + setattr(instance, pseudo_stream_name, None) + stream_fd = pseudo_stream.fileno() + expected_result.discard(stream_fd) + result = instance._get_exclude_file_descriptors() + self.assertEqual(expected_result, result) - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + +class DaemonContext_make_signal_handler_TestCase(DaemonContext_BaseTestCase): + """ Test cases for DaemonContext._make_signal_handler function. """ def test_returns_ignore_for_none(self): """ Should return SIG_IGN when None handler specified. """ instance = self.test_instance target = None - expect_result = signal.SIG_IGN + expected_result = signal.SIG_IGN result = instance._make_signal_handler(target) - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) def test_returns_method_for_name(self): """ Should return method of DaemonContext when name specified. """ instance = self.test_instance target = 'terminate' - expect_result = instance.terminate + expected_result = instance.terminate result = instance._make_signal_handler(target) - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) def test_raises_error_for_unknown_name(self): """ Should raise AttributeError for unknown method name. """ instance = self.test_instance target = 'b0gUs' - expect_error = AttributeError - self.failUnlessRaises( - expect_error, - instance._make_signal_handler, target) + expected_error = AttributeError + self.assertRaises( + expected_error, + instance._make_signal_handler, target) def test_returns_object_for_object(self): """ Should return same object for any other object. """ instance = self.test_instance target = object() - expect_result = target + expected_result = target result = instance._make_signal_handler(target) - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) -class DaemonContext_make_signal_handler_map_TestCase(scaffold.TestCase): +class DaemonContext_make_signal_handler_map_TestCase( + DaemonContext_BaseTestCase): """ Test cases for DaemonContext._make_signal_handler_map function. """ def setUp(self): """ Set up test fixtures. """ - setup_daemon_context_fixtures(self) + super(DaemonContext_make_signal_handler_map_TestCase, self).setUp() self.test_instance.signal_map = { - object(): object(), - object(): object(), - object(): object(), - } + object(): object(), + object(): object(), + object(): object(), + } self.test_signal_handlers = dict( - (key, object()) - for key in self.test_instance.signal_map.values()) + (key, object()) + for key in self.test_instance.signal_map.values()) self.test_signal_handler_map = dict( - (key, self.test_signal_handlers[target]) - for (key, target) in self.test_instance.signal_map.items()) + (key, self.test_signal_handlers[target]) + for (key, target) in self.test_instance.signal_map.items()) - def mock_make_signal_handler(target): + def fake_make_signal_handler(target): return self.test_signal_handlers[target] - scaffold.mock( - "daemon.daemon.DaemonContext._make_signal_handler", - returns_func=mock_make_signal_handler, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + + func_patcher_make_signal_handler = mock.patch.object( + daemon.daemon.DaemonContext, "_make_signal_handler", + side_effect=fake_make_signal_handler) + self.mock_func_make_signal_handler = ( + func_patcher_make_signal_handler.start()) + self.addCleanup(func_patcher_make_signal_handler.stop) def test_returns_constructed_signal_handler_items(self): """ Should return items as constructed via make_signal_handler. """ instance = self.test_instance - expect_result = self.test_signal_handler_map + expected_result = self.test_signal_handler_map result = instance._make_signal_handler_map() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) +try: + FileNotFoundError +except NameError: + # Python 2 uses IOError. + FileNotFoundError = functools.partial(IOError, errno.ENOENT) + + +@mock.patch.object(os, "chdir") class change_working_directory_TestCase(scaffold.TestCase): """ Test cases for change_working_directory function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "os.chdir", - tracker=self.mock_tracker) + super(change_working_directory_TestCase, self).setUp() self.test_directory = object() self.test_args = dict( - directory=self.test_directory, - ) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + directory=self.test_directory, + ) - def test_changes_working_directory_to_specified_directory(self): + def test_changes_working_directory_to_specified_directory( + self, + mock_func_os_chdir): """ Should change working directory to specified directory. """ args = self.test_args directory = self.test_directory - expect_mock_output = """\ - Called os.chdir(%(directory)r) - """ % vars() daemon.daemon.change_working_directory(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_chdir.assert_called_with(directory) - def test_raises_daemon_error_on_os_error(self): - """ Should raise a DaemonError on receiving and OSError. """ - args = self.test_args - test_error = OSError(errno.ENOENT, "No such directory") - os.chdir.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.change_working_directory, **args) - - def test_error_message_contains_original_error_message(self): + def test_raises_daemon_error_on_os_error( + self, + mock_func_os_chdir): + """ Should raise a DaemonError on receiving an IOError. """ + args = self.test_args + test_error = FileNotFoundError("No such directory") + mock_func_os_chdir.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_working_directory, **args) + self.assertEqual(test_error, exc.__cause__) + + def test_error_message_contains_original_error_message( + self, + mock_func_os_chdir): """ Should raise a DaemonError with original message. """ args = self.test_args - test_error = OSError(errno.ENOENT, "No such directory") - os.chdir.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - try: - daemon.daemon.change_working_directory(**args) - except expect_error, exc: - pass - self.failUnlessIn(str(exc), str(test_error)) + test_error = FileNotFoundError("No such directory") + mock_func_os_chdir.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_working_directory, **args) + self.assertIn(unicode(test_error), unicode(exc)) +@mock.patch.object(os, "chroot") +@mock.patch.object(os, "chdir") class change_root_directory_TestCase(scaffold.TestCase): """ Test cases for change_root_directory function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "os.chdir", - tracker=self.mock_tracker) - scaffold.mock( - "os.chroot", - tracker=self.mock_tracker) + super(change_root_directory_TestCase, self).setUp() self.test_directory = object() self.test_args = dict( - directory=self.test_directory, - ) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + directory=self.test_directory, + ) - def test_changes_working_directory_to_specified_directory(self): + def test_changes_working_directory_to_specified_directory( + self, + mock_func_os_chdir, mock_func_os_chroot): """ Should change working directory to specified directory. """ args = self.test_args directory = self.test_directory - expect_mock_output = """\ - Called os.chdir(%(directory)r) - ... - """ % vars() daemon.daemon.change_root_directory(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_chdir.assert_called_with(directory) - def test_changes_root_directory_to_specified_directory(self): + def test_changes_root_directory_to_specified_directory( + self, + mock_func_os_chdir, mock_func_os_chroot): """ Should change root directory to specified directory. """ args = self.test_args directory = self.test_directory - expect_mock_output = """\ - ... - Called os.chroot(%(directory)r) - """ % vars() daemon.daemon.change_root_directory(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_chroot.assert_called_with(directory) - def test_raises_daemon_error_on_os_error_from_chdir(self): - """ Should raise a DaemonError on receiving an OSError from chdir. """ - args = self.test_args - test_error = OSError(errno.ENOENT, "No such directory") - os.chdir.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.change_root_directory, **args) - - def test_raises_daemon_error_on_os_error_from_chroot(self): + def test_raises_daemon_error_on_os_error_from_chdir( + self, + mock_func_os_chdir, mock_func_os_chroot): + """ Should raise a DaemonError on receiving an IOError from chdir. """ + args = self.test_args + test_error = FileNotFoundError("No such directory") + mock_func_os_chdir.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_root_directory, **args) + self.assertEqual(test_error, exc.__cause__) + + def test_raises_daemon_error_on_os_error_from_chroot( + self, + mock_func_os_chdir, mock_func_os_chroot): """ Should raise a DaemonError on receiving an OSError from chroot. """ args = self.test_args test_error = OSError(errno.EPERM, "No chroot for you!") - os.chroot.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.change_root_directory, **args) - - def test_error_message_contains_original_error_message(self): + mock_func_os_chroot.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_root_directory, **args) + self.assertEqual(test_error, exc.__cause__) + + def test_error_message_contains_original_error_message( + self, + mock_func_os_chdir, mock_func_os_chroot): """ Should raise a DaemonError with original message. """ args = self.test_args - test_error = OSError(errno.ENOENT, "No such directory") - os.chdir.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - try: - daemon.daemon.change_root_directory(**args) - except expect_error, exc: - pass - self.failUnlessIn(str(exc), str(test_error)) + test_error = FileNotFoundError("No such directory") + mock_func_os_chdir.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_root_directory, **args) + self.assertIn(unicode(test_error), unicode(exc)) +@mock.patch.object(os, "umask") class change_file_creation_mask_TestCase(scaffold.TestCase): """ Test cases for change_file_creation_mask function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "os.umask", - tracker=self.mock_tracker) + super(change_file_creation_mask_TestCase, self).setUp() self.test_mask = object() self.test_args = dict( - mask=self.test_mask, - ) + mask=self.test_mask, + ) - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_changes_umask_to_specified_mask(self): + def test_changes_umask_to_specified_mask(self, mock_func_os_umask): """ Should change working directory to specified directory. """ args = self.test_args mask = self.test_mask - expect_mock_output = """\ - Called os.umask(%(mask)r) - """ % vars() daemon.daemon.change_file_creation_mask(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_umask.assert_called_with(mask) - def test_raises_daemon_error_on_os_error_from_chdir(self): + def test_raises_daemon_error_on_os_error_from_chdir( + self, + mock_func_os_umask): """ Should raise a DaemonError on receiving an OSError from umask. """ args = self.test_args test_error = OSError(errno.EINVAL, "Whatchoo talkin' 'bout?") - os.umask.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.change_file_creation_mask, **args) - - def test_error_message_contains_original_error_message(self): + mock_func_os_umask.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_file_creation_mask, **args) + self.assertEqual(test_error, exc.__cause__) + + def test_error_message_contains_original_error_message( + self, + mock_func_os_umask): """ Should raise a DaemonError with original message. """ args = self.test_args - test_error = OSError(errno.ENOENT, "No such directory") - os.umask.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - try: - daemon.daemon.change_file_creation_mask(**args) - except expect_error, exc: - pass - self.failUnlessIn(str(exc), str(test_error)) + test_error = FileNotFoundError("No such directory") + mock_func_os_umask.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_file_creation_mask, **args) + self.assertIn(unicode(test_error), unicode(exc)) +@mock.patch.object(os, "setgid") +@mock.patch.object(os, "setuid") class change_process_owner_TestCase(scaffold.TestCase): """ Test cases for change_process_owner function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "os.setuid", - tracker=self.mock_tracker) - scaffold.mock( - "os.setgid", - tracker=self.mock_tracker) + super(change_process_owner_TestCase, self).setUp() self.test_uid = object() self.test_gid = object() self.test_args = dict( - uid=self.test_uid, - gid=self.test_gid, - ) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_changes_gid_and_uid_in_order(self): + uid=self.test_uid, + gid=self.test_gid, + ) + + def test_changes_gid_and_uid_in_order( + self, + mock_func_os_setuid, mock_func_os_setgid): """ Should change process GID and UID in correct order. Since the process requires appropriate privilege to use @@ -1115,168 +1011,162 @@ """ args = self.test_args - expect_mock_output = """\ - Called os.setgid(...) - Called os.setuid(...) - """ % vars() daemon.daemon.change_process_owner(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_setuid.assert_called() + mock_func_os_setgid.assert_called() - def test_changes_group_id_to_gid(self): + def test_changes_group_id_to_gid( + self, + mock_func_os_setuid, mock_func_os_setgid): """ Should change process GID to specified value. """ args = self.test_args gid = self.test_gid - expect_mock_output = """\ - Called os.setgid(%(gid)r) - ... - """ % vars() daemon.daemon.change_process_owner(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_setgid.assert_called(gid) - def test_changes_user_id_to_uid(self): + def test_changes_user_id_to_uid( + self, + mock_func_os_setuid, mock_func_os_setgid): """ Should change process UID to specified value. """ args = self.test_args uid = self.test_uid - expect_mock_output = """\ - ... - Called os.setuid(%(uid)r) - """ % vars() daemon.daemon.change_process_owner(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_setuid.assert_called(uid) - def test_raises_daemon_error_on_os_error_from_setgid(self): + def test_raises_daemon_error_on_os_error_from_setgid( + self, + mock_func_os_setuid, mock_func_os_setgid): """ Should raise a DaemonError on receiving an OSError from setgid. """ args = self.test_args test_error = OSError(errno.EPERM, "No switching for you!") - os.setgid.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.change_process_owner, **args) - - def test_raises_daemon_error_on_os_error_from_setuid(self): + mock_func_os_setgid.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_process_owner, **args) + self.assertEqual(test_error, exc.__cause__) + + def test_raises_daemon_error_on_os_error_from_setuid( + self, + mock_func_os_setuid, mock_func_os_setgid): """ Should raise a DaemonError on receiving an OSError from setuid. """ args = self.test_args test_error = OSError(errno.EPERM, "No switching for you!") - os.setuid.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.change_process_owner, **args) - - def test_error_message_contains_original_error_message(self): + mock_func_os_setuid.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_process_owner, **args) + self.assertEqual(test_error, exc.__cause__) + + def test_error_message_contains_original_error_message( + self, + mock_func_os_setuid, mock_func_os_setgid): """ Should raise a DaemonError with original message. """ args = self.test_args test_error = OSError(errno.EINVAL, "Whatchoo talkin' 'bout?") - os.setuid.mock_raises = test_error - expect_error = daemon.daemon.DaemonOSEnvironmentError - try: - daemon.daemon.change_process_owner(**args) - except expect_error, exc: - pass - self.failUnlessIn(str(exc), str(test_error)) + mock_func_os_setuid.side_effect = test_error + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.change_process_owner, **args) + self.assertIn(unicode(test_error), unicode(exc)) +RLimitResult = collections.namedtuple('RLimitResult', ['soft', 'hard']) + +fake_RLIMIT_CORE = object() + +@mock.patch.object(resource, "RLIMIT_CORE", new=fake_RLIMIT_CORE) +@mock.patch.object(resource, "setrlimit", side_effect=(lambda x, y: None)) +@mock.patch.object(resource, "getrlimit", side_effect=(lambda x: None)) class prevent_core_dump_TestCase(scaffold.TestCase): """ Test cases for prevent_core_dump function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() + super(prevent_core_dump_TestCase, self).setUp() - self.RLIMIT_CORE = object() - scaffold.mock( - "resource.RLIMIT_CORE", mock_obj=self.RLIMIT_CORE, - tracker=self.mock_tracker) - scaffold.mock( - "resource.getrlimit", returns=None, - tracker=self.mock_tracker) - scaffold.mock( - "resource.setrlimit", returns=None, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_sets_core_limit_to_zero(self): + def test_sets_core_limit_to_zero( + self, + mock_func_resource_getrlimit, mock_func_resource_setrlimit): """ Should set the RLIMIT_CORE resource to zero. """ - expect_resource = self.RLIMIT_CORE - expect_limit = (0, 0) - expect_mock_output = """\ - Called resource.getrlimit( - %(expect_resource)r) - Called resource.setrlimit( - %(expect_resource)r, - %(expect_limit)r) - """ % vars() + expected_resource = fake_RLIMIT_CORE + expected_limit = tuple(RLimitResult(soft=0, hard=0)) daemon.daemon.prevent_core_dump() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_raises_error_when_no_core_resource(self): + mock_func_resource_getrlimit.assert_called_with(expected_resource) + mock_func_resource_setrlimit.assert_called_with( + expected_resource, expected_limit) + + def test_raises_error_when_no_core_resource( + self, + mock_func_resource_getrlimit, mock_func_resource_setrlimit): """ Should raise DaemonError if no RLIMIT_CORE resource. """ - def mock_getrlimit(res): + test_error = ValueError("Bogus platform doesn't have RLIMIT_CORE") + def fake_getrlimit(res): if res == resource.RLIMIT_CORE: - raise ValueError("Bogus platform doesn't have RLIMIT_CORE") + raise test_error else: return None - resource.getrlimit.mock_returns_func = mock_getrlimit - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.prevent_core_dump) + mock_func_resource_getrlimit.side_effect = fake_getrlimit + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.prevent_core_dump) + self.assertEqual(test_error, exc.__cause__) +@mock.patch.object(os, "close") class close_file_descriptor_if_open_TestCase(scaffold.TestCase): """ Test cases for close_file_descriptor_if_open function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - self.test_fd = 274 + super(close_file_descriptor_if_open_TestCase, self).setUp() - scaffold.mock( - "os.close", - tracker=self.mock_tracker) + self.fake_fd = 274 - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_requests_file_descriptor_close(self): + def test_requests_file_descriptor_close(self, mock_func_os_close): """ Should request close of file descriptor. """ - fd = self.test_fd - expect_mock_output = """\ - Called os.close(%(fd)r) - """ % vars() + fd = self.fake_fd daemon.daemon.close_file_descriptor_if_open(fd) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_close.assert_called_with(fd) - def test_ignores_badfd_error_on_close(self): + def test_ignores_badfd_error_on_close(self, mock_func_os_close): """ Should ignore OSError EBADF when closing. """ - fd = self.test_fd + fd = self.fake_fd test_error = OSError(errno.EBADF, "Bad file descriptor") - def os_close(fd): + def fake_os_close(fd): raise test_error - os.close.mock_returns_func = os_close - expect_mock_output = """\ - Called os.close(%(fd)r) - """ % vars() + mock_func_os_close.side_effect = fake_os_close daemon.daemon.close_file_descriptor_if_open(fd) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_close.assert_called_with(fd) - def test_raises_error_if_error_on_close(self): + def test_raises_error_if_oserror_on_close(self, mock_func_os_close): """ Should raise DaemonError if an OSError occurs when closing. """ - fd = self.test_fd + fd = self.fake_fd test_error = OSError(object(), "Unexpected error") - def os_close(fd): + def fake_os_close(fd): + raise test_error + mock_func_os_close.side_effect = fake_os_close + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.close_file_descriptor_if_open, fd) + self.assertEqual(test_error, exc.__cause__) + + def test_raises_error_if_ioerror_on_close(self, mock_func_os_close): + """ Should raise DaemonError if an IOError occurs when closing. """ + fd = self.fake_fd + test_error = IOError(object(), "Unexpected error") + def fake_os_close(fd): raise test_error - os.close.mock_returns_func = os_close - expect_error = daemon.daemon.DaemonOSEnvironmentError - self.failUnlessRaises( - expect_error, - daemon.daemon.close_file_descriptor_if_open, fd) + mock_func_os_close.side_effect = fake_os_close + expected_error = daemon.daemon.DaemonOSEnvironmentError + exc = self.assertRaises( + expected_error, + daemon.daemon.close_file_descriptor_if_open, fd) + self.assertEqual(test_error, exc.__cause__) class maxfd_TestCase(scaffold.TestCase): @@ -1285,12 +1175,12 @@ def test_positive(self): """ Should be a positive number. """ maxfd = daemon.daemon.MAXFD - self.failUnless(maxfd > 0) + self.assertTrue(maxfd > 0) def test_integer(self): """ Should be an integer. """ maxfd = daemon.daemon.MAXFD - self.failUnlessEqual(int(maxfd), maxfd) + self.assertEqual(int(maxfd), maxfd) def test_reasonably_high(self): """ Should be reasonably high for default open files limit. @@ -1301,130 +1191,104 @@ to catch most use cases. """ - expect_minimum = 2048 + expected_minimum = 2048 maxfd = daemon.daemon.MAXFD - self.failUnless( - expect_minimum <= maxfd, - msg="MAXFD should be at least %(expect_minimum)r (got %(maxfd)r)" - % vars()) - - + self.assertTrue( + expected_minimum <= maxfd, + msg=( + "MAXFD should be at least {minimum!r}" + " (got {maxfd!r})".format( + minimum=expected_minimum, maxfd=maxfd))) + + +fake_default_maxfd = 8 +fake_RLIMIT_NOFILE = object() +fake_RLIM_INFINITY = object() +fake_rlimit_nofile_large = 2468 + +def fake_getrlimit_nofile_soft_infinity(resource): + result = RLimitResult(soft=fake_RLIM_INFINITY, hard=object()) + if resource != fake_RLIMIT_NOFILE: + result = NotImplemented + return result + +def fake_getrlimit_nofile_hard_infinity(resource): + result = RLimitResult(soft=object(), hard=fake_RLIM_INFINITY) + if resource != fake_RLIMIT_NOFILE: + result = NotImplemented + return result + +def fake_getrlimit_nofile_hard_large(resource): + result = RLimitResult(soft=object(), hard=fake_rlimit_nofile_large) + if resource != fake_RLIMIT_NOFILE: + result = NotImplemented + return result + +@mock.patch.object(daemon.daemon, "MAXFD", new=fake_default_maxfd) +@mock.patch.object(resource, "RLIMIT_NOFILE", new=fake_RLIMIT_NOFILE) +@mock.patch.object(resource, "RLIM_INFINITY", new=fake_RLIM_INFINITY) +@mock.patch.object( + resource, "getrlimit", + side_effect=fake_getrlimit_nofile_hard_large) class get_maximum_file_descriptors_TestCase(scaffold.TestCase): """ Test cases for get_maximum_file_descriptors function. """ - def setUp(self): - """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - self.RLIMIT_NOFILE = object() - self.RLIM_INFINITY = object() - self.test_rlimit_nofile = 2468 - - def mock_getrlimit(resource): - result = (object(), self.test_rlimit_nofile) - if resource != self.RLIMIT_NOFILE: - result = NotImplemented - return result - - self.test_maxfd = object() - scaffold.mock( - "daemon.daemon.MAXFD", mock_obj=self.test_maxfd, - tracker=self.mock_tracker) - - scaffold.mock( - "resource.RLIMIT_NOFILE", mock_obj=self.RLIMIT_NOFILE, - tracker=self.mock_tracker) - scaffold.mock( - "resource.RLIM_INFINITY", mock_obj=self.RLIM_INFINITY, - tracker=self.mock_tracker) - scaffold.mock( - "resource.getrlimit", returns_func=mock_getrlimit, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_returns_system_hard_limit(self): + def test_returns_system_hard_limit(self, mock_func_resource_getrlimit): """ Should return process hard limit on number of files. """ - expect_result = self.test_rlimit_nofile + expected_result = fake_rlimit_nofile_large result = daemon.daemon.get_maximum_file_descriptors() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) - def test_returns_module_default_if_hard_limit_infinity(self): + def test_returns_module_default_if_hard_limit_infinity( + self, mock_func_resource_getrlimit): """ Should return module MAXFD if hard limit is infinity. """ - self.test_rlimit_nofile = self.RLIM_INFINITY - expect_result = self.test_maxfd + mock_func_resource_getrlimit.side_effect = ( + fake_getrlimit_nofile_hard_infinity) + expected_result = fake_default_maxfd result = daemon.daemon.get_maximum_file_descriptors() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) +def fake_get_maximum_file_descriptors(): + return fake_default_maxfd + +@mock.patch.object(resource, "RLIMIT_NOFILE", new=fake_RLIMIT_NOFILE) +@mock.patch.object(resource, "RLIM_INFINITY", new=fake_RLIM_INFINITY) +@mock.patch.object( + resource, "getrlimit", + new=fake_getrlimit_nofile_soft_infinity) +@mock.patch.object( + daemon.daemon, "get_maximum_file_descriptors", + new=fake_get_maximum_file_descriptors) +@mock.patch.object(daemon.daemon, "close_file_descriptor_if_open") class close_all_open_files_TestCase(scaffold.TestCase): """ Test cases for close_all_open_files function. """ - def setUp(self): - """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - self.RLIMIT_NOFILE = object() - self.RLIM_INFINITY = object() - self.test_rlimit_nofile = self.RLIM_INFINITY - - def mock_getrlimit(resource): - result = (self.test_rlimit_nofile, object()) - if resource != self.RLIMIT_NOFILE: - result = NotImplemented - return result - - self.test_maxfd = 8 - scaffold.mock( - "daemon.daemon.get_maximum_file_descriptors", - returns=self.test_maxfd, - tracker=self.mock_tracker) - - scaffold.mock( - "resource.RLIMIT_NOFILE", mock_obj=self.RLIMIT_NOFILE, - tracker=self.mock_tracker) - scaffold.mock( - "resource.RLIM_INFINITY", mock_obj=self.RLIM_INFINITY, - tracker=self.mock_tracker) - scaffold.mock( - "resource.getrlimit", returns_func=mock_getrlimit, - tracker=self.mock_tracker) - - scaffold.mock( - "daemon.daemon.close_file_descriptor_if_open", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_requests_all_open_files_to_close(self): + def test_requests_all_open_files_to_close( + self, mock_func_close_file_descriptor_if_open): """ Should request close of all open files. """ - expect_file_descriptors = reversed(range(self.test_maxfd)) - expect_mock_output = "...\n" + "".join( - "Called daemon.daemon.close_file_descriptor_if_open(%(fd)r)\n" - % vars() - for fd in expect_file_descriptors) + expected_file_descriptors = range(fake_default_maxfd) + expected_calls = [ + mock.call(fd) for fd in expected_file_descriptors] daemon.daemon.close_all_open_files() - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_close_file_descriptor_if_open.assert_has_calls( + expected_calls, any_order=True) - def test_requests_all_but_excluded_files_to_close(self): + def test_requests_all_but_excluded_files_to_close( + self, mock_func_close_file_descriptor_if_open): """ Should request close of all open files but those excluded. """ test_exclude = set([3, 7]) args = dict( - exclude = test_exclude, - ) - expect_file_descriptors = ( - fd for fd in reversed(range(self.test_maxfd)) - if fd not in test_exclude) - expect_mock_output = "...\n" + "".join( - "Called daemon.daemon.close_file_descriptor_if_open(%(fd)r)\n" - % vars() - for fd in expect_file_descriptors) + exclude=test_exclude, + ) + expected_file_descriptors = set( + fd for fd in range(fake_default_maxfd) + if fd not in test_exclude) + expected_calls = [ + mock.call(fd) for fd in expected_file_descriptors] daemon.daemon.close_all_open_files(**args) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_close_file_descriptor_if_open.assert_has_calls( + expected_calls, any_order=True) class detach_process_context_TestCase(scaffold.TestCase): @@ -1435,159 +1299,143 @@ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() + super(detach_process_context_TestCase, self).setUp() + + self.mock_module_os = mock.MagicMock(wraps=os) - test_pids = [0, 0] - scaffold.mock( - "os.fork", returns_iter=test_pids, - tracker=self.mock_tracker) - scaffold.mock( - "os.setsid", - tracker=self.mock_tracker) + fake_pids = [0, 0] + func_patcher_os_fork = mock.patch.object( + os, "fork", + side_effect=iter(fake_pids)) + self.mock_func_os_fork = func_patcher_os_fork.start() + self.addCleanup(func_patcher_os_fork.stop) + self.mock_module_os.attach_mock(self.mock_func_os_fork, "fork") + + func_patcher_os_setsid = mock.patch.object(os, "setsid") + self.mock_func_os_setsid = func_patcher_os_setsid.start() + self.addCleanup(func_patcher_os_setsid.stop) + self.mock_module_os.attach_mock(self.mock_func_os_setsid, "setsid") def raise_os_exit(status=None): raise self.FakeOSExit(status) - scaffold.mock( - "os._exit", returns_func=raise_os_exit, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + func_patcher_os_force_exit = mock.patch.object( + os, "_exit", + side_effect=raise_os_exit) + self.mock_func_os_force_exit = func_patcher_os_force_exit.start() + self.addCleanup(func_patcher_os_force_exit.stop) + self.mock_module_os.attach_mock(self.mock_func_os_force_exit, "_exit") def test_parent_exits(self): """ Parent process should exit. """ parent_pid = 23 - scaffold.mock("os.fork", returns_iter=[parent_pid], - tracker=self.mock_tracker) - expect_mock_output = """\ - Called os.fork() - Called os._exit(0) - """ - self.failUnlessRaises( - self.FakeOSExit, - daemon.daemon.detach_process_context) - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_func_os_fork.side_effect = iter([parent_pid]) + self.assertRaises( + self.FakeOSExit, + daemon.daemon.detach_process_context) + self.mock_module_os.assert_has_calls([ + mock.call.fork(), + mock.call._exit(0), + ]) def test_first_fork_error_raises_error(self): """ Error on first fork should raise DaemonProcessDetachError. """ fork_errno = 13 fork_strerror = "Bad stuff happened" - fork_error = OSError(fork_errno, fork_strerror) - test_pids_iter = iter([fork_error]) + test_error = OSError(fork_errno, fork_strerror) + test_pids_iter = iter([test_error]) - def mock_fork(): - next = test_pids_iter.next() - if isinstance(next, Exception): - raise next + def fake_fork(): + next_item = next(test_pids_iter) + if isinstance(next_item, Exception): + raise next_item else: - return next + return next_item - scaffold.mock("os.fork", returns_func=mock_fork, - tracker=self.mock_tracker) - expect_mock_output = """\ - Called os.fork() - """ - self.failUnlessRaises( - daemon.daemon.DaemonProcessDetachError, - daemon.daemon.detach_process_context) - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_func_os_fork.side_effect = fake_fork + exc = self.assertRaises( + daemon.daemon.DaemonProcessDetachError, + daemon.daemon.detach_process_context) + self.assertEqual(test_error, exc.__cause__) + self.mock_module_os.assert_has_calls([ + mock.call.fork(), + ]) def test_child_starts_new_process_group(self): """ Child should start new process group. """ - expect_mock_output = """\ - Called os.fork() - Called os.setsid() - ... - """ daemon.daemon.detach_process_context() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_os.assert_has_calls([ + mock.call.fork(), + mock.call.setsid(), + ]) def test_child_forks_next_parent_exits(self): """ Child should fork, then exit if parent. """ - test_pids = [0, 42] - scaffold.mock("os.fork", returns_iter=test_pids, - tracker=self.mock_tracker) - expect_mock_output = """\ - Called os.fork() - Called os.setsid() - Called os.fork() - Called os._exit(0) - """ - self.failUnlessRaises( - self.FakeOSExit, - daemon.daemon.detach_process_context) - self.failUnlessMockCheckerMatch(expect_mock_output) + fake_pids = [0, 42] + self.mock_func_os_fork.side_effect = iter(fake_pids) + self.assertRaises( + self.FakeOSExit, + daemon.daemon.detach_process_context) + self.mock_module_os.assert_has_calls([ + mock.call.fork(), + mock.call.setsid(), + mock.call.fork(), + mock.call._exit(0), + ]) def test_second_fork_error_reports_to_stderr(self): """ Error on second fork should cause report to stderr. """ fork_errno = 17 fork_strerror = "Nasty stuff happened" - fork_error = OSError(fork_errno, fork_strerror) - test_pids_iter = iter([0, fork_error]) + test_error = OSError(fork_errno, fork_strerror) + test_pids_iter = iter([0, test_error]) - def mock_fork(): - next = test_pids_iter.next() - if isinstance(next, Exception): - raise next + def fake_fork(): + next_item = next(test_pids_iter) + if isinstance(next_item, Exception): + raise next_item else: - return next + return next_item - scaffold.mock("os.fork", returns_func=mock_fork, - tracker=self.mock_tracker) - expect_mock_output = """\ - Called os.fork() - Called os.setsid() - Called os.fork() - """ - self.failUnlessRaises( - daemon.daemon.DaemonProcessDetachError, - daemon.daemon.detach_process_context) - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_func_os_fork.side_effect = fake_fork + exc = self.assertRaises( + daemon.daemon.DaemonProcessDetachError, + daemon.daemon.detach_process_context) + self.assertEqual(test_error, exc.__cause__) + self.mock_module_os.assert_has_calls([ + mock.call.fork(), + mock.call.setsid(), + mock.call.fork(), + ]) def test_child_forks_next_child_continues(self): """ Child should fork, then continue if child. """ - expect_mock_output = """\ - Called os.fork() - Called os.setsid() - Called os.fork() - """ % vars() daemon.daemon.detach_process_context() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_module_os.assert_has_calls([ + mock.call.fork(), + mock.call.setsid(), + mock.call.fork(), + ]) +@mock.patch("os.getppid", return_value=765) class is_process_started_by_init_TestCase(scaffold.TestCase): """ Test cases for is_process_started_by_init function. """ - def setUp(self): - """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - self.test_ppid = 765 - - scaffold.mock( - "os.getppid", - returns=self.test_ppid, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_returns_false_by_default(self): + def test_returns_false_by_default(self, mock_func_os_getppid): """ Should return False under normal circumstances. """ - expect_result = False + expected_result = False result = daemon.daemon.is_process_started_by_init() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) - def test_returns_true_if_parent_process_is_init(self): + def test_returns_true_if_parent_process_is_init( + self, mock_func_os_getppid): """ Should return True if parent process is `init`. """ init_pid = 1 - os.getppid.mock_returns = init_pid - expect_result = True + mock_func_os_getppid.return_value = init_pid + expected_result = True result = daemon.daemon.is_process_started_by_init() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) class is_socket_TestCase(scaffold.TestCase): @@ -1595,63 +1443,57 @@ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() + super(is_socket_TestCase, self).setUp() - def mock_getsockopt(level, optname, buflen=None): + def fake_getsockopt(level, optname, buflen=None): result = object() if optname is socket.SO_TYPE: result = socket.SOCK_RAW return result - self.mock_socket_getsockopt_func = mock_getsockopt + self.fake_socket_getsockopt_func = fake_getsockopt - self.mock_socket_error = socket.error( - errno.ENOTSOCK, - "Socket operation on non-socket") - - self.mock_socket = scaffold.Mock( - "socket.socket", - tracker=self.mock_tracker) - self.mock_socket.getsockopt.mock_raises = self.mock_socket_error + self.fake_socket_error = socket.error( + errno.ENOTSOCK, + "Socket operation on non-socket") - def mock_socket_fromfd(fd, family, type, proto=None): + self.mock_socket = mock.MagicMock(spec=socket.socket) + self.mock_socket.getsockopt.side_effect = self.fake_socket_error + + def fake_socket_fromfd(fd, family, type, proto=None): return self.mock_socket - scaffold.mock( - "socket.fromfd", - returns_func=mock_socket_fromfd, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + func_patcher_socket_fromfd = mock.patch.object( + socket, "fromfd", + side_effect=fake_socket_fromfd) + func_patcher_socket_fromfd.start() + self.addCleanup(func_patcher_socket_fromfd.stop) def test_returns_false_by_default(self): """ Should return False under normal circumstances. """ test_fd = 23 - expect_result = False + expected_result = False result = daemon.daemon.is_socket(test_fd) - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) def test_returns_true_if_stdin_is_socket(self): """ Should return True if `stdin` is a socket. """ test_fd = 23 getsockopt = self.mock_socket.getsockopt - getsockopt.mock_raises = None - getsockopt.mock_returns_func = self.mock_socket_getsockopt_func - expect_result = True + getsockopt.side_effect = self.fake_socket_getsockopt_func + expected_result = True result = daemon.daemon.is_socket(test_fd) - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) def test_returns_false_if_stdin_socket_raises_error(self): """ Should return True if `stdin` is a socket and raises error. """ test_fd = 23 getsockopt = self.mock_socket.getsockopt - getsockopt.mock_raises = socket.error( - object(), "Weird socket stuff") - expect_result = True + getsockopt.side_effect = socket.error( + object(), "Weird socket stuff") + expected_result = True result = daemon.daemon.is_socket(test_fd) - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) class is_process_started_by_superserver_TestCase(scaffold.TestCase): @@ -1659,145 +1501,133 @@ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() + super(is_process_started_by_superserver_TestCase, self).setUp() - def mock_is_socket(fd): + def fake_is_socket(fd): if sys.__stdin__.fileno() == fd: - result = self.mock_stdin_is_socket_func() + result = self.fake_stdin_is_socket_func() else: result = False return result - self.mock_stdin_is_socket_func = (lambda: False) + self.fake_stdin_is_socket_func = (lambda: False) - scaffold.mock( - "daemon.daemon.is_socket", - returns_func=mock_is_socket, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + func_patcher_is_socket = mock.patch.object( + daemon.daemon, "is_socket", + side_effect=fake_is_socket) + func_patcher_is_socket.start() + self.addCleanup(func_patcher_is_socket.stop) def test_returns_false_by_default(self): """ Should return False under normal circumstances. """ - expect_result = False + expected_result = False result = daemon.daemon.is_process_started_by_superserver() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) def test_returns_true_if_stdin_is_socket(self): """ Should return True if `stdin` is a socket. """ - self.mock_stdin_is_socket_func = (lambda: True) - expect_result = True + self.fake_stdin_is_socket_func = (lambda: True) + expected_result = True result = daemon.daemon.is_process_started_by_superserver() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) +@mock.patch.object( + daemon.daemon, "is_process_started_by_superserver", + return_value=False) +@mock.patch.object( + daemon.daemon, "is_process_started_by_init", + return_value=False) class is_detach_process_context_required_TestCase(scaffold.TestCase): """ Test cases for is_detach_process_context_required function. """ - def setUp(self): - """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "daemon.daemon.is_process_started_by_init", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.is_process_started_by_superserver", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_returns_true_by_default(self): - """ Should return False under normal circumstances. """ - expect_result = True + def test_returns_true_by_default( + self, + mock_func_is_process_started_by_init, + mock_func_is_process_started_by_superserver): + """ Should return True under normal circumstances. """ + expected_result = True result = daemon.daemon.is_detach_process_context_required() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) - def test_returns_false_if_started_by_init(self): + def test_returns_false_if_started_by_init( + self, + mock_func_is_process_started_by_init, + mock_func_is_process_started_by_superserver): """ Should return False if current process started by init. """ - daemon.daemon.is_process_started_by_init.mock_returns = True - expect_result = False + mock_func_is_process_started_by_init.return_value = True + expected_result = False result = daemon.daemon.is_detach_process_context_required() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) - def test_returns_true_if_started_by_superserver(self): + def test_returns_true_if_started_by_superserver( + self, + mock_func_is_process_started_by_init, + mock_func_is_process_started_by_superserver): """ Should return False if current process started by superserver. """ - daemon.daemon.is_process_started_by_superserver.mock_returns = True - expect_result = False + mock_func_is_process_started_by_superserver.return_value = True + expected_result = False result = daemon.daemon.is_detach_process_context_required() - self.failUnlessIs(expect_result, result) + self.assertIs(result, expected_result) def setup_streams_fixtures(testcase): """ Set up common test fixtures for standard streams. """ - testcase.mock_tracker = scaffold.MockTracker() - testcase.stream_file_paths = dict( - stdin = tempfile.mktemp(), - stdout = tempfile.mktemp(), - stderr = tempfile.mktemp(), - ) + stdin=tempfile.mktemp(), + stdout=tempfile.mktemp(), + stderr=tempfile.mktemp(), + ) testcase.stream_files_by_name = dict( - (name, FakeFileDescriptorStringIO()) - for name in ['stdin', 'stdout', 'stderr'] - ) + (name, FakeFileDescriptorStringIO()) + for name in ['stdin', 'stdout', 'stderr'] + ) testcase.stream_files_by_path = dict( - (testcase.stream_file_paths[name], - testcase.stream_files_by_name[name]) - for name in ['stdin', 'stdout', 'stderr'] - ) - - scaffold.mock( - "os.dup2", - tracker=testcase.mock_tracker) + (testcase.stream_file_paths[name], + testcase.stream_files_by_name[name]) + for name in ['stdin', 'stdout', 'stderr'] + ) +@mock.patch.object(os, "dup2") class redirect_stream_TestCase(scaffold.TestCase): """ Test cases for redirect_stream function. """ def setUp(self): """ Set up test fixtures. """ - setup_streams_fixtures(self) + super(redirect_stream_TestCase, self).setUp() self.test_system_stream = FakeFileDescriptorStringIO() self.test_target_stream = FakeFileDescriptorStringIO() self.test_null_file = FakeFileDescriptorStringIO() - def mock_open(path, flag, mode=None): + def fake_os_open(path, flag, mode=None): if path == os.devnull: result = self.test_null_file.fileno() else: - raise OSError(errno.NOENT, "No such file", path) + raise FileNotFoundError("No such file", path) return result - scaffold.mock( - "os.open", - returns_func=mock_open, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + func_patcher_os_open = mock.patch.object( + os, "open", + side_effect=fake_os_open) + self.mock_func_os_open = func_patcher_os_open.start() + self.addCleanup(func_patcher_os_open.stop) - def test_duplicates_target_file_descriptor(self): + def test_duplicates_target_file_descriptor( + self, mock_func_os_dup2): """ Should duplicate file descriptor from target to system stream. """ system_stream = self.test_system_stream system_fileno = system_stream.fileno() target_stream = self.test_target_stream target_fileno = target_stream.fileno() - expect_mock_output = """\ - Called os.dup2(%(target_fileno)r, %(system_fileno)r) - """ % vars() daemon.daemon.redirect_stream(system_stream, target_stream) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_os_dup2.assert_called_with(target_fileno, system_fileno) - def test_duplicates_null_file_descriptor_by_default(self): + def test_duplicates_null_file_descriptor_by_default( + self, mock_func_os_dup2): """ Should by default duplicate the null file to the system stream. """ system_stream = self.test_system_stream system_fileno = system_stream.fileno() @@ -1806,12 +1636,9 @@ null_flag = os.O_RDWR null_file = self.test_null_file null_fileno = null_file.fileno() - expect_mock_output = """\ - Called os.open(%(null_path)r, %(null_flag)r) - Called os.dup2(%(null_fileno)r, %(system_fileno)r) - """ % vars() daemon.daemon.redirect_stream(system_stream, target_stream) - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_func_os_open.assert_called_with(null_path, null_flag) + mock_func_os_dup2.assert_called_with(null_fileno, system_fileno) class make_default_signal_map_TestCase(scaffold.TestCase): @@ -1819,50 +1646,44 @@ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() + super(make_default_signal_map_TestCase, self).setUp() - mock_signal_module = ModuleType('signal') - mock_signal_names = [ - 'SIGHUP', - 'SIGCLD', - 'SIGSEGV', - 'SIGTSTP', - 'SIGTTIN', - 'SIGTTOU', - 'SIGTERM', - ] - for name in mock_signal_names: - setattr(mock_signal_module, name, object()) - - scaffold.mock( - "signal", - mock_obj=mock_signal_module, - tracker=self.mock_tracker) - scaffold.mock( - "daemon.daemon.signal", - mock_obj=mock_signal_module, - tracker=self.mock_tracker) + # Use whatever default string type this Python version needs. + signal_module_name = str('signal') + self.fake_signal_module = ModuleType(signal_module_name) + + fake_signal_names = [ + 'SIGHUP', + 'SIGCLD', + 'SIGSEGV', + 'SIGTSTP', + 'SIGTTIN', + 'SIGTTOU', + 'SIGTERM', + ] + for name in fake_signal_names: + setattr(self.fake_signal_module, name, object()) + + module_patcher_signal = mock.patch.object( + daemon.daemon, "signal", new=self.fake_signal_module) + module_patcher_signal.start() + self.addCleanup(module_patcher_signal.stop) default_signal_map_by_name = { - 'SIGTSTP': None, - 'SIGTTIN': None, - 'SIGTTOU': None, - 'SIGTERM': 'terminate', - } - + 'SIGTSTP': None, + 'SIGTTIN': None, + 'SIGTTOU': None, + 'SIGTERM': 'terminate', + } self.default_signal_map = dict( - (getattr(signal, name), target) - for (name, target) in default_signal_map_by_name.items()) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + (getattr(self.fake_signal_module, name), target) + for (name, target) in default_signal_map_by_name.items()) def test_returns_constructed_signal_map(self): """ Should return map per default. """ - expect_result = self.default_signal_map + expected_result = self.default_signal_map result = daemon.daemon.make_default_signal_map() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) def test_returns_signal_map_with_only_ids_in_signal_module(self): """ Should return map with only signals in the `signal` module. @@ -1873,65 +1694,51 @@ defined in the `signal` module. """ - del(self.default_signal_map[signal.SIGTTOU]) - del(signal.SIGTTOU) - expect_result = self.default_signal_map + del(self.default_signal_map[self.fake_signal_module.SIGTTOU]) + del(self.fake_signal_module.SIGTTOU) + expected_result = self.default_signal_map result = daemon.daemon.make_default_signal_map() - self.failUnlessEqual(expect_result, result) + self.assertEqual(expected_result, result) +@mock.patch.object(daemon.daemon.signal, "signal") class set_signal_handlers_TestCase(scaffold.TestCase): """ Test cases for set_signal_handlers function. """ def setUp(self): """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "signal.signal", - tracker=self.mock_tracker) + super(set_signal_handlers_TestCase, self).setUp() self.signal_handler_map = { - signal.SIGQUIT: object(), - signal.SIGSEGV: object(), - signal.SIGINT: object(), - } - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + signal.SIGQUIT: object(), + signal.SIGSEGV: object(), + signal.SIGINT: object(), + } - def test_sets_signal_handler_for_each_item(self): + def test_sets_signal_handler_for_each_item(self, mock_func_signal_signal): """ Should set signal handler for each item in map. """ signal_handler_map = self.signal_handler_map - expect_mock_output = "".join( - "Called signal.signal(%(signal_number)r, %(handler)r)\n" - % vars() - for (signal_number, handler) in signal_handler_map.items()) + expected_calls = [ + mock.call(signal_number, handler) + for (signal_number, handler) in signal_handler_map.items()] daemon.daemon.set_signal_handlers(signal_handler_map) - self.failUnlessMockCheckerMatch(expect_mock_output) + self.assertEquals(expected_calls, mock_func_signal_signal.mock_calls) +@mock.patch.object(daemon.daemon.atexit, "register") class register_atexit_function_TestCase(scaffold.TestCase): """ Test cases for register_atexit_function function. """ - def setUp(self): - """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - scaffold.mock( - "atexit.register", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_registers_function_for_atexit_processing(self): + def test_registers_function_for_atexit_processing( + self, mock_func_atexit_register): """ Should register specified function for atexit processing. """ func = object() - expect_mock_output = """\ - Called atexit.register(%(func)r) - """ % vars() daemon.daemon.register_atexit_function(func) - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_atexit_register.assert_called_with(func) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test/test_metadata.py python-daemon-2.0.5/test/test_metadata.py --- python-daemon-1.5.5/test/test_metadata.py 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/test/test_metadata.py 2015-02-02 04:43:28.000000000 +0000 @@ -0,0 +1,380 @@ +# -*- coding: utf-8 -*- +# +# test/test_metadata.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Unit test for ‘_metadata’ private module. + """ + +from __future__ import (absolute_import, unicode_literals) + +import sys +import errno +import re +try: + # Python 3 standard library. + import urllib.parse as urlparse +except ImportError: + # Python 2 standard library. + import urlparse +import functools +import collections +import json + +import pkg_resources +import mock +import testtools.helpers +import testtools.matchers +import testscenarios + +from . import scaffold +from .scaffold import (basestring, unicode) + +import daemon._metadata as metadata + + +class HasAttribute(testtools.matchers.Matcher): + """ A matcher to assert an object has a named attribute. """ + + def __init__(self, name): + self.attribute_name = name + + def match(self, instance): + """ Assert the object `instance` has an attribute named `name`. """ + result = None + if not testtools.helpers.safe_hasattr(instance, self.attribute_name): + result = AttributeNotFoundMismatch(instance, self.attribute_name) + return result + + +class AttributeNotFoundMismatch(testtools.matchers.Mismatch): + """ The specified instance does not have the named attribute. """ + + def __init__(self, instance, name): + self.instance = instance + self.attribute_name = name + + def describe(self): + """ Emit a text description of this mismatch. """ + text = ( + "{instance!r}" + " has no attribute named {name!r}").format( + instance=self.instance, name=self.attribute_name) + return text + + +class metadata_value_TestCase(scaffold.TestCaseWithScenarios): + """ Test cases for metadata module values. """ + + expected_str_attributes = set([ + 'version_installed', + 'author', + 'copyright', + 'license', + 'url', + ]) + + scenarios = [ + (name, {'attribute_name': name}) + for name in expected_str_attributes] + for (name, params) in scenarios: + if name == 'version_installed': + # No duck typing, this attribute might be None. + params['ducktype_attribute_name'] = NotImplemented + continue + # Expect an attribute of ‘str’ to test this value. + params['ducktype_attribute_name'] = 'isdigit' + + def test_module_has_attribute(self): + """ Metadata should have expected value as a module attribute. """ + self.assertThat( + metadata, HasAttribute(self.attribute_name)) + + def test_module_attribute_has_duck_type(self): + """ Metadata value should have expected duck-typing attribute. """ + if self.ducktype_attribute_name == NotImplemented: + self.skipTest("Can't assert this attribute's type") + instance = getattr(metadata, self.attribute_name) + self.assertThat( + instance, HasAttribute(self.ducktype_attribute_name)) + + +class parse_person_field_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘get_latest_version’ function. """ + + scenarios = [ + ('simple', { + 'test_person': "Foo Bar ", + 'expected_result': ("Foo Bar", "foo.bar@example.com"), + }), + ('empty', { + 'test_person': "", + 'expected_result': (None, None), + }), + ('none', { + 'test_person': None, + 'expected_error': TypeError, + }), + ('no email', { + 'test_person': "Foo Bar", + 'expected_result': ("Foo Bar", None), + }), + ] + + def test_returns_expected_result(self): + """ Should return expected result. """ + if hasattr(self, 'expected_error'): + self.assertRaises( + self.expected_error, + metadata.parse_person_field, self.test_person) + else: + result = metadata.parse_person_field(self.test_person) + self.assertEqual(self.expected_result, result) + + +class YearRange_TestCase(scaffold.TestCaseWithScenarios): + """ Test cases for ‘YearRange’ class. """ + + scenarios = [ + ('simple', { + 'begin_year': 1970, + 'end_year': 1979, + 'expected_text': "1970–1979", + }), + ('same year', { + 'begin_year': 1970, + 'end_year': 1970, + 'expected_text': "1970", + }), + ('no end year', { + 'begin_year': 1970, + 'end_year': None, + 'expected_text': "1970", + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(YearRange_TestCase, self).setUp() + + self.test_instance = metadata.YearRange( + self.begin_year, self.end_year) + + def test_text_representation_as_expected(self): + """ Text representation should be as expected. """ + result = unicode(self.test_instance) + self.assertEqual(result, self.expected_text) + + +FakeYearRange = collections.namedtuple('FakeYearRange', ['begin', 'end']) + +@mock.patch.object(metadata, 'YearRange', new=FakeYearRange) +class make_year_range_TestCase(scaffold.TestCaseWithScenarios): + """ Test cases for ‘make_year_range’ function. """ + + scenarios = [ + ('simple', { + 'begin_year': "1970", + 'end_date': "1979-01-01", + 'expected_range': FakeYearRange(begin=1970, end=1979), + }), + ('same year', { + 'begin_year': "1970", + 'end_date': "1970-01-01", + 'expected_range': FakeYearRange(begin=1970, end=1970), + }), + ('no end year', { + 'begin_year': "1970", + 'end_date': None, + 'expected_range': FakeYearRange(begin=1970, end=None), + }), + ('end date UNKNOWN token', { + 'begin_year': "1970", + 'end_date': "UNKNOWN", + 'expected_range': FakeYearRange(begin=1970, end=None), + }), + ('end date FUTURE token', { + 'begin_year': "1970", + 'end_date': "FUTURE", + 'expected_range': FakeYearRange(begin=1970, end=None), + }), + ] + + def test_result_matches_expected_range(self): + """ Result should match expected YearRange. """ + result = metadata.make_year_range(self.begin_year, self.end_date) + self.assertEqual(result, self.expected_range) + + +class metadata_content_TestCase(scaffold.TestCase): + """ Test cases for content of metadata. """ + + def test_copyright_formatted_correctly(self): + """ Copyright statement should be formatted correctly. """ + regex_pattern = ( + "Copyright © " + "\d{4}" # four-digit year + "(?:–\d{4})?" # optional range dash and ending four-digit year + ) + regex_flags = re.UNICODE + self.assertThat( + metadata.copyright, + testtools.matchers.MatchesRegex(regex_pattern, regex_flags)) + + def test_author_formatted_correctly(self): + """ Author information should be formatted correctly. """ + regex_pattern = ( + ".+ " # name + "<[^>]+>" # email address, in angle brackets + ) + regex_flags = re.UNICODE + self.assertThat( + metadata.author, + testtools.matchers.MatchesRegex(regex_pattern, regex_flags)) + + def test_copyright_contains_author(self): + """ Copyright information should contain author information. """ + self.assertThat( + metadata.copyright, + testtools.matchers.Contains(metadata.author)) + + def test_url_parses_correctly(self): + """ Homepage URL should parse correctly. """ + result = urlparse.urlparse(metadata.url) + self.assertIsInstance( + result, urlparse.ParseResult, + "URL value {url!r} did not parse correctly".format( + url=metadata.url)) + + +try: + FileNotFoundError +except NameError: + # Python 2 uses IOError. + FileNotFoundError = functools.partial(IOError, errno.ENOENT) + +version_info_filename = "version_info.json" + +def fake_func_has_metadata(testcase, resource_name): + """ Fake the behaviour of ‘pkg_resources.Distribution.has_metadata’. """ + if ( + resource_name != testcase.expected_resource_name + or not hasattr(testcase, 'test_version_info')): + return False + return True + + +def fake_func_get_metadata(testcase, resource_name): + """ Fake the behaviour of ‘pkg_resources.Distribution.get_metadata’. """ + if not fake_func_has_metadata(testcase, resource_name): + error = FileNotFoundError(resource_name) + raise error + content = testcase.test_version_info + return content + + +def fake_func_get_distribution(testcase, distribution_name): + """ Fake the behaviour of ‘pkg_resources.get_distribution’. """ + if distribution_name != metadata.distribution_name: + raise pkg_resources.DistributionNotFound + if hasattr(testcase, 'get_distribution_error'): + raise testcase.get_distribution_error + mock_distribution = testcase.mock_distribution + mock_distribution.has_metadata.side_effect = functools.partial( + fake_func_has_metadata, testcase) + mock_distribution.get_metadata.side_effect = functools.partial( + fake_func_get_metadata, testcase) + return mock_distribution + + +@mock.patch.object(metadata, 'distribution_name', new="mock-dist") +class get_distribution_version_info_TestCase(scaffold.TestCaseWithScenarios): + """ Test cases for ‘get_distribution_version_info’ function. """ + + default_version_info = { + 'release_date': "UNKNOWN", + 'version': "UNKNOWN", + 'maintainer': "UNKNOWN", + } + + scenarios = [ + ('version 0.0', { + 'test_version_info': json.dumps({ + 'version': "0.0", + }), + 'expected_version_info': {'version': "0.0"}, + }), + ('version 1.0', { + 'test_version_info': json.dumps({ + 'version': "1.0", + }), + 'expected_version_info': {'version': "1.0"}, + }), + ('file lorem_ipsum.json', { + 'version_info_filename': "lorem_ipsum.json", + 'test_version_info': json.dumps({ + 'version': "1.0", + }), + 'expected_version_info': {'version': "1.0"}, + }), + ('not installed', { + 'get_distribution_error': pkg_resources.DistributionNotFound(), + 'expected_version_info': default_version_info, + }), + ('no version_info', { + 'expected_version_info': default_version_info, + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(get_distribution_version_info_TestCase, self).setUp() + + if hasattr(self, 'expected_resource_name'): + self.test_args = {'filename': self.expected_resource_name} + else: + self.test_args = {} + self.expected_resource_name = version_info_filename + + self.mock_distribution = mock.MagicMock() + func_patcher_get_distribution = mock.patch.object( + pkg_resources, 'get_distribution') + func_patcher_get_distribution.start() + self.addCleanup(func_patcher_get_distribution.stop) + pkg_resources.get_distribution.side_effect = functools.partial( + fake_func_get_distribution, self) + + def test_requests_installed_distribution(self): + """ The package distribution should be retrieved. """ + expected_distribution_name = metadata.distribution_name + version_info = metadata.get_distribution_version_info(**self.test_args) + pkg_resources.get_distribution.assert_called_with( + expected_distribution_name) + + def test_requests_specified_filename(self): + """ The specified metadata resource name should be requested. """ + if hasattr(self, 'get_distribution_error'): + self.skipTest("No access to distribution") + version_info = metadata.get_distribution_version_info(**self.test_args) + self.mock_distribution.has_metadata.assert_called_with( + self.expected_resource_name) + + def test_result_matches_expected_items(self): + """ The result should match the expected items. """ + version_info = metadata.get_distribution_version_info(**self.test_args) + self.assertEqual(self.expected_version_info, version_info) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test/test_pidfile.py python-daemon-2.0.5/test/test_pidfile.py --- python-daemon-1.5.5/test/test_pidfile.py 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/test/test_pidfile.py 2015-02-02 04:43:28.000000000 +0000 @@ -0,0 +1,472 @@ +# -*- coding: utf-8 -*- +# +# test/test_pidfile.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Unit test for ‘pidfile’ module. + """ + +from __future__ import (absolute_import, unicode_literals) + +try: + # Python 3 standard library. + import builtins +except ImportError: + # Python 2 standard library. + import __builtin__ as builtins +import os +import itertools +import tempfile +import errno +import functools +try: + # Standard library of Python 2.7 and later. + from io import StringIO +except ImportError: + # Standard library of Python 2.6 and earlier. + from StringIO import StringIO + +import mock +import lockfile + +from . import scaffold + +import daemon.pidfile + + +class FakeFileDescriptorStringIO(StringIO, object): + """ A StringIO class that fakes a file descriptor. """ + + _fileno_generator = itertools.count() + + def __init__(self, *args, **kwargs): + self._fileno = next(self._fileno_generator) + super(FakeFileDescriptorStringIO, self).__init__(*args, **kwargs) + + def fileno(self): + return self._fileno + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +try: + FileNotFoundError + PermissionError +except NameError: + # Python 2 uses IOError. + FileNotFoundError = functools.partial(IOError, errno.ENOENT) + PermissionError = functools.partial(IOError, errno.EPERM) + + +def make_pidlockfile_scenarios(): + """ Make a collection of scenarios for testing `PIDLockFile` instances. + + :return: A collection of scenarios for tests involving + `PIDLockfFile` instances. + + The collection is a mapping from scenario name to a dictionary of + scenario attributes. + + """ + + fake_current_pid = 235 + fake_other_pid = 8642 + fake_pidfile_path = tempfile.mktemp() + + fake_pidfile_empty = FakeFileDescriptorStringIO() + fake_pidfile_current_pid = FakeFileDescriptorStringIO( + "{pid:d}\n".format(pid=fake_current_pid)) + fake_pidfile_other_pid = FakeFileDescriptorStringIO( + "{pid:d}\n".format(pid=fake_other_pid)) + fake_pidfile_bogus = FakeFileDescriptorStringIO( + "b0gUs") + + scenarios = { + 'simple': {}, + 'not-exist': { + 'open_func_name': 'fake_open_nonexist', + 'os_open_func_name': 'fake_os_open_nonexist', + }, + 'not-exist-write-denied': { + 'open_func_name': 'fake_open_nonexist', + 'os_open_func_name': 'fake_os_open_nonexist', + }, + 'not-exist-write-busy': { + 'open_func_name': 'fake_open_nonexist', + 'os_open_func_name': 'fake_os_open_nonexist', + }, + 'exist-read-denied': { + 'open_func_name': 'fake_open_read_denied', + 'os_open_func_name': 'fake_os_open_read_denied', + }, + 'exist-locked-read-denied': { + 'locking_pid': fake_other_pid, + 'open_func_name': 'fake_open_read_denied', + 'os_open_func_name': 'fake_os_open_read_denied', + }, + 'exist-empty': {}, + 'exist-invalid': { + 'pidfile': fake_pidfile_bogus, + }, + 'exist-current-pid': { + 'pidfile': fake_pidfile_current_pid, + 'pidfile_pid': fake_current_pid, + }, + 'exist-current-pid-locked': { + 'pidfile': fake_pidfile_current_pid, + 'pidfile_pid': fake_current_pid, + 'locking_pid': fake_current_pid, + }, + 'exist-other-pid': { + 'pidfile': fake_pidfile_other_pid, + 'pidfile_pid': fake_other_pid, + }, + 'exist-other-pid-locked': { + 'pidfile': fake_pidfile_other_pid, + 'pidfile_pid': fake_other_pid, + 'locking_pid': fake_other_pid, + }, + } + + for scenario in scenarios.values(): + scenario['pid'] = fake_current_pid + scenario['pidfile_path'] = fake_pidfile_path + if 'pidfile' not in scenario: + scenario['pidfile'] = fake_pidfile_empty + if 'pidfile_pid' not in scenario: + scenario['pidfile_pid'] = None + if 'locking_pid' not in scenario: + scenario['locking_pid'] = None + if 'open_func_name' not in scenario: + scenario['open_func_name'] = 'fake_open_okay' + if 'os_open_func_name' not in scenario: + scenario['os_open_func_name'] = 'fake_os_open_okay' + + return scenarios + + +def setup_pidfile_fixtures(testcase): + """ Set up common fixtures for PID file test cases. + + :param testcase: A `TestCase` instance to decorate. + + Decorate the `testcase` with attributes to be fixtures for tests + involving `PIDLockFile` instances. + + """ + scenarios = make_pidlockfile_scenarios() + testcase.pidlockfile_scenarios = scenarios + + def get_scenario_option(testcase, key, default=None): + value = default + try: + value = testcase.scenario[key] + except (NameError, TypeError, AttributeError, KeyError): + pass + return value + + func_patcher_os_getpid = mock.patch.object( + os, "getpid", + return_value=scenarios['simple']['pid']) + func_patcher_os_getpid.start() + testcase.addCleanup(func_patcher_os_getpid.stop) + + def make_fake_open_funcs(testcase): + + def fake_open_nonexist(filename, mode, buffering): + if mode.startswith('r'): + error = FileNotFoundError( + "No such file {filename!r}".format( + filename=filename)) + raise error + else: + result = testcase.scenario['pidfile'] + return result + + def fake_open_read_denied(filename, mode, buffering): + if mode.startswith('r'): + error = PermissionError( + "Read denied on {filename!r}".format( + filename=filename)) + raise error + else: + result = testcase.scenario['pidfile'] + return result + + def fake_open_okay(filename, mode, buffering): + result = testcase.scenario['pidfile'] + return result + + def fake_os_open_nonexist(filename, flags, mode): + if (flags & os.O_CREAT): + result = testcase.scenario['pidfile'].fileno() + else: + error = FileNotFoundError( + "No such file {filename!r}".format( + filename=filename)) + raise error + return result + + def fake_os_open_read_denied(filename, flags, mode): + if (flags & os.O_CREAT): + result = testcase.scenario['pidfile'].fileno() + else: + error = PermissionError( + "Read denied on {filename!r}".format( + filename=filename)) + raise error + return result + + def fake_os_open_okay(filename, flags, mode): + result = testcase.scenario['pidfile'].fileno() + return result + + funcs = dict( + (name, obj) for (name, obj) in vars().items() + if callable(obj)) + + return funcs + + testcase.fake_pidfile_open_funcs = make_fake_open_funcs(testcase) + + def fake_open(filename, mode='rt', buffering=None): + scenario_path = get_scenario_option(testcase, 'pidfile_path') + if filename == scenario_path: + func_name = testcase.scenario['open_func_name'] + fake_open_func = testcase.fake_pidfile_open_funcs[func_name] + result = fake_open_func(filename, mode, buffering) + else: + result = FakeFileDescriptorStringIO() + return result + + mock_open = mock.mock_open() + mock_open.side_effect = fake_open + + func_patcher_builtin_open = mock.patch.object( + builtins, "open", + new=mock_open) + func_patcher_builtin_open.start() + testcase.addCleanup(func_patcher_builtin_open.stop) + + def fake_os_open(filename, flags, mode=None): + scenario_path = get_scenario_option(testcase, 'pidfile_path') + if filename == scenario_path: + func_name = testcase.scenario['os_open_func_name'] + fake_os_open_func = testcase.fake_pidfile_open_funcs[func_name] + result = fake_os_open_func(filename, flags, mode) + else: + result = FakeFileDescriptorStringIO().fileno() + return result + + mock_os_open = mock.MagicMock(side_effect=fake_os_open) + + func_patcher_os_open = mock.patch.object( + os, "open", + new=mock_os_open) + func_patcher_os_open.start() + testcase.addCleanup(func_patcher_os_open.stop) + + def fake_os_fdopen(fd, mode='rt', buffering=None): + scenario_pidfile = get_scenario_option( + testcase, 'pidfile', FakeFileDescriptorStringIO()) + if fd == testcase.scenario['pidfile'].fileno(): + result = testcase.scenario['pidfile'] + else: + raise OSError(errno.EBADF, "Bad file descriptor") + return result + + mock_os_fdopen = mock.MagicMock(side_effect=fake_os_fdopen) + + func_patcher_os_fdopen = mock.patch.object( + os, "fdopen", + new=mock_os_fdopen) + func_patcher_os_fdopen.start() + testcase.addCleanup(func_patcher_os_fdopen.stop) + + +def make_lockfile_method_fakes(scenario): + """ Make common fake methods for lockfile class. + + :param scenario: A scenario for testing with PIDLockFile. + :return: A mapping from normal function name to the corresponding + fake function. + + Each fake function behaves appropriately for the specified `scenario`. + + """ + + def fake_func_read_pid(): + return scenario['pidfile_pid'] + def fake_func_is_locked(): + return (scenario['locking_pid'] is not None) + def fake_func_i_am_locking(): + return ( + scenario['locking_pid'] == scenario['pid']) + def fake_func_acquire(timeout=None): + if scenario['locking_pid'] is not None: + raise lockfile.AlreadyLocked() + scenario['locking_pid'] = scenario['pid'] + def fake_func_release(): + if scenario['locking_pid'] is None: + raise lockfile.NotLocked() + if scenario['locking_pid'] != scenario['pid']: + raise lockfile.NotMyLock() + scenario['locking_pid'] = None + def fake_func_break_lock(): + scenario['locking_pid'] = None + + fake_methods = dict( + ( + func_name.replace('fake_func_', ''), + mock.MagicMock(side_effect=fake_func)) + for (func_name, fake_func) in vars().items() + if func_name.startswith('fake_func_')) + + return fake_methods + + +def apply_lockfile_method_mocks(mock_lockfile, testcase, scenario): + """ Apply common fake methods to mock lockfile class. + + :param mock_lockfile: An object providing the `LockFile` interface. + :param testcase: The `TestCase` instance providing the context for + the patch. + :param scenario: The `PIDLockFile` test scenario to use. + + Mock the `LockFile` methods of `mock_lockfile`, by applying fake + methods customised for `scenario`. The mock is does by a patch + within the context of `testcase`. + + """ + fake_methods = dict( + (func_name, fake_func) + for (func_name, fake_func) in + make_lockfile_method_fakes(scenario).items() + if func_name not in ['read_pid']) + + for (func_name, fake_func) in fake_methods.items(): + func_patcher = mock.patch.object( + mock_lockfile, func_name, + new=fake_func) + func_patcher.start() + testcase.addCleanup(func_patcher.stop) + + +def setup_pidlockfile_fixtures(testcase, scenario_name=None): + """ Set up common fixtures for PIDLockFile test cases. + + :param testcase: The `TestCase` instance to decorate. + :param scenario_name: The name of the `PIDLockFile` scenario to use. + + Decorate the `testcase` with attributes that are fixtures for test + cases involving `PIDLockFile` instances.` + + """ + + setup_pidfile_fixtures(testcase) + + for func_name in [ + 'write_pid_to_pidfile', + 'remove_existing_pidfile', + ]: + func_patcher = mock.patch.object(lockfile.pidlockfile, func_name) + func_patcher.start() + testcase.addCleanup(func_patcher.stop) + + +class TimeoutPIDLockFile_TestCase(scaffold.TestCase): + """ Test cases for ‘TimeoutPIDLockFile’ class. """ + + def setUp(self): + """ Set up test fixtures. """ + super(TimeoutPIDLockFile_TestCase, self).setUp() + + pidlockfile_scenarios = make_pidlockfile_scenarios() + self.pidlockfile_scenario = pidlockfile_scenarios['simple'] + pidfile_path = self.pidlockfile_scenario['pidfile_path'] + + for func_name in ['__init__', 'acquire']: + func_patcher = mock.patch.object( + lockfile.pidlockfile.PIDLockFile, func_name) + func_patcher.start() + self.addCleanup(func_patcher.stop) + + self.scenario = { + 'pidfile_path': self.pidlockfile_scenario['pidfile_path'], + 'acquire_timeout': self.getUniqueInteger(), + } + + self.test_kwargs = dict( + path=self.scenario['pidfile_path'], + acquire_timeout=self.scenario['acquire_timeout'], + ) + self.test_instance = daemon.pidfile.TimeoutPIDLockFile( + **self.test_kwargs) + + def test_inherits_from_pidlockfile(self): + """ Should inherit from PIDLockFile. """ + instance = self.test_instance + self.assertIsInstance(instance, lockfile.pidlockfile.PIDLockFile) + + def test_init_has_expected_signature(self): + """ Should have expected signature for ‘__init__’. """ + def test_func(self, path, acquire_timeout=None, *args, **kwargs): pass + test_func.__name__ = str('__init__') + self.assertFunctionSignatureMatch( + test_func, + daemon.pidfile.TimeoutPIDLockFile.__init__) + + def test_has_specified_acquire_timeout(self): + """ Should have specified ‘acquire_timeout’ value. """ + instance = self.test_instance + expected_timeout = self.test_kwargs['acquire_timeout'] + self.assertEqual(expected_timeout, instance.acquire_timeout) + + @mock.patch.object( + lockfile.pidlockfile.PIDLockFile, "__init__", + autospec=True) + def test_calls_superclass_init(self, mock_init): + """ Should call the superclass ‘__init__’. """ + expected_path = self.test_kwargs['path'] + instance = daemon.pidfile.TimeoutPIDLockFile(**self.test_kwargs) + mock_init.assert_called_with(instance, expected_path) + + @mock.patch.object( + lockfile.pidlockfile.PIDLockFile, "acquire", + autospec=True) + def test_acquire_uses_specified_timeout(self, mock_func_acquire): + """ Should call the superclass ‘acquire’ with specified timeout. """ + instance = self.test_instance + test_timeout = self.getUniqueInteger() + expected_timeout = test_timeout + instance.acquire(test_timeout) + mock_func_acquire.assert_called_with(instance, expected_timeout) + + @mock.patch.object( + lockfile.pidlockfile.PIDLockFile, "acquire", + autospec=True) + def test_acquire_uses_stored_timeout_by_default(self, mock_func_acquire): + """ Should call superclass ‘acquire’ with stored timeout by default. """ + instance = self.test_instance + test_timeout = self.test_kwargs['acquire_timeout'] + expected_timeout = test_timeout + instance.acquire() + mock_func_acquire.assert_called_with(instance, expected_timeout) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test/test_pidlockfile.py python-daemon-2.0.5/test/test_pidlockfile.py --- python-daemon-1.5.5/test/test_pidlockfile.py 2010-01-20 11:33:10.000000000 +0000 +++ python-daemon-2.0.5/test/test_pidlockfile.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,791 +0,0 @@ -# -*- coding: utf-8 -*- -# -# test/test_pidlockfile.py -# Part of python-daemon, an implementation of PEP 3143. -# -# Copyright © 2008–2010 Ben Finney -# -# This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. - -""" Unit test for pidlockfile module. - """ - -import __builtin__ -import os -from StringIO import StringIO -import itertools -import tempfile -import errno - -import lockfile - -import scaffold -from daemon import pidlockfile - - -class FakeFileDescriptorStringIO(StringIO, object): - """ A StringIO class that fakes a file descriptor. """ - - _fileno_generator = itertools.count() - - def __init__(self, *args, **kwargs): - self._fileno = self._fileno_generator.next() - super_instance = super(FakeFileDescriptorStringIO, self) - super_instance.__init__(*args, **kwargs) - - def fileno(self): - return self._fileno - - -class Exception_TestCase(scaffold.Exception_TestCase): - """ Test cases for module exception classes. """ - - def __init__(self, *args, **kwargs): - """ Set up a new instance. """ - super(Exception_TestCase, self).__init__(*args, **kwargs) - - self.valid_exceptions = { - pidlockfile.PIDFileError: dict( - min_args = 1, - types = (Exception,), - ), - pidlockfile.PIDFileParseError: dict( - min_args = 2, - types = (pidlockfile.PIDFileError, ValueError), - ), - } - - -def make_pidlockfile_scenarios(): - """ Make a collection of scenarios for testing PIDLockFile instances. """ - - mock_current_pid = 235 - mock_other_pid = 8642 - mock_pidfile_path = tempfile.mktemp() - - mock_pidfile_empty = FakeFileDescriptorStringIO() - mock_pidfile_current_pid = FakeFileDescriptorStringIO( - "%(mock_current_pid)d\n" % vars()) - mock_pidfile_other_pid = FakeFileDescriptorStringIO( - "%(mock_other_pid)d\n" % vars()) - mock_pidfile_bogus = FakeFileDescriptorStringIO( - "b0gUs") - - scenarios = { - 'simple': {}, - 'not-exist': { - 'open_func_name': 'mock_open_nonexist', - 'os_open_func_name': 'mock_os_open_nonexist', - }, - 'not-exist-write-denied': { - 'open_func_name': 'mock_open_nonexist', - 'os_open_func_name': 'mock_os_open_nonexist', - }, - 'not-exist-write-busy': { - 'open_func_name': 'mock_open_nonexist', - 'os_open_func_name': 'mock_os_open_nonexist', - }, - 'exist-read-denied': { - 'open_func_name': 'mock_open_read_denied', - 'os_open_func_name': 'mock_os_open_read_denied', - }, - 'exist-locked-read-denied': { - 'locking_pid': mock_other_pid, - 'open_func_name': 'mock_open_read_denied', - 'os_open_func_name': 'mock_os_open_read_denied', - }, - 'exist-empty': {}, - 'exist-invalid': { - 'pidfile': mock_pidfile_bogus, - }, - 'exist-current-pid': { - 'pidfile': mock_pidfile_current_pid, - 'pidfile_pid': mock_current_pid, - }, - 'exist-current-pid-locked': { - 'pidfile': mock_pidfile_current_pid, - 'pidfile_pid': mock_current_pid, - 'locking_pid': mock_current_pid, - }, - 'exist-other-pid': { - 'pidfile': mock_pidfile_other_pid, - 'pidfile_pid': mock_other_pid, - }, - 'exist-other-pid-locked': { - 'pidfile': mock_pidfile_other_pid, - 'pidfile_pid': mock_other_pid, - 'locking_pid': mock_other_pid, - }, - } - - for scenario in scenarios.values(): - scenario['pid'] = mock_current_pid - scenario['path'] = mock_pidfile_path - if 'pidfile' not in scenario: - scenario['pidfile'] = mock_pidfile_empty - if 'pidfile_pid' not in scenario: - scenario['pidfile_pid'] = None - if 'locking_pid' not in scenario: - scenario['locking_pid'] = None - if 'open_func_name' not in scenario: - scenario['open_func_name'] = 'mock_open_okay' - if 'os_open_func_name' not in scenario: - scenario['os_open_func_name'] = 'mock_os_open_okay' - - return scenarios - - -def setup_pidfile_fixtures(testcase): - """ Set up common fixtures for PID file test cases. """ - testcase.mock_tracker = scaffold.MockTracker() - - scenarios = make_pidlockfile_scenarios() - testcase.pidlockfile_scenarios = scenarios - - def get_scenario_option(testcase, key, default=None): - value = default - try: - value = testcase.scenario[key] - except (NameError, TypeError, AttributeError, KeyError): - pass - return value - - scaffold.mock( - "os.getpid", - returns=scenarios['simple']['pid'], - tracker=testcase.mock_tracker) - - def make_mock_open_funcs(testcase): - - def mock_open_nonexist(filename, mode, buffering): - if 'r' in mode: - raise IOError( - errno.ENOENT, "No such file %(filename)r" % vars()) - else: - result = testcase.scenario['pidfile'] - return result - - def mock_open_read_denied(filename, mode, buffering): - if 'r' in mode: - raise IOError( - errno.EPERM, "Read denied on %(filename)r" % vars()) - else: - result = testcase.scenario['pidfile'] - return result - - def mock_open_okay(filename, mode, buffering): - result = testcase.scenario['pidfile'] - return result - - def mock_os_open_nonexist(filename, flags, mode): - if (flags & os.O_CREAT): - result = testcase.scenario['pidfile'].fileno() - else: - raise OSError( - errno.ENOENT, "No such file %(filename)r" % vars()) - return result - - def mock_os_open_read_denied(filename, flags, mode): - if (flags & os.O_CREAT): - result = testcase.scenario['pidfile'].fileno() - else: - raise OSError( - errno.EPERM, "Read denied on %(filename)r" % vars()) - return result - - def mock_os_open_okay(filename, flags, mode): - result = testcase.scenario['pidfile'].fileno() - return result - - funcs = dict( - (name, obj) for (name, obj) in vars().items() - if hasattr(obj, '__call__')) - - return funcs - - testcase.mock_pidfile_open_funcs = make_mock_open_funcs(testcase) - - def mock_open(filename, mode='r', buffering=None): - scenario_path = get_scenario_option(testcase, 'path') - if filename == scenario_path: - func_name = testcase.scenario['open_func_name'] - mock_open_func = testcase.mock_pidfile_open_funcs[func_name] - result = mock_open_func(filename, mode, buffering) - else: - result = FakeFileDescriptorStringIO() - return result - - scaffold.mock( - "__builtin__.open", - returns_func=mock_open, - tracker=testcase.mock_tracker) - - def mock_os_open(filename, flags, mode=None): - scenario_path = get_scenario_option(testcase, 'path') - if filename == scenario_path: - func_name = testcase.scenario['os_open_func_name'] - mock_os_open_func = testcase.mock_pidfile_open_funcs[func_name] - result = mock_os_open_func(filename, flags, mode) - else: - result = FakeFileDescriptorStringIO().fileno() - return result - - scaffold.mock( - "os.open", - returns_func=mock_os_open, - tracker=testcase.mock_tracker) - - def mock_os_fdopen(fd, mode='r', buffering=None): - scenario_pidfile = get_scenario_option( - testcase, 'pidfile', FakeFileDescriptorStringIO()) - if fd == testcase.scenario['pidfile'].fileno(): - result = testcase.scenario['pidfile'] - else: - raise OSError(errno.EBADF, "Bad file descriptor") - return result - - scaffold.mock( - "os.fdopen", - returns_func=mock_os_fdopen, - tracker=testcase.mock_tracker) - - testcase.scenario = NotImplemented - - -def setup_lockfile_method_mocks(testcase, scenario, class_name): - """ Set up common mock methods for lockfile class. """ - - def mock_read_pid(): - return scenario['pidfile_pid'] - def mock_is_locked(): - return (scenario['locking_pid'] is not None) - def mock_i_am_locking(): - return ( - scenario['locking_pid'] == scenario['pid']) - def mock_acquire(timeout=None): - if scenario['locking_pid'] is not None: - raise lockfile.AlreadyLocked() - scenario['locking_pid'] = scenario['pid'] - def mock_release(): - if scenario['locking_pid'] is None: - raise lockfile.NotLocked() - if scenario['locking_pid'] != scenario['pid']: - raise lockfile.NotMyLock() - scenario['locking_pid'] = None - def mock_break_lock(): - scenario['locking_pid'] = None - - for func_name in [ - 'read_pid', - 'is_locked', 'i_am_locking', - 'acquire', 'release', 'break_lock', - ]: - mock_func = vars()["mock_%(func_name)s" % vars()] - lockfile_func_name = "%(class_name)s.%(func_name)s" % vars() - mock_lockfile_func = scaffold.Mock( - lockfile_func_name, - returns_func=mock_func, - tracker=testcase.mock_tracker) - try: - scaffold.mock( - lockfile_func_name, - mock_obj=mock_lockfile_func, - tracker=testcase.mock_tracker) - except NameError: - pass - - -def setup_pidlockfile_fixtures(testcase, scenario_name=None): - """ Set up common fixtures for PIDLockFile test cases. """ - - setup_pidfile_fixtures(testcase) - - scaffold.mock( - "pidlockfile.write_pid_to_pidfile", - tracker=testcase.mock_tracker) - scaffold.mock( - "pidlockfile.remove_existing_pidfile", - tracker=testcase.mock_tracker) - - if scenario_name is not None: - set_pidlockfile_scenario(testcase, scenario_name, clear_tracker=False) - - -def set_pidlockfile_scenario(testcase, scenario_name, clear_tracker=True): - """ Set up the test case to the specified scenario. """ - testcase.scenario = testcase.pidlockfile_scenarios[scenario_name] - setup_lockfile_method_mocks( - testcase, testcase.scenario, "lockfile.LinkFileLock") - testcase.pidlockfile_args = dict( - path=testcase.scenario['path'], - ) - testcase.test_instance = pidlockfile.PIDLockFile( - **testcase.pidlockfile_args) - if clear_tracker: - testcase.mock_tracker.clear() - - -class PIDLockFile_TestCase(scaffold.TestCase): - """ Test cases for PIDLockFile class. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidlockfile_fixtures(self, 'exist-other-pid') - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_instantiate(self): - """ New instance of PIDLockFile should be created. """ - instance = self.test_instance - self.failUnlessIsInstance(instance, pidlockfile.PIDLockFile) - - def test_inherits_from_linkfilelock(self): - """ Should inherit from LinkFileLock. """ - instance = self.test_instance - self.failUnlessIsInstance(instance, lockfile.LinkFileLock) - - def test_has_specified_path(self): - """ Should have specified path. """ - instance = self.test_instance - expect_path = self.scenario['path'] - self.failUnlessEqual(expect_path, instance.path) - - -class PIDLockFile_read_pid_TestCase(scaffold.TestCase): - """ Test cases for PIDLockFile.read_pid method. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidlockfile_fixtures(self, 'exist-other-pid') - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_gets_pid_via_read_pid_from_pidfile(self): - """ Should get PID via read_pid_from_pidfile. """ - instance = self.test_instance - test_pid = self.scenario['pidfile_pid'] - expect_pid = test_pid - result = instance.read_pid() - self.failUnlessEqual(expect_pid, result) - - -class PIDLockFile_acquire_TestCase(scaffold.TestCase): - """ Test cases for PIDLockFile.acquire function. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidlockfile_fixtures(self) - set_pidlockfile_scenario(self, 'not-exist') - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_calls_linkfilelock_acquire(self): - """ Should first call LinkFileLock.acquire method. """ - instance = self.test_instance - expect_mock_output = """\ - Called lockfile.LinkFileLock.acquire() - ... - """ - instance.acquire() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_calls_linkfilelock_acquire_with_timeout(self): - """ Should call LinkFileLock.acquire method with specified timeout. """ - instance = self.test_instance - test_timeout = object() - expect_mock_output = """\ - Called lockfile.LinkFileLock.acquire(timeout=%(test_timeout)r) - ... - """ % vars() - instance.acquire(timeout=test_timeout) - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_writes_pid_to_specified_file(self): - """ Should request writing current PID to specified file. """ - instance = self.test_instance - pidfile_path = self.scenario['path'] - expect_mock_output = """\ - ... - Called pidlockfile.write_pid_to_pidfile(%(pidfile_path)r) - """ % vars() - instance.acquire() - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_raises_lock_failed_on_write_error(self): - """ Should raise LockFailed error if write fails. """ - set_pidlockfile_scenario(self, 'not-exist-write-busy') - instance = self.test_instance - pidfile_path = self.scenario['path'] - mock_error = OSError(errno.EBUSY, "Bad stuff", pidfile_path) - pidlockfile.write_pid_to_pidfile.mock_raises = mock_error - expect_error = pidlockfile.LockFailed - self.failUnlessRaises( - expect_error, - instance.acquire) - - -class PIDLockFile_release_TestCase(scaffold.TestCase): - """ Test cases for PIDLockFile.release function. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidlockfile_fixtures(self) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_does_not_remove_existing_pidfile_if_not_locking(self): - """ Should not request removal of PID file if not locking. """ - set_pidlockfile_scenario(self, 'exist-empty') - instance = self.test_instance - expect_error = lockfile.NotLocked - unwanted_mock_output = ( - "..." - "Called pidlockfile.remove_existing_pidfile" - "...") - self.failUnlessRaises( - expect_error, - instance.release) - self.failIfMockCheckerMatch(unwanted_mock_output) - - def test_does_not_remove_existing_pidfile_if_not_my_lock(self): - """ Should not request removal of PID file if we are not locking. """ - set_pidlockfile_scenario(self, 'exist-other-pid-locked') - instance = self.test_instance - expect_error = lockfile.NotMyLock - unwanted_mock_output = ( - "..." - "Called pidlockfile.remove_existing_pidfile" - "...") - self.failUnlessRaises( - expect_error, - instance.release) - self.failIfMockCheckerMatch(unwanted_mock_output) - - def test_removes_existing_pidfile_if_i_am_locking(self): - """ Should request removal of specified PID file if lock is ours. """ - set_pidlockfile_scenario(self, 'exist-current-pid-locked') - instance = self.test_instance - pidfile_path = self.scenario['path'] - expect_mock_output = """\ - ... - Called pidlockfile.remove_existing_pidfile(%(pidfile_path)r) - ... - """ % vars() - instance.release() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_calls_linkfilelock_release(self): - """ Should finally call LinkFileLock.release method. """ - set_pidlockfile_scenario(self, 'exist-current-pid-locked') - instance = self.test_instance - expect_mock_output = """\ - ... - Called lockfile.LinkFileLock.release() - """ - instance.release() - self.failUnlessMockCheckerMatch(expect_mock_output) - - -class PIDLockFile_break_lock_TestCase(scaffold.TestCase): - """ Test cases for PIDLockFile.break_lock function. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidlockfile_fixtures(self) - set_pidlockfile_scenario(self, 'exist-other-pid-locked') - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_calls_linkfilelock_break_lock(self): - """ Should first call LinkFileLock.break_lock method. """ - instance = self.test_instance - expect_mock_output = """\ - Called lockfile.LinkFileLock.break_lock() - ... - """ - instance.break_lock() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_removes_existing_pidfile(self): - """ Should request removal of specified PID file. """ - instance = self.test_instance - pidfile_path = self.scenario['path'] - expect_mock_output = """\ - ... - Called pidlockfile.remove_existing_pidfile(%(pidfile_path)r) - """ % vars() - instance.break_lock() - self.failUnlessMockCheckerMatch(expect_mock_output) - - -class read_pid_from_pidfile_TestCase(scaffold.TestCase): - """ Test cases for read_pid_from_pidfile function. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidfile_fixtures(self) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_opens_specified_filename(self): - """ Should attempt to open specified pidfile filename. """ - set_pidlockfile_scenario(self, 'exist-other-pid') - pidfile_path = self.scenario['path'] - expect_mock_output = """\ - Called __builtin__.open(%(pidfile_path)r, 'r') - """ % vars() - dummy = pidlockfile.read_pid_from_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_reads_pid_from_file(self): - """ Should read the PID from the specified file. """ - set_pidlockfile_scenario(self, 'exist-other-pid') - pidfile_path = self.scenario['path'] - expect_pid = self.scenario['pidfile_pid'] - pid = pidlockfile.read_pid_from_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessEqual(expect_pid, pid) - - def test_returns_none_when_file_nonexist(self): - """ Should return None when the PID file does not exist. """ - set_pidlockfile_scenario(self, 'not-exist') - pidfile_path = self.scenario['path'] - pid = pidlockfile.read_pid_from_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessIs(None, pid) - - def test_raises_error_when_file_read_fails(self): - """ Should raise error when the PID file read fails. """ - set_pidlockfile_scenario(self, 'exist-read-denied') - pidfile_path = self.scenario['path'] - expect_error = EnvironmentError - self.failUnlessRaises( - expect_error, - pidlockfile.read_pid_from_pidfile, pidfile_path) - - def test_raises_error_when_file_empty(self): - """ Should raise error when the PID file is empty. """ - set_pidlockfile_scenario(self, 'exist-empty') - pidfile_path = self.scenario['path'] - expect_error = pidlockfile.PIDFileParseError - self.failUnlessRaises( - expect_error, - pidlockfile.read_pid_from_pidfile, pidfile_path) - - def test_raises_error_when_file_contents_invalid(self): - """ Should raise error when the PID file contents are invalid. """ - set_pidlockfile_scenario(self, 'exist-invalid') - pidfile_path = self.scenario['path'] - expect_error = pidlockfile.PIDFileParseError - self.failUnlessRaises( - expect_error, - pidlockfile.read_pid_from_pidfile, pidfile_path) - - -class remove_existing_pidfile_TestCase(scaffold.TestCase): - """ Test cases for remove_existing_pidfile function. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidfile_fixtures(self) - - scaffold.mock( - "os.remove", - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_removes_specified_filename(self): - """ Should attempt to remove specified PID file filename. """ - set_pidlockfile_scenario(self, 'exist-current-pid') - pidfile_path = self.scenario['path'] - expect_mock_output = """\ - Called os.remove(%(pidfile_path)r) - """ % vars() - pidlockfile.remove_existing_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_ignores_file_not_exist_error(self): - """ Should ignore error if file does not exist. """ - set_pidlockfile_scenario(self, 'not-exist') - pidfile_path = self.scenario['path'] - mock_error = OSError(errno.ENOENT, "Not there", pidfile_path) - os.remove.mock_raises = mock_error - expect_mock_output = """\ - Called os.remove(%(pidfile_path)r) - """ % vars() - pidlockfile.remove_existing_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_propagates_arbitrary_oserror(self): - """ Should propagate any OSError other than ENOENT. """ - set_pidlockfile_scenario(self, 'exist-current-pid') - pidfile_path = self.scenario['path'] - mock_error = OSError(errno.EACCES, "Denied", pidfile_path) - os.remove.mock_raises = mock_error - self.failUnlessRaises( - type(mock_error), - pidlockfile.remove_existing_pidfile, - pidfile_path) - - -class write_pid_to_pidfile_TestCase(scaffold.TestCase): - """ Test cases for write_pid_to_pidfile function. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_pidfile_fixtures(self) - set_pidlockfile_scenario(self, 'not-exist') - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_opens_specified_filename(self): - """ Should attempt to open specified PID file filename. """ - pidfile_path = self.scenario['path'] - expect_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY) - expect_mode = 0644 - expect_mock_output = """\ - Called os.open(%(pidfile_path)r, %(expect_flags)r, %(expect_mode)r) - ... - """ % vars() - pidlockfile.write_pid_to_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_writes_pid_to_file(self): - """ Should write the current PID to the specified file. """ - pidfile_path = self.scenario['path'] - self.scenario['pidfile'].close = scaffold.Mock( - "PIDLockFile.close", - tracker=self.mock_tracker) - expect_line = "%(pid)d\n" % self.scenario - pidlockfile.write_pid_to_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessEqual(expect_line, self.scenario['pidfile'].getvalue()) - - def test_closes_file_after_write(self): - """ Should close the specified file after writing. """ - pidfile_path = self.scenario['path'] - self.scenario['pidfile'].write = scaffold.Mock( - "PIDLockFile.write", - tracker=self.mock_tracker) - self.scenario['pidfile'].close = scaffold.Mock( - "PIDLockFile.close", - tracker=self.mock_tracker) - expect_mock_output = """\ - ... - Called PIDLockFile.write(...) - Called PIDLockFile.close() - """ % vars() - pidlockfile.write_pid_to_pidfile(pidfile_path) - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - - -class TimeoutPIDLockFile_TestCase(scaffold.TestCase): - """ Test cases for ‘TimeoutPIDLockFile’ class. """ - - def setUp(self): - """ Set up test fixtures. """ - self.mock_tracker = scaffold.MockTracker() - - pidlockfile_scenarios = make_pidlockfile_scenarios() - self.pidlockfile_scenario = pidlockfile_scenarios['simple'] - pidfile_path = self.pidlockfile_scenario['path'] - - scaffold.mock( - "pidlockfile.PIDLockFile.__init__", - tracker=self.mock_tracker) - scaffold.mock( - "pidlockfile.PIDLockFile.acquire", - tracker=self.mock_tracker) - - self.scenario = { - 'pidfile_path': self.pidlockfile_scenario['path'], - 'acquire_timeout': object(), - } - - self.test_kwargs = dict( - path=self.scenario['pidfile_path'], - acquire_timeout=self.scenario['acquire_timeout'], - ) - self.test_instance = pidlockfile.TimeoutPIDLockFile(**self.test_kwargs) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_inherits_from_pidlockfile(self): - """ Should inherit from PIDLockFile. """ - instance = self.test_instance - self.failUnlessIsInstance(instance, pidlockfile.PIDLockFile) - - def test_init_has_expected_signature(self): - """ Should have expected signature for ‘__init__’. """ - def test_func(self, path, acquire_timeout=None, *args, **kwargs): pass - test_func.__name__ = '__init__' - self.failUnlessFunctionSignatureMatch( - test_func, - pidlockfile.TimeoutPIDLockFile.__init__) - - def test_has_specified_acquire_timeout(self): - """ Should have specified ‘acquire_timeout’ value. """ - instance = self.test_instance - expect_timeout = self.test_kwargs['acquire_timeout'] - self.failUnlessEqual(expect_timeout, instance.acquire_timeout) - - def test_calls_superclass_init(self): - """ Should call the superclass ‘__init__’. """ - expect_path = self.test_kwargs['path'] - expect_mock_output = """\ - Called pidlockfile.PIDLockFile.__init__( - %(expect_path)r) - """ % vars() - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_acquire_uses_specified_timeout(self): - """ Should call the superclass ‘acquire’ with specified timeout. """ - instance = self.test_instance - test_timeout = object() - expect_timeout = test_timeout - self.mock_tracker.clear() - expect_mock_output = """\ - Called pidlockfile.PIDLockFile.acquire(%(expect_timeout)r) - """ % vars() - instance.acquire(test_timeout) - self.failUnlessMockCheckerMatch(expect_mock_output) - - def test_acquire_uses_stored_timeout_by_default(self): - """ Should call superclass ‘acquire’ with stored timeout by default. """ - instance = self.test_instance - test_timeout = self.test_kwargs['acquire_timeout'] - expect_timeout = test_timeout - self.mock_tracker.clear() - expect_mock_output = """\ - Called pidlockfile.PIDLockFile.acquire(%(expect_timeout)r) - """ % vars() - instance.acquire() - self.failUnlessMockCheckerMatch(expect_mock_output) diff -Nru python-daemon-1.5.5/test/test_runner.py python-daemon-2.0.5/test/test_runner.py --- python-daemon-1.5.5/test/test_runner.py 2010-02-05 10:22:13.000000000 +0000 +++ python-daemon-2.0.5/test/test_runner.py 2015-01-10 11:29:24.000000000 +0000 @@ -1,141 +1,169 @@ # -*- coding: utf-8 -*- # # test/test_runner.py -# Part of python-daemon, an implementation of PEP 3143. +# Part of ‘python-daemon’, an implementation of PEP 3143. # -# Copyright © 2009–2010 Ben Finney +# Copyright © 2009–2015 Ben Finney # # This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. -""" Unit test for runner module. +""" Unit test for ‘runner’ module. """ -import __builtin__ +from __future__ import (absolute_import, unicode_literals) + +try: + # Python 3 standard library. + import builtins +except ImportError: + # Python 2 standard library. + import __builtin__ as builtins import os +import os.path import sys import tempfile import errno import signal +import functools -import scaffold -from test_pidlockfile import ( - FakeFileDescriptorStringIO, - setup_pidfile_fixtures, - make_pidlockfile_scenarios, - setup_lockfile_method_mocks, - ) -from test_daemon import ( - setup_streams_fixtures, - ) -import daemon.daemon +import lockfile +import mock +import testtools + +from . import scaffold +from .scaffold import (basestring, unicode) +from .test_pidfile import ( + FakeFileDescriptorStringIO, + setup_pidfile_fixtures, + make_pidlockfile_scenarios, + apply_lockfile_method_mocks, + ) +from .test_daemon import ( + setup_streams_fixtures, + ) -from daemon import pidlockfile -from daemon import runner +import daemon.daemon +import daemon.runner +import daemon.pidfile -class Exception_TestCase(scaffold.Exception_TestCase): +class ModuleExceptions_TestCase(scaffold.Exception_TestCase): """ Test cases for module exception classes. """ - def __init__(self, *args, **kwargs): - """ Set up a new instance. """ - super(Exception_TestCase, self).__init__(*args, **kwargs) - - self.valid_exceptions = { - runner.DaemonRunnerError: dict( + scenarios = scaffold.make_exception_scenarios([ + ('daemon.runner.DaemonRunnerError', dict( + exc_type = daemon.runner.DaemonRunnerError, min_args = 1, - types = (Exception,), - ), - runner.DaemonRunnerInvalidActionError: dict( + types = [Exception], + )), + ('daemon.runner.DaemonRunnerInvalidActionError', dict( + exc_type = daemon.runner.DaemonRunnerInvalidActionError, min_args = 1, - types = (runner.DaemonRunnerError, ValueError), - ), - runner.DaemonRunnerStartFailureError: dict( + types = [daemon.runner.DaemonRunnerError, ValueError], + )), + ('daemon.runner.DaemonRunnerStartFailureError', dict( + exc_type = daemon.runner.DaemonRunnerStartFailureError, min_args = 1, - types = (runner.DaemonRunnerError, RuntimeError), - ), - runner.DaemonRunnerStopFailureError: dict( + types = [daemon.runner.DaemonRunnerError, RuntimeError], + )), + ('daemon.runner.DaemonRunnerStopFailureError', dict( + exc_type = daemon.runner.DaemonRunnerStopFailureError, min_args = 1, - types = (runner.DaemonRunnerError, RuntimeError), - ), - } + types = [daemon.runner.DaemonRunnerError, RuntimeError], + )), + ]) def make_runner_scenarios(): - """ Make a collection of scenarios for testing DaemonRunner instances. """ + """ Make a collection of scenarios for testing `DaemonRunner` instances. + + :return: A collection of scenarios for tests involving + `DaemonRunner` instances. + + The collection is a mapping from scenario name to a dictionary of + scenario attributes. + + """ pidlockfile_scenarios = make_pidlockfile_scenarios() scenarios = { - 'simple': { - 'pidlockfile_scenario_name': 'simple', - }, - 'pidfile-locked': { - 'pidlockfile_scenario_name': 'exist-other-pid-locked', - }, - } + 'simple': { + 'pidlockfile_scenario_name': 'simple', + }, + 'pidfile-locked': { + 'pidlockfile_scenario_name': 'exist-other-pid-locked', + }, + } for scenario in scenarios.values(): if 'pidlockfile_scenario_name' in scenario: pidlockfile_scenario = pidlockfile_scenarios.pop( - scenario['pidlockfile_scenario_name']) + scenario['pidlockfile_scenario_name']) scenario['pid'] = pidlockfile_scenario['pid'] - scenario['pidfile_path'] = pidlockfile_scenario['path'] + scenario['pidfile_path'] = pidlockfile_scenario['pidfile_path'] scenario['pidfile_timeout'] = 23 scenario['pidlockfile_scenario'] = pidlockfile_scenario return scenarios -def set_runner_scenario(testcase, scenario_name, clear_tracker=True): - """ Set the DaemonRunner test scenario for the test case. """ +def set_runner_scenario(testcase, scenario_name): + """ Set the DaemonRunner test scenario for the test case. + + :param testcase: The `TestCase` instance to decorate. + :param scenario_name: The name of the scenario to use. + + Set the `DaemonRunner` test scenario name and decorate the + `testcase` with the corresponding scenario fixtures. + + """ scenarios = testcase.runner_scenarios testcase.scenario = scenarios[scenario_name] - set_pidlockfile_scenario( - testcase, testcase.scenario['pidlockfile_scenario_name']) - if clear_tracker: - testcase.mock_tracker.clear() + apply_lockfile_method_mocks( + testcase.mock_runner_lockfile, + testcase, + testcase.scenario['pidlockfile_scenario']) -def set_pidlockfile_scenario(testcase, scenario_name): - """ Set the PIDLockFile test scenario for the test case. """ - scenarios = testcase.pidlockfile_scenarios - testcase.pidlockfile_scenario = scenarios[scenario_name] - setup_lockfile_method_mocks( - testcase, testcase.pidlockfile_scenario, - testcase.lockfile_class_name) +def setup_runner_fixtures(testcase): + """ Set up common fixtures for `DaemonRunner` test cases. + :param testcase: A `TestCase` instance to decorate. -def setup_runner_fixtures(testcase): - """ Set up common test fixtures for DaemonRunner test case. """ - testcase.mock_tracker = scaffold.MockTracker() + Decorate the `testcase` with attributes to be fixtures for tests + involving `DaemonRunner` instances. + """ setup_pidfile_fixtures(testcase) setup_streams_fixtures(testcase) testcase.runner_scenarios = make_runner_scenarios() - testcase.mock_stderr = FakeFileDescriptorStringIO() - scaffold.mock( - "sys.stderr", - mock_obj=testcase.mock_stderr, - tracker=testcase.mock_tracker) + patcher_stderr = mock.patch.object( + sys, "stderr", + new=FakeFileDescriptorStringIO()) + testcase.fake_stderr = patcher_stderr.start() + testcase.addCleanup(patcher_stderr.stop) simple_scenario = testcase.runner_scenarios['simple'] - testcase.lockfile_class_name = "pidlockfile.TimeoutPIDLockFile" - - testcase.mock_runner_lock = scaffold.Mock( - testcase.lockfile_class_name, - tracker=testcase.mock_tracker) - testcase.mock_runner_lock.path = simple_scenario['pidfile_path'] - - scaffold.mock( - testcase.lockfile_class_name, - returns=testcase.mock_runner_lock, - tracker=testcase.mock_tracker) + testcase.mock_runner_lockfile = mock.MagicMock( + spec=daemon.pidfile.TimeoutPIDLockFile) + apply_lockfile_method_mocks( + testcase.mock_runner_lockfile, + testcase, + simple_scenario['pidlockfile_scenario']) + testcase.mock_runner_lockfile.path = simple_scenario['pidfile_path'] + + patcher_lockfile_class = mock.patch.object( + daemon.pidfile, "TimeoutPIDLockFile", + return_value=testcase.mock_runner_lockfile) + patcher_lockfile_class.start() + testcase.addCleanup(patcher_lockfile_class.stop) class TestApp(object): @@ -146,31 +174,27 @@ self.pidfile_path = simple_scenario['pidfile_path'] self.pidfile_timeout = simple_scenario['pidfile_timeout'] - run = scaffold.Mock( - "TestApp.run", - tracker=testcase.mock_tracker) + run = mock.MagicMock(name="TestApp.run") testcase.TestApp = TestApp - scaffold.mock( - "daemon.runner.DaemonContext", - returns=scaffold.Mock( - "DaemonContext", - tracker=testcase.mock_tracker), - tracker=testcase.mock_tracker) + patcher_runner_daemoncontext = mock.patch.object( + daemon.runner, "DaemonContext", autospec=True) + patcher_runner_daemoncontext.start() + testcase.addCleanup(patcher_runner_daemoncontext.stop) testcase.test_app = testcase.TestApp() testcase.test_program_name = "bazprog" - testcase.test_program_path = ( - "/foo/bar/%(test_program_name)s" % vars(testcase)) + testcase.test_program_path = os.path.join( + "/foo/bar", testcase.test_program_name) testcase.valid_argv_params = { - 'start': [testcase.test_program_path, 'start'], - 'stop': [testcase.test_program_path, 'stop'], - 'restart': [testcase.test_program_path, 'restart'], - } + 'start': [testcase.test_program_path, 'start'], + 'stop': [testcase.test_program_path, 'stop'], + 'restart': [testcase.test_program_path, 'restart'], + } - def mock_open(filename, mode=None, buffering=None): + def fake_open(filename, mode=None, buffering=None): if filename in testcase.stream_files_by_path: result = testcase.stream_files_by_path[filename] else: @@ -179,484 +203,473 @@ result.buffering = buffering return result - scaffold.mock( - "__builtin__.open", - returns_func=mock_open, - tracker=testcase.mock_tracker) - - scaffold.mock( - "os.kill", - tracker=testcase.mock_tracker) - - scaffold.mock( - "sys.argv", - mock_obj=testcase.valid_argv_params['start'], - tracker=testcase.mock_tracker) + mock_open = mock.mock_open() + mock_open.side_effect = fake_open - testcase.test_instance = runner.DaemonRunner(testcase.test_app) + func_patcher_builtin_open = mock.patch.object( + builtins, "open", + new=mock_open) + func_patcher_builtin_open.start() + testcase.addCleanup(func_patcher_builtin_open.stop) + + func_patcher_os_kill = mock.patch.object(os, "kill") + func_patcher_os_kill.start() + testcase.addCleanup(func_patcher_os_kill.stop) + + patcher_sys_argv = mock.patch.object( + sys, "argv", + new=testcase.valid_argv_params['start']) + patcher_sys_argv.start() + testcase.addCleanup(patcher_sys_argv.stop) + + testcase.test_instance = daemon.runner.DaemonRunner(testcase.test_app) testcase.scenario = NotImplemented - -class DaemonRunner_TestCase(scaffold.TestCase): - """ Test cases for DaemonRunner class. """ + +class DaemonRunner_BaseTestCase(scaffold.TestCase): + """ Base class for DaemonRunner test case classes. """ def setUp(self): """ Set up test fixtures. """ + super(DaemonRunner_BaseTestCase, self).setUp() + setup_runner_fixtures(self) set_runner_scenario(self, 'simple') - scaffold.mock( - "runner.DaemonRunner.parse_args", - tracker=self.mock_tracker) - - self.test_instance = runner.DaemonRunner(self.test_app) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + +class DaemonRunner_TestCase(DaemonRunner_BaseTestCase): + """ Test cases for DaemonRunner class. """ + + def setUp(self): + """ Set up test fixtures. """ + super(DaemonRunner_TestCase, self).setUp() + + func_patcher_parse_args = mock.patch.object( + daemon.runner.DaemonRunner, "parse_args") + func_patcher_parse_args.start() + self.addCleanup(func_patcher_parse_args.stop) + + # Create a new instance now with our custom patches. + self.test_instance = daemon.runner.DaemonRunner(self.test_app) def test_instantiate(self): """ New instance of DaemonRunner should be created. """ - self.failUnlessIsInstance(self.test_instance, runner.DaemonRunner) + self.assertIsInstance(self.test_instance, daemon.runner.DaemonRunner) def test_parses_commandline_args(self): """ Should parse commandline arguments. """ - expect_mock_output = """\ - Called runner.DaemonRunner.parse_args() - ... - """ - self.failUnlessMockCheckerMatch(expect_mock_output) + self.test_instance.parse_args.assert_called_with() def test_has_specified_app(self): """ Should have specified application object. """ - self.failUnlessIs(self.test_app, self.test_instance.app) + self.assertIs(self.test_app, self.test_instance.app) def test_sets_pidfile_none_when_pidfile_path_is_none(self): """ Should set ‘pidfile’ to ‘None’ when ‘pidfile_path’ is ‘None’. """ pidfile_path = None self.test_app.pidfile_path = pidfile_path - expect_pidfile = None - instance = runner.DaemonRunner(self.test_app) - self.failUnlessIs(expect_pidfile, instance.pidfile) + expected_pidfile = None + instance = daemon.runner.DaemonRunner(self.test_app) + self.assertIs(expected_pidfile, instance.pidfile) def test_error_when_pidfile_path_not_string(self): """ Should raise ValueError when PID file path not a string. """ pidfile_path = object() self.test_app.pidfile_path = pidfile_path - expect_error = ValueError - self.failUnlessRaises( - expect_error, - runner.DaemonRunner, self.test_app) + expected_error = ValueError + self.assertRaises( + expected_error, + daemon.runner.DaemonRunner, self.test_app) def test_error_when_pidfile_path_not_absolute(self): """ Should raise ValueError when PID file path not absolute. """ pidfile_path = "foo/bar.pid" self.test_app.pidfile_path = pidfile_path - expect_error = ValueError - self.failUnlessRaises( - expect_error, - runner.DaemonRunner, self.test_app) + expected_error = ValueError + self.assertRaises( + expected_error, + daemon.runner.DaemonRunner, self.test_app) def test_creates_lock_with_specified_parameters(self): """ Should create a TimeoutPIDLockFile with specified params. """ pidfile_path = self.scenario['pidfile_path'] pidfile_timeout = self.scenario['pidfile_timeout'] - lockfile_class_name = self.lockfile_class_name - expect_mock_output = """\ - ... - Called %(lockfile_class_name)s( - %(pidfile_path)r, - %(pidfile_timeout)r) - """ % vars() - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) - + daemon.pidfile.TimeoutPIDLockFile.assert_called_with( + pidfile_path, pidfile_timeout) + def test_has_created_pidfile(self): """ Should have new PID lock file as `pidfile` attribute. """ - expect_pidfile = self.mock_runner_lock + expected_pidfile = self.mock_runner_lockfile instance = self.test_instance - self.failUnlessIs( - expect_pidfile, instance.pidfile) - + self.assertIs( + expected_pidfile, instance.pidfile) + def test_daemon_context_has_created_pidfile(self): """ DaemonContext component should have new PID lock file. """ - expect_pidfile = self.mock_runner_lock + expected_pidfile = self.mock_runner_lockfile daemon_context = self.test_instance.daemon_context - self.failUnlessIs( - expect_pidfile, daemon_context.pidfile) + self.assertIs( + expected_pidfile, daemon_context.pidfile) def test_daemon_context_has_specified_stdin_stream(self): """ DaemonContext component should have specified stdin file. """ test_app = self.test_app - expect_file = self.stream_files_by_name['stdin'] + expected_file = self.stream_files_by_name['stdin'] daemon_context = self.test_instance.daemon_context - self.failUnlessEqual(expect_file, daemon_context.stdin) + self.assertEqual(expected_file, daemon_context.stdin) def test_daemon_context_has_stdin_in_read_mode(self): """ DaemonContext component should open stdin file for read. """ - expect_mode = 'r' + expected_mode = 'rt' daemon_context = self.test_instance.daemon_context - self.failUnlessIn(daemon_context.stdin.mode, expect_mode) + self.assertIn(expected_mode, daemon_context.stdin.mode) def test_daemon_context_has_specified_stdout_stream(self): """ DaemonContext component should have specified stdout file. """ test_app = self.test_app - expect_file = self.stream_files_by_name['stdout'] + expected_file = self.stream_files_by_name['stdout'] daemon_context = self.test_instance.daemon_context - self.failUnlessEqual(expect_file, daemon_context.stdout) + self.assertEqual(expected_file, daemon_context.stdout) def test_daemon_context_has_stdout_in_append_mode(self): """ DaemonContext component should open stdout file for append. """ - expect_mode = 'w+' + expected_mode = 'w+t' daemon_context = self.test_instance.daemon_context - self.failUnlessIn(daemon_context.stdout.mode, expect_mode) + self.assertIn(expected_mode, daemon_context.stdout.mode) def test_daemon_context_has_specified_stderr_stream(self): """ DaemonContext component should have specified stderr file. """ test_app = self.test_app - expect_file = self.stream_files_by_name['stderr'] + expected_file = self.stream_files_by_name['stderr'] daemon_context = self.test_instance.daemon_context - self.failUnlessEqual(expect_file, daemon_context.stderr) + self.assertEqual(expected_file, daemon_context.stderr) def test_daemon_context_has_stderr_in_append_mode(self): """ DaemonContext component should open stderr file for append. """ - expect_mode = 'w+' + expected_mode = 'w+t' daemon_context = self.test_instance.daemon_context - self.failUnlessIn(daemon_context.stderr.mode, expect_mode) + self.assertIn(expected_mode, daemon_context.stderr.mode) def test_daemon_context_has_stderr_with_no_buffering(self): """ DaemonContext component should open stderr file unbuffered. """ - expect_buffering = 0 + expected_buffering = 0 daemon_context = self.test_instance.daemon_context - self.failUnlessEqual( - expect_buffering, daemon_context.stderr.buffering) + self.assertEqual( + expected_buffering, daemon_context.stderr.buffering) -class DaemonRunner_usage_exit_TestCase(scaffold.TestCase): +class DaemonRunner_usage_exit_TestCase(DaemonRunner_BaseTestCase): """ Test cases for DaemonRunner.usage_exit method. """ - def setUp(self): - """ Set up test fixtures. """ - setup_runner_fixtures(self) - set_runner_scenario(self, 'simple') - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - def test_raises_system_exit(self): """ Should raise SystemExit exception. """ instance = self.test_instance argv = [self.test_program_path] - self.failUnlessRaises( - SystemExit, - instance._usage_exit, argv) + self.assertRaises( + SystemExit, + instance._usage_exit, argv) def test_message_follows_conventional_format(self): """ Should emit a conventional usage message. """ instance = self.test_instance - progname = self.test_program_name argv = [self.test_program_path] - expect_stderr_output = """\ - usage: %(progname)s ... - """ % vars() - self.failUnlessRaises( - SystemExit, - instance._usage_exit, argv) - self.failUnlessOutputCheckerMatch( - expect_stderr_output, self.mock_stderr.getvalue()) + expected_stderr_output = """\ + usage: {progname} ... + """.format( + progname=self.test_program_name) + self.assertRaises( + SystemExit, + instance._usage_exit, argv) + self.assertOutputCheckerMatch( + expected_stderr_output, self.fake_stderr.getvalue()) -class DaemonRunner_parse_args_TestCase(scaffold.TestCase): +class DaemonRunner_parse_args_TestCase(DaemonRunner_BaseTestCase): """ Test cases for DaemonRunner.parse_args method. """ def setUp(self): """ Set up test fixtures. """ - setup_runner_fixtures(self) - set_runner_scenario(self, 'simple') + super(DaemonRunner_parse_args_TestCase, self).setUp() - scaffold.mock( - "daemon.runner.DaemonRunner._usage_exit", - raises=NotImplementedError, - tracker=self.mock_tracker) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + func_patcher_usage_exit = mock.patch.object( + daemon.runner.DaemonRunner, "_usage_exit", + side_effect=NotImplementedError) + func_patcher_usage_exit.start() + self.addCleanup(func_patcher_usage_exit.stop) def test_emits_usage_message_if_insufficient_args(self): """ Should emit a usage message and exit if too few arguments. """ instance = self.test_instance argv = [self.test_program_path] - expect_mock_output = """\ - Called daemon.runner.DaemonRunner._usage_exit(%(argv)r) - """ % vars() - try: - instance.parse_args(argv) - except NotImplementedError: - pass - self.failUnlessMockCheckerMatch(expect_mock_output) + exc = self.assertRaises( + NotImplementedError, + instance.parse_args, argv) + daemon.runner.DaemonRunner._usage_exit.assert_called_with(argv) def test_emits_usage_message_if_unknown_action_arg(self): """ Should emit a usage message and exit if unknown action. """ instance = self.test_instance progname = self.test_program_name argv = [self.test_program_path, 'bogus'] - expect_mock_output = """\ - Called daemon.runner.DaemonRunner._usage_exit(%(argv)r) - """ % vars() - try: - instance.parse_args(argv) - except NotImplementedError: - pass - self.failUnlessMockCheckerMatch(expect_mock_output) + exc = self.assertRaises( + NotImplementedError, + instance.parse_args, argv) + daemon.runner.DaemonRunner._usage_exit.assert_called_with(argv) def test_should_parse_system_argv_by_default(self): """ Should parse sys.argv by default. """ instance = self.test_instance - expect_action = 'start' + expected_action = 'start' argv = self.valid_argv_params['start'] - scaffold.mock( - "sys.argv", - mock_obj=argv, - tracker=self.mock_tracker) - instance.parse_args() - self.failUnlessEqual(expect_action, instance.action) + with mock.patch.object(sys, "argv", new=argv): + instance.parse_args() + self.assertEqual(expected_action, instance.action) def test_sets_action_from_first_argument(self): """ Should set action from first commandline argument. """ instance = self.test_instance for name, argv in self.valid_argv_params.items(): - expect_action = name + expected_action = name instance.parse_args(argv) - self.failUnlessEqual(expect_action, instance.action) + self.assertEqual(expected_action, instance.action) -class DaemonRunner_do_action_TestCase(scaffold.TestCase): - """ Test cases for DaemonRunner.do_action method. """ - - def setUp(self): - """ Set up test fixtures. """ - setup_runner_fixtures(self) - set_runner_scenario(self, 'simple') +try: + ProcessLookupError +except NameError: + # Python 2 uses OSError. + ProcessLookupError = functools.partial(OSError, errno.ESRCH) - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() +class DaemonRunner_do_action_TestCase(DaemonRunner_BaseTestCase): + """ Test cases for DaemonRunner.do_action method. """ def test_raises_error_if_unknown_action(self): """ Should emit a usage message and exit if action is unknown. """ instance = self.test_instance instance.action = 'bogus' - expect_error = runner.DaemonRunnerInvalidActionError - self.failUnlessRaises( - expect_error, - instance.do_action) + expected_error = daemon.runner.DaemonRunnerInvalidActionError + self.assertRaises( + expected_error, + instance.do_action) -class DaemonRunner_do_action_start_TestCase(scaffold.TestCase): +class DaemonRunner_do_action_start_TestCase(DaemonRunner_BaseTestCase): """ Test cases for DaemonRunner.do_action method, action 'start'. """ def setUp(self): """ Set up test fixtures. """ - setup_runner_fixtures(self) - set_runner_scenario(self, 'simple') + super(DaemonRunner_do_action_start_TestCase, self).setUp() self.test_instance.action = 'start' - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - def test_raises_error_if_pidfile_locked(self): """ Should raise error if PID file is locked. """ - set_pidlockfile_scenario(self, 'exist-other-pid-locked') + instance = self.test_instance - instance.daemon_context.open.mock_raises = ( - pidlockfile.AlreadyLocked) + instance.daemon_context.open.side_effect = lockfile.AlreadyLocked pidfile_path = self.scenario['pidfile_path'] - expect_error = runner.DaemonRunnerStartFailureError - expect_message_content = pidfile_path - try: - instance.do_action() - except expect_error, exc: - pass - else: - raise self.failureException( - "Failed to raise " + expect_error.__name__) - self.failUnlessIn(str(exc), expect_message_content) + expected_error = daemon.runner.DaemonRunnerStartFailureError + expected_message_content = pidfile_path + exc = self.assertRaises( + expected_error, + instance.do_action) + self.assertIn(expected_message_content, unicode(exc)) def test_breaks_lock_if_no_such_process(self): """ Should request breaking lock if PID file process is not running. """ set_runner_scenario(self, 'pidfile-locked') instance = self.test_instance - self.mock_runner_lock.read_pid.mock_returns = ( - self.scenario['pidlockfile_scenario']['pidfile_pid']) + self.mock_runner_lockfile.read_pid.return_value = ( + self.scenario['pidlockfile_scenario']['pidfile_pid']) pidfile_path = self.scenario['pidfile_path'] test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid'] - expect_signal = signal.SIG_DFL - error = OSError(errno.ESRCH, "Not running") - os.kill.mock_raises = error - lockfile_class_name = self.lockfile_class_name - expect_mock_output = """\ - ... - Called os.kill(%(test_pid)r, %(expect_signal)r) - Called %(lockfile_class_name)s.break_lock() - ... - """ % vars() + expected_signal = signal.SIG_DFL + test_error = ProcessLookupError("Not running") + os.kill.side_effect = test_error instance.do_action() - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) + os.kill.assert_called_with(test_pid, expected_signal) + self.mock_runner_lockfile.break_lock.assert_called_with() def test_requests_daemon_context_open(self): """ Should request the daemon context to open. """ instance = self.test_instance - expect_mock_output = """\ - ... - Called DaemonContext.open() - ... - """ instance.do_action() - self.failUnlessMockCheckerMatch(expect_mock_output) + instance.daemon_context.open.assert_called_with() def test_emits_start_message_to_stderr(self): """ Should emit start message to stderr. """ instance = self.test_instance - current_pid = self.scenario['pid'] - expect_stderr = """\ - started with pid %(current_pid)d - """ % vars() + expected_stderr = """\ + started with pid {pid:d} + """.format( + pid=self.scenario['pid']) instance.do_action() - self.failUnlessOutputCheckerMatch( - expect_stderr, self.mock_stderr.getvalue()) + self.assertOutputCheckerMatch( + expected_stderr, self.fake_stderr.getvalue()) def test_requests_app_run(self): """ Should request the application to run. """ instance = self.test_instance - expect_mock_output = """\ - ... - Called TestApp.run() - """ instance.do_action() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.test_app.run.assert_called_with() -class DaemonRunner_do_action_stop_TestCase(scaffold.TestCase): +class DaemonRunner_do_action_stop_TestCase(DaemonRunner_BaseTestCase): """ Test cases for DaemonRunner.do_action method, action 'stop'. """ def setUp(self): """ Set up test fixtures. """ - setup_runner_fixtures(self) + super(DaemonRunner_do_action_stop_TestCase, self).setUp() + set_runner_scenario(self, 'pidfile-locked') self.test_instance.action = 'stop' - self.mock_runner_lock.is_locked.mock_returns = True - self.mock_runner_lock.i_am_locking.mock_returns = False - self.mock_runner_lock.read_pid.mock_returns = ( - self.scenario['pidlockfile_scenario']['pidfile_pid']) - - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() + self.mock_runner_lockfile.is_locked.return_value = True + self.mock_runner_lockfile.i_am_locking.return_value = False + self.mock_runner_lockfile.read_pid.return_value = ( + self.scenario['pidlockfile_scenario']['pidfile_pid']) def test_raises_error_if_pidfile_not_locked(self): """ Should raise error if PID file is not locked. """ set_runner_scenario(self, 'simple') instance = self.test_instance - self.mock_runner_lock.is_locked.mock_returns = False - self.mock_runner_lock.i_am_locking.mock_returns = False - self.mock_runner_lock.read_pid.mock_returns = ( - self.scenario['pidlockfile_scenario']['pidfile_pid']) + self.mock_runner_lockfile.is_locked.return_value = False + self.mock_runner_lockfile.i_am_locking.return_value = False + self.mock_runner_lockfile.read_pid.return_value = ( + self.scenario['pidlockfile_scenario']['pidfile_pid']) pidfile_path = self.scenario['pidfile_path'] - expect_error = runner.DaemonRunnerStopFailureError - expect_message_content = pidfile_path - try: - instance.do_action() - except expect_error, exc: - pass - else: - raise self.failureException( - "Failed to raise " + expect_error.__name__) - scaffold.mock_restore() - self.failUnlessIn(str(exc), expect_message_content) + expected_error = daemon.runner.DaemonRunnerStopFailureError + expected_message_content = pidfile_path + exc = self.assertRaises( + expected_error, + instance.do_action) + self.assertIn(expected_message_content, unicode(exc)) def test_breaks_lock_if_pidfile_stale(self): """ Should break lock if PID file is stale. """ instance = self.test_instance pidfile_path = self.scenario['pidfile_path'] test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid'] - expect_signal = signal.SIG_DFL - error = OSError(errno.ESRCH, "Not running") - os.kill.mock_raises = error - lockfile_class_name = self.lockfile_class_name - expect_mock_output = """\ - ... - Called %(lockfile_class_name)s.break_lock() - """ % vars() + expected_signal = signal.SIG_DFL + test_error = OSError(errno.ESRCH, "Not running") + os.kill.side_effect = test_error instance.do_action() - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) + self.mock_runner_lockfile.break_lock.assert_called_with() def test_sends_terminate_signal_to_process_from_pidfile(self): """ Should send SIGTERM to the daemon process. """ instance = self.test_instance test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid'] - expect_signal = signal.SIGTERM - expect_mock_output = """\ - ... - Called os.kill(%(test_pid)r, %(expect_signal)r) - """ % vars() + expected_signal = signal.SIGTERM instance.do_action() - scaffold.mock_restore() - self.failUnlessMockCheckerMatch(expect_mock_output) + os.kill.assert_called_with(test_pid, expected_signal) def test_raises_error_if_cannot_send_signal_to_process(self): """ Should raise error if cannot send signal to daemon process. """ instance = self.test_instance test_pid = self.scenario['pidlockfile_scenario']['pidfile_pid'] pidfile_path = self.scenario['pidfile_path'] - error = OSError(errno.EPERM, "Nice try") - os.kill.mock_raises = error - expect_error = runner.DaemonRunnerStopFailureError - expect_message_content = str(test_pid) - try: - instance.do_action() - except expect_error, exc: - pass - else: - raise self.failureException( - "Failed to raise " + expect_error.__name__) - self.failUnlessIn(str(exc), expect_message_content) + test_error = OSError(errno.EPERM, "Nice try") + os.kill.side_effect = test_error + expected_error = daemon.runner.DaemonRunnerStopFailureError + expected_message_content = unicode(test_pid) + exc = self.assertRaises( + expected_error, + instance.do_action) + self.assertIn(expected_message_content, unicode(exc)) -class DaemonRunner_do_action_restart_TestCase(scaffold.TestCase): +@mock.patch.object(daemon.runner.DaemonRunner, "_start") +@mock.patch.object(daemon.runner.DaemonRunner, "_stop") +class DaemonRunner_do_action_restart_TestCase(DaemonRunner_BaseTestCase): """ Test cases for DaemonRunner.do_action method, action 'restart'. """ def setUp(self): """ Set up test fixtures. """ - setup_runner_fixtures(self) + super(DaemonRunner_do_action_restart_TestCase, self).setUp() + set_runner_scenario(self, 'pidfile-locked') self.test_instance.action = 'restart' - def tearDown(self): - """ Tear down test fixtures. """ - scaffold.mock_restore() - - def test_requests_stop_then_start(self): + def test_requests_stop_then_start( + self, + mock_func_daemonrunner_start, mock_func_daemonrunner_stop): """ Should request stop, then start. """ instance = self.test_instance - scaffold.mock( - "daemon.runner.DaemonRunner._start", - tracker=self.mock_tracker) - scaffold.mock( - "daemon.runner.DaemonRunner._stop", - tracker=self.mock_tracker) - expect_mock_output = """\ - Called daemon.runner.DaemonRunner._stop() - Called daemon.runner.DaemonRunner._start() - """ instance.do_action() - self.failUnlessMockCheckerMatch(expect_mock_output) + mock_func_daemonrunner_start.assert_called_with() + mock_func_daemonrunner_stop.assert_called_with() + + +@mock.patch.object(sys, "stderr") +class emit_message_TestCase(scaffold.TestCase): + """ Test cases for ‘emit_message’ function. """ + + def test_writes_specified_message_to_stream(self, mock_stderr): + """ Should write specified message to stream. """ + test_message = self.getUniqueString() + expected_content = "{message}\n".format(message=test_message) + daemon.runner.emit_message(test_message, stream=mock_stderr) + mock_stderr.write.assert_called_with(expected_content) + + def test_writes_to_specified_stream(self, mock_stderr): + """ Should write message to specified stream. """ + test_message = self.getUniqueString() + mock_stream = mock.MagicMock() + daemon.runner.emit_message(test_message, stream=mock_stream) + mock_stream.write.assert_called_with(mock.ANY) + + def test_writes_to_stderr_by_default(self, mock_stderr): + """ Should write message to ‘sys.stderr’ by default. """ + test_message = self.getUniqueString() + daemon.runner.emit_message(test_message) + mock_stderr.write.assert_called_with(mock.ANY) + + +class is_pidfile_stale_TestCase(scaffold.TestCase): + """ Test cases for ‘is_pidfile_stale’ function. """ + + def setUp(self): + """ Set up test fixtures. """ + super(is_pidfile_stale_TestCase, self).setUp() + + func_patcher_os_kill = mock.patch.object(os, "kill") + func_patcher_os_kill.start() + self.addCleanup(func_patcher_os_kill.stop) + os.kill.return_value = None + + self.test_pid = self.getUniqueInteger() + self.test_pidfile = mock.MagicMock(daemon.pidfile.TimeoutPIDLockFile) + self.test_pidfile.read_pid.return_value = self.test_pid + + def test_returns_false_if_no_pid_in_file(self): + """ Should return False if the pidfile contains no PID. """ + self.test_pidfile.read_pid.return_value = None + expected_result = False + result = daemon.runner.is_pidfile_stale(self.test_pidfile) + self.assertEqual(expected_result, result) + + def test_returns_false_if_process_exists(self): + """ Should return False if the process with its PID exists. """ + expected_result = False + result = daemon.runner.is_pidfile_stale(self.test_pidfile) + self.assertEqual(expected_result, result) + + def test_returns_true_if_process_does_not_exist(self): + """ Should return True if the process does not exist. """ + test_error = ProcessLookupError("No such process") + del os.kill.return_value + os.kill.side_effect = test_error + expected_result = True + result = daemon.runner.is_pidfile_stale(self.test_pidfile) + self.assertEqual(expected_result, result) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/test_version.py python-daemon-2.0.5/test_version.py --- python-daemon-1.5.5/test_version.py 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/test_version.py 2015-02-02 04:43:28.000000000 +0000 @@ -0,0 +1,1373 @@ +# -*- coding: utf-8 -*- +# +# test_version.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; version 3 of that license or any later version. +# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details. + +""" Unit test for ‘version’ packaging module. """ + +from __future__ import (absolute_import, unicode_literals) + +import os +import os.path +import io +import errno +import functools +import collections +import textwrap +import json +import tempfile +import distutils.dist +import distutils.cmd +import distutils.errors +import distutils.fancy_getopt +try: + # Standard library of Python 2.7 and later. + from io import StringIO +except ImportError: + # Standard library of Python 2.6 and earlier. + from StringIO import StringIO + +import mock +import testtools +import testscenarios +import docutils +import docutils.writers +import docutils.nodes +import setuptools +import setuptools.command + +import version + +version.ensure_class_bases_begin_with( + version.__dict__, str('VersionInfoWriter'), docutils.writers.Writer) +version.ensure_class_bases_begin_with( + version.__dict__, str('VersionInfoTranslator'), + docutils.nodes.SparseNodeVisitor) + + +def make_test_classes_for_ensure_class_bases_begin_with(): + """ Make test classes for use with ‘ensure_class_bases_begin_with’. + + :return: Mapping {`name`: `type`} of the custom types created. + + """ + + class quux_metaclass(type): + def __new__(metaclass, name, bases, namespace): + return super(quux_metaclass, metaclass).__new__( + metaclass, name, bases, namespace) + + class Foo(object): + __metaclass__ = type + + class Bar(object): + pass + + class FooInheritingBar(Bar): + __metaclass__ = type + + class FooWithCustomMetaclass(object): + __metaclass__ = quux_metaclass + + result = dict( + (name, value) for (name, value) in locals().items() + if isinstance(value, type)) + + return result + +class ensure_class_bases_begin_with_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ensure_class_bases_begin_with’ function. """ + + test_classes = make_test_classes_for_ensure_class_bases_begin_with() + + scenarios = [ + ('simple', { + 'test_class': test_classes['Foo'], + 'base_class': test_classes['Bar'], + }), + ('custom metaclass', { + 'test_class': test_classes['FooWithCustomMetaclass'], + 'base_class': test_classes['Bar'], + 'expected_metaclass': test_classes['quux_metaclass'], + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(ensure_class_bases_begin_with_TestCase, self).setUp() + + self.class_name = self.test_class.__name__ + self.test_module_namespace = {self.class_name: self.test_class} + + if not hasattr(self, 'expected_metaclass'): + self.expected_metaclass = type + + patcher_metaclass = mock.patch.object( + self.test_class, '__metaclass__') + patcher_metaclass.start() + self.addCleanup(patcher_metaclass.stop) + + self.fake_new_class = type(object) + self.test_class.__metaclass__.return_value = ( + self.fake_new_class) + + def test_module_namespace_contains_new_class(self): + """ Specified module namespace should have new class. """ + version.ensure_class_bases_begin_with( + self.test_module_namespace, self.class_name, self.base_class) + self.assertIn(self.fake_new_class, self.test_module_namespace.values()) + + def test_calls_metaclass_with_expected_class_name(self): + """ Should call the metaclass with the expected class name. """ + version.ensure_class_bases_begin_with( + self.test_module_namespace, self.class_name, self.base_class) + expected_class_name = self.class_name + self.test_class.__metaclass__.assert_called_with( + expected_class_name, mock.ANY, mock.ANY) + + def test_calls_metaclass_with_expected_bases(self): + """ Should call the metaclass with the expected bases. """ + version.ensure_class_bases_begin_with( + self.test_module_namespace, self.class_name, self.base_class) + expected_bases = tuple( + [self.base_class] + + list(self.test_class.__bases__)) + self.test_class.__metaclass__.assert_called_with( + mock.ANY, expected_bases, mock.ANY) + + def test_calls_metaclass_with_expected_namespace(self): + """ Should call the metaclass with the expected class namespace. """ + version.ensure_class_bases_begin_with( + self.test_module_namespace, self.class_name, self.base_class) + expected_namespace = self.test_class.__dict__.copy() + del expected_namespace['__dict__'] + self.test_class.__metaclass__.assert_called_with( + mock.ANY, mock.ANY, expected_namespace) + + +class ensure_class_bases_begin_with_AlreadyHasBase_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ensure_class_bases_begin_with’ function. + + These test cases test the conditions where the class's base is + already the specified base class. + + """ + + test_classes = make_test_classes_for_ensure_class_bases_begin_with() + + scenarios = [ + ('already Bar subclass', { + 'test_class': test_classes['FooInheritingBar'], + 'base_class': test_classes['Bar'], + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super( + ensure_class_bases_begin_with_AlreadyHasBase_TestCase, + self).setUp() + + self.class_name = self.test_class.__name__ + self.test_module_namespace = {self.class_name: self.test_class} + + patcher_metaclass = mock.patch.object( + self.test_class, '__metaclass__') + patcher_metaclass.start() + self.addCleanup(patcher_metaclass.stop) + + def test_metaclass_not_called(self): + """ Should not call metaclass to create a new type. """ + version.ensure_class_bases_begin_with( + self.test_module_namespace, self.class_name, self.base_class) + self.assertFalse(self.test_class.__metaclass__.called) + + +class VersionInfoWriter_TestCase(testtools.TestCase): + """ Test cases for ‘VersionInfoWriter’ class. """ + + def setUp(self): + """ Set up test fixtures. """ + super(VersionInfoWriter_TestCase, self).setUp() + + self.test_instance = version.VersionInfoWriter() + + def test_declares_version_info_support(self): + """ Should declare support for ‘version_info’. """ + instance = self.test_instance + expected_support = "version_info" + result = instance.supports(expected_support) + self.assertTrue(result) + + +class VersionInfoWriter_translate_TestCase(testtools.TestCase): + """ Test cases for ‘VersionInfoWriter.translate’ method. """ + + def setUp(self): + """ Set up test fixtures. """ + super(VersionInfoWriter_translate_TestCase, self).setUp() + + patcher_translator = mock.patch.object( + version, 'VersionInfoTranslator') + self.mock_class_translator = patcher_translator.start() + self.addCleanup(patcher_translator.stop) + self.mock_translator = self.mock_class_translator.return_value + + self.test_instance = version.VersionInfoWriter() + patcher_document = mock.patch.object( + self.test_instance, 'document') + patcher_document.start() + self.addCleanup(patcher_document.stop) + + def test_creates_translator_with_document(self): + """ Should create a translator with the writer's document. """ + instance = self.test_instance + expected_document = self.test_instance.document + instance.translate() + self.mock_class_translator.assert_called_with(expected_document) + + def test_calls_document_walkabout_with_translator(self): + """ Should call document.walkabout with the translator. """ + instance = self.test_instance + instance.translate() + instance.document.walkabout.assert_called_with(self.mock_translator) + + def test_output_from_translator_astext(self): + """ Should have output from translator.astext(). """ + instance = self.test_instance + instance.translate() + expected_output = self.mock_translator.astext.return_value + self.assertEqual(expected_output, instance.output) + + +class ChangeLogEntry_TestCase(testtools.TestCase): + """ Test cases for ‘ChangeLogEntry’ class. """ + + def setUp(self): + """ Set up test fixtures. """ + super(ChangeLogEntry_TestCase, self).setUp() + + self.test_instance = version.ChangeLogEntry() + + def test_instantiate(self): + """ New instance of ‘ChangeLogEntry’ should be created. """ + self.assertIsInstance( + self.test_instance, version.ChangeLogEntry) + + def test_minimum_zero_arguments(self): + """ Initialiser should not require any arguments. """ + instance = version.ChangeLogEntry() + self.assertIsNot(instance, None) + + +class ChangeLogEntry_release_date_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ChangeLogEntry.release_date’ attribute. """ + + scenarios = [ + ('default', { + 'test_args': {}, + 'expected_release_date': + version.ChangeLogEntry.default_release_date, + }), + ('unknown token', { + 'test_args': {'release_date': "UNKNOWN"}, + 'expected_release_date': "UNKNOWN", + }), + ('future token', { + 'test_args': {'release_date': "FUTURE"}, + 'expected_release_date': "FUTURE", + }), + ('2001-01-01', { + 'test_args': {'release_date': "2001-01-01"}, + 'expected_release_date': "2001-01-01", + }), + ('bogus', { + 'test_args': {'release_date': "b0gUs"}, + 'expected_error': ValueError, + }), + ] + + def test_has_expected_release_date(self): + """ Should have default `release_date` attribute. """ + if hasattr(self, 'expected_error'): + self.assertRaises( + self.expected_error, + version.ChangeLogEntry, **self.test_args) + else: + instance = version.ChangeLogEntry(**self.test_args) + self.assertEqual(self.expected_release_date, instance.release_date) + + +class ChangeLogEntry_version_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ChangeLogEntry.version’ attribute. """ + + scenarios = [ + ('default', { + 'test_args': {}, + 'expected_version': + version.ChangeLogEntry.default_version, + }), + ('unknown token', { + 'test_args': {'version': "UNKNOWN"}, + 'expected_version': "UNKNOWN", + }), + ('0.0', { + 'test_args': {'version': "0.0"}, + 'expected_version': "0.0", + }), + ] + + def test_has_expected_version(self): + """ Should have default `version` attribute. """ + instance = version.ChangeLogEntry(**self.test_args) + self.assertEqual(self.expected_version, instance.version) + + +class ChangeLogEntry_maintainer_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ChangeLogEntry.maintainer’ attribute. """ + + scenarios = [ + ('default', { + 'test_args': {}, + 'expected_maintainer': None, + }), + ('person', { + 'test_args': {'maintainer': "Foo Bar "}, + 'expected_maintainer': "Foo Bar ", + }), + ('bogus', { + 'test_args': {'maintainer': "b0gUs"}, + 'expected_error': ValueError, + }), + ] + + def test_has_expected_maintainer(self): + """ Should have default `maintainer` attribute. """ + if hasattr(self, 'expected_error'): + self.assertRaises( + self.expected_error, + version.ChangeLogEntry, **self.test_args) + else: + instance = version.ChangeLogEntry(**self.test_args) + self.assertEqual(self.expected_maintainer, instance.maintainer) + + +class ChangeLogEntry_body_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ChangeLogEntry.body’ attribute. """ + + scenarios = [ + ('default', { + 'test_args': {}, + 'expected_body': None, + }), + ('simple', { + 'test_args': {'body': "Foo bar baz."}, + 'expected_body': "Foo bar baz.", + }), + ] + + def test_has_expected_body(self): + """ Should have default `body` attribute. """ + instance = version.ChangeLogEntry(**self.test_args) + self.assertEqual(self.expected_body, instance.body) + + +class ChangeLogEntry_as_version_info_entry_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘ChangeLogEntry.as_version_info_entry’ attribute. """ + + scenarios = [ + ('default', { + 'test_args': {}, + 'expected_result': collections.OrderedDict([ + ('release_date', version.ChangeLogEntry.default_release_date), + ('version', version.ChangeLogEntry.default_version), + ('maintainer', None), + ('body', None), + ]), + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(ChangeLogEntry_as_version_info_entry_TestCase, self).setUp() + + self.test_instance = version.ChangeLogEntry(**self.test_args) + + def test_returns_result(self): + """ Should return expected result. """ + result = self.test_instance.as_version_info_entry() + self.assertEqual(self.expected_result, result) + + +def make_mock_field_node(field_name, field_body): + """ Make a mock Docutils field node for tests. """ + + mock_field_node = mock.MagicMock( + name='field', spec=docutils.nodes.field) + + mock_field_name_node = mock.MagicMock( + name='field_name', spec=docutils.nodes.field_name) + mock_field_name_node.parent = mock_field_node + mock_field_name_node.children = [field_name] + + mock_field_body_node = mock.MagicMock( + name='field_body', spec=docutils.nodes.field_body) + mock_field_body_node.parent = mock_field_node + mock_field_body_node.children = [field_body] + + mock_field_node.children = [mock_field_name_node, mock_field_body_node] + + def fake_func_first_child_matching_class(node_class): + result = None + node_class_name = node_class.__name__ + for (index, node) in enumerate(mock_field_node.children): + if node._mock_name == node_class_name: + result = index + break + return result + + mock_field_node.first_child_matching_class.side_effect = ( + fake_func_first_child_matching_class) + + return mock_field_node + + +class JsonEqual(testtools.matchers.Matcher): + """ A matcher to compare the value of JSON streams. """ + + def __init__(self, expected): + self.expected_value = expected + + def match(self, content): + """ Assert the JSON `content` matches the `expected_content`. """ + result = None + actual_value = json.loads(content.decode('utf-8')) + if actual_value != self.expected_value: + result = JsonValueMismatch(self.expected_value, actual_value) + return result + + +class JsonValueMismatch(testtools.matchers.Mismatch): + """ The specified JSON stream does not evaluate to the expected value. """ + + def __init__(self, expected, actual): + self.expected_value = expected + self.actual_value = actual + + def describe(self): + """ Emit a text description of this mismatch. """ + expected_json_text = json.dumps(self.expected_value, indent=4) + actual_json_text = json.dumps(self.actual_value, indent=4) + text = ( + "\n" + "reference: {expected}\n" + "actual: {actual}\n").format( + expected=expected_json_text, actual=actual_json_text) + return text + + +class changelog_to_version_info_collection_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘changelog_to_version_info_collection’ function. """ + + scenarios = [ + ('single entry', { + 'test_input': textwrap.dedent("""\ + Version 1.0 + =========== + + :Released: 2009-01-01 + :Maintainer: Foo Bar + + * Lorem ipsum dolor sit amet. + """), + 'expected_version_info': [ + { + 'release_date': "2009-01-01", + 'version': "1.0", + 'maintainer': "Foo Bar ", + 'body': "* Lorem ipsum dolor sit amet.\n", + }, + ], + }), + ('multiple entries', { + 'test_input': textwrap.dedent("""\ + Version 1.0 + =========== + + :Released: 2009-01-01 + :Maintainer: Foo Bar + + * Lorem ipsum dolor sit amet. + + + Version 0.8 + =========== + + :Released: 2004-01-01 + :Maintainer: Foo Bar + + * Donec venenatis nisl aliquam ipsum. + + + Version 0.7.2 + ============= + + :Released: 2001-01-01 + :Maintainer: Foo Bar + + * Pellentesque elementum mollis finibus. + """), + 'expected_version_info': [ + { + 'release_date': "2009-01-01", + 'version': "1.0", + 'maintainer': "Foo Bar ", + 'body': "* Lorem ipsum dolor sit amet.\n", + }, + { + 'release_date': "2004-01-01", + 'version': "0.8", + 'maintainer': "Foo Bar ", + 'body': "* Donec venenatis nisl aliquam ipsum.\n", + }, + { + 'release_date': "2001-01-01", + 'version': "0.7.2", + 'maintainer': "Foo Bar ", + 'body': "* Pellentesque elementum mollis finibus.\n", + }, + ], + }), + ('trailing comment', { + 'test_input': textwrap.dedent("""\ + Version NEXT + ============ + + :Released: FUTURE + :Maintainer: + + * Lorem ipsum dolor sit amet. + + .. + Vivamus aliquam felis rutrum rutrum dictum. + """), + 'expected_version_info': [ + { + 'release_date': "FUTURE", + 'version': "NEXT", + 'maintainer': "", + 'body': "* Lorem ipsum dolor sit amet.\n", + }, + ], + }), + ('inline comment', { + 'test_input': textwrap.dedent("""\ + Version NEXT + ============ + + :Released: FUTURE + :Maintainer: + + .. + Vivamus aliquam felis rutrum rutrum dictum. + + * Lorem ipsum dolor sit amet. + """), + 'expected_version_info': [ + { + 'release_date': "FUTURE", + 'version': "NEXT", + 'maintainer': "", + 'body': "* Lorem ipsum dolor sit amet.\n", + }, + ], + }), + ('unreleased entry', { + 'test_input': textwrap.dedent("""\ + Version NEXT + ============ + + :Released: FUTURE + :Maintainer: + + * Lorem ipsum dolor sit amet. + + + Version 0.8 + =========== + + :Released: 2001-01-01 + :Maintainer: Foo Bar + + * Donec venenatis nisl aliquam ipsum. + """), + 'expected_version_info': [ + { + 'release_date': "FUTURE", + 'version': "NEXT", + 'maintainer': "", + 'body': "* Lorem ipsum dolor sit amet.\n", + }, + { + 'release_date': "2001-01-01", + 'version': "0.8", + 'maintainer': "Foo Bar ", + 'body': "* Donec venenatis nisl aliquam ipsum.\n", + }, + ], + }), + ('no section', { + 'test_input': textwrap.dedent("""\ + :Released: 2009-01-01 + :Maintainer: Foo Bar + + * Lorem ipsum dolor sit amet. + """), + 'expected_error': version.InvalidFormatError, + }), + ('subsection', { + 'test_input': textwrap.dedent("""\ + Version 1.0 + =========== + + :Released: 2009-01-01 + :Maintainer: Foo Bar + + * Lorem ipsum dolor sit amet. + + Ut ultricies fermentum quam + --------------------------- + + * In commodo magna facilisis in. + """), + 'expected_error': version.InvalidFormatError, + 'subsection': True, + }), + ('unknown field', { + 'test_input': textwrap.dedent("""\ + Version 1.0 + =========== + + :Released: 2009-01-01 + :Maintainer: Foo Bar + :Favourite: Spam + + * Lorem ipsum dolor sit amet. + """), + 'expected_error': version.InvalidFormatError, + }), + ('invalid version word', { + 'test_input': textwrap.dedent("""\ + BoGuS 1.0 + ========= + + :Released: 2009-01-01 + :Maintainer: Foo Bar + + * Lorem ipsum dolor sit amet. + """), + 'expected_error': version.InvalidFormatError, + }), + ('invalid section title', { + 'test_input': textwrap.dedent("""\ + Lorem Ipsum 1.0 + =============== + + :Released: 2009-01-01 + :Maintainer: Foo Bar + + * Lorem ipsum dolor sit amet. + """), + 'expected_error': version.InvalidFormatError, + }), + ] + + def test_returns_expected_version_info(self): + """ Should return expected version info mapping. """ + infile = StringIO(self.test_input) + if hasattr(self, 'expected_error'): + self.assertRaises( + self.expected_error, + version.changelog_to_version_info_collection, infile) + else: + result = version.changelog_to_version_info_collection(infile) + self.assertThat(result, JsonEqual(self.expected_version_info)) + + +try: + FileNotFoundError + PermissionError +except NameError: + # Python 2 uses OSError. + FileNotFoundError = functools.partial(IOError, errno.ENOENT) + PermissionError = functools.partial(IOError, errno.EPERM) + +fake_version_info = { + 'release_date': "2001-01-01", 'version': "2.0", + 'maintainer': None, 'body': None, + } + +@mock.patch.object( + version, "get_latest_version", return_value=fake_version_info) +class generate_version_info_from_changelog_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘generate_version_info_from_changelog’ function. """ + + fake_open_side_effects = { + 'success': ( + lambda *args, **kwargs: StringIO()), + 'file not found': FileNotFoundError(), + 'permission denied': PermissionError(), + } + + scenarios = [ + ('simple', { + 'open_scenario': 'success', + 'fake_versions_json': json.dumps([fake_version_info]), + 'expected_result': fake_version_info, + }), + ('file not found', { + 'open_scenario': 'file not found', + 'expected_result': {}, + }), + ('permission denied', { + 'open_scenario': 'permission denied', + 'expected_result': {}, + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(generate_version_info_from_changelog_TestCase, self).setUp() + + self.fake_changelog_file_path = tempfile.mktemp() + + def fake_open(filespec, *args, **kwargs): + if filespec == self.fake_changelog_file_path: + side_effect = self.fake_open_side_effects[self.open_scenario] + if callable(side_effect): + result = side_effect() + else: + raise side_effect + else: + result = StringIO() + return result + + func_patcher_io_open = mock.patch.object( + io, "open") + func_patcher_io_open.start() + self.addCleanup(func_patcher_io_open.stop) + io.open.side_effect = fake_open + + self.file_encoding = "utf-8" + + func_patcher_changelog_to_version_info_collection = mock.patch.object( + version, "changelog_to_version_info_collection") + func_patcher_changelog_to_version_info_collection.start() + self.addCleanup(func_patcher_changelog_to_version_info_collection.stop) + if hasattr(self, 'fake_versions_json'): + version.changelog_to_version_info_collection.return_value = ( + self.fake_versions_json.encode(self.file_encoding)) + + def test_returns_empty_collection_on_read_error( + self, + mock_func_get_latest_version): + """ Should return empty collection on error reading changelog. """ + test_error = PermissionError("Not for you") + version.changelog_to_version_info_collection.side_effect = test_error + result = version.generate_version_info_from_changelog( + self.fake_changelog_file_path) + expected_result = {} + self.assertDictEqual(expected_result, result) + + def test_opens_file_with_expected_encoding( + self, + mock_func_get_latest_version): + """ Should open changelog file in text mode with expected encoding. """ + result = version.generate_version_info_from_changelog( + self.fake_changelog_file_path) + expected_file_path = self.fake_changelog_file_path + expected_open_mode = 'rt' + expected_encoding = self.file_encoding + (open_args_positional, open_args_kwargs) = io.open.call_args + (open_args_filespec, open_args_mode) = open_args_positional[:2] + open_args_encoding = open_args_kwargs['encoding'] + self.assertEqual(expected_file_path, open_args_filespec) + self.assertEqual(expected_open_mode, open_args_mode) + self.assertEqual(expected_encoding, open_args_encoding) + + def test_returns_expected_result( + self, + mock_func_get_latest_version): + """ Should return expected result. """ + result = version.generate_version_info_from_changelog( + self.fake_changelog_file_path) + self.assertEqual(self.expected_result, result) + + +DefaultNoneDict = functools.partial(collections.defaultdict, lambda: None) + +class get_latest_version_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘get_latest_version’ function. """ + + scenarios = [ + ('simple', { + 'test_versions': [ + DefaultNoneDict({'release_date': "LATEST"}), + ], + 'expected_result': version.ChangeLogEntry.make_ordered_dict( + DefaultNoneDict({'release_date': "LATEST"})), + }), + ('no versions', { + 'test_versions': [], + 'expected_result': collections.OrderedDict(), + }), + ('ordered versions', { + 'test_versions': [ + DefaultNoneDict({'release_date': "1"}), + DefaultNoneDict({'release_date': "2"}), + DefaultNoneDict({'release_date': "LATEST"}), + ], + 'expected_result': version.ChangeLogEntry.make_ordered_dict( + DefaultNoneDict({'release_date': "LATEST"})), + }), + ('un-ordered versions', { + 'test_versions': [ + DefaultNoneDict({'release_date': "2"}), + DefaultNoneDict({'release_date': "LATEST"}), + DefaultNoneDict({'release_date': "1"}), + ], + 'expected_result': version.ChangeLogEntry.make_ordered_dict( + DefaultNoneDict({'release_date': "LATEST"})), + }), + ] + + def test_returns_expected_result(self): + """ Should return expected result. """ + result = version.get_latest_version(self.test_versions) + self.assertDictEqual(self.expected_result, result) + + +@mock.patch.object(json, "dumps", side_effect=json.dumps) +class serialise_version_info_from_mapping_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘get_latest_version’ function. """ + + scenarios = [ + ('simple', { + 'test_version_info': {'foo': "spam"}, + }), + ] + + for (name, scenario) in scenarios: + scenario['fake_json_dump'] = json.dumps(scenario['test_version_info']) + scenario['expected_value'] = scenario['test_version_info'] + + def test_passes_specified_object(self, mock_func_json_dumps): + """ Should pass the specified object to `json.dumps`. """ + result = version.serialise_version_info_from_mapping( + self.test_version_info) + mock_func_json_dumps.assert_called_with( + self.test_version_info, indent=mock.ANY) + + def test_returns_expected_result(self, mock_func_json_dumps): + """ Should return expected result. """ + mock_func_json_dumps.return_value = self.fake_json_dump + result = version.serialise_version_info_from_mapping( + self.test_version_info) + value = json.loads(result) + self.assertEqual(self.expected_value, value) + + +DistributionMetadata_defaults = { + name: None + for name in list(collections.OrderedDict.fromkeys( + distutils.dist.DistributionMetadata._METHOD_BASENAMES))} +FakeDistributionMetadata = collections.namedtuple( + 'FakeDistributionMetadata', DistributionMetadata_defaults.keys()) + +Distribution_defaults = { + 'metadata': None, + 'version': None, + 'release_date': None, + 'maintainer': None, + 'maintainer_email': None, + } +FakeDistribution = collections.namedtuple( + 'FakeDistribution', Distribution_defaults.keys()) + +def make_fake_distribution( + fields_override=None, metadata_fields_override=None): + metadata_fields = DistributionMetadata_defaults.copy() + if metadata_fields_override is not None: + metadata_fields.update(metadata_fields_override) + metadata = FakeDistributionMetadata(**metadata_fields) + + fields = Distribution_defaults.copy() + fields['metadata'] = metadata + if fields_override is not None: + fields.update(fields_override) + distribution = FakeDistribution(**fields) + + return distribution + + +class get_changelog_path_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘get_changelog_path’ function. """ + + default_path = "." + default_script_filename = "setup.py" + + scenarios = [ + ('simple', {}), + ('unusual script name', { + 'script_filename': "lorem_ipsum", + }), + ('relative script path', { + 'script_directory': "dolor/sit/amet", + }), + ('absolute script path', { + 'script_directory': "/dolor/sit/amet", + }), + ('specify filename', { + 'changelog_filename': "adipiscing", + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(get_changelog_path_TestCase, self).setUp() + + self.test_distribution = mock.MagicMock(distutils.dist.Distribution) + + if not hasattr(self, 'script_directory'): + self.script_directory = self.default_path + if not hasattr(self, 'script_filename'): + self.script_filename = self.default_script_filename + self.test_distribution.script_name = os.path.join( + self.script_directory, self.script_filename) + + changelog_filename = version.changelog_filename + if hasattr(self, 'changelog_filename'): + changelog_filename = self.changelog_filename + + self.expected_result = os.path.join( + self.script_directory, changelog_filename) + + def test_returns_expected_result(self): + """ Should return expected result. """ + args = { + 'distribution': self.test_distribution, + } + if hasattr(self, 'changelog_filename'): + args.update({'filename': self.changelog_filename}) + result = version.get_changelog_path(**args) + self.assertEqual(self.expected_result, result) + + +class WriteVersionInfoCommand_BaseTestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Base class for ‘WriteVersionInfoCommand’ test case classes. """ + + def setUp(self): + """ Set up test fixtures. """ + super(WriteVersionInfoCommand_BaseTestCase, self).setUp() + + fake_distribution_name = self.getUniqueString() + + self.test_distribution = distutils.dist.Distribution() + self.test_distribution.metadata.name = fake_distribution_name + + +class WriteVersionInfoCommand_TestCase(WriteVersionInfoCommand_BaseTestCase): + """ Test cases for ‘WriteVersionInfoCommand’ class. """ + + def test_subclass_of_distutils_command(self): + """ Should be a subclass of ‘distutils.cmd.Command’. """ + instance = version.WriteVersionInfoCommand(self.test_distribution) + self.assertIsInstance(instance, distutils.cmd.Command) + + +class WriteVersionInfoCommand_user_options_TestCase( + WriteVersionInfoCommand_BaseTestCase): + """ Test cases for ‘WriteVersionInfoCommand.user_options’ attribute. """ + + def setUp(self): + """ Set up test fixtures. """ + super(WriteVersionInfoCommand_user_options_TestCase, self).setUp() + + self.test_instance = version.WriteVersionInfoCommand( + self.test_distribution) + self.commandline_parser = distutils.fancy_getopt.FancyGetopt( + self.test_instance.user_options) + + def test_parses_correctly_as_fancy_getopt(self): + """ Should parse correctly in ‘FancyGetopt’. """ + self.assertIsInstance( + self.commandline_parser, distutils.fancy_getopt.FancyGetopt) + + def test_includes_base_class_user_options(self): + """ Should include base class's user_options. """ + base_command = setuptools.command.egg_info.egg_info + expected_user_options = base_command.user_options + self.assertThat( + set(expected_user_options), + IsSubset(set(self.test_instance.user_options))) + + def test_has_option_changelog_path(self): + """ Should have a ‘changelog-path’ option. """ + expected_option_name = "changelog-path=" + result = self.commandline_parser.has_option(expected_option_name) + self.assertTrue(result) + + def test_has_option_outfile_path(self): + """ Should have a ‘outfile-path’ option. """ + expected_option_name = "outfile-path=" + result = self.commandline_parser.has_option(expected_option_name) + self.assertTrue(result) + + +class WriteVersionInfoCommand_initialize_options_TestCase( + WriteVersionInfoCommand_BaseTestCase): + """ Test cases for ‘WriteVersionInfoCommand.initialize_options’ method. """ + + def setUp(self): + """ Set up test fixtures. """ + super( + WriteVersionInfoCommand_initialize_options_TestCase, self + ).setUp() + + patcher_func_egg_info_initialize_options = mock.patch.object( + setuptools.command.egg_info.egg_info, "initialize_options") + patcher_func_egg_info_initialize_options.start() + self.addCleanup(patcher_func_egg_info_initialize_options.stop) + + def test_calls_base_class_method(self): + """ Should call base class's ‘initialize_options’ method. """ + instance = version.WriteVersionInfoCommand(self.test_distribution) + base_command_class = setuptools.command.egg_info.egg_info + base_command_class.initialize_options.assert_called_with() + + def test_sets_changelog_path_to_none(self): + """ Should set ‘changelog_path’ attribute to ``None``. """ + instance = version.WriteVersionInfoCommand(self.test_distribution) + self.assertIs(instance.changelog_path, None) + + def test_sets_outfile_path_to_none(self): + """ Should set ‘outfile_path’ attribute to ``None``. """ + instance = version.WriteVersionInfoCommand(self.test_distribution) + self.assertIs(instance.outfile_path, None) + + +class WriteVersionInfoCommand_finalize_options_TestCase( + WriteVersionInfoCommand_BaseTestCase): + """ Test cases for ‘WriteVersionInfoCommand.finalize_options’ method. """ + + def setUp(self): + """ Set up test fixtures. """ + super(WriteVersionInfoCommand_finalize_options_TestCase, self).setUp() + + self.test_instance = version.WriteVersionInfoCommand(self.test_distribution) + + patcher_func_egg_info_finalize_options = mock.patch.object( + setuptools.command.egg_info.egg_info, "finalize_options") + patcher_func_egg_info_finalize_options.start() + self.addCleanup(patcher_func_egg_info_finalize_options.stop) + + self.fake_script_dir = self.getUniqueString() + self.test_distribution.script_name = os.path.join( + self.fake_script_dir, self.getUniqueString()) + + self.fake_egg_dir = self.getUniqueString() + self.test_instance.egg_info = self.fake_egg_dir + + patcher_func_get_changelog_path = mock.patch.object( + version, "get_changelog_path") + patcher_func_get_changelog_path.start() + self.addCleanup(patcher_func_get_changelog_path.stop) + + self.fake_changelog_path = self.getUniqueString() + version.get_changelog_path.return_value = self.fake_changelog_path + + def test_calls_base_class_method(self): + """ Should call base class's ‘finalize_options’ method. """ + base_command_class = setuptools.command.egg_info.egg_info + self.test_instance.finalize_options() + base_command_class.finalize_options.assert_called_with() + + def test_sets_force_to_none(self): + """ Should set ‘force’ attribute to ``None``. """ + self.test_instance.finalize_options() + self.assertIs(self.test_instance.force, None) + + def test_sets_changelog_path_using_get_changelog_path(self): + """ Should set ‘changelog_path’ attribute if it was ``None``. """ + self.test_instance.changelog_path = None + self.test_instance.finalize_options() + expected_changelog_path = self.fake_changelog_path + self.assertEqual(expected_changelog_path, self.test_instance.changelog_path) + + def test_leaves_changelog_path_if_already_set(self): + """ Should leave ‘changelog_path’ attribute set. """ + prior_changelog_path = self.getUniqueString() + self.test_instance.changelog_path = prior_changelog_path + self.test_instance.finalize_options() + expected_changelog_path = prior_changelog_path + self.assertEqual(expected_changelog_path, self.test_instance.changelog_path) + + def test_sets_outfile_path_to_default(self): + """ Should set ‘outfile_path’ attribute to default value. """ + fake_version_info_filename = self.getUniqueString() + with mock.patch.object( + version, "version_info_filename", + new=fake_version_info_filename): + self.test_instance.finalize_options() + expected_outfile_path = os.path.join( + self.fake_egg_dir, fake_version_info_filename) + self.assertEqual(expected_outfile_path, self.test_instance.outfile_path) + + def test_leaves_outfile_path_if_already_set(self): + """ Should leave ‘outfile_path’ attribute set. """ + prior_outfile_path = self.getUniqueString() + self.test_instance.outfile_path = prior_outfile_path + self.test_instance.finalize_options() + expected_outfile_path = prior_outfile_path + self.assertEqual(expected_outfile_path, self.test_instance.outfile_path) + + +class has_changelog_TestCase( + testscenarios.WithScenarios, testtools.TestCase): + """ Test cases for ‘has_changelog’ function. """ + + fake_os_path_exists_side_effects = { + 'true': (lambda path: True), + 'false': (lambda path: False), + } + + scenarios = [ + ('no changelog path', { + 'changelog_path': None, + 'expected_result': False, + }), + ('changelog exists', { + 'os_path_exists_scenario': 'true', + 'expected_result': True, + }), + ('changelog not found', { + 'os_path_exists_scenario': 'false', + 'expected_result': False, + }), + ] + + def setUp(self): + """ Set up test fixtures. """ + super(has_changelog_TestCase, self).setUp() + + self.test_distribution = distutils.dist.Distribution() + self.test_command = version.EggInfoCommand( + self.test_distribution) + + patcher_func_get_changelog_path = mock.patch.object( + version, "get_changelog_path") + patcher_func_get_changelog_path.start() + self.addCleanup(patcher_func_get_changelog_path.stop) + + self.fake_changelog_file_path = self.getUniqueString() + if hasattr(self, 'changelog_path'): + self.fake_changelog_file_path = self.changelog_path + version.get_changelog_path.return_value = self.fake_changelog_file_path + self.fake_changelog_file = StringIO() + + def fake_os_path_exists(path): + if path == self.fake_changelog_file_path: + side_effect = self.fake_os_path_exists_side_effects[ + self.os_path_exists_scenario] + if callable(side_effect): + result = side_effect(path) + else: + raise side_effect + else: + result = False + return result + + func_patcher_os_path_exists = mock.patch.object( + os.path, "exists") + func_patcher_os_path_exists.start() + self.addCleanup(func_patcher_os_path_exists.stop) + os.path.exists.side_effect = fake_os_path_exists + + def test_gets_changelog_path_from_distribution(self): + """ Should call ‘get_changelog_path’ with distribution. """ + result = version.has_changelog(self.test_command) + version.get_changelog_path.assert_called_with( + self.test_distribution) + + def test_returns_expected_result(self): + """ Should be a subclass of ‘distutils.cmd.Command’. """ + result = version.has_changelog(self.test_command) + self.assertEqual(self.expected_result, result) + + +@mock.patch.object(version, 'generate_version_info_from_changelog') +@mock.patch.object(version, 'serialise_version_info_from_mapping') +@mock.patch.object(version.EggInfoCommand, "write_file") +class WriteVersionInfoCommand_run_TestCase( + WriteVersionInfoCommand_BaseTestCase): + """ Test cases for ‘WriteVersionInfoCommand.run’ method. """ + + def setUp(self): + """ Set up test fixtures. """ + super(WriteVersionInfoCommand_run_TestCase, self).setUp() + + self.test_instance = version.WriteVersionInfoCommand( + self.test_distribution) + + self.fake_changelog_path = self.getUniqueString() + self.test_instance.changelog_path = self.fake_changelog_path + + self.fake_outfile_path = self.getUniqueString() + self.test_instance.outfile_path = self.fake_outfile_path + + def test_returns_none( + self, + mock_func_egg_info_write_file, + mock_func_serialise_version_info, + mock_func_generate_version_info): + """ Should return ``None``. """ + result = self.test_instance.run() + self.assertIs(result, None) + + def test_generates_version_info_from_changelog( + self, + mock_func_egg_info_write_file, + mock_func_serialise_version_info, + mock_func_generate_version_info): + """ Should generate version info from specified changelog. """ + self.test_instance.run() + expected_changelog_path = self.test_instance.changelog_path + mock_func_generate_version_info.assert_called_with( + expected_changelog_path) + + def test_serialises_version_info_from_mapping( + self, + mock_func_egg_info_write_file, + mock_func_serialise_version_info, + mock_func_generate_version_info): + """ Should serialise version info from specified mapping. """ + self.test_instance.run() + expected_version_info = mock_func_generate_version_info.return_value + mock_func_serialise_version_info.assert_called_with( + expected_version_info) + + def test_writes_file_using_command_context( + self, + mock_func_egg_info_write_file, + mock_func_serialise_version_info, + mock_func_generate_version_info): + """ Should write the metadata file using the command context. """ + self.test_instance.run() + expected_content = mock_func_serialise_version_info.return_value + mock_func_egg_info_write_file.assert_called_with( + "version info", self.fake_outfile_path, expected_content) + + +IsSubset = testtools.matchers.MatchesPredicateWithParams( + set.issubset, "{0} should be a subset of {1}") + +class EggInfoCommand_TestCase(testtools.TestCase): + """ Test cases for ‘EggInfoCommand’ class. """ + + def setUp(self): + """ Set up test fixtures. """ + super(EggInfoCommand_TestCase, self).setUp() + + self.test_distribution = distutils.dist.Distribution() + self.test_instance = version.EggInfoCommand(self.test_distribution) + + def test_subclass_of_setuptools_egg_info(self): + """ Should be a subclass of Setuptools ‘egg_info’. """ + self.assertIsInstance( + self.test_instance, setuptools.command.egg_info.egg_info) + + def test_sub_commands_include_base_class_sub_commands(self): + """ Should include base class's sub-commands in this sub_commands. """ + base_command = setuptools.command.egg_info.egg_info + expected_sub_commands = base_command.sub_commands + self.assertThat( + set(expected_sub_commands), + IsSubset(set(self.test_instance.sub_commands))) + + def test_sub_commands_includes_write_version_info_command(self): + """ Should include sub-command named ‘write_version_info’. """ + commands_by_name = dict(self.test_instance.sub_commands) + expected_predicate = version.has_changelog + expected_item = ('write_version_info', expected_predicate) + self.assertIn(expected_item, commands_by_name.items()) + + +@mock.patch.object(setuptools.command.egg_info.egg_info, "run") +class EggInfoCommand_run_TestCase(testtools.TestCase): + """ Test cases for ‘EggInfoCommand.run’ method. """ + + def setUp(self): + """ Set up test fixtures. """ + super(EggInfoCommand_run_TestCase, self).setUp() + + self.test_distribution = distutils.dist.Distribution() + self.test_instance = version.EggInfoCommand(self.test_distribution) + + base_command = setuptools.command.egg_info.egg_info + patcher_func_egg_info_get_sub_commands = mock.patch.object( + base_command, "get_sub_commands") + patcher_func_egg_info_get_sub_commands.start() + self.addCleanup(patcher_func_egg_info_get_sub_commands.stop) + + patcher_func_egg_info_run_command = mock.patch.object( + base_command, "run_command") + patcher_func_egg_info_run_command.start() + self.addCleanup(patcher_func_egg_info_run_command.stop) + + self.fake_sub_commands = ["spam", "eggs", "beans"] + base_command.get_sub_commands.return_value = self.fake_sub_commands + + def test_returns_none(self, mock_func_egg_info_run): + """ Should return ``None``. """ + result = self.test_instance.run() + self.assertIs(result, None) + + def test_runs_each_command_in_sub_commands( + self, mock_func_egg_info_run): + """ Should run each command in ‘self.get_sub_commands()’. """ + base_command = setuptools.command.egg_info.egg_info + self.test_instance.run() + expected_calls = [mock.call(name) for name in self.fake_sub_commands] + base_command.run_command.assert_has_calls(expected_calls) + + def test_calls_base_class_run(self, mock_func_egg_info_run): + """ Should call base class's ‘run’ method. """ + result = self.test_instance.run() + mock_func_egg_info_run.assert_called_with() + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff -Nru python-daemon-1.5.5/version.py python-daemon-2.0.5/version.py --- python-daemon-1.5.5/version.py 1970-01-01 00:00:00.000000000 +0000 +++ python-daemon-2.0.5/version.py 2015-02-02 04:43:28.000000000 +0000 @@ -0,0 +1,547 @@ +# -*- coding: utf-8 -*- + +# version.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; version 3 of that license or any later version. +# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details. + +""" Version information unified for human- and machine-readable formats. + + The project ‘ChangeLog’ file is a reStructuredText document, with + each section describing a version of the project. The document is + intended to be readable as-is by end users. + + This module handles transformation from the ‘ChangeLog’ to a + mapping of version information, serialised as JSON. It also + provides functionality for Distutils to use this information. + + Requires: + + * Docutils + * JSON + + """ + +from __future__ import (absolute_import, unicode_literals) + +import sys +import os +import io +import errno +import json +import datetime +import textwrap +import re +import functools +import collections +import distutils +import distutils.errors +import distutils.cmd +try: + # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text). + basestring = basestring + unicode = unicode +except NameError: + # Python 3 names the Unicode data type ‘str’. + basestring = str + unicode = str + +import setuptools +import setuptools.command.egg_info + + +def ensure_class_bases_begin_with(namespace, class_name, base_class): + """ Ensure the named class's bases start with the base class. + + :param namespace: The namespace containing the class name. + :param class_name: The name of the class to alter. + :param base_class: The type to be the first base class for the + newly created type. + :return: ``None``. + + This function is a hack to circumvent a circular dependency: + using classes from a module which is not installed at the time + this module is imported. + + Call this function after ensuring `base_class` is available, + before using the class named by `class_name`. + + """ + existing_class = namespace[class_name] + assert isinstance(existing_class, type) + + bases = list(existing_class.__bases__) + if base_class is bases[0]: + # Already bound to a type with the right bases. + return + bases.insert(0, base_class) + + new_class_namespace = existing_class.__dict__.copy() + # Type creation will assign the correct ‘__dict__’ attribute. + del new_class_namespace['__dict__'] + + metaclass = existing_class.__metaclass__ + new_class = metaclass(class_name, tuple(bases), new_class_namespace) + + namespace[class_name] = new_class + + +class VersionInfoWriter(object): + """ Docutils writer to produce a version info JSON data stream. """ + + # This class needs its base class to be a class from `docutils`. + # But that would create a circular dependency: Setuptools cannot + # ensure `docutils` is available before importing this module. + # + # Use `ensure_class_bases_begin_with` after importing `docutils`, to + # re-bind the `VersionInfoWriter` name to a new type that inherits + # from `docutils.writers.Writer`. + + __metaclass__ = type + + supported = ['version_info'] + """ Formats this writer supports. """ + + def __init__(self): + super(VersionInfoWriter, self).__init__() + self.translator_class = VersionInfoTranslator + + def translate(self): + visitor = self.translator_class(self.document) + self.document.walkabout(visitor) + self.output = visitor.astext() + + +rfc822_person_regex = re.compile( + "^(?P[^<]+) <(?P[^>]+)>$") + +class ChangeLogEntry: + """ An individual entry from the ‘ChangeLog’ document. """ + + __metaclass__ = type + + field_names = [ + 'release_date', + 'version', + 'maintainer', + 'body', + ] + + date_format = "%Y-%m-%d" + default_version = "UNKNOWN" + default_release_date = "UNKNOWN" + + def __init__( + self, + release_date=default_release_date, version=default_version, + maintainer=None, body=None): + self.validate_release_date(release_date) + self.release_date = release_date + + self.version = version + + self.validate_maintainer(maintainer) + self.maintainer = maintainer + self.body = body + + @classmethod + def validate_release_date(cls, value): + """ Validate the `release_date` value. + + :param value: The prospective `release_date` value. + :return: ``None`` if the value is valid. + :raises ValueError: If the value is invalid. + + """ + if value in ["UNKNOWN", "FUTURE"]: + # A valid non-date value. + return None + + # Raises `ValueError` if parse fails. + datetime.datetime.strptime(value, ChangeLogEntry.date_format) + + @classmethod + def validate_maintainer(cls, value): + """ Validate the `maintainer` value. + + :param value: The prospective `maintainer` value. + :return: ``None`` if the value is valid. + :raises ValueError: If the value is invalid. + + """ + valid = False + + if value is None: + valid = True + elif rfc822_person_regex.search(value): + valid = True + + if not valid: + raise ValueError("Not a valid person specification {value!r}") + else: + return None + + @classmethod + def make_ordered_dict(cls, fields): + """ Make an ordered dict of the fields. """ + result = collections.OrderedDict( + (name, fields[name]) + for name in cls.field_names) + return result + + def as_version_info_entry(self): + """ Format the changelog entry as a version info entry. """ + fields = vars(self) + entry = self.make_ordered_dict(fields) + + return entry + + +class InvalidFormatError(ValueError): + """ Raised when the document is not a valid ‘ChangeLog’ document. """ + + +class VersionInfoTranslator(object): + """ Translator from document nodes to a version info stream. """ + + # This class needs its base class to be a class from `docutils`. + # But that would create a circular dependency: Setuptools cannot + # ensure `docutils` is available before importing this module. + # + # Use `ensure_class_bases_begin_with` after importing `docutils`, + # to re-bind the `VersionInfoTranslator` name to a new type that + # inherits from `docutils.nodes.SparseNodeVisitor`. + + __metaclass__ = type + + wrap_width = 78 + bullet_text = "* " + + attr_convert_funcs_by_attr_name = { + 'released': ('release_date', unicode), + 'version': ('version', unicode), + 'maintainer': ('maintainer', unicode), + } + + def __init__(self, document): + super(VersionInfoTranslator, self).__init__(document) + self.settings = document.settings + self.current_section_level = 0 + self.current_field_name = None + self.content = [] + self.indent_width = 0 + self.initial_indent = "" + self.subsequent_indent = "" + self.current_entry = None + + # Docutils is not available when this class is defined. + # Get the `docutils` module dynamically. + self._docutils = sys.modules['docutils'] + + def astext(self): + """ Return the translated document as text. """ + text = json.dumps(self.content, indent=4) + return text + + def append_to_current_entry(self, text): + if self.current_entry is not None: + if self.current_entry.body is not None: + self.current_entry.body += text + + def visit_Text(self, node): + raw_text = node.astext() + text = textwrap.fill( + raw_text, + width=self.wrap_width, + initial_indent=self.initial_indent, + subsequent_indent=self.subsequent_indent) + self.append_to_current_entry(text) + + def depart_Text(self, node): + pass + + def visit_comment(self, node): + raise self._docutils.nodes.SkipNode + + def visit_field_body(self, node): + field_list_node = node.parent.parent + if not isinstance(field_list_node, self._docutils.nodes.field_list): + raise InvalidFormatError( + "Unexpected field within {node!r}".format( + node=field_list_node)) + (attr_name, convert_func) = self.attr_convert_funcs_by_attr_name[ + self.current_field_name] + attr_value = convert_func(node.astext()) + setattr(self.current_entry, attr_name, attr_value) + + def depart_field_body(self, node): + pass + + def visit_field_list(self, node): + pass + + def depart_field_list(self, node): + self.current_field_name = None + self.current_entry.body = "" + + def visit_field_name(self, node): + field_name = node.astext() + if self.current_section_level == 1: + # At a top-level section. + if field_name.lower() not in ["released", "maintainer"]: + raise InvalidFormatError( + "Unexpected field name {name!r}".format(name=field_name)) + self.current_field_name = field_name.lower() + + def depart_field_name(self, node): + pass + + def visit_bullet_list(self, node): + self.current_context = [] + + def depart_bullet_list(self, node): + self.current_entry.changes = self.current_context + self.current_context = None + + def adjust_indent_width(self, delta): + self.indent_width += delta + self.subsequent_indent = " " * self.indent_width + self.initial_indent = self.subsequent_indent + + def visit_list_item(self, node): + indent_delta = +len(self.bullet_text) + self.adjust_indent_width(indent_delta) + self.initial_indent = self.subsequent_indent[:-indent_delta] + self.append_to_current_entry(self.initial_indent + self.bullet_text) + + def depart_list_item(self, node): + indent_delta = +len(self.bullet_text) + self.adjust_indent_width(-indent_delta) + self.append_to_current_entry("\n") + + def visit_section(self, node): + self.current_section_level += 1 + if self.current_section_level == 1: + # At a top-level section. + self.current_entry = ChangeLogEntry() + else: + raise InvalidFormatError( + "Subsections not implemented for this writer") + + def depart_section(self, node): + self.current_section_level -= 1 + self.content.append( + self.current_entry.as_version_info_entry()) + self.current_entry = None + + _expected_title_word_length = len("Version FOO".split(" ")) + + def depart_title(self, node): + title_text = node.astext() + # At a top-level section. + words = title_text.split(" ") + version = None + if len(words) != self._expected_title_word_length: + raise InvalidFormatError( + "Unexpected title text {text!r}".format(text=title_text)) + if words[0].lower() not in ["version"]: + raise InvalidFormatError( + "Unexpected title text {text!r}".format(text=title_text)) + version = words[-1] + self.current_entry.version = version + + +def changelog_to_version_info_collection(infile): + """ Render the ‘ChangeLog’ document to a version info collection. + + :param infile: A file-like object containing the changelog. + :return: The serialised JSON data of the version info collection. + + """ + + # Docutils is not available when Setuptools needs this module, so + # delay the imports to this function instead. + import docutils.core + import docutils.nodes + import docutils.writers + + ensure_class_bases_begin_with( + globals(), str('VersionInfoWriter'), docutils.writers.Writer) + ensure_class_bases_begin_with( + globals(), str('VersionInfoTranslator'), + docutils.nodes.SparseNodeVisitor) + + writer = VersionInfoWriter() + settings_overrides = { + 'doctitle_xform': False, + } + version_info_json = docutils.core.publish_string( + infile.read(), writer=writer, + settings_overrides=settings_overrides) + + return version_info_json + + +try: + lru_cache = functools.lru_cache +except AttributeError: + # Python < 3.2 does not have the `functools.lru_cache` function. + # Not essential, so replace it with a no-op. + lru_cache = lambda maxsize=None, typed=False: lambda func: func + + +@lru_cache(maxsize=128) +def generate_version_info_from_changelog(infile_path): + """ Get the version info for the latest version in the changelog. + + :param infile_path: Filesystem path to the input changelog file. + :return: The generated version info mapping; or ``None`` if the + file cannot be read. + + The document is explicitly opened as UTF-8 encoded text. + + """ + version_info = collections.OrderedDict() + + versions_all_json = None + try: + with io.open(infile_path, 'rt', encoding="utf-8") as infile: + versions_all_json = changelog_to_version_info_collection(infile) + except EnvironmentError: + # If we can't read the input file, leave the collection empty. + pass + + if versions_all_json is not None: + versions_all = json.loads(versions_all_json.decode('utf-8')) + version_info = get_latest_version(versions_all) + + return version_info + + +def get_latest_version(versions): + """ Get the latest version from a collection of changelog entries. + + :param versions: A collection of mappings for changelog entries. + :return: An ordered mapping of fields for the latest version, + if `versions` is non-empty; otherwise, an empty mapping. + + """ + version_info = collections.OrderedDict() + + versions_by_release_date = { + item['release_date']: item + for item in versions} + if versions_by_release_date: + latest_release_date = max(versions_by_release_date.keys()) + version_info = ChangeLogEntry.make_ordered_dict( + versions_by_release_date[latest_release_date]) + + return version_info + + +def serialise_version_info_from_mapping(version_info): + """ Generate the version info serialised data. + + :param version_info: Mapping of version info items. + :return: The version info serialised to JSON. + + """ + content = json.dumps(version_info, indent=4) + + return content + + +changelog_filename = "ChangeLog" + +def get_changelog_path(distribution, filename=changelog_filename): + """ Get the changelog file path for the distribution. + + :param distribution: The distutils.dist.Distribution instance. + :param filename: The base filename of the changelog document. + :return: Filesystem path of the changelog document, or ``None`` + if not discoverable. + + """ + setup_dirname = os.path.dirname(distribution.script_name) + filepath = os.path.join(setup_dirname, filename) + + return filepath + + +def has_changelog(command): + """ Return ``True`` iff the distribution's changelog file exists. """ + result = False + + changelog_path = get_changelog_path(command.distribution) + if changelog_path is not None: + if os.path.exists(changelog_path): + result = True + + return result + + +class EggInfoCommand(setuptools.command.egg_info.egg_info, object): + """ Custom ‘egg_info’ command for this distribution. """ + + sub_commands = ([ + ('write_version_info', has_changelog), + ] + setuptools.command.egg_info.egg_info.sub_commands) + + def run(self): + """ Execute this command. """ + super(EggInfoCommand, self).run() + + for command_name in self.get_sub_commands(): + self.run_command(command_name) + + +version_info_filename = "version_info.json" + +class WriteVersionInfoCommand(EggInfoCommand, object): + """ Setuptools command to serialise version info metadata. """ + + user_options = ([ + ("changelog-path=", None, + "Filesystem path to the changelog document."), + ("outfile-path=", None, + "Filesystem path to the version info file."), + ] + EggInfoCommand.user_options) + + def initialize_options(self): + """ Initialise command options to defaults. """ + super(WriteVersionInfoCommand, self).initialize_options() + self.changelog_path = None + self.outfile_path = None + + def finalize_options(self): + """ Finalise command options before execution. """ + self.set_undefined_options( + 'build', + ('force', 'force')) + + super(WriteVersionInfoCommand, self).finalize_options() + + if self.changelog_path is None: + self.changelog_path = get_changelog_path(self.distribution) + + if self.outfile_path is None: + egg_dir = self.egg_info + self.outfile_path = os.path.join(egg_dir, version_info_filename) + + def run(self): + """ Execute this command. """ + version_info = generate_version_info_from_changelog(self.changelog_path) + content = serialise_version_info_from_mapping(version_info) + self.write_file("version info", self.outfile_path, content) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python :