Skip to content

Commit 1aa4eb1

Browse files
committed
Merge pull request #1 from dave-shawley/add-dist-support
Remove distribution directories.
2 parents 2d2397d + c071708 commit 1aa4eb1

File tree

7 files changed

+277
-8
lines changed

7 files changed

+277
-8
lines changed

HISTORY

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Changelog
22
=========
33

4-
* Next Release
4+
* 0.0.1
55

6-
- Create something amazing
6+
- Support removal of distribution directories.

README.rst

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,50 @@ keeping targets such as *clean*, *dist-clean*, and *maintainer-clean*.
2525
This extension is inspired by the same desire for a clean working
2626
environment.
2727

28-
Ok... Where?
29-
------------
28+
Installation
29+
~~~~~~~~~~~~
30+
The ``setuptools`` package contains a number of interesting ways in which
31+
it can be extended. If you develop Python packages, then you can include
32+
extension packages using the ``setup_requires`` and ``cmdclass`` keyword
33+
parameters to the ``setup`` function call. This is a little more
34+
difficult than it should be since the ``setupext`` package needs to be
35+
imported into *setup.py* so that it can be passed as a keyword parameter
36+
**before** it is downloaded. The easiest way to do this is to catch the
37+
``ImportError`` that happens if it is not already downloaded::
38+
39+
import setuptools
40+
try:
41+
from setupext import janitor
42+
CleanCommand = janitor.CleanCommand
43+
except ImportError:
44+
CleanCommand = None
45+
46+
cmd_classes = {}
47+
if CleanCommand is not None:
48+
cmd_classes['clean'] = CleanCommand
49+
50+
setup(
51+
# normal parameters
52+
setup_requires=['setupext.janitor'],
53+
cmdclass=cmd_classes,
54+
)
55+
56+
You can use a different approach if you are simply a developer that wants
57+
to have this functionality available for your own use, then you can install
58+
it into your working environment. This package installs itself into the
59+
environment as a `distutils extension`_ so that it is available to any
60+
*setup.py* script as if by magic.
61+
62+
Usage
63+
~~~~~
64+
Once the extension is installed, the ``clean`` command will accept a
65+
few new command line parameters.
66+
67+
``setup.py clean --dist``
68+
Removes directories that the various *dist* commands produce.
69+
70+
Where can I get this extension from?
71+
------------------------------------
3072
+---------------+-----------------------------------------------------+
3173
| Source | https://github.com/dave-shawley/setupext-janitor |
3274
+---------------+-----------------------------------------------------+
@@ -39,7 +81,10 @@ Ok... Where?
3981
| Issues | https://github.com/dave-shawley/setupext-janitor |
4082
+---------------+-----------------------------------------------------+
4183

84+
.. _distutils extension: https://pythonhosted.org/setuptools/setuptools.html
85+
#extending-and-reusing-setuptools
4286
.. _setuptools: https://pythonhosted.org/setuptools/
87+
4388
.. |Version| image:: https://badge.fury.io/py/setupext-janitor.svg
4489
:target: https://badge.fury.io/
4590
.. |Downloads| image:: https://pypip.in/d/setupext-janitor/badge.svg?
@@ -48,4 +93,3 @@ Ok... Where?
4893
:target: https://travis-ci.org/dave-shawley/setupext-janitor
4994
.. |License| image:: https://pypip.in/license/dave-shawley/badge.svg?
5095
:target: https://setupext-dave-shawley.readthedocs.org/
51-

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
-r test-requirements.txt
2+
detox>=0.9,<1
23
flake8>=2.2,<3
34
pyflakes>=0.8,<1
45
sphinx>=1.2,<2

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
test_requirements = ['nose>1.3,<2']
1414
if sys.version_info < (3, ):
1515
test_requirements.append('mock>1.0,<2')
16+
if sys.version_info < (2, 7):
17+
test_requirements.append('unittest2')
1618

1719

1820
setuptools.setup(
@@ -39,6 +41,7 @@
3941
],
4042
entry_points={
4143
'distutils.commands': [
44+
'clean = setupext.janitor:CleanCommand',
4245
],
4346
},
4447
)

