From 787097fe3394e8596a3bc86bf7c8bae52c65a1b4 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 15 Nov 2014 08:32:54 -0500 Subject: [PATCH 1/7] Add testing for 2.6 and PyPy3. Better off to start compatible and remove support if needed. --- setup.py | 2 ++ tox.ini | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2373c0c..64f78b5 100755 --- a/setup.py +++ b/setup.py @@ -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( diff --git a/tox.ini b/tox.ini index b16cf6c..4f47dfd 100644 --- a/tox.ini +++ b/tox.ini @@ -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 From 747eb3969795c63d591fc6736b7246bcb7d92e80 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 15 Nov 2014 08:34:04 -0500 Subject: [PATCH 2/7] dev-requirements: Use detox, it's just faster. --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 8b3a884..827cb92 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ -r test-requirements.txt +detox>=0.9,<1 flake8>=2.2,<3 pyflakes>=0.8,<1 sphinx>=1.2,<2 From bad9298fba8a9d2eb58769ae3d52b00b5e71f76d Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sat, 15 Nov 2014 08:35:22 -0500 Subject: [PATCH 3/7] CleanCommand: Add "dist" to `user_options`. --- setupext/janitor/__init__.py | 58 ++++++++++++++++++++++++++++++++++++ tests.py | 26 ++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/setupext/janitor/__init__.py b/setupext/janitor/__init__.py index 2e4dd40..26d3a46 100644 --- a/setupext/janitor/__init__.py +++ b/setupext/janitor/__init__.py @@ -1,2 +1,60 @@ +from distutils.command.clean import clean as _CleanCommand + + version_info = (0, 0, 0) __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` + pass + + +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'), + ]) + +_set_options() diff --git a/tests.py b/tests.py index e69de29..34bb4b0 100644 --- a/tests.py +++ b/tests.py @@ -0,0 +1,26 @@ +from distutils.command import clean +import sys + +if sys.version_info >= (2, 7): + import unittest +else: + import unittest2 as unittest + +from setupext import janitor + + +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) From 60503d96469536d94487dc3be2c7611071dbe904 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 16 Nov 2014 14:41:49 -0500 Subject: [PATCH 4/7] Add support for cleaning up after sdist. --- README.rst | 50 ++++++++++++++++++++++-- setup.py | 1 + setupext/janitor/__init__.py | 27 ++++++++++++- tests.py | 74 +++++++++++++++++++++++++++++++++++- 4 files changed, 147 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b2324c3..9bf3777 100644 --- a/README.rst +++ b/README.rst @@ -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 | +---------------+-----------------------------------------------------+ @@ -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? @@ -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/ - diff --git a/setup.py b/setup.py index 64f78b5..74e3447 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ ], entry_points={ 'distutils.commands': [ + 'clean = setupext.janitor:CleanCommand', ], }, ) diff --git a/setupext/janitor/__init__.py b/setupext/janitor/__init__.py index 26d3a46..79a6883 100644 --- a/setupext/janitor/__init__.py +++ b/setupext/janitor/__init__.py @@ -1,4 +1,6 @@ +from distutils import log from distutils.command.clean import clean as _CleanCommand +import shutil version_info = (0, 0, 0) @@ -33,7 +35,28 @@ class CleanCommand(_CleanCommand): """ # See _set_options for `user_options` - pass + + 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) + dist_dirs = set() + for cmd_name in self.distribution.commands: + if cmd_name == 'sdist': + 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(): @@ -56,5 +79,7 @@ def _set_options(): CleanCommand.user_options.extend([ ('dist', 'd', 'remove distribution directory'), ]) + CleanCommand.boolean_options = _CleanCommand.boolean_options[:] + CleanCommand.boolean_options.append('dist') _set_options() diff --git a/tests.py b/tests.py index 34bb4b0..a9e1d85 100644 --- a/tests.py +++ b/tests.py @@ -1,14 +1,59 @@ +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: +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): @@ -24,3 +69,30 @@ 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): + full_path = os.path.join(cls.temp_dir, dir_name) + os.mkdir(full_path) + return full_path + + 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 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) From 9119930b90f02f115aff235e6360f0dab2e251ae Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 16 Nov 2014 14:43:38 -0500 Subject: [PATCH 5/7] Add support for cleaning up after bdist. --- setupext/janitor/__init__.py | 2 +- tests.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/setupext/janitor/__init__.py b/setupext/janitor/__init__.py index 79a6883..eac291e 100644 --- a/setupext/janitor/__init__.py +++ b/setupext/janitor/__init__.py @@ -48,7 +48,7 @@ def run(self): _CleanCommand.run(self) dist_dirs = set() for cmd_name in self.distribution.commands: - if cmd_name == 'sdist': + if 'dist' in cmd_name: command = self.distribution.get_command_obj(cmd_name) command.ensure_finalized() if getattr(command, 'dist_dir', None): diff --git a/tests.py b/tests.py index a9e1d85..c64294c 100644 --- a/tests.py +++ b/tests.py @@ -96,3 +96,22 @@ def test_that_dist_directory_is_removed_for_sdist(self): '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) From b1902b9251ea0dd825232b4a0469ace64d1d81a1 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 16 Nov 2014 14:56:41 -0500 Subject: [PATCH 6/7] Don't remove directories unless told to. --- setupext/janitor/__init__.py | 3 +++ tests.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/setupext/janitor/__init__.py b/setupext/janitor/__init__.py index eac291e..a9ada45 100644 --- a/setupext/janitor/__init__.py +++ b/setupext/janitor/__init__.py @@ -46,6 +46,9 @@ def initialize_options(self): 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: diff --git a/tests.py b/tests.py index c64294c..68371d2 100644 --- a/tests.py +++ b/tests.py @@ -81,14 +81,16 @@ def setUpClass(cls): @classmethod def create_directory(cls, dir_name): - full_path = os.path.join(cls.temp_dir, dir_name) - os.mkdir(full_path) - return full_path + 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( @@ -115,3 +117,14 @@ def test_that_multiple_dist_directories_with_be_removed(self): ) 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) From c071708f0378a853b0bcf3aebce3555d5539edf6 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Sun, 16 Nov 2014 15:06:23 -0500 Subject: [PATCH 7/7] Update for 0.0.1 release. --- HISTORY | 4 ++-- setupext/janitor/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY b/HISTORY index 41d3a37..a571ee4 100644 --- a/HISTORY +++ b/HISTORY @@ -1,6 +1,6 @@ Changelog ========= -* Next Release +* 0.0.1 - - Create something amazing + - Support removal of distribution directories. diff --git a/setupext/janitor/__init__.py b/setupext/janitor/__init__.py index a9ada45..f946931 100644 --- a/setupext/janitor/__init__.py +++ b/setupext/janitor/__init__.py @@ -3,7 +3,7 @@ import shutil -version_info = (0, 0, 0) +version_info = (0, 0, 1) __version__ = '.'.join(str(v) for v in version_info)