diff options
author | PJ Eby <distutils-sig@python.org> | 2004-03-19 20:53:14 +0000 |
---|---|---|
committer | PJ Eby <distutils-sig@python.org> | 2004-03-19 20:53:14 +0000 |
commit | 8423e1ed14ac1691c2863c6e8cac9230cf558d7b (patch) | |
tree | 79f2d2cef146e08a9480357637cca4662307bd08 | |
download | external_python_setuptools-8423e1ed14ac1691c2863c6e8cac9230cf558d7b.tar.gz external_python_setuptools-8423e1ed14ac1691c2863c6e8cac9230cf558d7b.tar.bz2 external_python_setuptools-8423e1ed14ac1691c2863c6e8cac9230cf558d7b.zip |
Initial checkin of setuptools 0.0.1.
--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4040869
-rw-r--r-- | TODO.txt | 58 | ||||
-rwxr-xr-x | setup.py | 21 | ||||
-rw-r--r-- | setuptools/__init__.py | 82 | ||||
-rw-r--r-- | setuptools/command/__init__.py | 11 | ||||
-rw-r--r-- | setuptools/command/build_ext.py | 7 | ||||
-rw-r--r-- | setuptools/command/build_py.py | 123 | ||||
-rw-r--r-- | setuptools/command/depends.py | 27 | ||||
-rw-r--r-- | setuptools/command/install.py | 11 | ||||
-rw-r--r-- | setuptools/command/install_lib.py | 17 | ||||
-rw-r--r-- | setuptools/command/test.py | 82 | ||||
-rw-r--r-- | setuptools/depends.py | 246 | ||||
-rw-r--r-- | setuptools/dist.py | 453 | ||||
-rw-r--r-- | setuptools/extension.py | 27 | ||||
-rw-r--r-- | setuptools/tests/__init__.py | 410 | ||||
-rw-r--r-- | setuptools_boot.py | 123 |
15 files changed, 1698 insertions, 0 deletions
diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 00000000..de619c04 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,58 @@ +To-Do + +* Automatic download and installation of dependencies + + * install_deps command (install runtime dependencies) + + * compute child command line, abort if user specified incompatible options + + * OPEN ISSUE: should parent install command include child install's files? + + * Dependency class + + * Check for presence/version via file existence, regular expression match, + version comparison (using 'distutils.version' classes), installed on + sys.path, or require just installation directory + + * Find appropriate release, or explain why not + + * Base URL(s) and distribution name + + * Release class + + * Distro type - source v. binary (determine via extension?) + + * Platform requirements, whether compiler needed (how can we check?) + + * Download URL, default from extension + dependency + + * Download + extract to target dir + + * run child install + + * build_deps command (install build-time dependencies) + +* Build and install documentation sets + +* Installation database similar to PEP 262 + + * Needs to write file *before* installing anything, so an aborted install + can be uninstalled. Possibly should use 'unknown' for all metadata, then + replace with real metadata once it's known. + + * REQUIRES should probably just be list of dependencies + +* Bootstrap module + + The idea here is that you include the "bootstrap module" in your + distribution, and it downloads the right version of setuptools automatically + if a good-enough version isn't on sys.path. This would let you use + setuptools for your installer, without having to distribute the full + setuptools package. This would might look something like:: + + from boot_setuptools import require_version + require_version("0.6", "http://somewhere/setuptools-0.6.tar.gz") + + from setuptools import setup, Feature, findPackages + # ...etc + diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..1aadf759 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +"""Distutils setup file, used to install or test 'setuptools'""" + +from setuptools import setup, find_packages, Require + +setup( + name="setuptools", + version="0.0.1", + + description="Distutils enhancements", + author="Phillip J. Eby", + author_email="peak@eby-sarna.com", + license="PSF or ZPL", + + test_suite = 'setuptools.tests.test_suite', + requires = [Require('Distutils','1.0.3','distutils')], + packages = find_packages(), + py_modules = ['setuptools_boot'], +) + diff --git a/setuptools/__init__.py b/setuptools/__init__.py new file mode 100644 index 00000000..12b6b57c --- /dev/null +++ b/setuptools/__init__.py @@ -0,0 +1,82 @@ +"""Extensions to the 'distutils' for large or complex distributions""" + +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 +from distutils.util import convert_path +import os.path + +__version__ = '0.0.1' + +__all__ = [ + 'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require', + 'find_packages' +] + + +def find_packages(where='.'): + """Return a list all Python packages found within directory 'where' + + 'where' should be supplied as a "cross-platform" (i.e. URL-style) path; it + will be converted to the appropriate local path syntax. + """ + + out = [] + stack=[(convert_path(where), '')] + + while stack: + where,prefix = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where,name) + if (os.path.isdir(fn) and + os.path.isfile(os.path.join(fn,'__init__.py')) + ): + out.append(prefix+name); stack.append((fn,prefix+name+'.')) + 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) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py new file mode 100644 index 00000000..3429634c --- /dev/null +++ b/setuptools/command/__init__.py @@ -0,0 +1,11 @@ +import distutils.command + +__all__ = ['test', 'depends'] + + +# Make our commands available as though they were part of the distutils + +distutils.command.__path__.extend(__path__) +distutils.command.__all__.extend( + [cmd for cmd in __all__ if cmd not in distutils.command.__all__] +) diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py new file mode 100644 index 00000000..86ac13a9 --- /dev/null +++ b/setuptools/command/build_ext.py @@ -0,0 +1,7 @@ +# Attempt to use Pyrex for building extensions, if available + +try: + from Pyrex.Distutils.build_ext import build_ext +except ImportError: + from distutils.command.build_ext import build_ext + diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py new file mode 100644 index 00000000..7d5b6ffd --- /dev/null +++ b/setuptools/command/build_py.py @@ -0,0 +1,123 @@ +from distutils.command.build_py import build_py as _build_py +from distutils.util import convert_path +from glob import glob +import os.path + +class build_py(_build_py): + + """Enhanced 'build_py' command that includes data files with packages + + The data files are specified via a 'package_data' argument to 'setup()'. + See 'setuptools.dist.Distribution' for more details. + + Also, this version of the 'build_py' command allows you to specify both + 'py_modules' and 'packages' in the same setup operation. + """ + + def finalize_options(self): + _build_py.finalize_options(self) + self.package_data = self.distribution.package_data + self.data_files = self.get_data_files() + + + def run(self): + + """Build modules, packages, and copy data files to build directory""" + + if not self.py_modules and not self.packages: + return + + if self.py_modules: + self.build_modules() + + if self.packages: + self.build_packages() + self.build_package_data() + + # Only compile actual .py files, using our base class' idea of what our + # output files are. + self.byte_compile(_build_py.get_outputs(self,include_bytecode=0)) + + + def get_data_files(self): + + """Generate list of '(package,src_dir,build_dir,filenames)' tuples""" + + data = [] + + for package in self.packages: + # Locate package source directory + src_dir = self.get_package_dir(package) + + # Compute package build directory + build_dir = os.path.join(*([self.build_lib]+package.split('.'))) + + # Length of path to strip from found files + plen = len(src_dir)+1 + + # Strip directory from globbed filenames + filenames = [ + file[plen:] for file in self.find_data_files(package, src_dir) + ] + + data.append( (package, src_dir, build_dir, filenames) ) + + return data + + + def find_data_files(self, package, src_dir): + + """Return filenames for package's data files in 'src_dir'""" + + globs = self.package_data.get('',[])+self.package_data.get(package,[]) + files = [] + + for pattern in globs: + # Each pattern has to be converted to a platform-specific path + files.extend(glob(os.path.join(src_dir, convert_path(pattern)))) + + return files + + + + def build_package_data(self): + + """Copy data files into build directory""" + + lastdir = None + + for package, src_dir, build_dir, filenames in self.data_files: + + for filename in filenames: + target = os.path.join(build_dir,filename) + self.mkpath(os.path.dirname(target)) + self.copy_file(os.path.join(src_dir,filename), target) + + + def get_outputs(self, include_bytecode=1): + + """Return complete list of files copied to the build directory + + This includes both '.py' files and data files, as well as '.pyc' and + '.pyo' files if 'include_bytecode' is true. (This method is needed for + the 'install_lib' command to do its job properly, and to generate a + correct installation manifest.) + """ + + return _build_py.get_outputs(self,include_bytecode) + [ + os.path.join(build_dir,filename) + for package,src_dir,build_dir,filenames in self.data_files + for filename in filenames + ] + + + + + + + + + + + + diff --git a/setuptools/command/depends.py b/setuptools/command/depends.py new file mode 100644 index 00000000..e149faca --- /dev/null +++ b/setuptools/command/depends.py @@ -0,0 +1,27 @@ +from distutils.cmd import Command +import os + +class depends(Command): + """Download and install dependencies, if needed""" + + description = "download and install dependencies, if needed" + + user_options = [ + ('temp=', 't', + "directory where dependencies will be downloaded and built"), + ('ignore-extra-args', 'i', + "ignore options that won't be passed to child setup scripts"), + ] + + def initialize_options(self): + self.temp = None + self.install_purelib = self.install_platlib = None + self.install_lib = self.install_libbase = None + self.install_scripts = self.install_data = self.install_headers = None + self.compiler = self.debug = self.force = None + + def finalize_options(self): + self.set_undefined_options('build',('build_temp', 'temp')) + + def run(self): + self.announce("downloading and building here") diff --git a/setuptools/command/install.py b/setuptools/command/install.py new file mode 100644 index 00000000..82e7ebe8 --- /dev/null +++ b/setuptools/command/install.py @@ -0,0 +1,11 @@ +from distutils.command.install import install as _install + +class install(_install): + """Build dependencies before installation""" + + def has_dependencies(self): + return self.distribution.has_dependencies() + + sub_commands = [('depends',has_dependencies)] + _install.sub_commands + + diff --git a/setuptools/command/install_lib.py b/setuptools/command/install_lib.py new file mode 100644 index 00000000..ec406f7e --- /dev/null +++ b/setuptools/command/install_lib.py @@ -0,0 +1,17 @@ +from distutils.command.install_lib import install_lib as _install_lib + +class install_lib(_install_lib): + """Don't add compiled flags to filenames of non-Python files""" + + def _bytecode_filenames (self, py_filenames): + bytecode_files = [] + for py_file in py_filenames: + if not py_file.endswith('.py'): + continue + if self.compile: + bytecode_files.append(py_file + "c") + if self.optimize > 0: + bytecode_files.append(py_file + "o") + + return bytecode_files + diff --git a/setuptools/command/test.py b/setuptools/command/test.py new file mode 100644 index 00000000..6b37a9fd --- /dev/null +++ b/setuptools/command/test.py @@ -0,0 +1,82 @@ +from distutils.cmd import Command +from distutils.errors import DistutilsOptionError +import sys + +class test(Command): + + """Command to run unit tests after installation""" + + description = "run unit tests after installation" + + user_options = [ + ('test-module=','m', "Run 'test_suite' in specified module"), + ('test-suite=','s', + "Test suite to run (e.g. 'some_module.test_suite')"), + ] + + test_suite = None + test_module = None + + def initialize_options(self): + pass + + + def finalize_options(self): + + if self.test_suite is None: + if self.test_module is None: + self.test_suite = self.distribution.test_suite + else: + self.test_suite = self.test_module+".test_suite" + elif self.test_module: + raise DistutilsOptionError( + "You may specify a module or a suite, but not both" + ) + + self.test_args = [self.test_suite] + + if self.verbose: + self.test_args.insert(0,'--verbose') + + + def run(self): + + # Install before testing + self.run_command('install') + + if self.test_suite: + cmd = ' '.join(self.test_args) + + if self.dry_run: + self.announce('skipping "unittest %s" (dry run)' % cmd) + else: + self.announce('running "unittest %s"' % cmd) + import unittest + unittest.main(None, None, [unittest.__file__]+self.test_args) + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setuptools/depends.py b/setuptools/depends.py new file mode 100644 index 00000000..c3bc3334 --- /dev/null +++ b/setuptools/depends.py @@ -0,0 +1,246 @@ +from __future__ import generators +import sys, imp, marshal +from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN +from distutils.version import StrictVersion, LooseVersion + +__all__ = [ + 'Require', 'find_module', 'get_module_constant', 'extract_constant' +] + +class Require: + """A prerequisite to building or installing a distribution""" + + def __init__(self,name,requested_version,module,attribute=None,format=None): + + if format is None and requested_version is not None: + format = StrictVersion + + if format is not None: + requested_version = format(requested_version) + if attribute is None: + attribute = '__version__' + + self.name = name + self.requested_version = requested_version + self.module = module + self.attribute = attribute + self.format = format + + + + + + + + + + + + + + + def get_version(self, paths=None, default="unknown"): + + """Get version number of installed module, 'None', or 'default' + + Search 'paths' for module. If not found, return 'None'. If found, + return the extracted version attribute, or 'default' if no version + attribute was specified, or the value cannot be determined without + importing the module. The version is formatted according to the + requirement's version format (if any), unless it is 'None' or the + supplied 'default'. + """ + + if self.attribute is None: + try: + f,p,i = find_module(self.module,paths) + if f: f.close() + return default + except ImportError: + return None + + v = get_module_constant(self.module,self.attribute,default,paths) + + if v is not None and v is not default and self.format is not None: + return self.format(v) + + return v + + def is_present(self,paths=None): + """Return true if dependency is present on 'paths'""" + return self.get_version(paths) is not None + + def is_current(self,paths=None): + """Return true if dependency is present and up-to-date on 'paths'""" + version = self.get_version(paths) + if version is None: + return False + return self.attribute is None or self.format is None or \ + version >= self.requested_version + + + +def _iter_code(code): + + """Yield '(op,arg)' pair for each operation in code object 'code'""" + + from array import array + from dis import HAVE_ARGUMENT, EXTENDED_ARG + + bytes = array('b',code.co_code) + eof = len(code.co_code) + + ptr = 0 + extended_arg = 0 + + while ptr<eof: + + op = bytes[ptr] + + if op>=HAVE_ARGUMENT: + + arg = bytes[ptr+1] + bytes[ptr+2]*256 + extended_arg + ptr += 3 + + if op==EXTENDED_ARG: + extended_arg = arg * 65536L + continue + + else: + arg = None + ptr += 1 + + yield op,arg + + + + + + + + + + +def find_module(module, paths=None): + """Just like 'imp.find_module()', but with package support""" + + parts = module.split('.') + + while parts: + part = parts.pop(0) + f, path, (suffix,mode,kind) = info = imp.find_module(part, paths) + + if kind==PKG_DIRECTORY: + parts = parts or ['__init__'] + paths = [path] + + elif parts: + raise ImportError("Can't find %r in %s" % (parts,module)) + + return info + + + + + + + + + + + + + + + + + + + + + + + + +def get_module_constant(module, symbol, default=-1, paths=None): + + """Find 'module' by searching 'paths', and extract 'symbol' + + Return 'None' if 'module' does not exist on 'paths', or it does not define + 'symbol'. If the module defines 'symbol' as a constant, return the + constant. Otherwise, return 'default'.""" + + try: + f, path, (suffix,mode,kind) = find_module(module,paths) + except ImportError: + # Module doesn't exist + return None + + try: + if kind==PY_COMPILED: + f.read(8) # skip magic & date + code = marshal.load(f) + elif kind==PY_FROZEN: + code = imp.get_frozen_object(module) + elif kind==PY_SOURCE: + code = compile(f.read(), path, 'exec') + else: + # Not something we can parse; we'll have to import it. :( + if module not in sys.modules: + imp.load_module(module,f,path,(suffix,mode,kind)) + return getattr(sys.modules[module],symbol,None) + + finally: + if f: + f.close() + + return extract_constant(code,symbol,default) + + + + + + + + +def extract_constant(code,symbol,default=-1): + + """Extract the constant value of 'symbol' from 'code' + + If the name 'symbol' is bound to a constant value by the Python code + object 'code', return that value. If 'symbol' is bound to an expression, + return 'default'. Otherwise, return 'None'. + + Return value is based on the first assignment to 'symbol'. 'symbol' must + be a global, or at least a non-"fast" local in the code block. That is, + only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol' + must be present in 'code.co_names'. + """ + + if symbol not in code.co_names: + # name's not there, can't possibly be an assigment + return None + + name_idx = list(code.co_names).index(symbol) + + STORE_NAME = 90 + STORE_GLOBAL = 97 + LOAD_CONST = 100 + + const = default + + for op, arg in _iter_code(code): + + if op==LOAD_CONST: + const = code.co_consts[arg] + elif arg==name_idx and (op==STORE_NAME or op==STORE_GLOBAL): + return const + else: + const = default + + + + + + + diff --git a/setuptools/dist.py b/setuptools/dist.py new file mode 100644 index 00000000..2941f26e --- /dev/null +++ b/setuptools/dist.py @@ -0,0 +1,453 @@ +__all__ = ['Distribution', 'Feature'] + +from distutils.core import Distribution as _Distribution +from distutils.core import Extension +from setuptools.command.build_py import build_py +from setuptools.command.build_ext import build_ext +from setuptools.command.install import install +from setuptools.command.install_lib import install_lib +from distutils.errors import DistutilsOptionError, DistutilsPlatformError +from distutils.errors import DistutilsSetupError +sequence = tuple, list + +class Distribution(_Distribution): + + """Distribution with support for features, tests, and package data + + This is an enhanced version of 'distutils.dist.Distribution' that + effectively adds the following new optional keyword arguments to 'setup()': + + 'features' -- a dictionary mapping option names to 'setuptools.Feature' + objects. Features are a portion of the distribution that can be + included or excluded based on user options, inter-feature dependencies, + and availability on the current system. Excluded features are omitted + from all setup commands, including source and binary distributions, so + you can create multiple distributions from the same source tree. + + Feature names should be valid Python identifiers, except that they may + contain the '-' (minus) sign. Features can be included or excluded + via the command line options '--with-X' and '--without-X', where 'X' is + the name of the feature. Whether a feature is included by default, and + whether you are allowed to control this from the command line, is + determined by the Feature object. See the 'Feature' class for more + information. + + 'test_suite' -- the name of a test suite to run for the 'test' command. + If the user runs 'python setup.py test', the package will be installed, + and the named test suite will be run. The format is the same as + would be used on a 'unittest.py' command line. That is, it is the + dotted name of an object to import and call to generate a test suite. + + 'package_data' -- a dictionary mapping package names to lists of filenames + or globs to use to find data files contained in the named packages. + If the dictionary has filenames or globs listed under '""' (the empty + string), those names will be searched for in every package, in addition + to any names for the specific package. Data files found using these + names/globs will be installed along with the package, in the same + location as the package. Note that globs are allowed to reference + the contents of non-package subdirectories, as long as you use '/' as + a path separator. (Globs are automatically converted to + platform-specific paths at runtime.) + + In addition to these new keywords, this class also has several new methods + for manipulating the distribution's contents. For example, the 'include()' + and 'exclude()' methods can be thought of as in-place add and subtract + commands that add or remove packages, modules, extensions, and so on from + the distribution. They are used by the feature subsystem to configure the + distribution for the included and excluded features. + """ + + def __init__ (self, attrs=None): + self.features = {} + self.package_data = {} + self.test_suite = None + self.requires = [] + _Distribution.__init__(self,attrs) + self.cmdclass.setdefault('build_py',build_py) + self.cmdclass.setdefault('build_ext',build_ext) + self.cmdclass.setdefault('install',install) + self.cmdclass.setdefault('install_lib',install_lib) + + if self.features: + self._set_global_opts_from_features() + + def parse_command_line(self): + """Process features after parsing command line options""" + result = _Distribution.parse_command_line(self) + if self.features: + self._finalize_features() + return result + + def _feature_attrname(self,name): + """Convert feature name to corresponding option attribute name""" + return 'with_'+name.replace('-','_') + + def _set_global_opts_from_features(self): + """Add --with-X/--without-X options based on optional features""" + + go = [] + no = self.negative_opt.copy() + + for name,feature in self.features.items(): + self._set_feature(name,None) + feature.validate(self) + + if feature.optional: + descr = feature.description + incdef = ' (default)' + excdef='' + if not feature.include_by_default(): + excdef, incdef = incdef, excdef + + go.append(('with-'+name, None, 'include '+descr+incdef)) + go.append(('without-'+name, None, 'exclude '+descr+excdef)) + no['without-'+name] = 'with-'+name + + self.global_options = self.feature_options = go + self.global_options + self.negative_opt = self.feature_negopt = no + + 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 _set_feature(self,name,status): + """Set feature's inclusion status""" + setattr(self,self._feature_attrname(name),status) + + def feature_is_included(self,name): + """Return 1 if feature is included, 0 if excluded, 'None' if unknown""" + return getattr(self,self._feature_attrname(name)) + + def include_feature(self,name): + """Request inclusion of feature named 'name'""" + + if self.feature_is_included(name)==0: + descr = self.features[name].description + raise DistutilsOptionError( + descr + " is required, but was excluded or is not available" + ) + self.features[name].include_in(self) + self._set_feature(name,1) + + def include(self,**attrs): + """Add items to distribution that are named in keyword arguments + + For example, 'dist.exclude(py_modules=["x"])' would add 'x' to + the distribution's 'py_modules' attribute, if it was not already + there. + + Currently, this method only supports inclusion for attributes that are + lists or tuples. If you need to add support for adding to other + attributes in this or a subclass, you can add an '_include_X' method, + where 'X' is the name of the attribute. The method will be called with + the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})' + will try to call 'dist._include_foo({"bar":"baz"})', which can then + handle whatever special inclusion logic is needed. + """ + for k,v in attrs.items(): + include = getattr(self, '_include_'+k, None) + if include: + include(v) + else: + self._include_misc(k,v) + + def exclude_package(self,package): + """Remove packages, modules, and extensions in named package""" + + pfx = package+'.' + if self.packages: + self.packages = [ + p for p in self.packages + if p<>package and not p.startswith(pfx) + ] + + if self.py_modules: + self.py_modules = [ + p for p in self.py_modules + if p<>package and not p.startswith(pfx) + ] + + if self.ext_modules: + self.ext_modules = [ + p for p in self.ext_modules + if p.name<>package and not p.name.startswith(pfx) + ] + + + def has_contents_for(self,package): + """Return true if 'exclude_package(package)' would do something""" + + pfx = package+'.' + + for p in self.packages or (): + if p==package or p.startswith(pfx): + return True + + for p in self.py_modules or (): + if p==package or p.startswith(pfx): + return True + + for p in self.ext_modules or (): + if p.name==package or p.name.startswith(pfx): + return True + + + def _exclude_misc(self,name,value): + """Handle 'exclude()' for list/tuple attrs without a special handler""" + if not isinstance(value,sequence): + raise DistutilsSetupError( + "%s: setting must be a list or tuple (%r)" % (name, value) + ) + try: + old = getattr(self,name) + except AttributeError: + raise DistutilsSetupError( + "%s: No such distribution setting" % name + ) + if old is not None and not isinstance(old,sequence): + raise DistutilsSetupError( + name+": this setting cannot be changed via include/exclude" + ) + elif old: + setattr(self,name,[item for item in old if item not in value]) + + def _include_misc(self,name,value): + """Handle 'include()' for list/tuple attrs without a special handler""" + + if not isinstance(value,sequence): + raise DistutilsSetupError( + "%s: setting must be a list (%r)" % (name, value) + ) + try: + old = getattr(self,name) + except AttributeError: + raise DistutilsSetupError( + "%s: No such distribution setting" % name + ) + if old is None: + setattr(self,name,value) + elif not isinstance(old,sequence): + raise DistutilsSetupError( + name+": this setting cannot be changed via include/exclude" + ) + else: + setattr(self,name,old+[item for item in value if item not in old]) + + def exclude(self,**attrs): + """Remove items from distribution that are named in keyword arguments + + For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from + the distribution's 'py_modules' attribute. Excluding packages uses + the 'exclude_package()' method, so all of the package's contained + packages, modules, and extensions are also excluded. + + Currently, this method only supports exclusion from attributes that are + lists or tuples. If you need to add support for excluding from other + attributes in this or a subclass, you can add an '_exclude_X' method, + where 'X' is the name of the attribute. The method will be called with + the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})' + will try to call 'dist._exclude_foo({"bar":"baz"})', which can then + handle whatever special exclusion logic is needed. + """ + for k,v in attrs.items(): + exclude = getattr(self, '_exclude_'+k, None) + if exclude: + exclude(v) + else: + self._exclude_misc(k,v) + + def _exclude_packages(self,packages): + if not isinstance(packages,sequence): + raise DistutilsSetupError( + "packages: setting must be a list or tuple (%r)" % (packages,) + ) + map(self.exclude_package, packages) + + def _parse_command_opts(self, parser, args): + # Remove --with-X/--without-X options when processing command args + self.global_options = self.__class__.global_options + self.negative_opt = self.__class__.negative_opt + return _Distribution._parse_command_opts(self, parser, args) + + def has_dependencies(self): + return not not self.requires + + + +class Feature: + + """A subset of the distribution that can be excluded if unneeded/wanted + + Features are created using these keyword arguments: + + 'description' -- a short, human readable description of the feature, to + be used in error messages, and option help messages. + + 'standard' -- if true, the feature is included by default if it is + available on the current system. Otherwise, the feature is only + included if requested via a command line '--with-X' option, or if + another included feature requires it. The default setting is 'False'. + + 'available' -- if true, the feature is available for installation on the + current system. The default setting is 'True'. + + 'optional' -- if true, the feature's inclusion can be controlled from the + command line, using the '--with-X' or '--without-X' options. If + false, the feature's inclusion status is determined automatically, + based on 'availabile', 'standard', and whether any other feature + requires it. The default setting is 'True'. + + 'requires' -- a string or sequence of strings naming features that should + also be included if this feature is included. Defaults to empty list. + + 'remove' -- a string or list of strings naming packages to be removed + from the distribution if this feature is *not* included. If the + feature *is* included, this argument is ignored. This argument exists + to support removing features that "crosscut" a distribution, such as + defining a 'tests' feature that removes all the 'tests' subpackages + provided by other features. The default for this argument is an empty + list. (Note: the named package(s) or modules must exist in the base + distribution when the 'setup()' function is initially called.) + + other keywords -- any other keyword arguments are saved, and passed to + the distribution's 'include()' and 'exclude()' methods when the + feature is included or excluded, respectively. So, for example, you + could pass 'packages=["a","b"]' to cause packages 'a' and 'b' to be + added or removed from the distribution as appropriate. + + A feature must include at least one 'requires', 'remove', or other + keyword argument. Otherwise, it can't affect the distribution in any way. + Note also that you can subclass 'Feature' to create your own specialized + feature types that modify the distribution in other ways when included or + excluded. See the docstrings for the various methods here for more detail. + Aside from the methods, the only feature attributes that distributions look + at are 'description' and 'optional'. + """ + + def __init__(self, description, standard=False, available=True, + optional=True, requires=(), remove=(), **extras + ): + + self.description = description + self.standard = standard + self.available = available + self.optional = optional + + if isinstance(requires,str): + requires = requires, + + self.requires = requires + + if isinstance(remove,str): + remove = remove, + + self.remove = remove + self.extras = extras + + if not remove and not requires and not extras: + raise DistutilsSetupError( + "Feature %s: must define 'requires', 'remove', or at least one" + " of 'packages', 'py_modules', etc." + ) + + + def include_by_default(self): + """Should this feature be included by default?""" + return self.available and self.standard + + + def include_in(self,dist): + + """Ensure feature and its requirements are included in distribution + + You may override this in a subclass to perform additional operations on + the distribution. Note that this method may be called more than once + per feature, and so should be idempotent. + + """ + + if not self.available: + raise DistutilsPlatformError( + self.description+" is required," + "but is not available on this platform" + ) + + dist.include(**self.extras) + + for f in self.requires: + dist.include_feature(f) + + + + def exclude_from(self,dist): + + """Ensure feature is excluded from distribution + + You may override this in a subclass to perform additional operations on + the distribution. This method will be called at most once per + feature, and only after all included features have been asked to + include themselves. + """ + + dist.exclude(**self.extras) + + if self.remove: + for item in self.remove: + dist.exclude_package(item) + + + + def validate(self,dist): + + """Verify that feature makes sense in context of distribution + + This method is called by the distribution just before it parses its + command line. It checks to ensure that the 'remove' attribute, if any, + contains only valid package/module names that are present in the base + distribution when 'setup()' is called. You may override it in a + subclass to perform any other required validation of the feature + against a target distribution. + """ + + for item in self.remove: + if not dist.has_contents_for(item): + raise DistutilsSetupError( + "%s wants to be able to remove %s, but the distribution" + " doesn't contain any packages or modules under %s" + % (self.description, item, item) + ) + + + + + + + + + + + + + + + + + + + + + + diff --git a/setuptools/extension.py b/setuptools/extension.py new file mode 100644 index 00000000..55a4d946 --- /dev/null +++ b/setuptools/extension.py @@ -0,0 +1,27 @@ +from distutils.core import Extension as _Extension + +try: + from Pyrex.Distutils.build_ext import build_ext + +except ImportError: + + # Pyrex isn't around, so fix up the sources + + class Extension(_Extension): + + """Extension that uses '.c' files in place of '.pyx' files""" + + def __init__(self,*args,**kw): + _Extension.__init__(self,*args,**kw) + sources = [] + for s in self.sources: + if s.endswith('.pyx'): + sources.append(s[:-3]+'c') + else: + sources.append(s) + self.sources = sources + +else: + + # Pyrex is here, just use regular extension type + Extension = _Extension diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py new file mode 100644 index 00000000..662d4ec1 --- /dev/null +++ b/setuptools/tests/__init__.py @@ -0,0 +1,410 @@ +"""Tests for the 'setuptools' package""" + +from unittest import TestSuite, TestCase, makeSuite +import distutils.core, distutils.cmd +from distutils.errors import DistutilsOptionError, DistutilsPlatformError +from distutils.errors import DistutilsSetupError +import setuptools, setuptools.dist +from setuptools import Feature +from distutils.core import Extension +from setuptools.depends import extract_constant, get_module_constant +from setuptools.depends import find_module, Require +from distutils.version import StrictVersion, LooseVersion + +import sys, os.path + +def makeSetup(**args): + """Return distribution from 'setup(**args)', without executing commands""" + + distutils.core._setup_stop_after = "commandline" + + # Don't let system command line leak into tests! + args.setdefault('script_args',['install']) + + try: + return setuptools.setup(**args) + finally: + distutils.core_setup_stop_after = None + + + + + + + + + + + + + + +class DependsTests(TestCase): + + def testExtractConst(self): + + from setuptools.depends import extract_constant + + def f1(): + global x,y,z + x = "test" + y = z + + # unrecognized name + self.assertEqual(extract_constant(f1.func_code,'q', -1), None) + + # constant assigned + self.assertEqual(extract_constant(f1.func_code,'x', -1), "test") + + # expression assigned + self.assertEqual(extract_constant(f1.func_code,'y', -1), -1) + + # recognized name, not assigned + self.assertEqual(extract_constant(f1.func_code,'z', -1), None) + + + def testFindModule(self): + self.assertRaises(ImportError, find_module, 'no-such.-thing') + self.assertRaises(ImportError, find_module, 'setuptools.non-existent') + f,p,i = find_module('setuptools.tests'); f.close() + + def testModuleExtract(self): + from distutils import __version__ + self.assertEqual( + get_module_constant('distutils','__version__'), __version__ + ) + self.assertEqual( + get_module_constant('sys','version'), sys.version + ) + self.assertEqual( + get_module_constant('setuptools.tests','__doc__'),__doc__ + ) + + def testDependsCmd(self): + dist = makeSetup() + cmd = dist.get_command_obj('depends') + cmd.ensure_finalized() + self.assertEqual(cmd.temp, dist.get_command_obj('build').build_temp) + self.assertEqual(cmd.install_lib, dist.get_command_obj('install').install_lib) + + + def testRequire(self): + req = Require('Distutils','1.0.3','distutils') + + self.assertEqual(req.name, 'Distutils') + self.assertEqual(req.module, 'distutils') + self.assertEqual(req.requested_version, '1.0.3') + self.assertEqual(req.attribute, '__version__') + + from distutils import __version__ + self.assertEqual(req.get_version(), __version__) + + self.failUnless(req.is_present()) + self.failUnless(req.is_current()) + + req = Require('Distutils 3000','03000','distutils',format=LooseVersion) + self.failUnless(req.is_present()) + self.failIf(req.is_current()) + + req = Require('Do-what-I-mean','1.0','d-w-i-m') + self.failIf(req.is_present()) + self.failIf(req.is_current()) + + req = Require('Tests', None, 'tests') + self.assertEqual(req.format, None) + self.assertEqual(req.attribute, None) + self.assertEqual(req.requested_version, None) + + paths = [os.path.dirname(p) for p in __path__] + self.failUnless(req.is_present(paths)) + self.failUnless(req.is_current(paths)) + + + +class DistroTests(TestCase): + + def setUp(self): + self.e1 = Extension('bar.ext',['bar.c']) + self.e2 = Extension('c.y', ['y.c']) + + self.dist = makeSetup( + packages=['a', 'a.b', 'a.b.c', 'b', 'c'], + py_modules=['b.d','x'], + ext_modules = (self.e1, self.e2), + package_dir = {}, + ) + + + def testDistroType(self): + self.failUnless(isinstance(self.dist,setuptools.dist.Distribution)) + + + def testExcludePackage(self): + self.dist.exclude_package('a') + self.assertEqual(self.dist.packages, ['b','c']) + + self.dist.exclude_package('b') + self.assertEqual(self.dist.packages, ['c']) + self.assertEqual(self.dist.py_modules, ['x']) + self.assertEqual(self.dist.ext_modules, [self.e1, self.e2]) + + self.dist.exclude_package('c') + self.assertEqual(self.dist.packages, []) + self.assertEqual(self.dist.py_modules, ['x']) + self.assertEqual(self.dist.ext_modules, [self.e1]) + + # test removals from unspecified options + makeSetup().exclude_package('x') + + + + + + + + def testIncludeExclude(self): + # remove an extension + self.dist.exclude(ext_modules=[self.e1]) + self.assertEqual(self.dist.ext_modules, [self.e2]) + + # add it back in + self.dist.include(ext_modules=[self.e1]) + self.assertEqual(self.dist.ext_modules, [self.e2, self.e1]) + + # should not add duplicate + self.dist.include(ext_modules=[self.e1]) + self.assertEqual(self.dist.ext_modules, [self.e2, self.e1]) + + def testExcludePackages(self): + self.dist.exclude(packages=['c','b','a']) + self.assertEqual(self.dist.packages, []) + self.assertEqual(self.dist.py_modules, ['x']) + self.assertEqual(self.dist.ext_modules, [self.e1]) + + def testEmpty(self): + dist = makeSetup() + dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) + dist = makeSetup() + dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2]) + + def testContents(self): + self.failUnless(self.dist.has_contents_for('a')) + self.dist.exclude_package('a') + self.failIf(self.dist.has_contents_for('a')) + + self.failUnless(self.dist.has_contents_for('b')) + self.dist.exclude_package('b') + self.failIf(self.dist.has_contents_for('b')) + + self.failUnless(self.dist.has_contents_for('c')) + self.dist.exclude_package('c') + self.failIf(self.dist.has_contents_for('c')) + + + + + def testInvalidIncludeExclude(self): + self.assertRaises(DistutilsSetupError, + self.dist.include, nonexistent_option='x' + ) + self.assertRaises(DistutilsSetupError, + self.dist.exclude, nonexistent_option='x' + ) + self.assertRaises(DistutilsSetupError, + self.dist.include, packages={'x':'y'} + ) + self.assertRaises(DistutilsSetupError, + self.dist.exclude, packages={'x':'y'} + ) + self.assertRaises(DistutilsSetupError, + self.dist.include, ext_modules={'x':'y'} + ) + self.assertRaises(DistutilsSetupError, + self.dist.exclude, ext_modules={'x':'y'} + ) + + self.assertRaises(DistutilsSetupError, + self.dist.include, package_dir=['q'] + ) + self.assertRaises(DistutilsSetupError, + self.dist.exclude, package_dir=['q'] + ) + + + + + + + + + + + + + + + +class FeatureTests(TestCase): + + def setUp(self): + self.dist = makeSetup( + features={ + 'foo': Feature("foo",standard=True,requires='baz'), + 'bar': Feature("bar", standard=True, packages=['pkg.bar'], + py_modules=['bar_et'], remove=['bar.ext'], + ), + 'baz': Feature( + "baz", optional=False, packages=['pkg.baz'], + scripts = ['scripts/baz_it'], + libraries=[('libfoo','foo/foofoo.c')] + ), + 'dwim': Feature("DWIM", available=False, remove='bazish'), + }, + script_args=['--without-bar', 'install'], + packages = ['pkg.bar', 'pkg.foo'], + py_modules = ['bar_et', 'bazish'], + ext_modules = [Extension('bar.ext',['bar.c'])] + ) + + def testDefaults(self): + self.failIf( + Feature( + "test",standard=True,remove='x',available=False + ).include_by_default() + ) + self.failUnless( + Feature("test",standard=True,remove='x').include_by_default() + ) + # Feature must have either kwargs, removes, or requires + self.assertRaises(DistutilsSetupError, Feature, "test") + + def testAvailability(self): + self.assertRaises( + DistutilsPlatformError, + self.dist.features['dwim'].include_in, self.dist + ) + + + def testFeatureOptions(self): + dist = self.dist + self.failUnless( + ('with-dwim',None,'include DWIM') in dist.feature_options + ) + self.failUnless( + ('without-dwim',None,'exclude DWIM (default)') in dist.feature_options + ) + self.failUnless( + ('with-bar',None,'include bar (default)') in dist.feature_options + ) + self.failUnless( + ('without-bar',None,'exclude bar') in dist.feature_options + ) + self.assertEqual(dist.feature_negopt['without-foo'],'with-foo') + self.assertEqual(dist.feature_negopt['without-bar'],'with-bar') + self.assertEqual(dist.feature_negopt['without-dwim'],'with-dwim') + self.failIf('without-baz' in dist.feature_negopt) + + def testUseFeatures(self): + dist = self.dist + self.assertEqual(dist.with_foo,1) + self.assertEqual(dist.with_bar,0) + self.assertEqual(dist.with_baz,1) + + self.failIf('bar_et' in dist.py_modules) + self.failIf('pkg.bar' in dist.packages) + self.failUnless('pkg.baz' in dist.packages) + self.failUnless('scripts/baz_it' in dist.scripts) + self.failUnless(('libfoo','foo/foofoo.c') in dist.libraries) + self.assertEqual(dist.ext_modules,[]) + + # If we ask for bar, it should fail because we explicitly disabled + # it on the command line + self.assertRaises(DistutilsOptionError, dist.include_feature, 'bar') + + def testFeatureWithInvalidRemove(self): + self.assertRaises( + SystemExit, makeSetup, features = {'x':Feature('x', remove='y')} + ) + +class TestCommandTests(TestCase): + + def testTestIsCommand(self): + test_cmd = makeSetup().get_command_obj('test') + self.failUnless(isinstance(test_cmd, distutils.cmd.Command)) + + def testLongOptSuiteWNoDefault(self): + ts1 = makeSetup(script_args=['test','--test-suite=foo.tests.suite']) + ts1 = ts1.get_command_obj('test') + ts1.ensure_finalized() + self.assertEqual(ts1.test_suite, 'foo.tests.suite') + + def testDefaultSuite(self): + ts2 = makeSetup(test_suite='bar.tests.suite').get_command_obj('test') + ts2.ensure_finalized() + self.assertEqual(ts2.test_suite, 'bar.tests.suite') + + def testDefaultWModuleOnCmdLine(self): + ts3 = makeSetup( + test_suite='bar.tests', + script_args=['test','-m','foo.tests'] + ).get_command_obj('test') + ts3.ensure_finalized() + self.assertEqual(ts3.test_module, 'foo.tests') + self.assertEqual(ts3.test_suite, 'foo.tests.test_suite') + + def testConflictingOptions(self): + ts4 = makeSetup( + script_args=['test','-m','bar.tests', '-s','foo.tests.suite'] + ).get_command_obj('test') + self.assertRaises(DistutilsOptionError, ts4.ensure_finalized) + + def testNoSuite(self): + ts5 = makeSetup().get_command_obj('test') + ts5.ensure_finalized() + self.assertEqual(ts5.test_suite, None) + + + + + +testClasses = (DependsTests, DistroTests, FeatureTests, TestCommandTests) + +def test_suite(): + return TestSuite([makeSuite(t,'test') for t in testClasses]) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setuptools_boot.py b/setuptools_boot.py new file mode 100644 index 00000000..c13b6b3f --- /dev/null +++ b/setuptools_boot.py @@ -0,0 +1,123 @@ +"""Bootstrap module to download/quasi-install 'setuptools' package + +Usage:: + + from setuptools_boot import require_version + require_version('0.0.1') + + from setuptools import setup, Extension, ... + +Note that if a suitable version of 'setuptools' is not found on 'sys.path', +it will be downloaded and installed to the current directory. This means +that if you are using 'setuptools.find_packages()' in the same directory, you +will need to exclude the setuptools package from the distribution (unless you +want setuptools to be installed as part of your distribution). To do this, +you can simply use: + + setup( + # ... + packages = [pkg for pkg in find_packages() + if not pkg.startswith('setuptools') + ], + # ... + ) + +to eliminate the setuptools packages from consideration. However, if you are +using a 'lib' or 'src' directory to contain your distribution's packages, this +will not be an issue. +""" + +from distutils.version import StrictVersion +from distutils.util import convert_path +import os.path + +__all__ = ['require_version'] + + + + + + + +def require_version(version='0.0.1', dlbase='file:../../setuptools/dist'): + """Request to use setuptools of specified version + + 'dlbase', if provided, is the base URL that should be used to download + a particular version of setuptools. '/setuptools-VERSION.zip' will be + added to 'dlbase' to construct the download URL, if a download is needed. + + XXX current dlbase works for local testing only + """ + + if StrictVersion(version) > get_installed_version(): + unload_setuptools() + download_setuptools(version,dlbase) + + if StrictVersion(version) > get_installed_version(): + # Should never get here + raise SystemExit( + "Downloaded new version of setuptools, but it's not on sys.path" + ) + + +def get_installed_version(): + """Return version of currently-installed setuptools, or '"0.0.0"'""" + try: + from setuptools import __version__ + return __version__ + except ImportError: + return '0.0.0' + + +def download_setuptools(version,dlbase): + """Download setuptools-VERSION.zip from dlbase and extract in local dir""" + basename = 'setuptools-%s' % version + filename = basename+'.zip' + url = '%s/%s' % (dlbase,filename) + download_file(url,filename) + extract_zipdir(filename,basename+'/setuptools','setuptools') + + + + +def unload_setuptools(): + """Unload the current (outdated) 'setuptools' version from memory""" + import sys + for k in sys.modules.keys(): + if k.startswith('setuptools.') or k=='setuptools': + del sys.modules[k] + + +def download_file(url,filename): + """Download 'url', saving to 'filename'""" + from urllib2 import urlopen + f = urlopen(url); bytes = f.read(); f.close() + f = open(filename,'wb'); f.write(bytes); f.close() + + +def extract_zipdir(filename,zipdir,targetdir): + """Unpack zipfile 'filename', extracting 'zipdir' to 'targetdir'""" + + from zipfile import ZipFile + + f = ZipFile(filename) + if zipdir and not zipdir.endswith('/'): + zipdir+='/' + plen = len(zipdir) + paths = [ + path for path in f.namelist() + if path.startswith(zipdir) and not path.endswith('/') + ] + paths.sort() + paths.reverse() # unpack in reverse order so __init__ goes last! + + for path in paths: + out = os.path.join(targetdir,convert_path(path[plen:])) + dir = os.path.dirname(out) + if not os.path.isdir(dir): + os.makedirs(dir) + out=open(out,'wb'); out.write(f.read(path)); out.close() + + f.close() + + |