diff options
author | Jonathan Lange <jml@canonical.com> | 2012-07-22 10:29:08 +0100 |
---|---|---|
committer | Jonathan Lange <jml@canonical.com> | 2012-07-22 10:29:08 +0100 |
commit | 37eb13139dc4aa50cbe403075872a8ceb05be77f (patch) | |
tree | 59dfd8815e02aa3d066bc2e9b7d87968eb34bc2d | |
parent | 64bcc591df00bfe18e73098d381c75e2ef81e8d4 (diff) | |
parent | 3389f8257d6f1f31cc9e6cbe26852304d4871555 (diff) | |
download | fixtures-37eb13139dc4aa50cbe403075872a8ceb05be77f.tar.gz |
Add facility to make a tree of files on a TempDir (r=lifeless)
-rw-r--r-- | NEWS | 6 | ||||
-rw-r--r-- | README | 2 | ||||
-rw-r--r-- | lib/fixtures/_fixtures/tempdir.py | 87 | ||||
-rw-r--r-- | lib/fixtures/tests/_fixtures/test_tempdir.py | 157 | ||||
-rw-r--r-- | lib/fixtures/tests/_fixtures/test_temphomedir.py | 5 | ||||
-rw-r--r-- | lib/fixtures/tests/helpers.py | 21 |
6 files changed, 271 insertions, 7 deletions
@@ -6,6 +6,12 @@ fixtures release notes NEXT ~~~~ +CHANGES: + +* New method ``make_tree`` on ``TempDir``. Easily creates a structure of + files and directories underneath a temporary directory. + (Jonathan Lange) + 0.3.9 ~~~~~ @@ -28,7 +28,7 @@ Dependencies * Python 2.4+ This is the base language fixtures is written in and for. -* testtools <https://launchpad.net/testtools> 0.9.12 or newer. +* testtools <https://launchpad.net/testtools> 0.9.13 or newer. testtools provides helpful glue functions for the details API used to report information about a fixture (whether its used in a testing or production environment). diff --git a/lib/fixtures/_fixtures/tempdir.py b/lib/fixtures/_fixtures/tempdir.py index fd5502c..3af8d00 100644 --- a/lib/fixtures/_fixtures/tempdir.py +++ b/lib/fixtures/_fixtures/tempdir.py @@ -18,6 +18,8 @@ __all__ = [ 'TempDir', ] +import errno +import os import shutil import tempfile @@ -43,6 +45,20 @@ class TempDir(fixtures.Fixture): self.path = tempfile.mkdtemp(dir=self.rootdir) self.addCleanup(shutil.rmtree, self.path, ignore_errors=True) + def make_tree(self, *shape): + """Make a tree of files and directories underneath this temp dir. + + :param shape: A list of descriptions of files and directories to make. + Generally directories are described as ``"directory/"`` and + files are described as ``("filename", contents)``. Filenames can + also be specified without contents, in which case we'll make + something up. + + Directories can also be specified as ``(directory, None)`` or + ``(directory,)``. + """ + create_normal_shape(self.path, normalize_shape(shape)) + class NestedTempfile(fixtures.Fixture): """Nest all temporary files and directories inside another directory. @@ -58,3 +74,74 @@ class NestedTempfile(fixtures.Fixture): tempdir = self.useFixture(TempDir()).path patch = fixtures.MonkeyPatch("tempfile.tempdir", tempdir) self.useFixture(patch) + + +def normalize_entry(entry): + """Normalize a file shape entry. + + 'Normal' entries are either ("file", "content") or ("directory/", None). + + Standalone strings get turned into 2-tuples, with files getting made-up + contents. Singletons are treated the same. + + If something that looks like a file has no content, or something that + looks like a directory has content, we raise an error, as we don't know + whether the developer really intends a file or really intends a directory. + + :return: A list of 2-tuples containing paths and contents. + """ + if isinstance(entry, basestring): + if entry[-1] == '/': + return (entry, None) + else: + return (entry, "The file '%s'." % (entry,)) + else: + if len(entry) == 1: + return normalize_entry(entry[0]) + elif len(entry) == 2: + name, content = entry + is_dir = (name[-1] == '/') + if ((is_dir and content is not None) + or (not is_dir and content is None)): + raise ValueError( + "Directories must end with '/' and have no content, " + "files do not end with '/' and must have content, got %r" + % (entry,)) + return entry + else: + raise ValueError( + "Invalid file or directory description: %r" % (entry,)) + + +def normalize_shape(shape): + """Normalize a shape of a file tree to create. + + Normalizes each entry and returns a sorted list of entries. + """ + return sorted(map(normalize_entry, shape)) + + +def create_normal_shape(base_directory, shape): + """Create a file tree from 'shape' in 'base_directory'. + + 'shape' must be a list of 2-tuples of (name, contents). If name ends with + '/', then contents must be None, as it will be created as a directory. + Otherwise, contents must be provided. + + If either a file or directory is specified but the parent directory + doesn't exist, will create the parent directory. + """ + for name, contents in shape: + name = os.path.join(base_directory, name) + if name[-1] == '/': + os.makedirs(name) + else: + base_dir = os.path.dirname(name) + try: + os.makedirs(base_dir) + except OSError, e: + if e.errno != errno.EEXIST: + raise + f = open(name, 'w') + f.write(contents) + f.close() diff --git a/lib/fixtures/tests/_fixtures/test_tempdir.py b/lib/fixtures/tests/_fixtures/test_tempdir.py index 1e42257..c253a68 100644 --- a/lib/fixtures/tests/_fixtures/test_tempdir.py +++ b/lib/fixtures/tests/_fixtures/test_tempdir.py @@ -1,6 +1,6 @@ # fixtures: Fixtures with cleanups for testing and convenience. # -# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# Copyright (c) 2010, 2012 Robert Collins <robertc@robertcollins.net> # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the @@ -17,19 +17,30 @@ import os import tempfile import testtools -from testtools.matchers import StartsWith +from testtools.matchers import ( + DirContains, + DirExists, + FileContains, + StartsWith, + ) from fixtures import ( NestedTempfile, TempDir, ) +from fixtures._fixtures.tempdir import ( + create_normal_shape, + normalize_entry, + normalize_shape, + ) +from fixtures.tests.helpers import HasNoAttribute + class TestTempDir(testtools.TestCase): def test_basic(self): fixture = TempDir() - sentinel = object() - self.assertEqual(sentinel, getattr(fixture, 'path', sentinel)) + self.assertThat(fixture, HasNoAttribute('path')) fixture.setUp() try: path = fixture.path @@ -72,3 +83,141 @@ class NestedTempfileTest(testtools.TestCase): raise ContrivedException except ContrivedException: self.assertFalse(os.path.isdir(nested_tempdir)) + + +class TestFileTree(testtools.TestCase): + + def test_out_of_order(self): + # If a file or a subdirectory is listed before its parent directory, + # that doesn't matter. We'll create the directory first. + fixture = TempDir() + with fixture: + fixture.make_tree('a/b/', 'a/') + path = fixture.path + self.assertThat(path, DirContains(['a'])) + self.assertThat(os.path.join(path, 'a'), DirContains(['b'])) + self.assertThat(os.path.join(path, 'a', 'b'), DirExists()) + + def test_not_even_creating_parents(self): + fixture = TempDir() + with fixture: + fixture.make_tree('a/b/foo.txt', 'c/d/e/') + path = fixture.path + self.assertThat( + os.path.join(path, 'a', 'b', 'foo.txt'), + FileContains("The file 'a/b/foo.txt'.")) + self.assertThat(os.path.join(path, 'c', 'd', 'e'), DirExists()) + + +class TestNormalizeEntry(testtools.TestCase): + + def test_file_as_tuple(self): + # A tuple of filenames and contents is already normalized. + entry = normalize_entry(('foo', 'foo contents')) + self.assertEqual(('foo', 'foo contents'), entry) + + def test_directories_as_tuples(self): + # A tuple of directory name and None is already normalized. + directory = normalize_entry(('foo/', None)) + self.assertEqual(('foo/', None), directory) + + def test_directories_as_singletons(self): + # A singleton tuple of directory name is normalized to a 2-tuple of + # the directory name and None. + directory = normalize_entry(('foo/',)) + self.assertEqual(('foo/', None), directory) + + def test_directories_as_strings(self): + # If directories are just given as strings, then they are normalized + # to tuples of directory names and None. + directory = normalize_entry('foo/') + self.assertEqual(('foo/', None), directory) + + def test_directories_with_content(self): + # If we're given a directory with content, we raise an error, since + # it's ambiguous and we don't want to guess. + bad_entry = ('dir/', "stuff") + e = self.assertRaises(ValueError, normalize_entry, bad_entry) + self.assertEqual( + "Directories must end with '/' and have no content, files do not " + "end with '/' and must have content, got %r" % (bad_entry,), + str(e)) + + def test_filenames_as_strings(self): + # If file names are just given as strings, then they are normalized to + # tuples of filenames and made-up contents. + entry = normalize_entry('foo') + self.assertEqual(('foo', "The file 'foo'."), entry) + + def test_filenames_as_singletons(self): + # A singleton tuple of a filename is normalized to a 2-tuple of + # the file name and made-up contents. + entry = normalize_entry(('foo',)) + self.assertEqual(('foo', "The file 'foo'."), entry) + + def test_filenames_without_content(self): + # If we're given a filename without content, we raise an error, since + # it's ambiguous and we don't want to guess. + bad_entry = ('filename', None) + e = self.assertRaises(ValueError, normalize_entry, bad_entry) + self.assertEqual( + "Directories must end with '/' and have no content, files do not " + "end with '/' and must have content, got %r" % (bad_entry,), + str(e)) + + def test_too_long_tuple(self): + bad_entry = ('foo', 'bar', 'baz') + e = self.assertRaises(ValueError, normalize_entry, bad_entry) + self.assertEqual( + "Invalid file or directory description: %r" % (bad_entry,), + str(e)) + + +class TestNormalizeShape(testtools.TestCase): + + def test_empty(self): + # The normal form of an empty list is the empty list. + empty = normalize_shape([]) + self.assertEqual([], empty) + + def test_sorts_entries(self): + # The normal form a list of entries is the sorted list of normal + # entries. + entries = normalize_shape(['a/b/', 'a/']) + self.assertEqual([('a/', None), ('a/b/', None)], entries) + + +class TestCreateNormalShape(testtools.TestCase): + + def test_empty(self): + tempdir = self.useFixture(TempDir()).path + create_normal_shape(tempdir, []) + self.assertThat(tempdir, DirContains([])) + + def test_creates_files(self): + # When given a list of file specifications, it creates those files + # underneath the temporary directory. + path = self.useFixture(TempDir()).path + create_normal_shape(path, [('a', 'foo'), ('b', 'bar')]) + self.assertThat(path, DirContains(['a', 'b'])) + self.assertThat(os.path.join(path, 'a'), FileContains('foo')) + self.assertThat(os.path.join(path, 'b'), FileContains('bar')) + + def test_creates_directories(self): + # When given directory specifications, it creates those directories. + path = self.useFixture(TempDir()).path + create_normal_shape(path, [('a/', None), ('b/', None)]) + self.assertThat(path, DirContains(['a', 'b'])) + self.assertThat(os.path.join(path, 'a'), DirExists()) + self.assertThat(os.path.join(path, 'b'), DirExists()) + + def test_creates_parent_directories(self): + # If the parents of a file or directory don't exist, they get created + # too. + path = self.useFixture(TempDir()).path + create_normal_shape(path, [('a/b/', None), ('c/d.txt', 'text')]) + self.assertThat(path, DirContains(['a', 'c'])) + self.assertThat(os.path.join(path, 'a'), DirContains('b')) + self.assertThat(os.path.join(path, 'a', 'b'), DirExists()) + self.assertThat(os.path.join(path, 'c'), DirExists()) + self.assertThat(os.path.join(path, 'c', 'd.txt'), FileContains('text')) diff --git a/lib/fixtures/tests/_fixtures/test_temphomedir.py b/lib/fixtures/tests/_fixtures/test_temphomedir.py index 339ce2c..028ebbe 100644 --- a/lib/fixtures/tests/_fixtures/test_temphomedir.py +++ b/lib/fixtures/tests/_fixtures/test_temphomedir.py @@ -22,13 +22,14 @@ from fixtures import ( TempDir, TempHomeDir, ) +from fixtures.tests.helpers import HasNoAttribute + class TestTempDir(testtools.TestCase): def test_basic(self): fixture = TempHomeDir() - sentinel = object() - self.assertEqual(sentinel, getattr(fixture, 'path', sentinel)) + self.assertThat(fixture, HasNoAttribute('path')) fixture.setUp() try: path = fixture.path diff --git a/lib/fixtures/tests/helpers.py b/lib/fixtures/tests/helpers.py index ae0d8d3..6ae07cc 100644 --- a/lib/fixtures/tests/helpers.py +++ b/lib/fixtures/tests/helpers.py @@ -15,6 +15,9 @@ import fixtures +from testtools.matchers import Mismatch + + class LoggingFixture(fixtures.Fixture): def __init__(self, suffix='', calls=None): @@ -31,3 +34,21 @@ class LoggingFixture(fixtures.Fixture): def reset(self): self.calls.append('reset' + self.suffix) + + +class HasNoAttribute(object): + """For asserting that an object does not have a particular attribute.""" + + def __init__(self, attr_name): + self._attr_name = attr_name + + def __str__(self): + return 'HasNoAttribute(%s)' % (self._attr_name,) + + def match(self, obj): + sentinel = object() + value = getattr(obj, self._attr_name, sentinel) + if value is not sentinel: + return Mismatch( + "%s is an attribute of %r: %r" % ( + self._attr_name, obj, value)) |