# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
==============
desiutil.setup
==============
This module supplies :command:`desi_update_version`, which simplifies
setting and updating version strings in Python packages.
This module also supports *deprecated* ``python setup.py <command>`` actions.
For historical reasons, this module is retains an outdated name ``setup.py``.
"""
import os
import re
import sys
import unittest
from argparse import ArgumentParser
from setuptools import Command
from setuptools.command.test import test as BaseTest
from pkg_resources import _namespace_packages
from distutils.log import DEBUG, INFO, WARN, ERROR
from . import __version__ as desiutilVersion
from .log import log
from .svn import version as svn_version
from .git import version as git_version
from .modules import configure_module, process_module, default_module
[docs]class DesiAPI(Command):
"""Generate an api.rst file.
"""
description = "create/update doc/api.rst"
user_options = [('api=', 'a',
'Set the name of the API file (default doc/api.rst).'),
('overwrite', 'o',
'Overwrite the existing API file.')]
boolean_options = ['overwrite']
_exclude_file = ('_version.py',)
[docs] def initialize_options(self):
self.overwrite = False
self.api = os.path.join(os.path.abspath('.'), 'doc', 'api.rst')
[docs] def finalize_options(self):
pass
[docs] def run(self):
self.announce("WARNING: This functionality is deprecated and will be removed from a future version of desiutil.", level=WARN)
self.announce("WARNING: Use the command-line script desi_api_file instead.", level=WARN)
n = self.distribution.metadata.get_name()
productroot = find_version_directory(n)
modules = []
for dirpath, dirnames, filenames in os.walk(productroot):
if dirpath == productroot:
d = ''
else:
d = dirpath.replace(productroot + '/', '')
self.announce(d, level=DEBUG)
for f in filenames:
mod = [n]
if f.endswith('.py') and f not in self._exclude_file and not self._test_file(d, f):
if d:
mod += d.split('/')
if f != '__init__.py':
mod.append(f.replace('.py', ''))
modules.append('.'.join(mod))
self.announce('.'.join(mod), level=DEBUG)
self._print(n, modules)
def _print(self, name, modules):
lines = []
title = "{0} API".format(name)
lines = ['='*len(title), title, '='*len(title), '']
for m in sorted(modules):
lines += ['.. automodule:: {0}'.format(m), ' :members:', '']
if os.path.exists(self.api):
if self.overwrite:
self.announce("{0} will be overwritten!".format(self.api),
level=WARN)
else:
self.announce("{0} already exists!".format(self.api),
level=ERROR)
exit(1)
with open(self.api, 'w') as a:
a.write('\n'.join(lines))
def _test_file(self, d, f):
return os.path.basename(d) == 'test' or os.path.basename(d) == 'tests'
[docs]class DesiModule(Command):
"""Allow users to install module files with
``python setup.py module_file``.
"""
description = "install a module file for this package"
user_options = [('default', 'd',
'Set this version as the default Module file.'),
('modules=', 'm', 'Set the Module install directory.')
]
boolean_options = ['default']
[docs] def initialize_options(self):
self.modules = None
self.default = False
[docs] def finalize_options(self):
if self.modules is None:
try:
self.modules = os.path.join('/global/common/software/desi',
os.environ['NERSC_HOST'],
'desiconda',
'current',
'modulefiles')
except KeyError:
try:
self.modules = os.path.join(os.environ['DESI_PRODUCT_ROOT'],
'modulefiles')
except KeyError:
self.announce("Could not determine a Module install directory!",
level=ERROR)
exit(1)
[docs] def run(self):
self.announce("WARNING: This functionality is deprecated and will be removed from a future version of desiutil.", level=WARN)
self.announce("WARNING: Use the command-line script desi_module_file instead.", level=WARN)
meta = self.distribution.metadata
name = meta.get_name()
version = meta.get_version()
dev = 'dev' in version
working_dir = os.path.abspath('.')
product_root = os.path.join(os.path.dirname(self.modules), 'code')
module_keywords = configure_module(name, version, product_root, dev=dev)
module_file = os.path.join(working_dir, 'etc', '{0}.module'.format(name))
if os.path.exists(module_file):
process_module(module_file, module_keywords, self.modules)
else:
self.announce("Could not find a Module file: {0}.".format(module_file),
level=ERROR)
if self.default:
default_module(module_keywords, self.modules)
return
[docs]class DesiTest(BaseTest, object):
"""Add coverage to test commands.
"""
description = "run unit tests after in-place build"
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-runner=', 'r', "Test runner to use"),
('coverage', 'c', ('Create a coverage report. ' +
'Requires the coverage package.'))
]
boolean_options = ['coverage']
[docs] def initialize_options(self):
self.coverage = False
super(DesiTest, self).initialize_options()
[docs] def finalize_options(self):
if self.coverage:
try:
import coverage
except ImportError:
self.announce(("--coverage requires that the coverage " +
"package is installed, disabling coverage" +
"option."), level=WARN)
self.coverage = False
super(DesiTest, self).finalize_options()
def run_tests(self):
# Purge modules under test from sys.modules. The test loader will
# re-import them from the build location. Required when 2to3 is used
# with namespace packages.
self.announce("WARNING: This functionality is deprecated and will be removed from a future version of desiutil.", level=WARN)
self.announce("WARNING: Use pytest or pytest --cov (for test coverage) instead.", level=WARN)
if getattr(self.distribution, 'use_2to3', False):
module = self.test_args[-1].split('.')[0]
if module in _namespace_packages:
del_modules = []
if module in sys.modules:
del_modules.append(module)
module += '.'
for name in sys.modules:
if name.startswith(module):
del_modules.append(name)
list(map(sys.modules.__delitem__, del_modules))
if self.coverage:
self.announce("Coverage selected!", level=INFO)
import coverage
cov = coverage.coverage(data_file=os.path.abspath(".coverage"),
config_file=os.path.abspath(".coveragerc"))
cov.start()
result = unittest.main(None, None,
([unittest.__file__] + self.test_args),
testLoader=self._resolve_as_ep(self.test_loader),
testRunner=self._resolve_as_ep(self.test_runner),
exit=False)
if result.result.wasSuccessful():
if self.coverage:
cov.stop()
self.announce('Saving coverage data in .coverage...',
level=INFO)
cov.save()
self.announce('Saving HTML coverage report in htmlcov...',
level=INFO)
cov.html_report(directory=os.path.abspath('htmlcov'))
else:
exit(1)
[docs]class DesiVersion(Command):
"""Allow users to easily update the package version with
``python setup.py version``.
"""
description = "update _version.py from git repo"
user_options = [('tag=', 't',
'Set the version to a name in preparation for tagging.'),
]
boolean_options = []
[docs] def initialize_options(self):
self.tag = None
[docs] def finalize_options(self):
pass
[docs] def run(self):
self.announce("WARNING: This functionality is deprecated and will be removed from a future version of desiutil.", level=WARN)
self.announce("WARNING: Use the command-line script desi_update_version instead.", level=WARN)
meta = self.distribution.metadata
update_version(meta.get_name(), tag=self.tag)
ver = get_version(meta.get_name())
self.announce("Version is now {}.".format(ver), level=INFO)
[docs]def find_version_directory(productname):
"""Return the name of a directory containing version information.
Looks for files in the following places:
* py/`productname`/_version.py
* `productname`/_version.py
Parameters
----------
productname : :class:`str`
The name of the package.
Returns
-------
:class:`str`
Name of a directory that can or does contain version information.
Raises
------
IOError
If no valid directory can be found.
"""
setup_dir = os.path.abspath('.')
if os.path.isdir(os.path.join(setup_dir, 'py', productname)):
version_dir = os.path.join(setup_dir, 'py', productname)
elif os.path.isdir(os.path.join(setup_dir, productname)):
version_dir = os.path.join(setup_dir, productname)
else:
raise IOError("Could not find a directory containing version information!")
return version_dir
[docs]def get_version(productname):
"""Get the value of ``__version__`` without having to import the module.
Parameters
----------
productname : :class:`str`
The name of the package.
Returns
-------
:class:`str`
The value of ``__version__``.
"""
ver = 'unknown'
try:
version_dir = find_version_directory(productname)
except IOError:
return ver
version_file = os.path.join(version_dir, '_version.py')
if not os.path.isfile(version_file):
update_version(productname)
with open(version_file, "r") as f:
for line in f.readlines():
mo = re.match("__version__ = '(.*)'", line)
if mo:
ver = mo.group(1)
return ver
[docs]def update_version(productname, tag=None):
"""Update the _version.py file.
Parameters
----------
productname : :class:`str`
The name of the package.
tag : :class:`str`, optional
Set the version to this string, unconditionally.
Raises
------
IOError
If the repository type could not be determined.
"""
version_dir = find_version_directory(productname)
if tag is not None:
ver = tag
else:
if os.path.isdir(".svn"):
ver = svn_version(productname)
elif os.path.isdir(".git"):
ver = git_version()
else:
raise IOError("Could not determine repository type.")
version_file = os.path.join(version_dir, '_version.py')
with open(version_file, "w") as f:
f.write("__version__ = '{}'\n".format(ver))
return
[docs]def main():
"""Entry-point for command-line scripts.
Returns
-------
:class:`int`
An integer suitable for passing to :func:`sys.exit`.
"""
parser = ArgumentParser(description="Update a package version string.",
prog=os.path.basename(sys.argv[0]))
parser.add_argument('-t', '--tag', dest='tag', help='Set the version to a name in preparation for tagging.')
parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + desiutilVersion)
parser.add_argument('product', help='Name of product.')
options = parser.parse_args()
update_version(options.product, tag=options.tag)
ver = get_version(options.product)
log.info("Version is now %s.", ver)
return 0