Commit 8423e1ed authored by PJ Eby's avatar PJ Eby

Initial checkin of setuptools 0.0.1.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4040869
parents
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
#!/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'],
)
"""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)
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__]
)
# 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
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
]
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")
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
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
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)
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
This diff is collapsed.
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
This diff is collapsed.
"""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()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment