diff options
-rwxr-xr-x | setup.py | 12 | ||||
-rwxr-xr-x | setuptools.egg-info/entry_points.txt | 13 | ||||
-rwxr-xr-x | setuptools.txt | 136 | ||||
-rw-r--r-- | setuptools/__init__.py | 22 | ||||
-rwxr-xr-x | setuptools/command/egg_info.py | 14 | ||||
-rw-r--r-- | setuptools/dist.py | 296 | ||||
-rw-r--r-- | setuptools/extension.py | 10 |
7 files changed, 356 insertions, 147 deletions
@@ -46,8 +46,18 @@ setup( "%(cmd)s = setuptools.command.%(cmd)s:%(cmd)s" % locals() for cmd in SETUP_COMMANDS if cmd!="build_py" or sys.version<"2.4" ], + "distutils.setup_keywords": [ + "eager_resources = setuptools.dist:assert_string_list", + "namespace_packages = setuptools.dist:check_nsp", + "extras_require = setuptools.dist:check_extras", + "entry_points = setuptools.dist:check_entry_points", + "test_suite = setuptools.dist:check_test_suite", + "zip_safe = setuptools.dist:assert_bool", + ] }, + setup_requires = ['setuptools>=0.6a0'], + classifiers = [f.strip() for f in """ Development Status :: 3 - Alpha Intended Audience :: Developers @@ -78,5 +88,3 @@ setup( - - diff --git a/setuptools.egg-info/entry_points.txt b/setuptools.egg-info/entry_points.txt index 8baf6137..def14fac 100755 --- a/setuptools.egg-info/entry_points.txt +++ b/setuptools.egg-info/entry_points.txt @@ -1,13 +1,22 @@ +[distutils.setup_keywords] +entry_points = setuptools.dist:check_entry_points +extras_require = setuptools.dist:check_extras +namespace_packages = setuptools.dist:check_nsp +test_suite = setuptools.dist:check_test_suite +eager_resources = setuptools.dist:assert_string_list +zip_safe = setuptools.dist:assert_bool + [distutils.commands] rotate = setuptools.command.rotate:rotate develop = setuptools.command.develop:develop setopt = setuptools.command.setopt:setopt +build_py = setuptools.command.build_py:build_py saveopts = setuptools.command.saveopts:saveopts egg_info = setuptools.command.egg_info:egg_info -depends = setuptools.command.depends:depends +easy_install = setuptools.command.easy_install:easy_install upload = setuptools.command.upload:upload alias = setuptools.command.alias:alias -easy_install = setuptools.command.easy_install:easy_install +depends = setuptools.command.depends:depends bdist_egg = setuptools.command.bdist_egg:bdist_egg install = setuptools.command.install:install test = setuptools.command.test:test diff --git a/setuptools.txt b/setuptools.txt index 2bd40905..7f9c8551 100755 --- a/setuptools.txt +++ b/setuptools.txt @@ -64,7 +64,11 @@ Installing ``setuptools`` ========================= Download `ez_setup.py`_ and run it; this will download and install the -appropriate egg for your Python version. +appropriate egg for your Python version. (Note: if you are behind +an NTLM-based firewall that prevents Python programs from accessing the net +directly, you may wish to install and use the `APS proxy server +<http://ntlmaps.sf.net/>`_, which lets you get past such firewalls in the same +way that your web browser(s) do.) .. _ez_setup.py: `bootstrap module`_ @@ -80,6 +84,16 @@ To get the in-development version of setuptools, run:: You can then install it using the usual "setup.py install" incantation. +Note that ``setuptools`` *must* be installed as an egg directory; it will not +operate correctly otherwise. If you are unable to install to a valid +``site-packages`` directory (e.g. a "non-root install"), you will therefore +need to manually add the setuptools egg to your ``PYTHONPATH``. (You won't +need to do this for every egg you install, because the ``pkg_resources`` module +can automatically find eggs and add them to ``sys.path`` at runtime. It's just +that the ``setuptools`` egg contains ``pkg_resources`` and therefore has to +be manually bootstrapped if you can't install it to a ``site-packages`` +directory.) + Basic Use ========= @@ -175,6 +189,15 @@ unless you need the associated ``setuptools`` feature. installed to support those features. See the section below on `Declaring Dependencies`_ for details and examples of the format of this argument. +``setup_requires`` + A string or list of strings specifying what other distributions need to + be present in order for the *setup script* to run. ``setuptools`` will + attempt to obtain these (even going so far as to download them using + ``EasyInstall``) before processing the rest of the setup script or commands. + This argument is needed if you are using distutils extensions as part of + your build process; for example, extensions that process setup() arguments + and turn them into EGG-INFO metadata files. + ``namespace_packages`` A list of strings naming the project's "namespace packages". A namespace package is a package that may be split across multiple project @@ -1517,19 +1540,28 @@ The ``upload`` command has a few options worth noting: Extending and Reusing ``setuptools`` ------------------------------------ -Sorry, this section isn't written yet, and neither is a lot of what's below -this point, except for the change log. You might want to `subscribe to changes -in this page <setuptools?action=subscribe>`_ to see when new documentation is -added or updated. +Creating ``distutils`` Extensions +================================= + +It can be hard to add new commands or setup arguments to the distutils. But +the ``setuptools`` package makes it a bit easier, by allowing you to distribute +a distutils extension as a separate project, and then have projects that need +the extension just refer to it in their ``setup_requires`` argument. + +With ``setuptools``, your distutils extension projects can hook in new +commands and ``setup()`` arguments just by defining "entry points". These +are mappings from command or argument names to a specification of where to +import a handler from. (See the section on `Dynamic Discovery of Services and +Plugins`_ above for some more background on entry points.) Adding Commands -=============== +--------------- -You can create add-on packages that extend setuptools with additional commands -by defining entry points in the ``distutils.commands`` group. For example, if -you wanted to add a ``foo`` command, you might add something like this to your -setup script:: +You can add new ``setup`` commands by defining entry points in the +``distutils.commands`` group. For example, if you wanted to add a ``foo`` +command, you might add something like this to your distutils extension +project's setup script:: setup( # ... @@ -1540,8 +1572,8 @@ setup script:: }, ) -Assuming, of course, that the ``foo`` class in ``mypackage.some_module`` is -a ``setuptools.Command`` subclass. +(Assuming, of course, that the ``foo`` class in ``mypackage.some_module`` is +a ``setuptools.Command`` subclass.) Once a project containing such entry points has been activated on ``sys.path``, (e.g. by running "install" or "develop" with a site-packages installation @@ -1550,18 +1582,76 @@ scripts. It is not necessary to use the ``--command-packages`` option or to monkeypatch the ``distutils.command`` package to install your commands; ``setuptools`` automatically adds a wrapper to the distutils to search for entry points in the active distributions on ``sys.path``. In fact, this is -how setuptools' own commands are installed: the setuptools setup script defines -entry points for them. +how setuptools' own commands are installed: the setuptools project's setup +script defines entry points for them! + + +Adding ``setup()`` Arguments +---------------------------- + +Sometimes, your commands may need additional arguments to the ``setup()`` +script. You can enable this by defining entry points in the +``distutils.setup_keywords`` group. For example, if you wanted a ``setup()`` +argument called ``bar_baz``, you might add something like this to your +distutils extension project's setup script:: + + setup( + # ... + entry_points = { + "distutils.commands": [ + "foo = mypackage.some_module:foo", + ], + "distutils.setup_keywords": [ + "bar_baz = mypackage.some_module:validate_bar_baz", + ], + }, + ) + +The idea here is that the entry point defines a function that will be called +to validate the ``setup()`` argument, if it's supplied. The ``Distribution`` +object will have the initial value of the attribute set to ``None``, and the +validation function will only be called if the ``setup()`` call sets it to +a non-None value. Here's an example validation function:: + + def assert_bool(dist, attr, value): + """Verify that value is True, False, 0, or 1""" + if bool(value) != value: + raise DistutilsSetupError( + "%r must be a boolean value (got %r)" % (attr,value) + ) + +Your function should accept three arguments: the ``Distribution`` object, +the attribute name, and the attribute value. It should raise a +``DistutilsSetupError`` (from the ``distutils.error`` module) if the argument +is invalid. Remember, your function will only be called with non-None values, +and the default value of arguments defined this way is always None. So, your +commands should always be prepared for the possibility that the attribute will +be ``None`` when they access it later. + +If more than one active distribution defines an entry point for the same +``setup()`` argument, *all* of them will be called. This allows multiple +distutils extensions to define a common argument, as long as they agree on +what values of that argument are valid. + +Also note that as with commands, it is not necessary to subclass or monkeypatch +the distutils ``Distribution`` class in order to add your arguments; it is +sufficient to define the entry points in your extension, as long as the setup +script lists your extension in its ``setup_requires`` argument. Subclassing ``Command`` ----------------------- +Sorry, this section isn't written yet, and neither is a lot of what's below +this point, except for the change log. You might want to `subscribe to changes +in this page <setuptools?action=subscribe>`_ to see when new documentation is +added or updated. + XXX -Utility Modules -=============== +Reusing ``setuptools`` Code +=========================== ``ez_setup`` ------------ @@ -1604,14 +1694,24 @@ Release Notes/Change History * The ``sdist`` command now recognizes Subversion "deleted file" entries and does not include them in source distributions. - + + * ``setuptools`` now embeds itself more thoroughly into the distutils, so that + other distutils extensions (e.g. py2exe, py2app) will subclass setuptools' + versions of things, rather than the native distutils ones. + * Fixed some problems using ``pkg_resources`` w/PEP 302 loaders other than ``zipimport``, and the previously-broken "eager resource" support. * Fixed ``pkg_resources.resource_exists()`` not working correctly, along with some other resource API bugs. - * Added ``entry_points`` argument to ``setup()`` + * Added ``entry_points`` and ``setup_requires`` arguments to ``setup()``; + ``setup_requires`` allows you to automatically find and download packages + that are needed in order to *build* your project (as opposed to running it). + + * ``setuptools`` now finds its commands and ``setup()`` argument validators + using entry points, so that they are extensible by third-party packages. + See `Creating distutils Extensions`_ above for more details. * Many ``pkg_resources`` API changes and enhancements: diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 05c4a73e..eeb2975b 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -1,6 +1,6 @@ """Extensions to the 'distutils' for large or complex distributions""" +from setuptools.dist import Distribution, Feature, _get_unpatched import distutils.core, setuptools.command -from setuptools.dist import Distribution, Feature from setuptools.extension import Extension from setuptools.depends import Require from distutils.core import Command as _Command @@ -39,17 +39,9 @@ def find_packages(where='.', exclude=()): out = [item for item in out if not fnmatchcase(item,pat)] return out -def setup(**attrs): - """Do package setup - - This function takes the same arguments as 'distutils.core.setup()', except - that the default distribution class is 'setuptools.dist.Distribution'. See - that class' documentation for details on the new keyword arguments that it - makes available via this function. - """ - attrs.setdefault("distclass",Distribution) - return distutils.core.setup(**attrs) +setup = distutils.core.setup +_Command = _get_unpatched(_Command) class Command(_Command): __doc__ = _Command.__doc__ @@ -68,6 +60,14 @@ class Command(_Command): setattr(cmd,k,v) # update command with keywords return cmd +import distutils.core +distutils.core.Command = Command # we can't patch distutils.cmd, alas + + + + + + diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 8577230f..7d0a1473 100755 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -9,7 +9,6 @@ from distutils.errors import * from distutils import log from pkg_resources import parse_requirements, safe_name, \ safe_version, yield_lines, EntryPoint -from setuptools.dist import iter_distribution_names class egg_info(Command): @@ -39,6 +38,7 @@ class egg_info(Command): + def finalize_options (self): self.egg_name = safe_name(self.distribution.get_name()) self.egg_version = self.tagged_version() @@ -149,7 +149,7 @@ class egg_info(Command): def write_toplevel_names(self): pkgs = dict.fromkeys( [k.split('.',1)[0] - for k in iter_distribution_names(self.distribution) + for k in self.distribution.iter_distribution_names() ] ) toplevel = os.path.join(self.egg_info, "top_level.txt") @@ -164,12 +164,8 @@ class egg_info(Command): def write_or_delete_dist_arg(self, argname, filename=None): value = getattr(self.distribution, argname, None) - if value is None: - return - filename = filename or argname+'.txt' filename = os.path.join(self.egg_info,filename) - if value: log.info("writing %s", filename) if not self.dry_run: @@ -177,8 +173,12 @@ class egg_info(Command): f.write('\n'.join(value)) f.write('\n') f.close() - elif os.path.exists(filename): + if value is None: + log.warn( + "%s not set in setup(), but %s exists", argname, filename + ) + return log.info("deleting %s", filename) if not self.dry_run: os.unlink(filename) diff --git a/setuptools/dist.py b/setuptools/dist.py index 40234b4e..6d226d68 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -9,36 +9,118 @@ from setuptools.command.sdist import sdist from setuptools.command.install_lib import install_lib from distutils.errors import DistutilsOptionError, DistutilsPlatformError from distutils.errors import DistutilsSetupError -import setuptools, pkg_resources - -def get_command_class(self, command): - """Pluggable version of get_command_class()""" - if command in self.cmdclass: - return self.cmdclass[command] - - for ep in pkg_resources.iter_entry_points('distutils.commands',command): - self.cmdclass[command] = cmdclass = ep.load() - return cmdclass - else: - return _old_get_command_class(self, command) - -def print_commands(self): - for ep in pkg_resources.iter_entry_points('distutils.commands'): - if ep.name not in self.cmdclass: - cmdclass = ep.load(False) # don't require extras, we're not running - self.cmdclass[ep.name] = cmdclass - return _old_print_commands(self) - -for meth in 'print_commands', 'get_command_class': - if getattr(_Distribution,meth).im_func.func_globals is not globals(): - globals()['_old_'+meth] = getattr(_Distribution,meth) - setattr(_Distribution, meth, globals()[meth]) +import setuptools, pkg_resources, distutils.core, distutils.dist, distutils.cmd +import os + +def _get_unpatched(cls): + """Protect against re-patching the distutils if reloaded + + Also ensures that no other distutils extension monkeypatched the distutils + first. + """ + while cls.__module__.startswith('setuptools'): + cls, = cls.__bases__ + if not cls.__module__.startswith('distutils'): + raise AssertionError( + "distutils has already been patched by %r" % cls + ) + return cls + +_Distribution = _get_unpatched(_Distribution) sequence = tuple, list + + + + + + +def assert_string_list(dist, attr, value): + """Verify that value is a string list or None""" + try: + assert ''.join(value)!=value + except (TypeError,ValueError,AttributeError,AssertionError): + raise DistutilsSetupError( + "%r must be a list of strings (got %r)" % (attr,value) + ) + +def check_nsp(dist, attr, value): + """Verify that namespace packages are valid""" + assert_string_list(dist,attr,value) + + for nsp in value: + for name in dist.iter_distribution_names(): + if name.startswith(nsp+'.'): break + else: + raise DistutilsSetupError( + "Distribution contains no modules or packages for " + + "namespace package %r" % nsp + ) + +def check_extras(dist, attr, value): + """Verify that extras_require mapping is valid""" + try: + for k,v in value.items(): + list(pkg_resources.parse_requirements(v)) + except (TypeError,ValueError,AttributeError): + raise DistutilsSetupError( + "'extras_require' must be a dictionary whose values are " + "strings or lists of strings containing valid project/version " + "requirement specifiers." + ) + +def assert_bool(dist, attr, value): + """Verify that value is True, False, 0, or 1""" + if bool(value) != value: + raise DistutilsSetupError( + "%r must be a boolean value (got %r)" % (attr,value) + ) + +def check_install_requires(dist, attr, value): + """Verify that install_requires is a valid requirements list""" + try: + list(pkg_resources.parse_requirements(value)) + except (TypeError,ValueError): + raise DistutilsSetupError( + "'install_requires' must be a string or list of strings " + "containing valid project/version requirement specifiers" + ) + +def check_entry_points(dist, attr, value): + """Verify that entry_points map is parseable""" + try: + pkg_resources.EntryPoint.parse_map(value) + except ValueError, e: + raise DistutilsSetupError(e) + + +def check_test_suite(dist, attr, value): + if not isinstance(value,basestring): + raise DistutilsSetupError("test_suite must be a string") + + + + + + + + + + + + + + + + + + + + class Distribution(_Distribution): """Distribution with support for features, tests, and package data @@ -125,16 +207,19 @@ class Distribution(_Distribution): have_package_data = hasattr(self, "package_data") if not have_package_data: self.package_data = {} + self.features = {} - self.test_suite = None self.requires = [] - self.install_requires = [] - self.extras_require = {} self.dist_files = [] - self.zip_safe = None - self.namespace_packages = None - self.eager_resources = None - self.entry_points = None + + if attrs and 'setup_requires' in attrs: + # Make sure we have any eggs needed to interpret 'attrs' + self.fetch_build_eggs(attrs.pop('setup_requires')) + + for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): + if not hasattr(self,ep.name): + setattr(self,ep.name,None) + _Distribution.__init__(self,attrs) @@ -145,20 +230,17 @@ class Distribution(_Distribution): self._finalize_features() return result - def _feature_attrname(self,name): """Convert feature name to corresponding option attribute name""" return 'with_'+name.replace('-','_') - - - - - - - - - + def fetch_build_eggs(self, requires): + """Resolve pre-setup requirements""" + from pkg_resources import working_set, parse_requirements + for dist in working_set.resolve( + parse_requirements(requires), installer=self.fetch_build_egg + ): + working_set.add(dist) @@ -174,49 +256,34 @@ class Distribution(_Distribution): "setuptools. Please remove it from your setup script." ) - try: - list(pkg_resources.parse_requirements(self.install_requires)) - except (TypeError,ValueError): - raise DistutilsSetupError( - "'install_requires' must be a string or list of strings " - "containing valid project/version requirement specifiers" - ) + for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): + value = getattr(self,ep.name,None) + if value is not None: + ep.require(installer=self.fetch_build_egg) + ep.load()(self, ep.name, value) + + def fetch_build_egg(self, req): + """Fetch an egg needed for building""" try: - for k,v in self.extras_require.items(): - list(pkg_resources.parse_requirements(v)) - except (TypeError,ValueError,AttributeError): - raise DistutilsSetupError( - "'extras_require' must be a dictionary whose values are " - "strings or lists of strings containing valid project/version " - "requirement specifiers." + cmd = self._egg_fetcher + except AttributeError: + from setuptools.command.easy_install import easy_install + cmd = easy_install( + self.__class__({'script_args':['easy_install']}), + args="x", install_dir=os.curdir, exclude_scripts=True, + always_copy=False, build_directory=None, editable=False, + upgrade=False ) + cmd.ensure_finalized() + cmd.zip_ok = None # override any setup.cfg setting for these + cmd.build_directory = None + self._egg_fetcher = cmd - for attr in 'namespace_packages','eager_resources': - value = getattr(self,attr,None) - if value is not None: - try: - assert ''.join(value)!=value - except (TypeError,ValueError,AttributeError,AssertionError): - raise DistutilsSetupError( - "%r must be a list of strings (got %r)" % (attr,value) - ) + return cmd.easy_install(req) - for nsp in self.namespace_packages or (): - for name in iter_distribution_names(self): - if name.startswith(nsp+'.'): break - else: - raise DistutilsSetupError( - "Distribution contains no modules or packages for " + - "namespace package %r" % nsp - ) - if self.entry_points is not None: - try: - pkg_resources.EntryPoint.parse_map(self.entry_points) - except ValueError, e: - raise DistutilsSetupError(e) def _set_global_opts_from_features(self): """Add --with-X/--without-X options based on optional features""" @@ -244,22 +311,7 @@ class Distribution(_Distribution): - def _finalize_features(self): - """Add/remove features and resolve dependencies between them""" - - # First, flag all the enabled items (and thus their dependencies) - for name,feature in self.features.items(): - enabled = self.feature_is_included(name) - if enabled or (enabled is None and feature.include_by_default()): - feature.include_in(self) - self._set_feature(name,1) - # Then disable the rest, so that off-by-default features don't - # get flagged as errors when they're required by an enabled feature - for name,feature in self.features.items(): - if not self.feature_is_included(name): - feature.exclude_from(self) - self._set_feature(name,0) @@ -274,12 +326,42 @@ class Distribution(_Distribution): + def _finalize_features(self): + """Add/remove features and resolve dependencies between them""" + # First, flag all the enabled items (and thus their dependencies) + for name,feature in self.features.items(): + enabled = self.feature_is_included(name) + if enabled or (enabled is None and feature.include_by_default()): + feature.include_in(self) + self._set_feature(name,1) + # Then disable the rest, so that off-by-default features don't + # get flagged as errors when they're required by an enabled feature + for name,feature in self.features.items(): + if not self.feature_is_included(name): + feature.exclude_from(self) + self._set_feature(name,0) + def get_command_class(self, command): + """Pluggable version of get_command_class()""" + if command in self.cmdclass: + return self.cmdclass[command] + for ep in pkg_resources.iter_entry_points('distutils.commands',command): + ep.require(installer=self.fetch_build_egg) + self.cmdclass[command] = cmdclass = ep.load() + return cmdclass + else: + return _Distribution.get_command_class(self, command) + def print_commands(self): + for ep in pkg_resources.iter_entry_points('distutils.commands'): + if ep.name not in self.cmdclass: + cmdclass = ep.load(False) # don't require extras, we're not running + self.cmdclass[ep.name] = cmdclass + return _Distribution.print_commands(self) @@ -572,25 +654,25 @@ class Distribution(_Distribution): return d -def iter_distribution_names(distribution): - """Yield all packages, modules, and extensions declared by distribution""" - - for pkg in distribution.packages or (): - yield pkg - - for module in distribution.py_modules or (): - yield module - - for ext in distribution.ext_modules or (): - if isinstance(ext,tuple): - name,buildinfo = ext - yield name - else: - yield ext.name + def iter_distribution_names(self): + """Yield all packages, modules, and extension names in distribution""" + for pkg in self.packages or (): + yield pkg + for module in self.py_modules or (): + yield module + for ext in self.ext_modules or (): + if isinstance(ext,tuple): + name,buildinfo = ext + yield name + else: + yield ext.name +# Install it throughout the distutils +for module in distutils.dist, distutils.core, distutils.cmd: + module.Distribution = Distribution diff --git a/setuptools/extension.py b/setuptools/extension.py index 55a4d946..37b62576 100644 --- a/setuptools/extension.py +++ b/setuptools/extension.py @@ -7,6 +7,9 @@ except ImportError: # Pyrex isn't around, so fix up the sources + from dist import _get_unpatched + _Extension = _get_unpatched(_Extension) + class Extension(_Extension): """Extension that uses '.c' files in place of '.pyx' files""" @@ -21,7 +24,14 @@ except ImportError: sources.append(s) self.sources = sources + import sys, distutils.core, distutils.extension + distutils.core.Extension = Extension + distutils.extension.Extension = Extension + if 'distutils.command.build_ext' in sys.modules: + sys.modules['distutils.command.build_ext'].Extension = Extension + else: # Pyrex is here, just use regular extension type Extension = _Extension + |