setupext/janitor/__init__.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,88 @@
1-
version_info = (0, 0, 0)
1+
from distutils import log
2+
from distutils.command.clean import clean as _CleanCommand
3+
import shutil
4+
5+
6+
version_info = (0, 0, 1)
27
__version__ = '.'.join(str(v) for v in version_info)
8+
9+
10+
class CleanCommand(_CleanCommand):
11+
"""
12+
Extend the clean command to do additional house keeping.
13+
14+
The traditional distutils clean command removes the by-products of
15+
compiling extension code. This class extends it to remove the
16+
similar by-products generated by producing a Python distribution.
17+
Most notably, it will remove .egg/.egg-info directories, the
18+
generated distribution, those pesky *__pycache__* directories,
19+
and even the virtual environment that it is running in.
20+
21+
The level of cleanliness is controlled by command-line options as
22+
you might expect. The targets that are removed are influenced by
23+
the commands that created them. For example, if you set a custom
24+
distribution directory using the ``--dist-dir`` option or the
25+
matching snippet in *setup.cfg*, then this extension will honor
26+
that setting. It even goes as far as to detect the virtual
27+
environment directory based on environment variables.
28+
29+
This all sounds a little dangerous... there is little to worry
30+
about though. This command only removes what it is configured to
31+
remove which is nothing by default. It also honors the
32+
``--dry-run`` global option so that there should be no question
33+
what it is going to remove.
34+
35+
"""
36+
37+
# See _set_options for `user_options`
38+
39+
def __init__(self, *args, **kwargs):
40+
_CleanCommand.__init__(self, *args, **kwargs)
41+
self.dist = None
42+
43+
def initialize_options(self):
44+
_CleanCommand.initialize_options(self)
45+
self.dist = False
46+
47+
def run(self):
48+
_CleanCommand.run(self)
49+
if not self.dist:
50+
return
51+
52+
dist_dirs = set()
53+
for cmd_name in self.distribution.commands:
54+
if 'dist' in cmd_name:
55+
command = self.distribution.get_command_obj(cmd_name)
56+
command.ensure_finalized()
57+
if getattr(command, 'dist_dir', None):
58+
dist_dirs.add(command.dist_dir)
59+
60+
for dir_name in dist_dirs:
61+
self.announce('removing {0}'.format(dir_name), level=log.DEBUG)
62+
shutil.rmtree(dir_name, ignore_errors=True)
63+
64+
65+
def _set_options():
66+
"""
67+
Set the options for CleanCommand.
68+
69+
There are a number of reasons that this has to be done in an
70+
external function instead of inline in the class. First of all,
71+
the setuptools machinery really wants the options to be defined
72+
in a class attribute - otherwise, the help command doesn't work
73+
so we need a class attribute. However, we are extending an
74+
existing command and do not want to "monkey patch" over it so
75+
we need to define a *new* class attribute with the same name
76+
that contains a copy of the base class value. This could be
77+
accomplished using some magic in ``__new__`` but I would much
78+
rather set the class attribute externally... it's just cleaner.
79+
80+
"""
81+
CleanCommand.user_options = _CleanCommand.user_options[:]
82+
CleanCommand.user_options.extend([
83+
('dist', 'd', 'remove distribution directory'),
84+
])
85+
CleanCommand.boolean_options = _CleanCommand.boolean_options[:]
86+
CleanCommand.boolean_options.append('dist')
87+
88+
_set_options()

