├── .gitignore ├── CHANGES.rst ├── HACKING.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── TODO.rst ├── archive ├── __init__.py └── compat.py ├── setup.py ├── tests ├── files │ ├── bad │ │ ├── absolute.tar.gz │ │ ├── relative.tar.gz │ │ └── unrecognized.txt │ ├── foobar.tar │ ├── foobar.tar.bz2 │ ├── foobar.tar.gz │ ├── foobar.zip │ ├── foobar_tar_gz │ ├── 圧縮.tgz │ └── 圧縮.zip └── test_archive.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .tox/ 3 | build/ 4 | dist/ 5 | MANIFEST 6 | *.swp 7 | *.pyc 8 | .project 9 | .pydevproject 10 | .settings 11 | .coverage 12 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 6 | In Development 7 | ============== 8 | * Moved tests outside of the archive package directory. 9 | 10 | 11 | Version 0.2 - 2012-07-12 12 | ======================== 13 | * Added Python 3 compatibility. 14 | * Added support for passing file-like objects instead of just a file path. 15 | * Added the ability to override the file-type guess performed using a file's 16 | extension. 17 | * Added security check when extracting to ensure a file will not get extracted 18 | outside of the target directory. This is the new default behavior, though 19 | the check can be disabled if needed. 20 | 21 | 22 | Version 0.1 - 2010-07-27 23 | ======================== 24 | * Initial release. 25 | * Support for extracting zip and tar files. 26 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Hacking 3 | ======= 4 | 5 | 6 | Running tests 7 | ============= 8 | 9 | Install a virtualenv:: 10 | 11 | virtualenv myenv 12 | 13 | Activate virtualenv:: 14 | 15 | source myenv/bin/activate 16 | 17 | Install tox:: 18 | 19 | pip install tox 20 | 21 | To run all tests, you'll need python2.6, python2.7, and python3.2 installed. 22 | With one or more of these Python versions installed, you should be able to run 23 | some tests:: 24 | 25 | cd python-archive 26 | tox 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Gary Wilson Jr. and contributors. 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include *.ini 3 | include *.rst 4 | include *.txt 5 | recursive-include tests *.py 6 | recursive-include tests/files * 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | python-archive 3 | ============== 4 | 5 | This package provides a simple, pure-Python interface for handling various 6 | archive file formats. Currently, archive extraction is the only supported 7 | action. Supported file formats include: 8 | 9 | * Zip formats and equivalents: ``.zip``, ``.egg``, ``.jar``. 10 | * Tar and compressed tar formats: ``.tar``, ``.tar.gz``, ``.tgz``, 11 | ``.tar.bz2``, ``.tz2``. 12 | 13 | python-archive is compatible and tested with Python versions 2.5, 2.6, 14 | and 3.2. 15 | 16 | 17 | Example usage 18 | ============= 19 | 20 | Using the ``Archive`` class:: 21 | 22 | from archive import Archive 23 | 24 | a = Archive('files.tar.gz') 25 | a.extract() 26 | 27 | Using the ``extract`` convenience function:: 28 | 29 | from archive import extract 30 | # Extract in current directory. 31 | extract('files.tar.gz') 32 | # Extract in directory 'unpack_dir'. 33 | extract('files.tar.gz', 'unpack_dir') 34 | 35 | By default, an ``archive.UnsafeArchive`` exception will be raised if any 36 | file(s) in the archive would be unpacked outside of the target directory 37 | (e.g. the archive contains absolute paths or relative paths that navigate up 38 | and out of the target directory). If you can trust the source of your archive 39 | files, then it's possible to override this behavior, e.g.:: 40 | 41 | extract('files.tar.gz', method='insecure') 42 | 43 | 44 | More examples 45 | ------------- 46 | Passing a file-like object is also supported:: 47 | 48 | f = open('files.tar.gz', 'rb') 49 | Archive(f).extract() 50 | 51 | If a file does not have the correct extension, or is not recognized correctly, 52 | you may try explicitly specifying an extension (with leading period):: 53 | 54 | Archive('files.wrongext', ext='.tar.gz').extract() 55 | # or 56 | extract('files.wrongext', ext='.tar.gz') 57 | 58 | 59 | Similar tools 60 | ============= 61 | 62 | * http://pypi.python.org/pypi/patool/ - portable command line archive file 63 | manager. 64 | * http://pypi.python.org/pypi/gocept.download/ - zc.buildout recipe for 65 | downloading and extracting an archive. 66 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | TODO 3 | ==== 4 | 5 | * Add support for gzip- and bzip2-compressed files. 6 | * Add more actions to Archive class. 7 | -------------------------------------------------------------------------------- /archive/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tarfile 4 | import zipfile 5 | 6 | from archive.compat import IS_PY2, is_string 7 | 8 | 9 | class ArchiveException(Exception): 10 | """Base exception class for all archive errors.""" 11 | 12 | 13 | class UnrecognizedArchiveFormat(ArchiveException): 14 | """Error raised when passed file is not a recognized archive format.""" 15 | 16 | 17 | class UnsafeArchive(ArchiveException): 18 | """ 19 | Error raised when passed file contains paths that would be extracted 20 | outside of the target directory. 21 | """ 22 | 23 | 24 | def extract(path, to_path='', ext='', **kwargs): 25 | """ 26 | Unpack the tar or zip file at the specified path to the directory 27 | specified by to_path. 28 | """ 29 | Archive(path, ext=ext).extract(to_path, **kwargs) 30 | 31 | 32 | class Archive(object): 33 | """ 34 | The external API class that encapsulates an archive implementation. 35 | """ 36 | 37 | def __init__(self, file, ext=''): 38 | """ 39 | Arguments: 40 | * 'file' can be a string path to a file or a file-like object. 41 | * Optional 'ext' argument can be given to override the file-type 42 | guess that is normally performed using the file extension of the 43 | given 'file'. Should start with a dot, e.g. '.tar.gz'. 44 | """ 45 | self._archive = self._archive_cls(file, ext=ext)(file) 46 | 47 | @staticmethod 48 | def _archive_cls(file, ext=''): 49 | """ 50 | Return the proper Archive implementation class, based on the file type. 51 | """ 52 | cls = None 53 | filename = None 54 | if is_string(file): 55 | filename = file 56 | else: 57 | try: 58 | filename = file.name 59 | except AttributeError: 60 | raise UnrecognizedArchiveFormat( 61 | "File object not a recognized archive format.") 62 | lookup_filename = filename + ext 63 | base, tail_ext = os.path.splitext(lookup_filename.lower()) 64 | cls = extension_map.get(tail_ext) 65 | if not cls: 66 | base, ext = os.path.splitext(base) 67 | cls = extension_map.get(ext) 68 | if not cls: 69 | raise UnrecognizedArchiveFormat( 70 | "Path not a recognized archive format: %s" % filename) 71 | return cls 72 | 73 | def extract(self, *args, **kwargs): 74 | self._archive.extract(*args, **kwargs) 75 | 76 | def list(self): 77 | self._archive.list() 78 | 79 | 80 | class BaseArchive(object): 81 | """ 82 | Base Archive class. Implementations should inherit this class. 83 | """ 84 | def __del__(self): 85 | if hasattr(self, "_archive"): 86 | self._archive.close() 87 | 88 | def list(self): 89 | raise NotImplementedError() 90 | 91 | def filenames(self): 92 | """ 93 | Return a list of the filenames contained in the archive. 94 | """ 95 | raise NotImplementedError() 96 | 97 | def _extract(self, to_path): 98 | """ 99 | Performs the actual extraction. Separate from 'extract' method so that 100 | we don't recurse when subclasses don't declare their own 'extract' 101 | method. 102 | """ 103 | self._archive.extractall(to_path) 104 | 105 | def extract(self, to_path='', method='safe'): 106 | if method == 'safe': 107 | self.check_files(to_path) 108 | elif method == 'insecure': 109 | pass 110 | else: 111 | raise ValueError("Invalid method option") 112 | self._extract(to_path) 113 | 114 | def check_files(self, to_path=None): 115 | """ 116 | Check that all of the files contained in the archive are within the 117 | target directory. 118 | """ 119 | if to_path: 120 | target_path = os.path.normpath(os.path.realpath(to_path)) 121 | else: 122 | target_path = os.getcwd() 123 | for filename in self.filenames(): 124 | extract_path = os.path.join(target_path, filename) 125 | extract_path = os.path.normpath(os.path.realpath(extract_path)) 126 | if not extract_path.startswith(target_path): 127 | raise UnsafeArchive( 128 | "Archive member destination is outside the target" 129 | " directory. member: %s" % filename) 130 | 131 | 132 | class TarArchive(BaseArchive): 133 | 134 | def __init__(self, file): 135 | # tarfile's open uses different parameters for file path vs. file obj. 136 | if is_string(file): 137 | self._archive = tarfile.open(name=file) 138 | else: 139 | self._archive = tarfile.open(fileobj=file) 140 | 141 | def list(self, *args, **kwargs): 142 | self._archive.list(*args, **kwargs) 143 | 144 | def filenames(self): 145 | return self._archive.getnames() 146 | 147 | 148 | class ZipArchive(BaseArchive): 149 | 150 | def __init__(self, file): 151 | # ZipFile's 'file' parameter can be path (string) or file-like obj. 152 | self._archive = zipfile.ZipFile(file) 153 | 154 | def list(self, *args, **kwargs): 155 | self._archive.printdir(*args, **kwargs) 156 | 157 | def filenames(self): 158 | return self._archive.namelist() 159 | 160 | extension_map = { 161 | '.docx': ZipArchive, 162 | '.egg': ZipArchive, 163 | '.jar': ZipArchive, 164 | '.odg': ZipArchive, 165 | '.odp': ZipArchive, 166 | '.ods': ZipArchive, 167 | '.odt': ZipArchive, 168 | '.pptx': ZipArchive, 169 | '.tar': TarArchive, 170 | '.tar.bz2': TarArchive, 171 | '.tar.gz': TarArchive, 172 | '.tgz': TarArchive, 173 | '.tz2': TarArchive, 174 | '.xlsx': ZipArchive, 175 | '.zip': ZipArchive, 176 | } 177 | -------------------------------------------------------------------------------- /archive/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module for providing backwards compatibility for Python versions. 3 | """ 4 | 5 | import sys 6 | 7 | 8 | IS_PY2 = sys.version_info[0] == 2 9 | 10 | 11 | if IS_PY2: 12 | def is_string(obj): 13 | return isinstance(obj, basestring) 14 | else: 15 | def is_string(obj): 16 | return isinstance(obj, str) 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | VERSION = '0.3-dev' 4 | 5 | CLASSIFIERS = [ 6 | 'Development Status :: 3 - Alpha', 7 | 'Intended Audience :: Developers', 8 | 'Intended Audience :: System Administrators', 9 | 'License :: OSI Approved :: MIT License', 10 | 'Operating System :: OS Independent', 11 | 'Programming Language :: Python', 12 | 'Programming Language :: Python :: 2.6', 13 | 'Programming Language :: Python :: 2.7', 14 | 'Programming Language :: Python :: 3.2', 15 | 'Topic :: System :: Archiving', 16 | 'Topic :: System :: Software Distribution', 17 | ] 18 | 19 | setup( 20 | name = 'python-archive', 21 | version = VERSION, 22 | classifiers = CLASSIFIERS, 23 | author = 'Gary Wilson Jr.', 24 | author_email = 'gary@thegarywilson.com', 25 | packages = ['archive'], 26 | url = 'https://github.com/gdub/python-archive', 27 | license = 'MIT License', 28 | description = ('Simple library that provides a common interface for' 29 | ' extracting zip and tar archives.'), 30 | long_description = open('README.rst').read(), 31 | tests_require=["tox", "pytest", "pep8"], 32 | ) 33 | -------------------------------------------------------------------------------- /tests/files/bad/absolute.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/bad/absolute.tar.gz -------------------------------------------------------------------------------- /tests/files/bad/relative.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/bad/relative.tar.gz -------------------------------------------------------------------------------- /tests/files/bad/unrecognized.txt: -------------------------------------------------------------------------------- 1 | File with unrecognized archive extension. 2 | -------------------------------------------------------------------------------- /tests/files/foobar.tar: -------------------------------------------------------------------------------- 1 | ./0000755000175000017500000000000011422706520007636 5ustar gdubgdub./10000644000175000017500000000000011422701477007715 0ustar gdubgdub./20000644000175000017500000000000011422701500007701 0ustar gdubgdub./foo/0000755000175000017500000000000011422701454010422 5ustar gdubgdub./foo/10000644000175000017500000000000011422701452010471 0ustar gdubgdub./foo/20000644000175000017500000000000011422701454010474 0ustar gdubgdub./foo/bar/0000755000175000017500000000000011422701475011171 5ustar gdubgdub./foo/bar/10000644000175000017500000000000011422701462011236 0ustar gdubgdub./foo/bar/20000644000175000017500000000000011422701475011243 0ustar gdubgdub -------------------------------------------------------------------------------- /tests/files/foobar.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/foobar.tar.bz2 -------------------------------------------------------------------------------- /tests/files/foobar.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/foobar.tar.gz -------------------------------------------------------------------------------- /tests/files/foobar.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/foobar.zip -------------------------------------------------------------------------------- /tests/files/foobar_tar_gz: -------------------------------------------------------------------------------- 1 | foobar.tar.gz -------------------------------------------------------------------------------- /tests/files/圧縮.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/圧縮.tgz -------------------------------------------------------------------------------- /tests/files/圧縮.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdub/python-archive/61b9afde21621a8acce964d3336a7fb5d2d07ca6/tests/files/圧縮.zip -------------------------------------------------------------------------------- /tests/test_archive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import shutil 4 | import tempfile 5 | import unittest 6 | from os.path import isfile, join as pathjoin 7 | 8 | from archive.compat import IS_PY2 9 | from archive import Archive, extract, UnsafeArchive, UnrecognizedArchiveFormat 10 | 11 | 12 | TEST_DIR = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | 15 | class TempDirMixin(object): 16 | """ 17 | Mixin class for TestCase subclasses to set up and tear down a temporary 18 | directory for unpacking archives during tests. 19 | """ 20 | 21 | def setUp(self): 22 | """ 23 | Create temporary directory for testing extraction. 24 | """ 25 | self.tmpdir = tempfile.mkdtemp() 26 | # Always start off in TEST_DIR. 27 | os.chdir(TEST_DIR) 28 | 29 | def tearDown(self): 30 | """ 31 | Clean up temporary directory. 32 | """ 33 | shutil.rmtree(self.tmpdir) 34 | 35 | def check_files(self, tmpdir): 36 | self.assertTrue(isfile(pathjoin(tmpdir, '1'))) 37 | self.assertTrue(isfile(pathjoin(tmpdir, '2'))) 38 | self.assertTrue(isfile(pathjoin(tmpdir, 'foo', '1'))) 39 | self.assertTrue(isfile(pathjoin(tmpdir, 'foo', '2'))) 40 | self.assertTrue(isfile(pathjoin(tmpdir, 'foo', 'bar', '1'))) 41 | self.assertTrue(isfile(pathjoin(tmpdir, 'foo', 'bar', '2'))) 42 | 43 | 44 | class MiscTests(TempDirMixin, unittest.TestCase): 45 | 46 | def test_Archive_instantiation_unrecognized_ext(self): 47 | f = pathjoin(TEST_DIR, 'files', 'bad', 'unrecognized.txt') 48 | self.assertRaises(UnrecognizedArchiveFormat, Archive, f) 49 | 50 | def test_extract_method_insecure(self): 51 | """ 52 | Tests that the insecure extract method indeed unpacks absolute paths 53 | outside the target directory. 54 | """ 55 | f = pathjoin('files', 'bad', 'absolute.tar.gz') 56 | Archive(f).extract(self.tmpdir, method='insecure') 57 | self.check_files('/tmp') 58 | 59 | 60 | class ArchiveTester(TempDirMixin): 61 | """ 62 | A mixin class to be used for testing many Archive methods for a single 63 | archive file. 64 | """ 65 | 66 | archive = None 67 | ext = '' 68 | 69 | def setUp(self): 70 | super(ArchiveTester, self).setUp() 71 | self.archive_path = pathjoin(TEST_DIR, 'files', self.archive) 72 | 73 | def test_extract_method(self): 74 | Archive(self.archive_path, ext=self.ext).extract(self.tmpdir) 75 | self.check_files(self.tmpdir) 76 | 77 | def test_extract_method_fileobject(self): 78 | f = open(self.archive_path, 'rb') 79 | Archive(f, ext=self.ext).extract(self.tmpdir) 80 | self.check_files(self.tmpdir) 81 | 82 | def test_extract_method_no_to_path(self): 83 | os.chdir(self.tmpdir) 84 | Archive(self.archive_path, ext=self.ext).extract() 85 | self.check_files(self.tmpdir) 86 | 87 | def test_extract_method_invalid_method(self): 88 | self.assertRaises(ValueError, 89 | Archive(self.archive_path, ext=self.ext).extract, 90 | (self.tmpdir,), {'method': 'foobar'}) 91 | 92 | def test_extract_function(self): 93 | extract(self.archive_path, self.tmpdir, ext=self.ext) 94 | self.check_files(self.tmpdir) 95 | 96 | def test_extract_function_fileobject(self): 97 | f = open(self.archive_path, 'rb') 98 | extract(f, self.tmpdir, ext=self.ext) 99 | self.check_files(self.tmpdir) 100 | 101 | def test_extract_function_bad_fileobject(self): 102 | class File: 103 | pass 104 | f = File() 105 | self.assertRaises(UnrecognizedArchiveFormat, extract, 106 | (f, self.tmpdir), {'ext': self.ext}) 107 | 108 | def test_extract_function_no_to_path(self): 109 | os.chdir(self.tmpdir) 110 | extract(self.archive_path, ext=self.ext) 111 | self.check_files(self.tmpdir) 112 | 113 | 114 | class TestZip(ArchiveTester, unittest.TestCase): 115 | archive = 'foobar.zip' 116 | 117 | 118 | class TestTar(ArchiveTester, unittest.TestCase): 119 | archive = 'foobar.tar' 120 | 121 | 122 | class TestGzipTar(ArchiveTester, unittest.TestCase): 123 | archive = 'foobar.tar.gz' 124 | 125 | 126 | class TestBzip2Tar(ArchiveTester, unittest.TestCase): 127 | archive = 'foobar.tar.bz2' 128 | 129 | 130 | class TestNonAsciiNamedTar(ArchiveTester, unittest.TestCase): 131 | archive = '圧縮.tgz' 132 | 133 | 134 | if IS_PY2: 135 | _UNICODE_NAME = unicode('圧縮.zip', 'utf-8') 136 | else: 137 | _UNICODE_NAME = '圧縮.zip' 138 | 139 | 140 | class TestUnicodeNamedZip(ArchiveTester, unittest.TestCase): 141 | archive = _UNICODE_NAME 142 | 143 | 144 | class TestExplicitExt(ArchiveTester, unittest.TestCase): 145 | archive = 'foobar_tar_gz' 146 | ext = '.tar.gz' 147 | 148 | 149 | # Archives that have files outside the target directory. 150 | 151 | class UnsafeArchiveTester(ArchiveTester): 152 | """ 153 | Test archives that contain files with destinations outside of the target 154 | directory, e.g. use absolute paths or relative paths that go up in sthe 155 | directory hierarchy. 156 | """ 157 | 158 | def test_extract_method(self): 159 | parent = super(UnsafeArchiveTester, self) 160 | self.assertRaises(UnsafeArchive, parent.test_extract_method) 161 | 162 | def test_extract_method_fileobject(self): 163 | parent = super(UnsafeArchiveTester, self) 164 | self.assertRaises(UnsafeArchive, parent.test_extract_method_fileobject) 165 | 166 | def test_extract_method_no_to_path(self): 167 | parent = super(UnsafeArchiveTester, self) 168 | self.assertRaises(UnsafeArchive, parent.test_extract_method_no_to_path) 169 | 170 | def test_extract_function(self): 171 | parent = super(UnsafeArchiveTester, self) 172 | self.assertRaises(UnsafeArchive, parent.test_extract_function) 173 | 174 | def test_extract_function_fileobject(self): 175 | parent = super(UnsafeArchiveTester, self) 176 | self.assertRaises(UnsafeArchive, 177 | parent.test_extract_function_fileobject) 178 | 179 | def test_extract_function_no_to_path(self): 180 | parent = super(UnsafeArchiveTester, self) 181 | self.assertRaises(UnsafeArchive, 182 | parent.test_extract_function_no_to_path) 183 | 184 | 185 | class TestOutsideRelative(UnsafeArchiveTester, unittest.TestCase): 186 | """An archive that goes outside the destination using relative paths.""" 187 | archive = pathjoin('bad', 'relative.tar.gz') 188 | 189 | 190 | class TestOutsideAbsolute(UnsafeArchiveTester, unittest.TestCase): 191 | """An archive that goes outside the destination using absolute paths.""" 192 | archive = pathjoin('bad', 'absolute.tar.gz') 193 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py32 3 | indexserver = 4 | pypi = http://pypi.python.org/simple 5 | 6 | [testenv] 7 | deps= 8 | :pypi:pytest 9 | :pypi:pep8 10 | 11 | commands = 12 | py.test -v tests 13 | pep8 archive 14 | --------------------------------------------------------------------------------