summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Lange <jml@canonical.com>2012-07-22 10:29:08 +0100
committerJonathan Lange <jml@canonical.com>2012-07-22 10:29:08 +0100
commit37eb13139dc4aa50cbe403075872a8ceb05be77f (patch)
tree59dfd8815e02aa3d066bc2e9b7d87968eb34bc2d
parent64bcc591df00bfe18e73098d381c75e2ef81e8d4 (diff)
parent3389f8257d6f1f31cc9e6cbe26852304d4871555 (diff)
downloadfixtures-37eb13139dc4aa50cbe403075872a8ceb05be77f.tar.gz
Add facility to make a tree of files on a TempDir (r=lifeless)
-rw-r--r--NEWS6
-rw-r--r--README2
-rw-r--r--lib/fixtures/_fixtures/tempdir.py87
-rw-r--r--lib/fixtures/tests/_fixtures/test_tempdir.py157
-rw-r--r--lib/fixtures/tests/_fixtures/test_temphomedir.py5
-rw-r--r--lib/fixtures/tests/helpers.py21
6 files changed, 271 insertions, 7 deletions
diff --git a/NEWS b/NEWS
index 7338b62..4d18674 100644
--- a/NEWS
+++ b/NEWS
@@ -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
~~~~~
diff --git a/README b/README
index be3a589..a3ceda1 100644
--- a/README
+++ b/README
@@ -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))