tests.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from distutils import core, dist
2+
from distutils.command import clean
3+
import atexit
4+
import os.path
5+
import shutil
6+
import sys
7+
import tempfile
8+
9+
if sys.version_info >= (2, 7):
10+
import unittest
11+
else: # noinspection PyPackageRequirements,PyUnresolvedReferences
12+
import unittest2 as unittest
13+
14+
from setupext import janitor
15+
16+
17+
def run_setup(*command_line):
18+
"""
19+
Run the setup command with `command_line`.
20+
21+
:param command_line: the command line arguments to pass
22+
as the simulated command line
23+
24+
This function runs :func:`distutils.core.setup` after it
25+
configures an environment that mimics passing the specified
26+
command line arguments. The ``distutils`` internals are
27+
replaced with a :class:`~distutils.dist.Distribution`
28+
instance that will only execute the clean command. Other
29+
commands can be passed freely to simulate command line usage
30+
patterns.
31+
32+
"""
33+
class FakeDistribution(dist.Distribution):
34+
35+
def __init__(self, *args, **kwargs):
36+
"""Enable verbose output to make tests easier to debug."""
37+
dist.Distribution.__init__(self, *args, **kwargs)
38+
self.verbose = 3
39+
40+
def run_command(self, command):
41+
"""Only run the clean command."""
42+
if command == 'clean':
43+
dist.Distribution.run_command(self, command)
44+
45+
def parse_config_files(self, filenames=None):
46+
"""Skip processing of configuration files."""
47+
pass
48+
49+
core.setup(
50+
distclass=FakeDistribution,
51+
script_name='testsetup.py',
52+
script_args=command_line,
53+
cmdclass={'clean': janitor.CleanCommand},
54+
)
55+
56+
57+
class CommandOptionTests(unittest.TestCase):
58+
59+
def test_that_distutils_options_are_present(self):
60+
defined_options = set(t[0] for t in janitor.CleanCommand.user_options)
61+
superclass_options = set(t[0] for t in clean.clean.user_options)
62+
self.assertTrue(defined_options.issuperset(superclass_options))
63+
64+
def test_that_janitor_user_options_are_not_clean_options(self):
65+
self.assertIsNot(
66+
janitor.CleanCommand.user_options, clean.clean.user_options)
67+
68+
def test_that_janitor_defines_dist_command(self):
69+
self.assertIn(
70+
('dist', 'd', 'remove distribution directory'),
71+
janitor.CleanCommand.user_options)
72+
73+
74+
class DirectoryCleanupTests(unittest.TestCase):
75+
temp_dir = tempfile.mkdtemp()
76+
77+
@classmethod
78+
def setUpClass(cls):
79+
super(DirectoryCleanupTests, cls).setUpClass()
80+
atexit.register(shutil.rmtree, cls.temp_dir)
81+
82+
@classmethod
83+
def create_directory(cls, dir_name):
84+
return tempfile.mkdtemp(dir=cls.temp_dir, prefix=dir_name)
85+
86+
def assert_path_does_not_exist(self, full_path):
87+
if os.path.exists(full_path):
88+
raise AssertionError('{0} should not exist'.format(full_path))
89+
90+
def assert_path_exists(self, full_path):
91+
if not os.path.exists(full_path):
92+
raise AssertionError('{0} should exist'.format(full_path))
93+
94+
def test_that_dist_directory_is_removed_for_sdist(self):
95+
dist_dir = self.create_directory('dist-dir')
96+
run_setup(
97+
'sdist', '--dist-dir={0}'.format(dist_dir),
98+
'clean', '--dist',
99+
)
100+
self.assert_path_does_not_exist(dist_dir)
101+
102+
def test_that_dist_directory_is_removed_for_bdist_dumb(self):
103+
dist_dir = self.create_directory('dist-dir')
104+
run_setup(
105+
'bdist_dumb', '--dist-dir={0}'.format(dist_dir),
106+
'clean', '--dist',
107+
)
108+
self.assert_path_does_not_exist(dist_dir)
109+
110+
def test_that_multiple_dist_directories_with_be_removed(self):
111+
sdist_dir = self.create_directory('sdist-dir')
112+
bdist_dir = self.create_directory('bdist_dumb')
113+
run_setup(
114+
'sdist', '--dist-dir={0}'.format(sdist_dir),
115+
'bdist_dumb', '--dist-dir={0}'.format(bdist_dir),
116+
'clean', '--dist',
117+
)
118+
self.assert_path_does_not_exist(sdist_dir)
119+
self.assert_path_does_not_exist(bdist_dir)
120+
121+
def test_that_directories_are_not_removed_without_parameter(self):
122+
sdist_dir = self.create_directory('sdist-dir')
123+
bdist_dir = self.create_directory('bdist_dumb')
124+
run_setup(
125+
'sdist', '--dist-dir={0}'.format(sdist_dir),
126+
'bdist_dumb', '--dist-dir={0}'.format(bdist_dir),
127+
'clean',
128+
)
129+
self.assert_path_exists(sdist_dir)
130+
self.assert_path_exists(bdist_dir)

tox.ini

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
[tox]
2-
envlist = py27,py33,py34
2+
envlist = py26,py27,py33,py34,pypy3
33
toxworkdir = {toxinidir}/build/tox
44

55
[testenv]
6-
deps = nose
6+
deps = -rtest-requirements.txt
77
commands = {envbindir}/nosetests
88

99
[testenv:py27]
1010
deps =
1111
{[testenv]deps}
1212
mock
13+
14+
[testenv:py26]
15+
deps =
16+
{[testenv:py27]deps}
17+
unittest2

0 commit comments

Comments
 (0)