summaryrefslogtreecommitdiffstats
path: root/Lib/packaging/tests/support.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/packaging/tests/support.py')
-rw-r--r--Lib/packaging/tests/support.py400
1 files changed, 400 insertions, 0 deletions
diff --git a/Lib/packaging/tests/support.py b/Lib/packaging/tests/support.py
new file mode 100644
index 0000000..d76d3db
--- /dev/null
+++ b/Lib/packaging/tests/support.py
@@ -0,0 +1,400 @@
+"""Support code for packaging test cases.
+
+*This module should not be considered public: its content and API may
+change in incompatible ways.*
+
+A few helper classes are provided: LoggingCatcher, TempdirManager and
+EnvironRestorer. They are written to be used as mixins::
+
+ from packaging.tests import unittest
+ from packaging.tests.support import LoggingCatcher
+
+ class SomeTestCase(LoggingCatcher, unittest.TestCase):
+ ...
+
+If you need to define a setUp method on your test class, you have to
+call the mixin class' setUp method or it won't work (same thing for
+tearDown):
+
+ def setUp(self):
+ super(SomeTestCase, self).setUp()
+ ... # other setup code
+
+Also provided is a DummyCommand class, useful to mock commands in the
+tests of another command that needs them, for example to fake
+compilation in build_ext (this requires that the mock build_ext command
+be injected into the distribution object's command_obj dictionary).
+
+For tests that need to compile an extension module, use the
+copy_xxmodule_c and fixup_build_ext functions.
+
+Each class or function has a docstring to explain its purpose and usage.
+Existing tests should also be used as examples.
+"""
+
+import os
+import sys
+import shutil
+import logging
+import weakref
+import tempfile
+import sysconfig
+
+from packaging.dist import Distribution
+from packaging.util import resolve_name
+from packaging.command import set_command, _COMMANDS
+
+from packaging.tests import unittest
+from test.support import requires_zlib, unlink
+
+# define __all__ to make pydoc more useful
+__all__ = [
+ # TestCase mixins
+ 'LoggingCatcher', 'TempdirManager', 'EnvironRestorer',
+ # mocks
+ 'DummyCommand', 'TestDistribution', 'Inputs',
+ # misc. functions and decorators
+ 'fake_dec', 'create_distribution', 'use_command',
+ 'copy_xxmodule_c', 'fixup_build_ext',
+ 'skip_2to3_optimize',
+ # imported from this module for backport purposes
+ 'unittest', 'requires_zlib', 'skip_unless_symlink',
+]
+
+
+logger = logging.getLogger('packaging')
+logger2to3 = logging.getLogger('RefactoringTool')
+
+
+class _TestHandler(logging.handlers.BufferingHandler):
+ # stolen and adapted from test.support
+
+ def __init__(self):
+ super(_TestHandler, self).__init__(0)
+ self.setLevel(logging.DEBUG)
+
+ def shouldFlush(self):
+ return False
+
+ def emit(self, record):
+ self.buffer.append(record)
+
+
+class LoggingCatcher:
+ """TestCase-compatible mixin to receive logging calls.
+
+ Upon setUp, instances of this classes get a BufferingHandler that's
+ configured to record all messages logged to the 'packaging' logger.
+
+ Use get_logs to retrieve messages and self.loghandler.flush to discard
+ them. get_logs automatically flushes the logs, unless you pass
+ *flush=False*, for example to make multiple calls to the method with
+ different level arguments. If your test calls some code that generates
+ logging message and then you don't call get_logs, you will need to flush
+ manually before testing other code in the same test_* method, otherwise
+ get_logs in the next lines will see messages from the previous lines.
+ See example in test_command_check.
+ """
+
+ def setUp(self):
+ super(LoggingCatcher, self).setUp()
+ self.loghandler = handler = _TestHandler()
+ self._old_levels = logger.level, logger2to3.level
+ logger.addHandler(handler)
+ logger.setLevel(logging.DEBUG) # we want all messages
+ logger2to3.setLevel(logging.CRITICAL) # we don't want 2to3 messages
+
+ def tearDown(self):
+ handler = self.loghandler
+ # All this is necessary to properly shut down the logging system and
+ # avoid a regrtest complaint. Thanks to Vinay Sajip for the help.
+ handler.close()
+ logger.removeHandler(handler)
+ for ref in weakref.getweakrefs(handler):
+ logging._removeHandlerRef(ref)
+ del self.loghandler
+ logger.setLevel(self._old_levels[0])
+ logger2to3.setLevel(self._old_levels[1])
+ super(LoggingCatcher, self).tearDown()
+
+ def get_logs(self, level=logging.WARNING, flush=True):
+ """Return all log messages with given level.
+
+ *level* defaults to logging.WARNING.
+
+ For log calls with arguments (i.e. logger.info('bla bla %r', arg)),
+ the messages will be formatted before being returned (e.g. "bla bla
+ 'thing'").
+
+ Returns a list. Automatically flushes the loghandler after being
+ called, unless *flush* is False (this is useful to get e.g. all
+ warnings then all info messages).
+ """
+ messages = [log.getMessage() for log in self.loghandler.buffer
+ if log.levelno == level]
+ if flush:
+ self.loghandler.flush()
+ return messages
+
+
+class TempdirManager:
+ """TestCase-compatible mixin to create temporary directories and files.
+
+ Directories and files created in a test_* method will be removed after it
+ has run.
+ """
+
+ def setUp(self):
+ super(TempdirManager, self).setUp()
+ self._olddir = os.getcwd()
+ self._basetempdir = tempfile.mkdtemp()
+ self._files = []
+
+ def tearDown(self):
+ for handle, name in self._files:
+ handle.close()
+ unlink(name)
+
+ os.chdir(self._olddir)
+ shutil.rmtree(self._basetempdir)
+ super(TempdirManager, self).tearDown()
+
+ def mktempfile(self):
+ """Create a read-write temporary file and return it."""
+ fd, fn = tempfile.mkstemp(dir=self._basetempdir)
+ os.close(fd)
+ fp = open(fn, 'w+')
+ self._files.append((fp, fn))
+ return fp
+
+ def mkdtemp(self):
+ """Create a temporary directory and return its path."""
+ d = tempfile.mkdtemp(dir=self._basetempdir)
+ return d
+
+ def write_file(self, path, content='xxx', encoding=None):
+ """Write a file at the given path.
+
+ path can be a string, a tuple or a list; if it's a tuple or list,
+ os.path.join will be used to produce a path.
+ """
+ if isinstance(path, (list, tuple)):
+ path = os.path.join(*path)
+ with open(path, 'w', encoding=encoding) as f:
+ f.write(content)
+
+ def create_dist(self, **kw):
+ """Create a stub distribution object and files.
+
+ This function creates a Distribution instance (use keyword arguments
+ to customize it) and a temporary directory with a project structure
+ (currently an empty directory).
+
+ It returns the path to the directory and the Distribution instance.
+ You can use self.write_file to write any file in that
+ directory, e.g. setup scripts or Python modules.
+ """
+ if 'name' not in kw:
+ kw['name'] = 'foo'
+ tmp_dir = self.mkdtemp()
+ project_dir = os.path.join(tmp_dir, kw['name'])
+ os.mkdir(project_dir)
+ dist = Distribution(attrs=kw)
+ return project_dir, dist
+
+ def assertIsFile(self, *args):
+ path = os.path.join(*args)
+ dirname = os.path.dirname(path)
+ file = os.path.basename(path)
+ if os.path.isdir(dirname):
+ files = os.listdir(dirname)
+ msg = "%s not found in %s: %s" % (file, dirname, files)
+ assert os.path.isfile(path), msg
+ else:
+ raise AssertionError(
+ '%s not found. %s does not exist' % (file, dirname))
+
+ def assertIsNotFile(self, *args):
+ path = os.path.join(*args)
+ self.assertFalse(os.path.isfile(path), "%r exists" % path)
+
+
+class EnvironRestorer:
+ """TestCase-compatible mixin to restore or delete environment variables.
+
+ The variables to restore (or delete if they were not originally present)
+ must be explicitly listed in self.restore_environ. It's better to be
+ aware of what we're modifying instead of saving and restoring the whole
+ environment.
+ """
+
+ def setUp(self):
+ super(EnvironRestorer, self).setUp()
+ self._saved = []
+ self._added = []
+ for key in self.restore_environ:
+ if key in os.environ:
+ self._saved.append((key, os.environ[key]))
+ else:
+ self._added.append(key)
+
+ def tearDown(self):
+ for key, value in self._saved:
+ os.environ[key] = value
+ for key in self._added:
+ os.environ.pop(key, None)
+ super(EnvironRestorer, self).tearDown()
+
+
+class DummyCommand:
+ """Class to store options for retrieval via set_undefined_options().
+
+ Useful for mocking one dependency command in the tests for another
+ command, see e.g. the dummy build command in test_build_scripts.
+ """
+ # XXX does not work with dist.reinitialize_command, which typechecks
+ # and wants a finalized attribute
+
+ def __init__(self, **kwargs):
+ for kw, val in kwargs.items():
+ setattr(self, kw, val)
+
+ def ensure_finalized(self):
+ pass
+
+
+class TestDistribution(Distribution):
+ """Distribution subclasses that avoids the default search for
+ configuration files.
+
+ The ._config_files attribute must be set before
+ .parse_config_files() is called.
+ """
+
+ def find_config_files(self):
+ return self._config_files
+
+
+class Inputs:
+ """Fakes user inputs."""
+ # TODO document usage
+ # TODO use context manager or something for auto cleanup
+
+ def __init__(self, *answers):
+ self.answers = answers
+ self.index = 0
+
+ def __call__(self, prompt=''):
+ try:
+ return self.answers[self.index]
+ finally:
+ self.index += 1
+
+
+def create_distribution(configfiles=()):
+ """Prepares a distribution with given config files parsed."""
+ d = TestDistribution()
+ d.config.find_config_files = d.find_config_files
+ d._config_files = configfiles
+ d.parse_config_files()
+ d.parse_command_line()
+ return d
+
+
+def use_command(testcase, fullname):
+ """Register command at *fullname* for the duration of a test."""
+ set_command(fullname)
+ # XXX maybe set_command should return the class object
+ name = resolve_name(fullname).get_command_name()
+ # XXX maybe we need a public API to remove commands
+ testcase.addCleanup(_COMMANDS.__delitem__, name)
+
+
+def fake_dec(*args, **kw):
+ """Fake decorator"""
+ def _wrap(func):
+ def __wrap(*args, **kw):
+ return func(*args, **kw)
+ return __wrap
+ return _wrap
+
+
+def copy_xxmodule_c(directory):
+ """Helper for tests that need the xxmodule.c source file.
+
+ Example use:
+
+ def test_compile(self):
+ copy_xxmodule_c(self.tmpdir)
+ self.assertIn('xxmodule.c', os.listdir(self.tmpdir))
+
+ If the source file can be found, it will be copied to *directory*. If not,
+ the test will be skipped. Errors during copy are not caught.
+ """
+ filename = _get_xxmodule_path()
+ if filename is None:
+ raise unittest.SkipTest('cannot find xxmodule.c')
+ shutil.copy(filename, directory)
+
+
+def _get_xxmodule_path():
+ if sysconfig.is_python_build():
+ srcdir = sysconfig.get_config_var('projectbase')
+ path = os.path.join(os.getcwd(), srcdir, 'Modules', 'xxmodule.c')
+ else:
+ path = os.path.join(os.path.dirname(__file__), 'xxmodule.c')
+ if os.path.exists(path):
+ return path
+
+
+def fixup_build_ext(cmd):
+ """Function needed to make build_ext tests pass.
+
+ When Python was built with --enable-shared on Unix, -L. is not enough to
+ find libpython<blah>.so, because regrtest runs in a tempdir, not in the
+ source directory where the .so lives. (Mac OS X embeds absolute paths
+ to shared libraries into executables, so the fixup is a no-op on that
+ platform.)
+
+ When Python was built with in debug mode on Windows, build_ext commands
+ need their debug attribute set, and it is not done automatically for
+ some reason.
+
+ This function handles both of these things, and also fixes
+ cmd.distribution.include_dirs if the running Python is an uninstalled
+ build. Example use:
+
+ cmd = build_ext(dist)
+ support.fixup_build_ext(cmd)
+ cmd.ensure_finalized()
+ """
+ if os.name == 'nt':
+ cmd.debug = sys.executable.endswith('_d.exe')
+ elif sysconfig.get_config_var('Py_ENABLE_SHARED'):
+ # To further add to the shared builds fun on Unix, we can't just add
+ # library_dirs to the Extension() instance because that doesn't get
+ # plumbed through to the final compiler command.
+ runshared = sysconfig.get_config_var('RUNSHARED')
+ if runshared is None:
+ cmd.library_dirs = ['.']
+ else:
+ if sys.platform == 'darwin':
+ cmd.library_dirs = []
+ else:
+ name, equals, value = runshared.partition('=')
+ cmd.library_dirs = value.split(os.pathsep)
+
+ # Allow tests to run with an uninstalled Python
+ if sysconfig.is_python_build():
+ pysrcdir = sysconfig.get_config_var('projectbase')
+ cmd.distribution.include_dirs.append(os.path.join(pysrcdir, 'Include'))
+
+
+try:
+ from test.support import skip_unless_symlink
+except ImportError:
+ skip_unless_symlink = unittest.skip(
+ 'requires test.support.skip_unless_symlink')
+
+skip_2to3_optimize = unittest.skipIf(sys.flags.optimize,
+ "2to3 doesn't work under -O")