Commit 7ce55cab authored by PJ Eby's avatar PJ Eby

Flesh out 'depends' command to display dependencies' status, and halt if

all requirements aren't met.  (Also, check planned install location for
the dependencies, as well as checking sys.path.)  Also:

* Allow 'Feature()' objects to include 'Require()' objects, so that
  dependencies can be optional

* 'Require()' objects can set a homepage, whose URL will be displayed by
  the 'depends' command if the dependency needs to be installed.

* Misc. fixes/refactoring of version validation to properly handle
  "unknown" versions, and to decouple version fetching from version
  checking.

* Updated TODO to remove various completed items.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4040876
parent 8423e1ed
......@@ -10,10 +10,6 @@ To-Do
* 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
......@@ -42,17 +38,3 @@ To-Do
* 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
......@@ -3,6 +3,7 @@
"""Distutils setup file, used to install or test 'setuptools'"""
from setuptools import setup, find_packages, Require
from distutils.version import LooseVersion
setup(
name="setuptools",
......@@ -14,7 +15,12 @@ setup(
license="PSF or ZPL",
test_suite = 'setuptools.tests.test_suite',
requires = [Require('Distutils','1.0.3','distutils')],
requires = [
Require('Distutils','1.0.3','distutils',
"http://www.python.org/sigs/distutils-sig/"
),
Require('PyUnit', None, 'unittest', "http://pyunit.sf.net/"),
],
packages = find_packages(),
py_modules = ['setuptools_boot'],
)
......
from distutils.cmd import Command
import os
import os, sys
class depends(Command):
"""Download and install dependencies, if needed"""
description = "download and install dependencies, if needed"
......@@ -13,15 +15,68 @@ class depends(Command):
"ignore options that won't be passed to child setup scripts"),
]
path_attrs = [
# Note: these must be in *reverse* order, as they are pushed onto the
# *front* of a copy of sys.path.
('install','install_libbase'), # installation base if extra_path
('install_lib','install_dir'), # where modules are installed
]
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'))
self.set_search_path()
def set_search_path(self):
"""Determine paths to check for installed dependencies"""
path = sys.path[:] # copy sys path
for cmd,attr in self.path_attrs:
dir = getattr(self.get_finalized_command(cmd),attr,None)
if dir and dir not in path:
path.insert(0,dir) # prepend
self.search_path = path
def run(self):
self.announce("downloading and building here")
self.announce("checking for installed dependencies")
needed = [
dep for dep in self.distribution.requires if self.is_needed(dep)
]
if not needed:
self.announce("all dependencies are present and up-to-date")
return
# Alert the user to missing items
fmt = "\t%s\t%s\n"
items = [fmt % (dep.full_name(),dep.homepage) for dep in needed]
items.insert(0,"Please install the following packages first:\n")
items.append('')
raise SystemExit('\n'.join(items)) # dump msg to stderr and exit
def is_needed(self,dep):
"""Does the specified dependency need to be installed/updated?"""
self.announce("searching for "+dep.full_name())
version = dep.get_version(self.search_path)
if version is None:
self.announce(name+" not found!")
return True
if str(version)=="unknown":
status = dep.name+" is installed"
else:
status = dep.name+" version "+str(version)+" found"
if dep.version_ok(version):
self.announce(status+" (OK)")
return False
else:
self.announce(status+" (update needed)")
return True
......@@ -10,7 +10,9 @@ __all__ = [
class Require:
"""A prerequisite to building or installing a distribution"""
def __init__(self,name,requested_version,module,attribute=None,format=None):
def __init__(self,name,requested_version,module,homepage='',
attribute=None,format=None
):
if format is None and requested_version is not None:
format = StrictVersion
......@@ -20,23 +22,21 @@ class Require:
if attribute is None:
attribute = '__version__'
self.name = name
self.requested_version = requested_version
self.module = module
self.attribute = attribute
self.format = format
self.__dict__.update(locals())
del self.self
def full_name(self):
"""Return full package/distribution name, w/version"""
if self.requested_version is not None:
return '%s-%s' % (self.name,self.requested_version)
return self.name
def version_ok(self,version):
"""Is 'version' sufficiently up-to-date?"""
return self.attribute is None or self.format is None or \
str(version)<>"unknown" and version >= self.requested_version
def get_version(self, paths=None, default="unknown"):
......@@ -66,18 +66,18 @@ class Require:
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
return self.version_ok(version)
def _iter_code(code):
......
__all__ = ['Distribution', 'Feature']
from distutils.core import Distribution as _Distribution
from distutils.core import Extension
from setuptools.depends import Require
from setuptools.command.build_py import build_py
from setuptools.command.build_ext import build_ext
from setuptools.command.install import install
......@@ -11,7 +11,6 @@ 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
......@@ -67,7 +66,6 @@ class Distribution(_Distribution):
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()
......@@ -288,7 +286,6 @@ class Distribution(_Distribution):
class Feature:
"""A subset of the distribution that can be excluded if unneeded/wanted
Features are created using these keyword arguments:
......@@ -312,6 +309,8 @@ class Feature:
'requires' -- a string or sequence of strings naming features that should
also be included if this feature is included. Defaults to empty list.
May also contain 'Require' objects that should be added/removed from
the distribution.
'remove' -- a string or list of strings naming packages to be removed
from the distribution if this feature is *not* included. If the
......@@ -345,15 +344,15 @@ class Feature:
self.standard = standard
self.available = available
self.optional = optional
if isinstance(requires,str):
if isinstance(requires,(str,Require)):
requires = requires,
self.requires = requires
self.requires = [r for r in requires if isinstance(r,str)]
er = [r for r in requires if not isinstance(r,str)]
if er: extras['requires'] = er
if isinstance(remove,str):
remove = remove,
self.remove = remove
self.extras = extras
......@@ -368,7 +367,6 @@ class Feature:
"""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
......
......@@ -10,9 +10,10 @@ 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
from distutils.util import convert_path
import sys, os.path
def makeSetup(**args):
"""Return distribution from 'setup(**args)', without executing commands"""
......@@ -38,7 +39,6 @@ def makeSetup(**args):
class DependsTests(TestCase):
def testExtractConst(self):
......@@ -80,24 +80,21 @@ class DependsTests(TestCase):
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__')
self.assertEqual(req.full_name(), 'Distutils-1.0.3')
from distutils import __version__
self.assertEqual(req.get_version(), __version__)
self.failUnless(req.version_ok('1.0.9'))
self.failIf(req.version_ok('0.9.1'))
self.failIf(req.version_ok('unknown'))
self.failUnless(req.is_present())
self.failUnless(req.is_current())
......@@ -105,19 +102,63 @@ class DependsTests(TestCase):
req = Require('Distutils 3000','03000','distutils',format=LooseVersion)
self.failUnless(req.is_present())
self.failIf(req.is_current())
self.failIf(req.version_ok('unknown'))
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')
req = Require('Tests', None, 'tests', homepage="http://example.com")
self.assertEqual(req.format, None)
self.assertEqual(req.attribute, None)
self.assertEqual(req.requested_version, None)
self.assertEqual(req.full_name(), 'Tests')
self.assertEqual(req.homepage, 'http://example.com')
paths = [os.path.dirname(p) for p in __path__]
self.failUnless(req.is_present(paths))
self.failUnless(req.is_current(paths))
def testDependsCmd(self):
path1 = convert_path('foo/bar/baz')
path2 = convert_path('foo/bar/baz/spam')
dist = makeSetup(
extra_path='spam',
script_args=['install','--install-lib',path1]
)
cmd = dist.get_command_obj('depends')
cmd.ensure_finalized()
self.assertEqual(cmd.temp, dist.get_command_obj('build').build_temp)
self.assertEqual(cmd.search_path, [path2,path1]+sys.path)
......@@ -247,9 +288,10 @@ class DistroTests(TestCase):
class FeatureTests(TestCase):
def setUp(self):
self.req = Require('Distutils','1.0.3','distutils')
self.dist = makeSetup(
features={
'foo': Feature("foo",standard=True,requires='baz'),
'foo': Feature("foo",standard=True,requires=['baz',self.req]),
'bar': Feature("bar", standard=True, packages=['pkg.bar'],
py_modules=['bar_et'], remove=['bar.ext'],
),
......@@ -284,7 +326,6 @@ class FeatureTests(TestCase):
self.dist.features['dwim'].include_in, self.dist
)
def testFeatureOptions(self):
dist = self.dist
self.failUnless(
......@@ -309,13 +350,13 @@ class FeatureTests(TestCase):
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,[])
self.assertEqual(dist.requires, [self.req])
# If we ask for bar, it should fail because we explicitly disabled
# it on the command line
......
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