Skip to content

Commit

Permalink
Merge pull request #234 from nipy/package_check-fix
Browse files Browse the repository at this point in the history
MRG: adapt package_check to setuptools

Now pip is working pretty well, adapt package_check to fill setuptools
arguments instead of giving errors, when setuptools has been imported.
  • Loading branch information
matthew-brett committed Apr 4, 2014
2 parents f66c262 + 45bce4d commit ec2e2ec
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 46 deletions.
112 changes: 91 additions & 21 deletions nisext/sexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import os
from os.path import join as pjoin, split as psplit, splitext
import sys
PY3 = sys.version_info[0] >= 3
if PY3:
string_types = str,
else:
string_types = basestring,
try:
from ConfigParser import ConfigParser
except ImportError:
Expand Down Expand Up @@ -69,14 +75,37 @@ def run(self):
return MyBuildPy


def _add_append_key(in_dict, key, value):
""" Helper for appending dependencies to setuptools args """
# If in_dict[key] does not exist, create it
# If in_dict[key] is a string, make it len 1 list of strings
# Append value to in_dict[key] list
if key not in in_dict:
in_dict[key] = []
elif isinstance(in_dict[key], string_types):
in_dict[key] = [in_dict[key]]
in_dict[key].append(value)


# Dependency checks
def package_check(pkg_name, version=None,
optional=False,
checker=LooseVersion,
version_getter=None,
messages=None
messages=None,
setuptools_args=None
):
''' Check if package `pkg_name` is present, and correct version
''' Check if package `pkg_name` is present and has good enough version
Has two modes of operation. If `setuptools_args` is None (the default),
raise an error for missing non-optional dependencies and log warnings for
missing optional dependencies. If `setuptools_args` is a dict, then fill
``install_requires`` key value with any missing non-optional dependencies,
and the ``extras_requires`` key value with optional dependencies.
This allows us to work with and without setuptools. It also means we can
check for packages that have not been installed with setuptools to avoid
installing them again.
Parameters
----------
Expand All @@ -85,9 +114,11 @@ def package_check(pkg_name, version=None,
version : {None, str}, optional
minimum version of the package that we require. If None, we don't
check the version. Default is None
optional : {False, True}, optional
If False, raise error for absent package or wrong version;
otherwise warn
optional : bool or str, optional
If ``bool(optional)`` is False, raise error for absent package or wrong
version; otherwise warn. If ``setuptools_args`` is not None, and
``bool(optional)`` is not False, then `optional` should be a string
giving the feature name for the ``extras_require`` argument to setup.
checker : callable, optional
callable with which to return comparable thing from version
string. Default is ``distutils.version.LooseVersion``
Expand All @@ -102,7 +133,13 @@ def package_check(pkg_name, version=None,
mod = __import__(pkg_name); version = mod.__version__``
messages : None or dict, optional
dictionary giving output messages
setuptools_args : None or dict
If None, raise errors / warnings for missing non-optional / optional
dependencies. If dict fill key values ``install_requires`` and
``extras_require`` for non-optional and optional dependencies.
'''
setuptools_mode = not setuptools_args is None
optional_tf = bool(optional)
if version_getter is None:
def version_getter(pkg_name):
mod = __import__(pkg_name)
Expand All @@ -116,30 +153,63 @@ def version_getter(pkg_name):
'version too old': 'You have version %s of package "%s"'
' but we need version >= %s', }
msgs.update(messages)
status, have_version = _package_status(pkg_name,
version,
version_getter,
checker)
if status == 'satisfied':
return
if not setuptools_mode:
if status == 'missing':
if not optional_tf:
raise RuntimeError(msgs['missing'] % pkg_name)
log.warn(msgs['missing opt'] % pkg_name +
msgs['opt suffix'])
return
elif status == 'no-version':
raise RuntimeError('Cannot find version for %s' % pkg_name)
assert status == 'low-version'
if not optional_tf:
raise RuntimeError(msgs['version too old'] % (have_version,
pkg_name,
version))
log.warn(msgs['version too old'] % (have_version,
pkg_name,
version)
+ msgs['opt suffix'])
return
# setuptools mode
if optional_tf and not isinstance(optional, string_types):
raise RuntimeError('Not-False optional arg should be string')
dependency = pkg_name
if version:
dependency += '>=' + version
if optional_tf:
if not 'extras_require' in setuptools_args:
setuptools_args['extras_require'] = {}
_add_append_key(setuptools_args['extras_require'],
optional,
dependency)
return
_add_append_key(setuptools_args, 'install_requires', dependency)
return


def _package_status(pkg_name, version, version_getter, checker):
try:
__import__(pkg_name)
except ImportError:
if not optional:
raise RuntimeError(msgs['missing'] % pkg_name)
log.warn(msgs['missing opt'] % pkg_name +
msgs['opt suffix'])
return
return 'missing', None
if not version:
return
return 'satisfied', None
try:
have_version = version_getter(pkg_name)
except AttributeError:
raise RuntimeError('Cannot find version for %s' % pkg_name)
return 'no-version', None
if checker(have_version) < checker(version):
if optional:
log.warn(msgs['version too old'] % (have_version,
pkg_name,
version)
+ msgs['opt suffix'])
else:
raise RuntimeError(msgs['version too old'] % (have_version,
pkg_name,
version))
return 'low-version', have_version
return 'satisfied', have_version


BAT_TEMPLATE = \
r"""@echo off
Expand Down
110 changes: 97 additions & 13 deletions nisext/tests/test_sexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,100 @@ def test_package_check():
assert_raises(RuntimeError, package_check, FAKE_NAME)
# Optional, log.warn
package_check(FAKE_NAME, optional=True)
# Make a package
sys.modules[FAKE_NAME] = FAKE_MODULE
# Now it passes if we don't check the version
package_check(FAKE_NAME)
# A fake version
FAKE_MODULE.__version__ = '0.2'
package_check(FAKE_NAME, version='0.2')
# fails when version not good enough
assert_raises(RuntimeError, package_check, FAKE_NAME, '0.3')
# Unless optional in which case log.warns
package_check(FAKE_NAME, version='0.3', optional=True)
# Might do custom version check
package_check(FAKE_NAME, version='0.2', version_getter=lambda x: '0.2')
# Can also pass a string
package_check(FAKE_NAME, optional='some-package')
try:
# Make a package
sys.modules[FAKE_NAME] = FAKE_MODULE
# Now it passes if we don't check the version
package_check(FAKE_NAME)
# A fake version
FAKE_MODULE.__version__ = '0.2'
package_check(FAKE_NAME, version='0.2')
# fails when version not good enough
assert_raises(RuntimeError, package_check, FAKE_NAME, '0.3')
# Unless optional in which case log.warns
package_check(FAKE_NAME, version='0.3', optional=True)
# Might do custom version check
package_check(FAKE_NAME, version='0.2', version_getter=lambda x: '0.2')
finally:
del sys.modules[FAKE_NAME]


def test_package_check_setuptools():
# If setuptools arg not None, missing package just adds it to arg
assert_raises(RuntimeError, package_check, FAKE_NAME, setuptools_args=None)
def pkg_chk_sta(*args, **kwargs):
st_args = {}
package_check(*args, setuptools_args=st_args, **kwargs)
return st_args
assert_equal(pkg_chk_sta(FAKE_NAME),
{'install_requires': ['nisext_improbable']})
# Check that this gets appended to existing value
old_sta = {'install_requires': ['something']}
package_check(FAKE_NAME, setuptools_args=old_sta)
assert_equal(old_sta,
{'install_requires': ['something', 'nisext_improbable']})
# That existing value as string gets converted to a list
old_sta = {'install_requires': 'something'}
package_check(FAKE_NAME, setuptools_args=old_sta)
assert_equal(old_sta,
{'install_requires': ['something', 'nisext_improbable']})
# Optional, add to extras_require
assert_equal(pkg_chk_sta(FAKE_NAME, optional='something'),
{'extras_require': {'something': ['nisext_improbable']}})
# Check that this gets appended to existing value
old_sta = {'extras_require': {'something': ['amodule']}}
package_check(FAKE_NAME, optional='something', setuptools_args=old_sta)
assert_equal(old_sta,
{'extras_require':
{'something': ['amodule', 'nisext_improbable']}})
# That string gets converted to a list here too
old_sta = {'extras_require': {'something': 'amodule'}}
package_check(FAKE_NAME, optional='something', setuptools_args=old_sta)
assert_equal(old_sta,
{'extras_require':
{'something': ['amodule', 'nisext_improbable']}})
# But optional has to be a string if not empty and setuptools_args defined
assert_raises(RuntimeError,
package_check, FAKE_NAME, optional=True, setuptools_args={})
try:
# Make a package
sys.modules[FAKE_NAME] = FAKE_MODULE
# No install_requires because we already have it
assert_equal(pkg_chk_sta(FAKE_NAME), {})
# A fake version still works
FAKE_MODULE.__version__ = '0.2'
assert_equal(pkg_chk_sta(FAKE_NAME, version='0.2'), {})
# goes into install requires when version not good enough
exp_spec = [FAKE_NAME + '>=0.3']
assert_equal(pkg_chk_sta(FAKE_NAME, version='0.3'),
{'install_requires': exp_spec})
# Unless optional in which case goes into extras_require
package_check(FAKE_NAME, version='0.2', version_getter=lambda x: '0.2')
assert_equal(
pkg_chk_sta(FAKE_NAME, version='0.3', optional='afeature'),
{'extras_require': {'afeature': exp_spec}})
# Might do custom version check
assert_equal(
pkg_chk_sta(FAKE_NAME,
version='0.2',
version_getter=lambda x: '0.2'),
{})
# If the version check fails, put into requires
bad_getter = lambda x: x.not_an_attribute
exp_spec = [FAKE_NAME + '>=0.2']
assert_equal(
pkg_chk_sta(FAKE_NAME,
version='0.2',
version_getter=bad_getter),
{'install_requires': exp_spec})
# Likewise for optional dependency
assert_equal(
pkg_chk_sta(FAKE_NAME,
version='0.2',
optional='afeature',
version_getter=bad_getter),
{'extras_require': {'afeature': [FAKE_NAME + '>=0.2']}})
finally:
del sys.modules[FAKE_NAME]
29 changes: 17 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import os
from os.path import join as pjoin
import sys
from functools import partial

# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
# update it when the contents of directories change.
Expand All @@ -35,26 +36,30 @@
ver_file = os.path.join('nibabel', 'info.py')
exec(open(ver_file).read())

# Do dependency checking
package_check('numpy', NUMPY_MIN_VERSION)
custom_pydicom_messages = {'missing opt': 'Missing optional package "%s"'
' provided by package "pydicom"'
}
package_check('dicom',
PYDICOM_MIN_VERSION,
optional=True,
messages = custom_pydicom_messages)
extra_setuptools_args = {}
# Prepare setuptools args
if 'setuptools' in sys.modules:
extra_setuptools_args = dict(
tests_require=['nose'],
test_suite='nose.collector',
zip_safe=False,
extras_require = dict(
doc='Sphinx>=0.3',
test='nose>=0.10.1',
nicom = 'dicom>=' + PYDICOM_MIN_VERSION)
test='nose>=0.10.1'),
)
pkg_chk = partial(package_check, setuptools_args = extra_setuptools_args)
else:
extra_setuptools_args = {}
pkg_chk = package_check

# Do dependency checking
pkg_chk('numpy', NUMPY_MIN_VERSION)
custom_pydicom_messages = {'missing opt': 'Missing optional package "%s"'
' provided by package "pydicom"'
}
pkg_chk('dicom',
PYDICOM_MIN_VERSION,
optional='dicom',
messages = custom_pydicom_messages)

def main(**extra_args):
setup(name=NAME,
Expand Down

0 comments on commit ec2e2ec

Please sign in to comment.