Skip to content

Commit

Permalink
Merge pull request #1 from dave-shawley/add-dist-support
Browse files Browse the repository at this point in the history
Remove distribution directories.
  • Loading branch information
dave-shawley committed Nov 16, 2014
2 parents 2d2397d + c071708 commit 1aa4eb1
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 8 deletions.
4 changes: 2 additions & 2 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Changelog
=========

* Next Release
* 0.0.1

- Create something amazing
- Support removal of distribution directories.
50 changes: 47 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,50 @@ keeping targets such as *clean*, *dist-clean*, and *maintainer-clean*.
This extension is inspired by the same desire for a clean working
environment.

Ok... Where?
------------
Installation
~~~~~~~~~~~~
The ``setuptools`` package contains a number of interesting ways in which
it can be extended. If you develop Python packages, then you can include
extension packages using the ``setup_requires`` and ``cmdclass`` keyword
parameters to the ``setup`` function call. This is a little more
difficult than it should be since the ``setupext`` package needs to be
imported into *setup.py* so that it can be passed as a keyword parameter
**before** it is downloaded. The easiest way to do this is to catch the
``ImportError`` that happens if it is not already downloaded::

import setuptools
try:
from setupext import janitor
CleanCommand = janitor.CleanCommand
except ImportError:
CleanCommand = None

cmd_classes = {}
if CleanCommand is not None:
cmd_classes['clean'] = CleanCommand

setup(
# normal parameters
setup_requires=['setupext.janitor'],
cmdclass=cmd_classes,
)

You can use a different approach if you are simply a developer that wants
to have this functionality available for your own use, then you can install
it into your working environment. This package installs itself into the
environment as a `distutils extension`_ so that it is available to any
*setup.py* script as if by magic.

Usage
~~~~~
Once the extension is installed, the ``clean`` command will accept a
few new command line parameters.

``setup.py clean --dist``
Removes directories that the various *dist* commands produce.

Where can I get this extension from?
------------------------------------
+---------------+-----------------------------------------------------+
| Source | https://github.com/dave-shawley/setupext-janitor |
+---------------+-----------------------------------------------------+
Expand All @@ -39,7 +81,10 @@ Ok... Where?
| Issues | https://github.com/dave-shawley/setupext-janitor |
+---------------+-----------------------------------------------------+

.. _distutils extension: https://pythonhosted.org/setuptools/setuptools.html
#extending-and-reusing-setuptools
.. _setuptools: https://pythonhosted.org/setuptools/

.. |Version| image:: https://badge.fury.io/py/setupext-janitor.svg
:target: https://badge.fury.io/
.. |Downloads| image:: https://pypip.in/d/setupext-janitor/badge.svg?
Expand All @@ -48,4 +93,3 @@ Ok... Where?
:target: https://travis-ci.org/dave-shawley/setupext-janitor
.. |License| image:: https://pypip.in/license/dave-shawley/badge.svg?
:target: https://setupext-dave-shawley.readthedocs.org/

1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
-r test-requirements.txt
detox>=0.9,<1
flake8>=2.2,<3
pyflakes>=0.8,<1
sphinx>=1.2,<2
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
test_requirements = ['nose>1.3,<2']
if sys.version_info < (3, ):
test_requirements.append('mock>1.0,<2')
if sys.version_info < (2, 7):
test_requirements.append('unittest2')


setuptools.setup(
Expand All @@ -39,6 +41,7 @@
],
entry_points={
'distutils.commands': [
'clean = setupext.janitor:CleanCommand',
],
},
)
88 changes: 87 additions & 1 deletion setupext/janitor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,88 @@
version_info = (0, 0, 0)
from distutils import log
from distutils.command.clean import clean as _CleanCommand
import shutil


version_info = (0, 0, 1)
__version__ = '.'.join(str(v) for v in version_info)


class CleanCommand(_CleanCommand):
"""
Extend the clean command to do additional house keeping.
The traditional distutils clean command removes the by-products of
compiling extension code. This class extends it to remove the
similar by-products generated by producing a Python distribution.
Most notably, it will remove .egg/.egg-info directories, the
generated distribution, those pesky *__pycache__* directories,
and even the virtual environment that it is running in.
The level of cleanliness is controlled by command-line options as
you might expect. The targets that are removed are influenced by
the commands that created them. For example, if you set a custom
distribution directory using the ``--dist-dir`` option or the
matching snippet in *setup.cfg*, then this extension will honor
that setting. It even goes as far as to detect the virtual
environment directory based on environment variables.
This all sounds a little dangerous... there is little to worry
about though. This command only removes what it is configured to
remove which is nothing by default. It also honors the
``--dry-run`` global option so that there should be no question
what it is going to remove.
"""

# See _set_options for `user_options`

def __init__(self, *args, **kwargs):
_CleanCommand.__init__(self, *args, **kwargs)
self.dist = None

def initialize_options(self):
_CleanCommand.initialize_options(self)
self.dist = False

def run(self):
_CleanCommand.run(self)
if not self.dist:
return

dist_dirs = set()
for cmd_name in self.distribution.commands:
if 'dist' in cmd_name:
command = self.distribution.get_command_obj(cmd_name)
command.ensure_finalized()
if getattr(command, 'dist_dir', None):
dist_dirs.add(command.dist_dir)

for dir_name in dist_dirs:
self.announce('removing {0}'.format(dir_name), level=log.DEBUG)
shutil.rmtree(dir_name, ignore_errors=True)


