diff options
author | Monty Taylor <mordred@inaugust.com> | 2013-07-06 14:16:17 -0400 |
---|---|---|
committer | Monty Taylor <mordred@inaugust.com> | 2013-07-06 14:16:17 -0400 |
commit | 295dbe1da3315d2f21acc02f0fce83e96224041e (patch) | |
tree | b8d0606e313bdada68d1fd35172aa2f37c4528f8 | |
parent | ce11c732ac2f59a7cd252f9fd10bb4c7c025518f (diff) | |
parent | 712f639977d5c229e70a6d404bc5912e700a270d (diff) | |
download | pbr-295dbe1da3315d2f21acc02f0fce83e96224041e.tar.gz |
Merge d2to1 into the tree, complete with history.
27 files changed, 2311 insertions, 0 deletions
diff --git a/pbr/d2to1/__init__.py b/pbr/d2to1/__init__.py new file mode 100644 index 0000000..4089e8f --- /dev/null +++ b/pbr/d2to1/__init__.py @@ -0,0 +1,4 @@ +try: + __version__ = __import__('pkg_resources').get_distribution('d2to1').version +except: + __version__ = '' diff --git a/pbr/d2to1/core.py b/pbr/d2to1/core.py new file mode 100644 index 0000000..1c72eae --- /dev/null +++ b/pbr/d2to1/core.py @@ -0,0 +1,82 @@ +import os +import sys +import warnings + +from distutils.core import Distribution as _Distribution +from distutils.errors import DistutilsFileError, DistutilsSetupError +from setuptools.dist import _get_unpatched + +from .extern import six +from .util import DefaultGetDict, IgnoreDict, cfg_to_args + + +_Distribution = _get_unpatched(_Distribution) + + +def d2to1(dist, attr, value): + """Implements the actual d2to1 setup() keyword. When used, this should be + the only keyword in your setup() aside from `setup_requires`. + + If given as a string, the value of d2to1 is assumed to be the relative path + to the setup.cfg file to use. Otherwise, if it evaluates to true, it + simply assumes that d2to1 should be used, and the default 'setup.cfg' is + used. + + This works by reading the setup.cfg file, parsing out the supported + metadata and command options, and using them to rebuild the + `DistributionMetadata` object and set the newly added command options. + + The reason for doing things this way is that a custom `Distribution` class + will not play nicely with setup_requires; however, this implementation may + not work well with distributions that do use a `Distribution` subclass. + """ + + if not value: + return + if isinstance(value, six.string_types): + path = os.path.abspath(value) + else: + path = os.path.abspath('setup.cfg') + if not os.path.exists(path): + raise DistutilsFileError( + 'The setup.cfg file %s does not exist.' % path) + + # Converts the setup.cfg file to setup() arguments + try: + attrs = cfg_to_args(path) + except: + e = sys.exc_info()[1] + raise DistutilsSetupError( + 'Error parsing %s: %s: %s' % (path, e.__class__.__name__, e)) + + # Repeat some of the Distribution initialization code with the newly + # provided attrs + if attrs: + # Skips 'options' and 'licence' support which are rarely used; may add + # back in later if demanded + for key, val in six.iteritems(attrs): + if hasattr(dist.metadata, 'set_' + key): + getattr(dist.metadata, 'set_' + key)(val) + elif hasattr(dist.metadata, key): + setattr(dist.metadata, key, val) + elif hasattr(dist, key): + setattr(dist, key, val) + else: + msg = 'Unknown distribution option: %s' % repr(key) + warnings.warn(msg) + + # Re-finalize the underlying Distribution + _Distribution.finalize_options(dist) + + # This bit comes out of distribute/setuptools + if isinstance(dist.metadata.version, six.integer_types + (float,)): + # Some people apparently take "version number" too literally :) + dist.metadata.version = str(dist.metadata.version) + + # This bit of hackery is necessary so that the Distribution will ignore + # normally unsupport command options (namely pre-hooks and post-hooks). + # dist.command_options is normally a dict mapping command names to dicts of + # their options. Now it will be a defaultdict that returns IgnoreDicts for + # the each command's options so we can pass through the unsupported options + ignore = ['pre_hook.*', 'post_hook.*'] + dist.command_options = DefaultGetDict(lambda: IgnoreDict(ignore)) diff --git a/pbr/d2to1/extern/__init__.py b/pbr/d2to1/extern/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/extern/__init__.py diff --git a/pbr/d2to1/extern/six.py b/pbr/d2to1/extern/six.py new file mode 100644 index 0000000..0cdd1c7 --- /dev/null +++ b/pbr/d2to1/extern/six.py @@ -0,0 +1,386 @@ +# Copyright (c) 2010-2011 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +import operator +import sys +import types + +__author__ = "Benjamin Peterson <benjamin@python.org>" +__version__ = "1.2.0" + + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform == "java": + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules["six.moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_code = "__code__" + _func_defaults = "__defaults__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_code = "func_code" + _func_defaults = "func_defaults" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +if PY3: + def get_unbound_function(unbound): + return unbound + + Iterator = object + + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) +else: + def get_unbound_function(unbound): + return unbound.im_func + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) + + +def iterkeys(d): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)()) + +def itervalues(d): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)()) + +def iteritems(d): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)()) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + int2byte = chr + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, base=object): + """Create a base class with a metaclass.""" + return meta("NewBase", (base,), {}) diff --git a/pbr/d2to1/tests/__init__.py b/pbr/d2to1/tests/__init__.py new file mode 100644 index 0000000..6146af2 --- /dev/null +++ b/pbr/d2to1/tests/__init__.py @@ -0,0 +1,90 @@ +from __future__ import with_statement +import os +import shutil +import subprocess +import sys +import tempfile + +import pkg_resources + +from .util import rmtree, open_config + + +D2TO1_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir)) + + +def fake_d2to1_dist(): + # Fake a d2to1 distribution from the d2to1 package that these tests reside + # in and make sure it's active on the path with the appropriate entry + # points installed + + class _FakeProvider(pkg_resources.EmptyProvider): + """A fake metadata provider that does almost nothing except to return + entry point metadata. + """ + + def has_metadata(self, name): + return name == 'entry_points.txt' + + def get_metadata(self, name): + if name == 'entry_points.txt': + return '[distutils.setup_keywords]\nd2to1 = d2to1.core:d2to1\n' + else: + return '' + + + sys.path.insert(0, D2TO1_DIR) + if 'd2to1' in sys.modules: + del sys.modules['d2to1'] + if 'd2to1' in pkg_resources.working_set.by_key: + del pkg_resources.working_set.by_key['d2to1'] + dist = pkg_resources.Distribution(location=D2TO1_DIR, project_name='d2to1', + metadata=_FakeProvider()) + pkg_resources.working_set.add(dist) + + +class D2to1TestCase(object): + def setup(self): + self.temp_dir = tempfile.mkdtemp(prefix='d2to1-test-') + self.package_dir = os.path.join(self.temp_dir, 'testpackage') + shutil.copytree(os.path.join(os.path.dirname(__file__), 'testpackage'), + self.package_dir) + self.oldcwd = os.getcwd() + os.chdir(self.package_dir) + + def teardown(self): + os.chdir(self.oldcwd) + # Remove d2to1.testpackage from sys.modules so that it can be freshly + # re-imported by the next test + for k in list(sys.modules): + if (k == 'd2to1_testpackage' or + k.startswith('d2to1_testpackage.')): + del sys.modules[k] + rmtree(self.temp_dir) + + def run_setup(self, *args): + cmd = ('-c', + 'import sys;sys.path.insert(0, %r);' + 'from d2to1.tests import fake_d2to1_dist;' + 'from d2to1.extern.six import exec_;' + 'fake_d2to1_dist();exec_(open("setup.py").read())' % D2TO1_DIR) + return self._run_cmd(sys.executable, cmd + args) + + def run_svn(self, *args): + return self._run_cmd('svn', args) + + def _run_cmd(self, cmd, args): + """ + Runs a command, with the given argument list, in the root of the test + working copy--returns the stdout and stderr streams and the exit code + from the subprocess. + """ + + os.chdir(self.package_dir) + p = subprocess.Popen([cmd] + list(args), stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + streams = tuple(s.decode('latin1').strip() for s in p.communicate()) + print(streams) + return (streams) + (p.returncode,) diff --git a/pbr/d2to1/tests/test_commands.py b/pbr/d2to1/tests/test_commands.py new file mode 100644 index 0000000..29342da --- /dev/null +++ b/pbr/d2to1/tests/test_commands.py @@ -0,0 +1,13 @@ +from . import D2to1TestCase + + +class TestCommands(D2to1TestCase): + def test_custom_build_py_command(self): + """ + Test that a custom subclass of the build_py command runs when listed in + the commands [global] option, rather than the normal build command. + """ + + stdout, _, return_code = self.run_setup('build_py') + assert 'Running custom build_py command.' in stdout + assert return_code == 0 diff --git a/pbr/d2to1/tests/test_core.py b/pbr/d2to1/tests/test_core.py new file mode 100644 index 0000000..d7962f4 --- /dev/null +++ b/pbr/d2to1/tests/test_core.py @@ -0,0 +1,48 @@ +import glob +import os +import tarfile + +from . import D2to1TestCase + + +VERSION = '0.1.dev' + + +class TestCore(D2to1TestCase): + def test_setup_py_version(self): + """ + Test that the `./setup.py --version` command returns the correct + value without balking. + """ + + self.run_setup('egg_info') + stdout, _, _ = self.run_setup('--version') + assert stdout == VERSION + + def test_setup_py_keywords(self): + """ + Test that the `./setup.py --keywords` command returns the correct + value without balking. + """ + + self.run_setup('egg_info') + stdout, _, _ = self.run_setup('--keywords') + assert stdout == 'packaging,distutils,setuptools' + + def test_sdist_extra_files(self): + """ + Test that the extra files are correctly added. + """ + + stdout, _, return_code = self.run_setup('sdist', '--formats=gztar') + + # There can be only one + try: + tf_path = glob.glob(os.path.join('dist', '*.tar.gz'))[0] + except IndexError: + assert False, 'source dist not found' + + tf = tarfile.open(tf_path) + names = ['/'.join(p.split('/')[1:]) for p in tf.getnames()] + + assert 'extra-file.txt' in names diff --git a/pbr/d2to1/tests/test_hooks.py b/pbr/d2to1/tests/test_hooks.py new file mode 100644 index 0000000..047e0b9 --- /dev/null +++ b/pbr/d2to1/tests/test_hooks.py @@ -0,0 +1,52 @@ +from __future__ import with_statement + +import os +import textwrap + +from . import D2to1TestCase +from .util import open_config + + +class TestHooks(D2to1TestCase): + def setup(self): + super(TestHooks, self).setup() + with open_config(os.path.join(self.package_dir, 'setup.cfg')) as cfg: + cfg.set('global', 'setup-hooks', + 'd2to1_testpackage._setup_hooks.test_hook_1\n' + 'd2to1_testpackage._setup_hooks.test_hook_2') + cfg.set('build_ext', 'pre-hook.test_pre_hook', + 'd2to1_testpackage._setup_hooks.test_pre_hook') + cfg.set('build_ext', 'post-hook.test_post_hook', + 'd2to1_testpackage._setup_hooks.test_post_hook') + + def test_global_setup_hooks(self): + """ + Test that setup_hooks listed in the [global] section of setup.cfg are + executed in order. + """ + + stdout, _, return_code = self.run_setup('egg_info') + assert 'test_hook_1\ntest_hook_2' in stdout + assert return_code == 0 + + def test_command_hooks(self): + """ + Simple test that the appropriate command hooks run at the + beginning/end of the appropriate command. + """ + + stdout, _, return_code = self.run_setup('egg_info') + assert 'build_ext pre-hook' not in stdout + assert 'build_ext post-hook' not in stdout + assert return_code == 0 + + stdout, _, return_code = self.run_setup('build_ext') + assert textwrap.dedent(""" + running build_ext + running pre_hook d2to1_testpackage._setup_hooks.test_pre_hook for command build_ext + build_ext pre-hook + """) in stdout + assert stdout.endswith('build_ext post-hook') + assert return_code == 0 + + diff --git a/pbr/d2to1/tests/testpackage/CHANGES.txt b/pbr/d2to1/tests/testpackage/CHANGES.txt new file mode 100644 index 0000000..709b9d4 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/CHANGES.txt @@ -0,0 +1,86 @@ +Changelog +=========== + +0.3 (unreleased) +------------------ + +- The ``glob_data_files`` hook became a pre-command hook for the install_data + command instead of being a setup-hook. This is to support the additional + functionality of requiring data_files with relative destination paths to be + install relative to the package's install path (i.e. site-packages). + +- Dropped support for and deprecated the easier_install custom command. + Although it should still work, it probably won't be used anymore for + stsci_python packages. + +- Added support for the ``build_optional_ext`` command, which replaces/extends + the default ``build_ext`` command. See the README for more details. + +- Added the ``tag_svn_revision`` setup_hook as a replacement for the + setuptools-specific tag_svn_revision option to the egg_info command. This + new hook is easier to use than the old tag_svn_revision option: It's + automatically enabled by the presence of ``.dev`` in the version string, and + disabled otherwise. + +- The ``svn_info_pre_hook`` and ``svn_info_post_hook`` have been replaced with + ``version_pre_command_hook`` and ``version_post_command_hook`` respectively. + However, a new ``version_setup_hook``, which has the same purpose, has been + added. It is generally easier to use and will give more consistent results + in that it will run every time setup.py is run, regardless of which command + is used. ``stsci.distutils`` itself uses this hook--see the `setup.cfg` file + and `stsci/distutils/__init__.py` for example usage. + +- Instead of creating an `svninfo.py` module, the new ``version_`` hooks create + a file called `version.py`. In addition to the SVN info that was included + in `svninfo.py`, it includes a ``__version__`` variable to be used by the + package's `__init__.py`. This allows there to be a hard-coded + ``__version__`` variable included in the source code, rather than using + pkg_resources to get the version. + +- In `version.py`, the variables previously named ``__svn_version__`` and + ``__full_svn_info__`` are now named ``__svn_revision__`` and + ``__svn_full_info__``. + +- Fixed a bug when using stsci.distutils in the installation of other packages + in the ``stsci.*`` namespace package. If stsci.distutils was not already + installed, and was downloaded automatically by distribute through the + setup_requires option, then ``stsci.distutils`` would fail to import. This + is because the way the namespace package (nspkg) mechanism currently works, + all packages belonging to the nspkg *must* be on the import path at initial + import time. + + So when installing stsci.tools, for example, if ``stsci.tools`` is imported + from within the source code at install time, but before ``stsci.distutils`` + is downloaded and added to the path, the ``stsci`` package is already + imported and can't be extended to include the path of ``stsci.distutils`` + after the fact. The easiest way of dealing with this, it seems, is to + delete ``stsci`` from ``sys.modules``, which forces it to be reimported, now + the its ``__path__`` extended to include ``stsci.distutil``'s path. + + +0.2.2 (2011-11-09) +------------------ + +- Fixed check for the issue205 bug on actual setuptools installs; before it + only worked on distribute. setuptools has the issue205 bug prior to version + 0.6c10. + +- Improved the fix for the issue205 bug, especially on setuptools. + setuptools, prior to 0.6c10, did not back of sys.modules either before + sandboxing, which causes serious problems. In fact, it's so bad that it's + not enough to add a sys.modules backup to the current sandbox: It's in fact + necessary to monkeypatch setuptools.sandbox.run_setup so that any subsequent + calls to it also back up sys.modules. + + +0.2.1 (2011-09-02) +------------------ + +- Fixed the dependencies so that setuptools is requirement but 'distribute' + specifically. Previously installation could fail if users had plain + setuptools installed and not distribute + +0.2 (2011-08-23) +------------------ + +- Initial public release diff --git a/pbr/d2to1/tests/testpackage/LICENSE.txt b/pbr/d2to1/tests/testpackage/LICENSE.txt new file mode 100644 index 0000000..7e8019a --- /dev/null +++ b/pbr/d2to1/tests/testpackage/LICENSE.txt @@ -0,0 +1,29 @@ +Copyright (C) 2005 Association of Universities for Research in Astronomy (AURA) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + 3. The name of AURA and its representatives may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + diff --git a/pbr/d2to1/tests/testpackage/MANIFEST.in b/pbr/d2to1/tests/testpackage/MANIFEST.in new file mode 100644 index 0000000..cdc95ea --- /dev/null +++ b/pbr/d2to1/tests/testpackage/MANIFEST.in @@ -0,0 +1 @@ +include data_files/* diff --git a/pbr/d2to1/tests/testpackage/README.txt b/pbr/d2to1/tests/testpackage/README.txt new file mode 100644 index 0000000..4f00d32 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/README.txt @@ -0,0 +1,148 @@ +Introduction +============ +This package contains utilities used to package some of STScI's Python +projects; specifically those projects that comprise stsci_python_ and +Astrolib_. + +It currently consists mostly of some setup_hook scripts meant for use with +`distutils2/packaging`_ and/or d2to1_, and a customized easy_install command +meant for use with distribute_. + +This package is not meant for general consumption, though it might be worth +looking at for examples of how to do certain things with your own packages, but +YMMV. + +Features +======== + +Hook Scripts +------------ +Currently the main features of this package are a couple of setup_hook scripts. +In distutils2, a setup_hook is a script that runs at the beginning of any +pysetup command, and can modify the package configuration read from setup.cfg. +There are also pre- and post-command hooks that only run before/after a +specific setup command (eg. build_ext, install) is run. + +stsci.distutils.hooks.use_packages_root +''''''''''''''''''''''''''''''''''''''' +If using the ``packages_root`` option under the ``[files]`` section of +setup.cfg, this hook will add that path to ``sys.path`` so that modules in your +package can be imported and used in setup. This can be used even if +``packages_root`` is not specified--in this case it adds ``''`` to +``sys.path``. + +stsci.distutils.hooks.version_setup_hook +'''''''''''''''''''''''''''''''''''''''' +Creates a Python module called version.py which currently contains four +variables: + +* ``__version__`` (the release version) +* ``__svn_revision__`` (the SVN revision info as returned by the ``svnversion`` + command) +* ``__svn_full_info__`` (as returned by the ``svn info`` command) +* ``__setup_datetime__`` (the date and time that setup.py was last run). + +These variables can be imported in the package's `__init__.py` for degugging +purposes. The version.py module will *only* be created in a package that +imports from the version module in its `__init__.py`. It should be noted that +this is generally preferable to writing these variables directly into +`__init__.py`, since this provides more control and is less likely to +unexpectedly break things in `__init__.py`. + +stsci.distutils.hooks.version_pre_command_hook +'''''''''''''''''''''''''''''''''''''''''''''' +Identical to version_setup_hook, but designed to be used as a pre-command +hook. + +stsci.distutils.hooks.version_post_command_hook +''''''''''''''''''''''''''''''''''''''''''''''' +The complement to version_pre_command_hook. This will delete any version.py +files created during a build in order to prevent them from cluttering an SVN +working copy (note, however, that version.py is *not* deleted from the build/ +directory, so a copy of it is still preserved). It will also not be deleted +if the current directory is not an SVN working copy. For example, if source +code extracted from a source tarball it will be preserved. + +stsci.distutils.hooks.tag_svn_revision +'''''''''''''''''''''''''''''''''''''' +A setup_hook to add the SVN revision of the current working copy path to the +package version string, but only if the version ends in .dev. + +For example, ``mypackage-1.0.dev`` becomes ``mypackage-1.0.dev1234``. This is +in accordance with the version string format standardized by PEP 386. + +This should be used as a replacement for the ``tag_svn_revision`` option to +the egg_info command. This hook is more compatible with packaging/distutils2, +which does not include any VCS support. This hook is also more flexible in +that it turns the revision number on/off depending on the presence of ``.dev`` +in the version string, so that it's not automatically added to the version in +final releases. + +This hook does require the ``svnversion`` command to be available in order to +work. It does not examine the working copy metadata directly. + +stsci.distutils.hooks.numpy_extension_hook +'''''''''''''''''''''''''''''''''''''''''' +This is a pre-command hook for the build_ext command. To use it, add a +``[build_ext]`` section to your setup.cfg, and add to it:: + + pre-hook.numpy-extension-hook = stsci.distutils.hooks.numpy_extension_hook + +This hook must be used to build extension modules that use Numpy. The primary +side-effect of this hook is to add the correct numpy include directories to +`include_dirs`. To use it, add 'numpy' to the 'include-dirs' option of each +extension module that requires numpy to build. The value 'numpy' will be +replaced with the actual path to the numpy includes. + +stsci.distutils.hooks.is_display_option +''''''''''''''''''''''''''''''''''''''' +This is not actually a hook, but is a useful utility function that can be used +in writing other hooks. Basically, it returns ``True`` if setup.py was run +with a "display option" such as --version or --help. This can be used to +prevent your hook from running in such cases. + +stsci.distutils.hooks.glob_data_files +''''''''''''''''''''''''''''''''''''' +A pre-command hook for the install_data command. Allows filename wildcards as +understood by ``glob.glob()`` to be used in the data_files option. This hook +must be used in order to have this functionality since it does not normally +exist in distutils. + +This hook also ensures that data files are installed relative to the package +path. data_files shouldn't normally be installed this way, but the +functionality is required for a few special cases. + + +Commands +-------- +build_optional_ext +'''''''''''''''''' +This serves as an optional replacement for the default built_ext command, +which compiles C extension modules. Its purpose is to allow extension modules +to be *optional*, so that if their build fails the rest of the package is +still allowed to be built and installed. This can be used when an extension +module is not definitely required to use the package. + +To use this custom command, add:: + + commands = stsci.distutils.command.build_optional_ext.build_optional_ext + +under the ``[global]`` section of your package's setup.cfg. Then, to mark +an individual extension module as optional, under the setup.cfg section for +that extension add:: + + optional = True + +Optionally, you may also add a custom failure message by adding:: + + fail_message = The foobar extension module failed to compile. + This could be because you lack such and such headers. + This package will still work, but such and such features + will be disabled. + + +.. _stsci_python: http://www.stsci.edu/resources/software_hardware/pyraf/stsci_python +.. _Astrolib: http://www.scipy.org/AstroLib/ +.. _distutils2/packaging: http://distutils2.notmyidea.org/ +.. _d2to1: http://pypi.python.org/pypi/d2to1 +.. _distribute: http://pypi.python.org/pypi/distribute diff --git a/pbr/d2to1/tests/testpackage/d2to1_testpackage/__init__.py b/pbr/d2to1/tests/testpackage/d2to1_testpackage/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/d2to1_testpackage/__init__.py diff --git a/pbr/d2to1/tests/testpackage/d2to1_testpackage/_setup_hooks.py b/pbr/d2to1/tests/testpackage/d2to1_testpackage/_setup_hooks.py new file mode 100644 index 0000000..77005b2 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/d2to1_testpackage/_setup_hooks.py @@ -0,0 +1,25 @@ +from distutils.command.build_py import build_py + + +def test_hook_1(config): + print('test_hook_1') + + +def test_hook_2(config): + print('test_hook_2') + + +class test_command(build_py): + command_name = 'build_py' + + def run(self): + print('Running custom build_py command.') + return build_py.run(self) + + +def test_pre_hook(cmdobj): + print('build_ext pre-hook') + + +def test_post_hook(cmdobj): + print('build_ext post-hook') diff --git a/pbr/d2to1/tests/testpackage/d2to1_testpackage/package_data/1.txt b/pbr/d2to1/tests/testpackage/d2to1_testpackage/package_data/1.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/d2to1_testpackage/package_data/1.txt diff --git a/pbr/d2to1/tests/testpackage/d2to1_testpackage/package_data/2.txt b/pbr/d2to1/tests/testpackage/d2to1_testpackage/package_data/2.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/d2to1_testpackage/package_data/2.txt diff --git a/pbr/d2to1/tests/testpackage/data_files/a.txt b/pbr/d2to1/tests/testpackage/data_files/a.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/data_files/a.txt diff --git a/pbr/d2to1/tests/testpackage/data_files/b.txt b/pbr/d2to1/tests/testpackage/data_files/b.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/data_files/b.txt diff --git a/pbr/d2to1/tests/testpackage/data_files/c.rst b/pbr/d2to1/tests/testpackage/data_files/c.rst new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/data_files/c.rst diff --git a/pbr/d2to1/tests/testpackage/distribute_setup.py b/pbr/d2to1/tests/testpackage/distribute_setup.py new file mode 100644 index 0000000..bbb6f3c --- /dev/null +++ b/pbr/d2to1/tests/testpackage/distribute_setup.py @@ -0,0 +1,485 @@ +#!python +"""Bootstrap distribute installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from distribute_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import os +import sys +import time +import fnmatch +import tempfile +import tarfile +from distutils import log + +try: + from site import USER_SITE +except ImportError: + USER_SITE = None + +try: + import subprocess + + def _python_cmd(*args): + args = (sys.executable,) + args + return subprocess.call(args) == 0 + +except ImportError: + # will be used for python 2.3 + def _python_cmd(*args): + args = (sys.executable,) + args + # quoting arguments if windows + if sys.platform == 'win32': + def quote(arg): + if ' ' in arg: + return '"%s"' % arg + return arg + args = [quote(arg) for arg in args] + return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 + +DEFAULT_VERSION = "0.6.19" +DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" +SETUPTOOLS_FAKED_VERSION = "0.6c11" + +SETUPTOOLS_PKG_INFO = """\ +Metadata-Version: 1.0 +Name: setuptools +Version: %s +Summary: xxxx +Home-page: xxx +Author: xxx +Author-email: xxx +License: xxx +Description: xxx +""" % SETUPTOOLS_FAKED_VERSION + + +def _install(tarball): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # installing + log.warn('Installing Distribute') + if not _python_cmd('setup.py', 'install'): + log.warn('Something went wrong during the installation.') + log.warn('See the error message above.') + finally: + os.chdir(old_wd) + + +def _build_egg(egg, tarball, to_dir): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # building an egg + log.warn('Building a Distribute egg in %s', to_dir) + _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + + finally: + os.chdir(old_wd) + # returning the result + log.warn(egg) + if not os.path.exists(egg): + raise IOError('Could not build the egg.') + + +def _do_download(version, download_base, to_dir, download_delay): + egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' + % (version, sys.version_info[0], sys.version_info[1])) + if not os.path.exists(egg): + tarball = download_setuptools(version, download_base, + to_dir, download_delay) + _build_egg(egg, tarball, to_dir) + sys.path.insert(0, egg) + import setuptools + setuptools.bootstrap_install_from = egg + + +def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, download_delay=15, no_fake=True): + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + was_imported = 'pkg_resources' in sys.modules or \ + 'setuptools' in sys.modules + try: + try: + import pkg_resources + if not hasattr(pkg_resources, '_distribute'): + if not no_fake: + _fake_setuptools() + raise ImportError + except ImportError: + return _do_download(version, download_base, to_dir, download_delay) + try: + pkg_resources.require("distribute>="+version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + if not hasattr(DirectorySandbox, '_old'): + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + else: + patched = False + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + +def _patch_file(path, content): + """Will backup the file then patch it""" + existing_content = open(path).read() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + +_patch_file = _no_sandbox(_patch_file) + +def _same_content(path, content): + return open(path).read() == content + +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s into %s', path, new_name) + os.rename(path, new_name) + return new_name + +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Removing elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + +_remove_flat_installation = _no_sandbox(_remove_flat_installation) + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) + +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + +_patch_egg_dir = _no_sandbox(_patch_egg_dir) + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install')+1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index+1] + return location.startswith(top_dir) + if arg == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', + replacement=False)) + except TypeError: + # old distribute API + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patched done.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + # pip marker to avoid a relaunch bug + if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: + sys.argv[0] = 'setup.py' + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + tarball = download_setuptools() + _install(tarball) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/pbr/d2to1/tests/testpackage/extra-file.txt b/pbr/d2to1/tests/testpackage/extra-file.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/extra-file.txt diff --git a/pbr/d2to1/tests/testpackage/setup.cfg b/pbr/d2to1/tests/testpackage/setup.cfg new file mode 100644 index 0000000..a200616 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/setup.cfg @@ -0,0 +1,46 @@ +[metadata] +name = d2to1_testpackage +version = 0.1.dev +author = Erik M. Bray +author-email = embray@stsci.edu +home-page = http://www.stsci.edu/resources/software_hardware/stsci_python +summary = Test package for testing d2to1 +description-file = + README.txt + CHANGES.txt +requires-python = >=2.5 + +requires-dist = + setuptools + +classifier = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Programming Language :: Python + Topic :: Scientific/Engineering + Topic :: Software Development :: Build Tools + Topic :: Software Development :: Libraries :: Python Modules + Topic :: System :: Archiving :: Packaging + +keywords = packaging, distutils, setuptools + +[files] +packages = d2to1_testpackage +package-data = testpackage = package_data/*.txt +data-files = testpackage/data_files = data_files/*.txt +extra-files = extra-file.txt + +[extension=d2to1_testpackage.testext] +sources = src/testext.c +optional = True + +[global] +#setup-hooks = +# d2to1_testpackage._setup_hooks.test_hook_1 +# d2to1_testpackage._setup_hooks.test_hook_2 +commands = d2to1_testpackage._setup_hooks.test_command + +[build_ext] +#pre-hook.test_pre_hook = d2to1_testpackage._setup_hooks.test_pre_hook +#post-hook.test_post_hook = d2to1_testpackage._setup_hooks.test_post_hook diff --git a/pbr/d2to1/tests/testpackage/setup.py b/pbr/d2to1/tests/testpackage/setup.py new file mode 100755 index 0000000..dbaba47 --- /dev/null +++ b/pbr/d2to1/tests/testpackage/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +try: + from setuptools import setup +except ImportError: + from distribute_setup import use_setuptools + use_setuptools() + from setuptools import setup + +setup( + setup_requires=['d2to1'], + d2to1=True, +) diff --git a/pbr/d2to1/tests/testpackage/src/testext.c b/pbr/d2to1/tests/testpackage/src/testext.c new file mode 100644 index 0000000..872d43c --- /dev/null +++ b/pbr/d2to1/tests/testpackage/src/testext.c @@ -0,0 +1,28 @@ +#include <Python.h> + + +static PyMethodDef TestextMethods[] = { + {NULL, NULL, 0, NULL} +}; + + +#if PY_MAJOR_VERSION >=3 +static struct PyModuleDef testextmodule = { + PyModuleDef_HEAD_INIT, + "testext", + -1, + TestextMethods +}; + +PyObject* +PyInit_testext(void) +{ + return PyModule_Create(&testextmodule); +} +#else +PyMODINIT_FUNC +inittestext(void) +{ + Py_InitModule("testext", TestextMethods); +} +#endif diff --git a/pbr/d2to1/tests/util.py b/pbr/d2to1/tests/util.py new file mode 100644 index 0000000..fa55587 --- /dev/null +++ b/pbr/d2to1/tests/util.py @@ -0,0 +1,35 @@ +from __future__ import with_statement + +import contextlib +import os +import shutil +import stat + + +from ..extern.six import moves as m +ConfigParser = m.configparser.ConfigParser + + +@contextlib.contextmanager +def open_config(filename): + cfg = ConfigParser() + cfg.read(filename) + yield cfg + with open(filename, 'w') as fp: + cfg.write(fp) + + +def rmtree(path): + """ + shutil.rmtree() with error handler for 'access denied' from trying to + delete read-only files. + """ + + def onerror(func, path, exc_info): + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + return shutil.rmtree(path, onerror=onerror) diff --git a/pbr/d2to1/util.py b/pbr/d2to1/util.py new file mode 100644 index 0000000..9d67f80 --- /dev/null +++ b/pbr/d2to1/util.py @@ -0,0 +1,582 @@ +"""The code in this module is mostly copy/pasted out of the distutils2 source +code, as recommended by Tarek Ziade. As such, it may be subject to some change +as distutils2 development continues, and will have to be kept up to date. + +I didn't want to use it directly from distutils2 itself, since I do not want it +to be an installation dependency for our packages yet--it is still too unstable +(the latest version on PyPI doesn't even install). +""" + +# These first two imports are not used, but are needed to get around an +# irritating Python bug that can crop up when using ./setup.py test. +# See: http://www.eby-sarna.com/pipermail/peak/2010-May/003355.html +try: + import multiprocessing +except ImportError: + pass +import logging + +import os +import re +import sys +import traceback + +from collections import defaultdict + +import distutils.ccompiler + +from distutils import log +from distutils.errors import (DistutilsOptionError, DistutilsModuleError, + DistutilsFileError) +from setuptools.command.egg_info import manifest_maker +from setuptools.dist import Distribution +from setuptools.extension import Extension + +from .extern.six import moves as m +RawConfigParser = m.configparser.RawConfigParser + + +# A simplified RE for this; just checks that the line ends with version +# predicates in () +_VERSION_SPEC_RE = re.compile(r'\s*(.*?)\s*\((.*)\)\s*$') + + +# Mappings from setup() keyword arguments to setup.cfg options; +# The values are (section, option) tuples, or simply (section,) tuples if +# the option has the same name as the setup() argument +D1_D2_SETUP_ARGS = { + "name": ("metadata",), + "version": ("metadata",), + "author": ("metadata",), + "author_email": ("metadata",), + "maintainer": ("metadata",), + "maintainer_email": ("metadata",), + "url": ("metadata", "home_page"), + "description": ("metadata", "summary"), + "keywords": ("metadata",), + "long_description": ("metadata", "description"), + "download-url": ("metadata",), + "classifiers": ("metadata", "classifier"), + "platforms": ("metadata", "platform"), # ** + "license": ("metadata",), + # Use setuptools install_requires, not + # broken distutils requires + "install_requires": ("metadata", "requires_dist"), + "setup_requires": ("metadata", "setup_requires_dist"), + "provides": ("metadata", "provides_dist"), # ** + "obsoletes": ("metadata", "obsoletes_dist"), # ** + "package_dir": ("files", 'packages_root'), + "packages": ("files",), + "package_data": ("files",), + "namespace_packages": ("files",), + "data_files": ("files",), + "scripts": ("files",), + "py_modules": ("files", "modules"), # ** + "cmdclass": ("global", "commands"), + # Not supported in distutils2, but provided for + # backwards compatibility with setuptools + "use_2to3": ("backwards_compat", "use_2to3"), + "zip_safe": ("backwards_compat", "zip_safe"), + "tests_require": ("backwards_compat", "tests_require"), + "dependency_links": ("backwards_compat",), + "include_package_data": ("backwards_compat",), +} + +# setup() arguments that can have multiple values in setup.cfg +MULTI_FIELDS = ("classifiers", + "platforms", + "install_requires", + "provides", + "obsoletes", + "namespace_packages", + "packages", + "package_data", + "data_files", + "scripts", + "py_modules", + "dependency_links", + "setup_requires", + "tests_require", + "cmdclass") + +# setup() arguments that contain boolean values +BOOL_FIELDS = ("use_2to3", "zip_safe", "include_package_data") + + +CSV_FIELDS = ("keywords",) + + +log.set_verbosity(log.INFO) + + +def resolve_name(name): + """Resolve a name like ``module.object`` to an object and return it. + + Raise ImportError if the module or name is not found. + """ + + parts = name.split('.') + cursor = len(parts) - 1 + module_name = parts[:cursor] + attr_name = parts[-1] + + while cursor > 0: + try: + ret = __import__('.'.join(module_name), fromlist=[attr_name]) + break + except ImportError: + if cursor == 0: + raise + cursor -= 1 + module_name = parts[:cursor] + attr_name = parts[cursor] + ret = '' + + for part in parts[cursor:]: + try: + ret = getattr(ret, part) + except AttributeError: + raise ImportError(name) + + return ret + + +def cfg_to_args(path='setup.cfg'): + """ Distutils2 to distutils1 compatibility util. + + This method uses an existing setup.cfg to generate a dictionary of + keywords that can be used by distutils.core.setup(kwargs**). + + :param file: + The setup.cfg path. + :raises DistutilsFileError: + When the setup.cfg file is not found. + + """ + + # The method source code really starts here. + parser = RawConfigParser() + if not os.path.exists(path): + raise DistutilsFileError("file '%s' does not exist" % + os.path.abspath(path)) + parser.read(path) + config = {} + for section in parser.sections(): + config[section] = dict(parser.items(section)) + + # Run setup_hooks, if configured + setup_hooks = has_get_option(config, 'global', 'setup_hooks') + package_dir = has_get_option(config, 'files', 'packages_root') + + # Add the source package directory to sys.path in case it contains + # additional hooks, and to make sure it's on the path before any existing + # installations of the package + if package_dir: + package_dir = os.path.abspath(package_dir) + sys.path.insert(0, package_dir) + + try: + if setup_hooks: + setup_hooks = split_multiline(setup_hooks) + for hook in setup_hooks: + hook_fn = resolve_name(hook) + try : + hook_fn(config) + except SystemExit: + log.error('setup hook %s terminated the installation') + except: + e = sys.exc_info()[1] + log.error('setup hook %s raised exception: %s\n' % + (hook, e)) + log.error(traceback.format_exc()) + sys.exit(1) + + kwargs = setup_cfg_to_setup_kwargs(config) + + register_custom_compilers(config) + + ext_modules = get_extension_modules(config) + if ext_modules: + kwargs['ext_modules'] = ext_modules + + entry_points = get_entry_points(config) + if entry_points: + kwargs['entry_points'] = entry_points + + wrap_commands(kwargs) + + # Handle the [files]/extra_files option + extra_files = has_get_option(config, 'files', 'extra_files') + if extra_files: + extra_files = split_multiline(extra_files) + # Let's do a sanity check + for filename in extra_files: + if not os.path.exists(filename): + raise DistutilsFileError( + '%s from the extra_files option in setup.cfg does not ' + 'exist' % filename) + # Unfortunately the only really sensible way to do this is to + # monkey-patch the manifest_maker class + @monkeypatch_method(manifest_maker) + def add_defaults(self, extra_files=extra_files, log=log): + log.info('[d2to1] running patched manifest_maker command ' + 'with extra_files support') + add_defaults._orig(self) + self.filelist.extend(extra_files) + + finally: + # Perform cleanup if any paths were added to sys.path + if package_dir: + sys.path.pop(0) + + return kwargs + + +def setup_cfg_to_setup_kwargs(config): + """Processes the setup.cfg options and converts them to arguments accepted + by setuptools' setup() function. + """ + + kwargs = {} + + for arg in D1_D2_SETUP_ARGS: + if len(D1_D2_SETUP_ARGS[arg]) == 2: + # The distutils field name is different than distutils2's. + section, option = D1_D2_SETUP_ARGS[arg] + + elif len(D1_D2_SETUP_ARGS[arg]) == 1: + # The distutils field name is the same thant distutils2's. + section = D1_D2_SETUP_ARGS[arg][0] + option = arg + + in_cfg_value = has_get_option(config, section, option) + if not in_cfg_value: + # There is no such option in the setup.cfg + if arg == "long_description": + in_cfg_value = has_get_option(config, section, + "description_file") + if in_cfg_value: + in_cfg_value = split_multiline(in_cfg_value) + value = '' + for filename in in_cfg_value: + description_file = open(filename) + try: + value += description_file.read().strip() + '\n\n' + finally: + description_file.close() + in_cfg_value = value + else: + continue + + if arg in CSV_FIELDS: + in_cfg_value = split_csv(in_cfg_value) + if arg in MULTI_FIELDS: + in_cfg_value = split_multiline(in_cfg_value) + elif arg in BOOL_FIELDS: + # Provide some flexibility here... + if in_cfg_value.lower() in ('true', 't', '1', 'yes', 'y'): + in_cfg_value = True + else: + in_cfg_value = False + + if in_cfg_value: + if arg in ('install_requires', 'tests_require'): + # Replaces PEP345-style version specs with the sort expected by + # setuptools + in_cfg_value = [_VERSION_SPEC_RE.sub(r'\1\2', pred) + for pred in in_cfg_value] + elif arg == 'package_dir': + in_cfg_value = {'': in_cfg_value} + elif arg in ('package_data', 'data_files'): + data_files = {} + firstline = True + prev = None + for line in in_cfg_value: + if '=' in line: + key, value = line.split('=', 1) + key, value = (key.strip(), value.strip()) + if key in data_files: + # Multiple duplicates of the same package name; + # this is for backwards compatibility of the old + # format prior to d2to1 0.2.6. + prev = data_files[key] + prev.extend(value.split()) + else: + prev = data_files[key.strip()] = value.split() + elif firstline: + raise DistutilsOptionError( + 'malformed package_data first line %r (misses ' + '"=")' % line) + else: + prev.extend(line.strip().split()) + firstline = False + if arg == 'data_files': + # the data_files value is a pointlessly different structure + # from the package_data value + data_files = data_files.items() + in_cfg_value = data_files + elif arg == 'cmdclass': + cmdclass = {} + dist = Distribution() + for cls in in_cfg_value: + cls = resolve_name(cls) + cmd = cls(dist) + cmdclass[cmd.get_command_name()] = cls + in_cfg_value = cmdclass + + kwargs[arg] = in_cfg_value + + return kwargs + + +def register_custom_compilers(config): + """Handle custom compilers; this has no real equivalent in distutils, where + additional compilers could only be added programmatically, so we have to + hack it in somehow. + """ + + compilers = has_get_option(config, 'global', 'compilers') + if compilers: + compilers = split_multiline(compilers) + for compiler in compilers: + compiler = resolve_name(compiler) + + # In distutils2 compilers these class attributes exist; for + # distutils1 we just have to make something up + if hasattr(compiler, 'name'): + name = compiler.name + else: + name = compiler.__name__ + if hasattr(compiler, 'description'): + desc = compiler.description + else: + desc = 'custom compiler %s' % name + + module_name = compiler.__module__ + # Note; this *will* override built in compilers with the same name + # TODO: Maybe display a warning about this? + cc = distutils.ccompiler.compiler_class + cc[name] = (module_name, compiler.__name__, desc) + + # HACK!!!! Distutils assumes all compiler modules are in the + # distutils package + sys.modules['distutils.' + module_name] = sys.modules[module_name] + + +def get_extension_modules(config): + """Handle extension modules""" + + EXTENSION_FIELDS = ("sources", + "include_dirs", + "define_macros", + "undef_macros", + "library_dirs", + "libraries", + "runtime_library_dirs", + "extra_objects", + "extra_compile_args", + "extra_link_args", + "export_symbols", + "swig_opts", + "depends") + + ext_modules = [] + for section in config: + if ':' in section: + labels = section.split(':', 1) + else: + # Backwards compatibility for old syntax; don't use this though + labels = section.split('=', 1) + labels = [l.strip() for l in labels] + if (len(labels) == 2) and (labels[0] == 'extension'): + ext_args = {} + for field in EXTENSION_FIELDS: + value = has_get_option(config, section, field) + # All extension module options besides name can have multiple + # values + if not value: + continue + value = split_multiline(value) + if field == 'define_macros': + macros = [] + for macro in value: + macro = macro.split('=', 1) + if len(macro) == 1: + macro = (macro[0].strip(), None) + else: + macro = (macro[0].strip(), macro[1].strip()) + macros.append(macro) + value = macros + ext_args[field] = value + if ext_args: + if 'name' not in ext_args: + ext_args['name'] = labels[1] + ext_modules.append(Extension(ext_args.pop('name'), + **ext_args)) + return ext_modules + + +def get_entry_points(config): + """Process the [entry_points] section of setup.cfg to handle setuptools + entry points. This is, of course, not a standard feature of + distutils2/packaging, but as there is not currently a standard alternative + in packaging, we provide support for them. + """ + + if not 'entry_points' in config: + return {} + + return dict((option, split_multiline(value)) + for option, value in config['entry_points'].items()) + + +def wrap_commands(kwargs): + dist = Distribution() + + # This should suffice to get the same config values and command classes + # that the actual Distribution will see (not counting cmdclass, which is + # handled below) + dist.parse_config_files() + + for cmd, _ in dist.get_command_list(): + hooks = {} + for opt, val in dist.get_option_dict(cmd).items(): + val = val[1] + if opt.startswith('pre_hook.') or opt.startswith('post_hook.'): + hook_type, alias = opt.split('.', 1) + hook_dict = hooks.setdefault(hook_type, {}) + hook_dict[alias] = val + if not hooks: + continue + + if 'cmdclass' in kwargs and cmd in kwargs['cmdclass']: + cmdclass = kwargs['cmdclass'][cmd] + else: + cmdclass = dist.get_command_class(cmd) + + new_cmdclass = wrap_command(cmd, cmdclass, hooks) + kwargs.setdefault('cmdclass', {})[cmd] = new_cmdclass + + +def wrap_command(cmd, cmdclass, hooks): + def run(self, cmdclass=cmdclass): + self.run_command_hooks('pre_hook') + cmdclass.run(self) + self.run_command_hooks('post_hook') + + return type(cmd, (cmdclass, object), + {'run': run, 'run_command_hooks': run_command_hooks, + 'pre_hook': hooks.get('pre_hook'), + 'post_hook': hooks.get('post_hook')}) + + +def run_command_hooks(cmd_obj, hook_kind): + """Run hooks registered for that command and phase. + + *cmd_obj* is a finalized command object; *hook_kind* is either + 'pre_hook' or 'post_hook'. + """ + + if hook_kind not in ('pre_hook', 'post_hook'): + raise ValueError('invalid hook kind: %r' % hook_kind) + + hooks = getattr(cmd_obj, hook_kind, None) + + if hooks is None: + return + + for hook in hooks.values(): + if isinstance(hook, str): + try: + hook_obj = resolve_name(hook) + except ImportError: + err = sys.exc_info()[1] # For py3k + raise DistutilsModuleError('cannot find hook %s: %s' % + (hook,err)) + else: + hook_obj = hook + + if not hasattr(hook_obj, '__call__'): + raise DistutilsOptionError('hook %r is not callable' % hook) + + log.info('running %s %s for command %s', + hook_kind, hook, cmd_obj.get_command_name()) + + try : + hook_obj(cmd_obj) + except: + e = sys.exc_info()[1] + log.error('hook %s raised exception: %s\n' % (hook, e)) + log.error(traceback.format_exc()) + sys.exit(1) + + +def has_get_option(config, section, option): + if section in config and option in config[section]: + return config[section][option] + elif section in config and option.replace('_', '-') in config[section]: + return config[section][option.replace('_', '-')] + else: + return False + + +def split_multiline(value): + """Special behaviour when we have a multi line options""" + + value = [element for element in + (line.strip() for line in value.split('\n')) + if element] + return value + + +def split_csv(value): + """Special behaviour when we have a comma separated options""" + + value = [element for element in + (chunk.strip() for chunk in value.split(',')) + if element] + return value + + +def monkeypatch_method(cls): + """A function decorator to monkey-patch a method of the same name on the + given class. + """ + + def wrapper(func): + orig = getattr(cls, func.__name__, None) + if orig and not hasattr(orig, '_orig'): # Already patched + setattr(func, '_orig', orig) + setattr(cls, func.__name__, func) + return func + + return wrapper + + +# The following classes are used to hack Distribution.command_options a bit +class DefaultGetDict(defaultdict): + """Like defaultdict, but the get() method also sets and returns the default + value. + """ + + def get(self, key, default=None): + if default is None: + default = self.default_factory() + return super(DefaultGetDict, self).setdefault(key, default) + + +class IgnoreDict(dict): + """A dictionary that ignores any insertions in which the key is a string + matching any string in `ignore`. The ignore list can also contain wildcard + patterns using '*'. + """ + + def __init__(self, ignore): + self.__ignore = re.compile(r'(%s)' % ('|'.join( + [pat.replace('*', '.*') + for pat in ignore]))) + + def __setitem__(self, key, val): + if self.__ignore.match(key): + return + super(IgnoreDict, self).__setitem__(key, val) diff --git a/pbr/d2to1/zestreleaser.py b/pbr/d2to1/zestreleaser.py new file mode 100644 index 0000000..326baca --- /dev/null +++ b/pbr/d2to1/zestreleaser.py @@ -0,0 +1,159 @@ +"""zest.releaser entry points to support projects using distutils2-like +setup.cfg files. The only actual functionality this adds is to update the +version option in a setup.cfg file, if it exists. If setup.cfg does not exist, +or does not contain a version option, then this does nothing. + +TODO: d2to1 theoretically supports using a different filename for setup.cfg; +this does not support that. We could hack in support, though I'm not sure how +useful the original functionality is to begin with (and it might be removed) so +we ignore that for now. + +TODO: There exists a proposal +(http://mail.python.org/pipermail/distutils-sig/2011-March/017628.html) to add +a 'version-from-file' option (or something of the like) to distutils2; if this +is added then support for it should be included here as well. +""" + + +import logging +import os + +from .extern.six import moves as m +ConfigParser = m.configparser.ConfigParser + + +logger = logging.getLogger(__name__) + + + +def update_setupcfg_version(filename, version): + """Opens the given setup.cfg file, locates the version option in the + [metadata] section, updates it to the new version. + """ + + setup_cfg = open(filename).readlines() + current_section = None + updated = False + + for idx, line in enumerate(setup_cfg): + m = ConfigParser.SECTCRE.match(line) + if m: + if current_section == 'metadata': + # We already parsed the entire metadata section without finding + # a version line, and are now moving into a new section + break + current_section = m.group('header') + continue + + if '=' not in line: + continue + + opt, val = line.split('=', 1) + opt, val = opt.strip(), val.strip() + if current_section == 'metadata' and opt == 'version': + setup_cfg[idx] = 'version = %s\n' % version + updated = True + break + + if updated: + open(filename, 'w').writelines(setup_cfg) + logger.info("Set %s's version to %r" % (os.path.basename(filename), + version)) + + +def prereleaser_middle(data): + filename = os.path.join(data['workingdir'], 'setup.cfg') + if os.path.exists(filename): + update_setupcfg_version(filename, data['new_version']) + + +def releaser_middle(data): + """ + releaser.middle hook to monkey-patch zest.releaser to support signed + tagging--currently this is the only way to do this. Also monkey-patches to + disable an annoyance where zest.releaser only creates .zip source + distributions. This is supposedly a workaround for a bug in Python 2.4, + but we don't care about Python 2.4. + """ + + import os + import sys + + from zest.releaser.git import Git + from zest.releaser.release import Releaser + + # Copied verbatim from zest.releaser, but with the cmd string modified to + # use the -s option to create a signed tag + def _my_create_tag(self, version): + msg = "Tagging %s" % (version,) + cmd = 'git tag -s %s -m "%s"' % (version, msg) + if os.path.isdir('.git/svn'): + print "\nEXPERIMENTAL support for git-svn tagging!\n" + cur_branch = open('.git/HEAD').read().strip().split('/')[-1] + print "You are on branch %s." % (cur_branch,) + if cur_branch != 'master': + print "Only the master branch is supported for git-svn tagging." + print "Please tag yourself." + print "'git tag' needs to list tag named %s." % (version,) + sys.exit() + cmd = [cmd] + local_head = open('.git/refs/heads/master').read() + trunk = open('.git/refs/remotes/trunk').read() + if local_head != trunk: + print "Your local master diverges from trunk.\n" + # dcommit before local tagging + cmd.insert(0, 'git svn dcommit') + # create tag in svn + cmd.append('git svn tag -m "%s" %s' % (msg, version)) + return cmd + + # Similarly copied from zer.releaser to support use of 'v' in front + # of the version number + def _my_make_tag(self): + from zest.releaser import utils + from os import system + + if self.data['tag_already_exists']: + return + cmds = self.vcs.cmd_create_tag(self.data['version']) + if not isinstance(cmds, list): + cmds = [cmds] + if len(cmds) == 1: + print "Tag needed to proceed, you can use the following command:" + for cmd in cmds: + print cmd + if utils.ask("Run this command"): + print system(cmd) + else: + # all commands are needed in order to proceed normally + print "Please create a tag for %s yourself and rerun." % \ + (self.data['version'],) + sys.exit() + if not self.vcs.tag_exists('v' + self.data['version']): + print "\nFailed to create tag %s!" % (self.data['version'],) + sys.exit() + + # Normally all this does is to return '--formats=zip', which is currently + # hard-coded as an option to always add to the sdist command; they ought to + # make this actually optional + def _my_sdist_options(self): + return '' + + Git.cmd_create_tag = _my_create_tag + Releaser._make_tag = _my_make_tag + Releaser._sdist_options = _my_sdist_options + + +def postreleaser_before(data): + """ + Fix the irritating .dev0 default appended to new development versions by + zest.releaser to just append ".dev" without the "0". + """ + + data['dev_version_template'] = '%(new_version)s.dev' + + +def postreleaser_middle(data): + filename = os.path.join(data['workingdir'], 'setup.cfg') + if os.path.exists(filename): + update_setupcfg_version(filename, data['dev_version']) |