def _set_options():
"""
Set the options for CleanCommand.
There are a number of reasons that this has to be done in an
external function instead of inline in the class. First of all,
the setuptools machinery really wants the options to be defined
in a class attribute - otherwise, the help command doesn't work
so we need a class attribute. However, we are extending an
existing command and do not want to "monkey patch" over it so
we need to define a *new* class attribute with the same name
that contains a copy of the base class value. This could be
accomplished using some magic in ``__new__`` but I would much
rather set the class attribute externally... it's just cleaner.
"""
CleanCommand.user_options = _CleanCommand.user_options[:]
CleanCommand.user_options.extend([
('dist', 'd', 'remove distribution directory'),
])
CleanCommand.boolean_options = _CleanCommand.boolean_options[:]
CleanCommand.boolean_options.append('dist')

_set_options()
130 changes: 130 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from distutils import core, dist
from distutils.command import clean
import atexit
import os.path
import shutil
import sys
import tempfile

if sys.version_info >= (2, 7):
import unittest
else: # noinspection PyPackageRequirements,PyUnresolvedReferences
import unittest2 as unittest

from setupext import janitor


def run_setup(*command_line):
"""
Run the setup command with `command_line`.
:param command_line: the command line arguments to pass
as the simulated command line
This function runs :func:`distutils.core.setup` after it
configures an environment that mimics passing the specified
command line arguments. The ``distutils`` internals are
replaced with a :class:`~distutils.dist.Distribution`
instance that will only execute the clean command. Other
commands can be passed freely to simulate command line usage
patterns.
"""
class FakeDistribution(dist.Distribution):

def __init__(self, *args, **kwargs):
"""Enable verbose output to make tests easier to debug."""
dist.Distribution.__init__(self, *args, **kwargs)
self.verbose = 3

def run_command(self, command):
"""Only run the clean command."""
if command == 'clean':
dist.Distribution.run_command(self, command)

def parse_config_files(self, filenames=None):
"""Skip processing of configuration files."""
pass

core.setup(
distclass=FakeDistribution,
script_name='testsetup.py',
script_args=command_line,
cmdclass={'clean': janitor.CleanCommand},
)


class CommandOptionTests(unittest.TestCase):

def test_that_distutils_options_are_present(self):
defined_options = set(t[0] for t in janitor.CleanCommand.user_options)
superclass_options = set(t[0] for t in clean.clean.user_options)
self.assertTrue(defined_options.issuperset(superclass_options))

def test_that_janitor_user_options_are_not_clean_options(self):
self.assertIsNot(
janitor.CleanCommand.user_options, clean.clean.user_options)

def test_that_janitor_defines_dist_command(self):
self.assertIn(
('dist', 'd', 'remove distribution directory'),
janitor.CleanCommand.user_options)


class DirectoryCleanupTests(unittest.TestCase):
temp_dir = tempfile.mkdtemp()

@classmethod
def setUpClass(cls):
super(DirectoryCleanupTests, cls).setUpClass()
atexit.register(shutil.rmtree, cls.temp_dir)

@classmethod
def create_directory(cls, dir_name):
return tempfile.mkdtemp(dir=cls.temp_dir, prefix=dir_name)

def assert_path_does_not_exist(self, full_path):
if os.path.exists(full_path):
raise AssertionError('{0} should not exist'.format(full_path))

def assert_path_exists(self, full_path):
if not os.path.exists(full_path):
raise AssertionError('{0} should exist'.format(full_path))

def test_that_dist_directory_is_removed_for_sdist(self):
dist_dir = self.create_directory('dist-dir')
run_setup(
'sdist', '--dist-dir={0}'.format(dist_dir),
'clean', '--dist',
)
self.assert_path_does_not_exist(dist_dir)

def test_that_dist_directory_is_removed_for_bdist_dumb(self):
dist_dir = self.create_directory('dist-dir')
run_setup(
'bdist_dumb', '--dist-dir={0}'.format(dist_dir),
'clean', '--dist',
)
self.assert_path_does_not_exist(dist_dir)

def test_that_multiple_dist_directories_with_be_removed(self):
sdist_dir = self.create_directory('sdist-dir')
bdist_dir = self.create_directory('bdist_dumb')
run_setup(
'sdist', '--dist-dir={0}'.format(sdist_dir),
'bdist_dumb', '--dist-dir={0}'.format(bdist_dir),
'clean', '--dist',
)
self.assert_path_does_not_exist(sdist_dir)
self.assert_path_does_not_exist(bdist_dir)

def test_that_directories_are_not_removed_without_parameter(self):
sdist_dir = self.create_directory('sdist-dir')
bdist_dir = self.create_directory('bdist_dumb')
run_setup(
'sdist', '--dist-dir={0}'.format(sdist_dir),
'bdist_dumb', '--dist-dir={0}'.format(bdist_dir),
'clean',
)
self.assert_path_exists(sdist_dir)
self.assert_path_exists(bdist_dir)
9 changes: 7 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
[tox]
envlist = py27,py33,py34
envlist = py26,py27,py33,py34,pypy3
toxworkdir = {toxinidir}/build/tox

[testenv]
deps = nose
deps = -rtest-requirements.txt
commands = {envbindir}/nosetests

[testenv:py27]
deps =
{[testenv]deps}
mock

[testenv:py26]
deps =
{[testenv:py27]deps}
unittest2

0 comments on commit 1aa4eb1

Please sign in to comment.