summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.rst7
-rw-r--r--README.rst31
-rw-r--r--doc/source/conf.py5
-rw-r--r--doc/source/index.rst56
-rw-r--r--doc/source/packagers.rst39
-rw-r--r--doc/source/testing.rst27
-rw-r--r--pbr/find_package.py3
-rw-r--r--pbr/hooks/__init__.py2
-rw-r--r--pbr/hooks/backwards.py2
-rw-r--r--pbr/hooks/base.py2
-rw-r--r--pbr/hooks/commands.py2
-rw-r--r--pbr/hooks/files.py2
-rw-r--r--pbr/hooks/metadata.py2
-rw-r--r--pbr/packaging.py412
-rw-r--r--pbr/testr_command.py2
-rw-r--r--pbr/tests/base.py18
-rw-r--r--pbr/tests/test_files.py2
-rw-r--r--pbr/tests/test_packaging.py245
-rw-r--r--pbr/tests/test_setup.py32
-rw-r--r--pbr/tests/test_version.py330
-rw-r--r--pbr/tests/testpackage/pbr_testpackage/__init__.py3
-rw-r--r--pbr/tests/testpackage/setup.cfg2
-rw-r--r--pbr/version.py422
-rw-r--r--setup.cfg2
-rw-r--r--test-requirements.txt2
-rw-r--r--tools/integration.sh22
-rw-r--r--tox.ini12
27 files changed, 1452 insertions, 234 deletions
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 3115f03..51b3e69 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,14 +1,13 @@
If you would like to contribute to the development of OpenStack,
-you must follow the steps in the "If you're a developer, start here"
-section of this page:
+you must follow the steps in this page:
- http://wiki.openstack.org/HowToContribute
+ http://docs.openstack.org/infra/manual/developers.html
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
- http://wiki.openstack.org/GerritWorkflow
+ http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
diff --git a/README.rst b/README.rst
index 59c57d6..4ac46b8 100644
--- a/README.rst
+++ b/README.rst
@@ -24,34 +24,11 @@ them as quickly as possible.
You can read more in `the documentation`_.
-Running Tests
-=============
-The testing system is based on a combination of `tox`_ and `testr`_. The canonical
-approach to running tests is to simply run the command ``tox``. This will
-create virtual environments, populate them with dependencies and run all of
-the tests that OpenStack CI systems run. Behind the scenes, tox is running
-``testr run --parallel``, but is set up such that you can supply any additional
-testr arguments that are needed to tox. For example, you can run:
-``tox -- --analyze-isolation`` to cause tox to tell testr to add
-``--analyze-isolation`` to its argument list.
+Bugs are tracked using launchpad_.
-It is also possible to run the tests inside of a virtual environment
-you have created, or it is possible that you have all of the dependencies
-installed locally already. If you'd like to go this route, the requirements
-are listed in ``requirements.txt`` and the requirements for testing are in
-``test-requirements.txt``. Installing them via pip, for instance, is simply::
-
- pip install -r requirements.txt -r test-requirements.txt
-
-In you go this route, you can interact with the testr command directly.
-Running ``testr run`` will run the entire test suite. ``testr run --parallel``
-will run it in parallel (this is the default incantation tox uses). More
-information about testr can be found at: http://wiki.openstack.org/testr
-
-.. _OpenStack: https://www.openstack.org/
-.. _`the documentation`: http://docs.openstack.org/developer/pbr/
-.. _tox: http://tox.testrun.org/
.. _d2to1: https://pypi.python.org/pypi/d2to1
.. _distutils2: https://pypi.python.org/pypi/Distutils2
.. _PEP 426: http://legacy.python.org/dev/peps/pep-0426/
-.. _testr: https://wiki.openstack.org/wiki/Testr
+.. _OpenStack: https://www.openstack.org/
+.. _`the documentation`: http://docs.openstack.org/developer/pbr/
+.. _launchpad: https://launchpad.net/pbr
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 101ef13..13ca86b 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -8,7 +8,7 @@ sys.path.insert(0, os.path.abspath('../..'))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
+extensions = ['sphinx.ext.autodoc']
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
@@ -57,6 +57,3 @@ latex_documents = [
'%s Documentation' % project,
'OpenStack Foundation', 'manual'),
]
-
-# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/doc/source/index.rst b/doc/source/index.rst
index f4d604e..f36fc6f 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -26,11 +26,37 @@ PBR can and does do a bunch of things for you:
Version
-------
-Version strings will be inferred from git. If a given revision is tagged,
-that's the version. If it's not, and you don't provide a version, the version
-will be very similar to git describe. If you do, then we'll assume that's the
-version you are working towards, and will generate alpha version strings
-based on commits since last tag and the current git sha.
+Versions can be managed two ways - postversioning and preversioning.
+Postversioning is the default, and preversioning is enabeld by setting
+``version`` in the setup.cfg ``metadata`` section. In both cases
+version strings are inferred from git.
+
+If a given revision is tagged, that's the version.
+
+If it's not, then we take the last tagged version number and increment it to
+get a minimum target version.
+
+We then walk git history back to the last release. Within each commit we look
+for a Sem-Ver: pseudo header, and if found parse it looking for keywords.
+Unknown symbols are not an error (so that folk can't wedge pbr or break their
+tree), but we will emit an info level warning message. Known symbols:
+``feature``, ``api-break``, ``deprecation``, ``bugfix``. A missing
+Sem-Ver line is equivalent to ``Sem-Ver: bugfix``. The ``bugfix`` symbol causes
+a patch level increment to the version. The ``feature`` and ``deprecation``
+symbols cause a minor version increment. The ``api-break`` symbol causes a
+major version increment.
+
+If postversioning is in use, we use the resulting version number as the target
+version.
+
+If preversioning is in use - that is if there is a version set in setup.cfg
+metadata - then we check that that version is higher than the target version
+we inferred above. If it is not, we raise an error, otherwise we use the
+version from setup.cfg as the target.
+
+We then generate dev version strings based on the commits since the last
+release and include the current git sha to disambiguate multiple dev versions
+with the same number of commits since the release.
.. note::
@@ -39,6 +65,11 @@ based on commits since last tag and the current git sha.
The versions are expected to be compliant with :doc:`semver`.
+The ``version.SemanticVersion`` class can be used to query versions of a
+package and present it in various forms - ``debian_version()``,
+``release_string()``, ``rpm_string()``, ``version_string()``, or
+``version_tuple()``.
+
AUTHORS and ChangeLog
---------------------
@@ -76,11 +107,21 @@ You can also have a requirement file for each specific major version of
Python. If you want to have a different package list for Python 3, just drop
a requirements-py3.txt, and it will be used instead.
+It's also possible to select a requirement file specific for an OS. The format
+is requirements-{osname}.txt, where ``{osname}`` is the equivalent of
+``platform.system()``. The two approaches, Python version and OS version, can
+be combined.
+
The requirement files are tried in that order (N being the Python major
-version number used to install the package):
+version number used to install the package and OS being the current
+platform's name in lowercase, retrieved with ``platform.system()``):
+* requirements-OS-pyN.txt
+* tools/pip-requires-OS-pyN
+* requirements-OS.txt
+* tools/pip-requires-OS
* requirements-pyN.txt
-* tools/pip-requires-py3
+* tools/pip-requires-pyN
* requirements.txt
* tools/pip-requires
@@ -221,6 +262,7 @@ Additional Docs
packagers
semver
+ testing
Indices and tables
==================
diff --git a/doc/source/packagers.rst b/doc/source/packagers.rst
index c799432..e135b57 100644
--- a/doc/source/packagers.rst
+++ b/doc/source/packagers.rst
@@ -1,6 +1,6 @@
-=====================
- Notes for Packagers
-=====================
+===============================
+ Notes for Package maintainers
+===============================
If you are maintaining packages of software that uses `pbr`, there are some
features you probably want to be aware of that can make your life easier.
@@ -12,10 +12,11 @@ Versioning
`pbr`, when run in a git repo, derives the version of a package from the
git tags. When run in a tarball with a proper egg-info dir, it will happily
-pull the version from that. So for the most part, the packager shouldn't need
-to care. However, if you are doing something like keeping a git repo with
-the sources and the packaging intermixed and it's causing pbr to get confused
-about whether its in its own git repo or not, you can set `PBR_VERSION`:
+pull the version from that. So for the most part, the package maintainers
+shouldn't need to care. However, if you are doing something like keeping a
+git repo with the sources and the packaging intermixed and it's causing pbr
+to get confused about whether its in its own git repo or not, you can set
+`PBR_VERSION`:
::
@@ -24,6 +25,20 @@ about whether its in its own git repo or not, you can set `PBR_VERSION`:
and all version calculation logic will be completely skipped and the supplied
version will be considered absolute.
+Distribution version numbers
+============================
+
+`pbr` will automatically calculate upstream version numbers for dpkg and rpm
+using systems. Releases are easy (and obvious). When packaging preleases though
+things get more complex. Firstly, semver does not provide for any sort order
+between pre-releases and development snapshots, so it can be complex (perhaps
+intractable) to package both into one repository - we recommend with either
+packaging pre-release releases (alpha/beta/rc's) or dev snapshots but not both.
+Secondly, as pre-releases and snapshots have the same major/minor/patch version
+as the version they lead up to, but have to sort before it, we cannot map their
+version naturally into the rpm version namespace: instead we represent their
+versions as versions of the release before.
+
Dependencies
============
@@ -43,10 +58,10 @@ Tarballs
========
`pbr` includes everything in a source tarball that is in the original `git`
-repository. This can again cause havoc if a packager is doing fancy things
-with combined `git` repos, and is generating a source tarball using `python
-setup.py sdist` from that repo. If that is the workflow the packager is using,
-setting `SKIP_GIT_SDIST`:
+repository. This can again cause havoc if a package maintainer is doing fancy
+things with combined `git` repos, and is generating a source tarball using
+`python setup.py sdist` from that repo. If that is the workflow the packager
+is using, setting `SKIP_GIT_SDIST`:
::
@@ -62,7 +77,7 @@ AUTHORS and ChangeLog
=====================
`pbr` generates AUTHORS and ChangeLog files from git information. This
-can cause problem in distro packaging if packager is using git
+can cause problem in distro packaging if package maintainer is using git
repository for packaging source. If that is the case setting
`SKIP_GENERATE_AUTHORS`
diff --git a/doc/source/testing.rst b/doc/source/testing.rst
new file mode 100644
index 0000000..eccb1e3
--- /dev/null
+++ b/doc/source/testing.rst
@@ -0,0 +1,27 @@
+Running the Tests for pbr
+=========================
+
+The testing system is based on a combination of `tox`_ and `testr`_. The canonical
+approach to running tests is to simply run the command ``tox``. This will
+create virtual environments, populate them with dependencies and run all of
+the tests that OpenStack CI systems run. Behind the scenes, tox is running
+``testr run --parallel``, but is set up such that you can supply any additional
+testr arguments that are needed to tox. For example, you can run:
+``tox -- --analyze-isolation`` to cause tox to tell testr to add
+``--analyze-isolation`` to its argument list.
+
+It is also possible to run the tests inside of a virtual environment
+you have created, or it is possible that you have all of the dependencies
+installed locally already. If you'd like to go this route, the requirements
+are listed in ``requirements.txt`` and the requirements for testing are in
+``test-requirements.txt``. Installing them via pip, for instance, is simply::
+
+ pip install -r requirements.txt -r test-requirements.txt
+
+In you go this route, you can interact with the testr command directly.
+Running ``testr run`` will run the entire test suite. ``testr run --parallel``
+will run it in parallel (this is the default incantation tox uses). More
+information about testr can be found at: http://wiki.openstack.org/testr
+
+.. _tox: http://tox.testrun.org/
+.. _testr: https://wiki.openstack.org/wiki/Testr
diff --git a/pbr/find_package.py b/pbr/find_package.py
index 2319c06..717e93d 100644
--- a/pbr/find_package.py
+++ b/pbr/find_package.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
@@ -16,6 +14,7 @@
# under the License.
import os
+
import setuptools
diff --git a/pbr/hooks/__init__.py b/pbr/hooks/__init__.py
index b35cd42..f0056c0 100644
--- a/pbr/hooks/__init__.py
+++ b/pbr/hooks/__init__.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/hooks/backwards.py b/pbr/hooks/backwards.py
index d9183b3..adbacbf 100644
--- a/pbr/hooks/backwards.py
+++ b/pbr/hooks/backwards.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/hooks/base.py b/pbr/hooks/base.py
index 925573a..6672a36 100644
--- a/pbr/hooks/base.py
+++ b/pbr/hooks/base.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/hooks/commands.py b/pbr/hooks/commands.py
index b4206ed..3885148 100644
--- a/pbr/hooks/commands.py
+++ b/pbr/hooks/commands.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/hooks/files.py b/pbr/hooks/files.py
index ba24aac..a274014 100644
--- a/pbr/hooks/files.py
+++ b/pbr/hooks/files.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/hooks/metadata.py b/pbr/hooks/metadata.py
index 687e149..3f65b6d 100644
--- a/pbr/hooks/metadata.py
+++ b/pbr/hooks/metadata.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/packaging.py b/pbr/packaging.py
index 44c14cc..18d18e6 100644
--- a/pbr/packaging.py
+++ b/pbr/packaging.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2011 OpenStack LLC.
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
@@ -22,16 +20,23 @@ Utilities with minimum-depends for use in setup.py
from __future__ import unicode_literals
+from distutils.command import install as du_install
+import distutils.errors
+from distutils import log
import email
+import functools
import io
+import itertools
import os
+import platform
import re
import subprocess
import sys
+try:
+ import cStringIO
+except ImportError:
+ import io as cStringIO
-from distutils.command import install as du_install
-import distutils.errors
-from distutils import log
import pkg_resources
from setuptools.command import easy_install
from setuptools.command import egg_info
@@ -39,12 +44,8 @@ from setuptools.command import install
from setuptools.command import install_scripts
from setuptools.command import sdist
-try:
- import cStringIO
-except ImportError:
- import io as cStringIO
-
from pbr import extra_files
+from pbr import version
TRUE_VALUES = ('true', '1', 'yes')
REQUIREMENTS_FILES = ('requirements.txt', 'tools/pip-requires')
@@ -58,10 +59,26 @@ def get_requirements_files():
# Returns a list composed of:
# - REQUIREMENTS_FILES with -py2 or -py3 in the name
# (e.g. requirements-py3.txt)
+ # - REQUIREMENTS_FILES with -{platform.system} in the name
+ # (e.g. requirements-windows.txt)
+ # - REQUIREMENTS_FILES with both Python version and platform's
+ # system in the name
+ # (e.g. requirements-freebsd-py2.txt)
# - REQUIREMENTS_FILES
- return (list(map(('-py' + str(sys.version_info[0])).join,
- map(os.path.splitext, REQUIREMENTS_FILES)))
- + list(REQUIREMENTS_FILES))
+ pyversion = sys.version_info[0]
+ system = platform.system().lower()
+ parts = list(map(os.path.splitext, REQUIREMENTS_FILES))
+
+ version_cb = functools.partial("{1}-py{0}{2}".format, pyversion)
+ platform_cb = functools.partial("{1}-{0}{2}".format, system)
+ both_cb = functools.partial("{2}-{0}-py{1}{3}".format, system, pyversion)
+
+ return list(itertools.chain(
+ itertools.starmap(both_cb, parts),
+ itertools.starmap(platform_cb, parts),
+ itertools.starmap(version_cb, parts),
+ REQUIREMENTS_FILES,
+ ))
def append_text_list(config, key, text_list):
@@ -241,63 +258,108 @@ def get_boolean_option(option_dict, option_name, env_name):
str(os.getenv(env_name)).lower() in TRUE_VALUES)
-def write_git_changelog(git_dir=None, dest_dir=os.path.curdir,
- option_dict=dict()):
- """Write a changelog based on the git changelog."""
+def _iter_changelog(changelog):
+ """Convert a oneline log iterator to formatted strings.
+
+ :param changelog: An iterator of one line log entries like
+ that given by _iter_log_oneline.
+ :return: An iterator over (release, formatted changelog) tuples.
+ """
+ first_line = True
+ current_release = None
+ yield current_release, "CHANGES\n=======\n\n"
+ for hash, tags, msg in changelog:
+ if tags:
+ current_release = _get_highest_tag(tags)
+ underline = len(current_release) * '-'
+ if not first_line:
+ yield current_release, '\n'
+ yield current_release, (
+ "%(tag)s\n%(underline)s\n\n" %
+ dict(tag=current_release, underline=underline))
+
+ if not msg.startswith("Merge "):
+ if msg.endswith("."):
+ msg = msg[:-1]
+ yield current_release, "* %(msg)s\n" % dict(msg=msg)
+ first_line = False
+
+
+def _iter_log_oneline(git_dir=None, option_dict=None):
+ """Iterate over --oneline log entries if possible.
+
+ This parses the output into a structured form but does not apply
+ presentation logic to the output - making it suitable for different
+ uses.
+
+ :return: An iterator of (hash, tags_set, 1st_line) tuples, or None if
+ changelog generation is disabled / not available.
+ """
+ if not option_dict:
+ option_dict = {}
should_skip = get_boolean_option(option_dict, 'skip_changelog',
'SKIP_WRITE_GIT_CHANGELOG')
if should_skip:
return
-
- new_changelog = os.path.join(dest_dir, 'ChangeLog')
- # If there's already a ChangeLog and it's not writable, just use it
- if (os.path.exists(new_changelog)
- and not os.access(new_changelog, os.W_OK)):
- return
- log.info('[pbr] Writing ChangeLog')
if git_dir is None:
git_dir = _get_git_directory()
if not git_dir:
return
+ return _iter_log_inner(git_dir)
+
+
+def _iter_log_inner(git_dir):
+ """Iterate over --oneline log entries.
+ This parses the output intro a structured form but does not apply
+ presentation logic to the output - making it suitable for different
+ uses.
+
+ :return: An iterator of (hash, tags_set, 1st_line) tuples.
+ """
+ log.info('[pbr] Generating ChangeLog')
log_cmd = ['log', '--oneline', '--decorate']
changelog = _run_git_command(log_cmd, git_dir)
- first_line = True
- with io.open(new_changelog, "w",
- encoding="utf-8") as changelog_file:
- changelog_file.write("CHANGES\n=======\n\n")
- for line in changelog.split('\n'):
- line_parts = line.split()
- if len(line_parts) < 2:
- continue
- # Tags are in a list contained in ()'s. If a commit
- # subject that is tagged happens to have ()'s in it
- # this will fail
- if line_parts[1].startswith('(') and ')' in line:
- msg = line.split(')')[1].strip()
- else:
- msg = " ".join(line_parts[1:])
-
- if "tag:" in line:
- tags = [
- tag.split(",")[0]
- for tag in line.split(")")[0].split("tag: ")[1:]]
- tag = _get_highest_tag(tags)
-
- underline = len(tag) * '-'
- if not first_line:
- changelog_file.write('\n')
- changelog_file.write(
- ("%(tag)s\n%(underline)s\n\n" %
- dict(tag=tag,
- underline=underline)))
-
- if not msg.startswith("Merge "):
- if msg.endswith("."):
- msg = msg[:-1]
- changelog_file.write(
- ("* %(msg)s\n" % dict(msg=msg)))
- first_line = False
+ for line in changelog.split('\n'):
+ line_parts = line.split()
+ if len(line_parts) < 2:
+ continue
+ # Tags are in a list contained in ()'s. If a commit
+ # subject that is tagged happens to have ()'s in it
+ # this will fail
+ if line_parts[1].startswith('(') and ')' in line:
+ msg = line.split(')')[1].strip()
+ else:
+ msg = " ".join(line_parts[1:])
+
+ if "tag:" in line:
+ tags = set([
+ tag.split(",")[0]
+ for tag in line.split(")")[0].split("tag: ")[1:]])
+ else:
+ tags = set()
+
+ yield line_parts[0], tags, msg
+
+
+def write_git_changelog(git_dir=None, dest_dir=os.path.curdir,
+ option_dict=dict(), changelog=None):
+ """Write a changelog based on the git changelog."""
+ if not changelog:
+ changelog = _iter_log_oneline(git_dir=git_dir, option_dict=option_dict)
+ if changelog:
+ changelog = _iter_changelog(changelog)
+ if not changelog:
+ return
+ log.info('[pbr] Writing ChangeLog')
+ new_changelog = os.path.join(dest_dir, 'ChangeLog')
+ # If there's already a ChangeLog and it's not writable, just use it
+ if (os.path.exists(new_changelog)
+ and not os.access(new_changelog, os.W_OK)):
+ return
+ with io.open(new_changelog, "w", encoding="utf-8") as changelog_file:
+ for release, content in changelog:
+ changelog_file.write(content)
def generate_authors(git_dir=None, dest_dir='.', option_dict=dict()):
@@ -306,7 +368,6 @@ def generate_authors(git_dir=None, dest_dir='.', option_dict=dict()):
'SKIP_GENERATE_AUTHORS')
if should_skip:
return
-
old_authors = os.path.join(dest_dir, 'AUTHORS.in')
new_authors = os.path.join(dest_dir, 'AUTHORS')
# If there's already an AUTHORS file and it's not writable, just use it
@@ -628,7 +689,10 @@ class LocalSDist(sdist.sdist):
def run(self):
option_dict = self.distribution.get_option_dict('pbr')
- write_git_changelog(option_dict=option_dict)
+ changelog = _iter_log_oneline(option_dict=option_dict)
+ if changelog:
+ changelog = _iter_changelog(changelog)
+ write_git_changelog(option_dict=option_dict, changelog=changelog)
generate_authors(option_dict=option_dict)
# sdist.sdist is an old style class, can't use super()
sdist.sdist.run(self)
@@ -654,7 +718,7 @@ try:
os.makedirs(source_dir)
return source_dir
- def generate_autoindex(self):
+ def generate_autoindex(self, excluded_modules=None):
log.info("[pbr] Autodocumenting from %s"
% os.path.abspath(os.curdir))
modules = {}
@@ -663,8 +727,10 @@ try:
if '.' not in pkg:
for dirpath, dirnames, files in os.walk(pkg):
_find_modules(modules, dirpath, files)
- module_list = list(modules.keys())
- module_list.sort()
+ module_list = set(modules.keys())
+ if excluded_modules is not None:
+ module_list -= set(excluded_modules)
+ module_list = sorted(module_list)
autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
with open(autoindex_filename, 'w') as autoindex:
autoindex.write(""".. toctree::
@@ -687,7 +753,8 @@ try:
def _sphinx_tree(self):
source_dir = self._get_source_dir()
- apidoc.main(['apidoc', '.', '-H', 'Modules', '-o', source_dir])
+ cmd = ['apidoc', '.', '-H', 'Modules', '-o', source_dir]
+ apidoc.main(cmd + self.autodoc_tree_excludes)
def _sphinx_run(self):
if not self.verbose:
@@ -730,6 +797,9 @@ try:
def run(self):
option_dict = self.distribution.get_option_dict('pbr')
+ if _git_is_installed():
+ write_git_changelog(option_dict=option_dict)
+ generate_authors(option_dict=option_dict)
tree_index = get_boolean_option(option_dict,
'autodoc_tree_index_modules',
'AUTODOC_TREE_INDEX_MODULES')
@@ -737,12 +807,15 @@ try:
'autodoc_index_modules',
'AUTODOC_INDEX_MODULES')
if not os.getenv('SPHINX_DEBUG'):
- #NOTE(afazekas): These options can be used together,
- # but they do a very similar thing in a difffernet way
+ # NOTE(afazekas): These options can be used together,
+ # but they do a very similar thing in a different way
if tree_index:
self._sphinx_tree()
if auto_index:
- self.generate_autoindex()
+ self.generate_autoindex(
+ option_dict.get(
+ "autodoc_exclude_modules",
+ [None, ""])[1].split())
for builder in self.builders:
self.builder = builder
@@ -750,11 +823,19 @@ try:
self.project = self.distribution.get_name()
self.version = self.distribution.get_version()
self.release = self.distribution.get_version()
- if 'warnerrors' in option_dict:
+ if get_boolean_option(option_dict, 'warnerrors', 'WARNERRORS'):
self._sphinx_run()
else:
setup_command.BuildDoc.run(self)
+ def initialize_options(self):
+ # Not a new style class, super keyword does not work.
+ setup_command.BuildDoc.initialize_options(self)
+
+ # NOTE(dstanek): exclude setup.py from the autodoc tree index
+ # builds because all projects will have an issue with it
+ self.autodoc_tree_excludes = ['setup.py']
+
def finalize_options(self):
# Not a new style class, super keyword does not work.
setup_command.BuildDoc.finalize_options(self)
@@ -762,6 +843,14 @@ try:
if not isinstance(self.builders, list) and self.builders:
self.builders = self.builders.split(',')
+ # NOTE(dstanek): check for autodoc tree exclusion overrides
+ # in the setup.cfg
+ opt = 'autodoc_tree_excludes'
+ option_dict = self.distribution.get_option_dict('pbr')
+ if opt in option_dict:
+ self.autodoc_tree_excludes = option_dict[opt][1]
+ self.ensure_string_list(opt)
+
class LocalBuildLatex(LocalBuildDoc):
builders = ['latex']
command_name = 'build_sphinx_latex'
@@ -776,44 +865,130 @@ def have_sphinx():
return _have_sphinx
-def _get_revno(git_dir):
- """Return the number of commits since the most recent tag.
+def _get_increment_kwargs(git_dir, tag):
+ """Calculate the sort of semver increment needed from git history.
+
+ Every commit from HEAD to tag is consider for Sem-Ver metadata lines.
+ See the pbr docs for their syntax.
+
+ :return: a dict of kwargs for passing into SemanticVersion.increment.
+ """
+ result = {}
+ if tag:
+ version_spec = tag + "..HEAD"
+ else:
+ version_spec = "HEAD"
+ changelog = _run_git_command(['log', version_spec], git_dir)
+ header_len = len(' sem-ver:')
+ commands = [line[header_len:].strip() for line in changelog.split('\n')
+ if line.lower().startswith(' sem-ver:')]
+ symbols = set()
+ for command in commands:
+ symbols.update([symbol.strip() for symbol in command.split(',')])
+
+ def _handle_symbol(symbol, symbols, impact):
+ if symbol in symbols:
+ result[impact] = True
+ symbols.discard(symbol)
+ _handle_symbol('bugfix', symbols, 'patch')
+ _handle_symbol('feature', symbols, 'minor')
+ _handle_symbol('deprecation', symbols, 'minor')
+ _handle_symbol('api-break', symbols, 'major')
+ for symbol in symbols:
+ log.info('[pbr] Unknown Sem-Ver symbol %r' % symbol)
+ # We don't want patch in the kwargs since it is not a keyword argument -
+ # its the default minimum increment.
+ result.pop('patch', None)
+ return result
+
+
+def _get_revno_and_last_tag(git_dir):
+ """Return the commit data about the most recent tag.
We use git-describe to find this out, but if there are no
tags then we fall back to counting commits since the beginning
of time.
"""
- describe = _run_git_command(['describe', '--always'], git_dir)
- if "-" in describe:
- return describe.rsplit("-", 2)[-2]
+ changelog = _iter_log_oneline(git_dir=git_dir)
+ row_count = 0
+ for row_count, (ignored, tag_set, ignored) in enumerate(changelog):
+ version_tags = set()
+ for tag in list(tag_set):
+ try:
+ version_tags.add(version.SemanticVersion.from_pip_string(tag))
+ except Exception:
+ pass
+ if version_tags:
+ return max(version_tags).release_string(), row_count
+ return "", row_count
+
+
+def _get_version_from_git_target(git_dir, target_version):
+ """Calculate a version from a target version in git_dir.
+
+ This is used for untagged versions only. A new version is calculated as
+ necessary based on git metadata - distance to tags, current hash, contents
+ of commit messages.
+
+ :param git_dir: The git directory we're working from.
+ :param target_version: If None, the last tagged version (or 0 if there are
+ no tags yet) is incremented as needed to produce an appropriate target
+ version following semver rules. Otherwise target_version is used as a
+ constraint - if semver rules would result in a newer version then an
+ exception is raised.
+ :return: A semver version object.
+ """
+ sha = _run_git_command(
+ ['log', '-n1', '--pretty=format:%h'], git_dir)
+ tag, distance = _get_revno_and_last_tag(git_dir)
+ last_semver = version.SemanticVersion.from_pip_string(tag or '0')
+ if distance == 0:
+ new_version = last_semver
+ else:
+ new_version = last_semver.increment(
+ **_get_increment_kwargs(git_dir, tag))
+ if target_version is not None and new_version > target_version:
+ raise ValueError(
+ "git history requires a target version of %(new)s, but target "
+ "version is %(target)s" %
+ dict(new=new_version, target=target_version))
+ if distance == 0:
+ return last_semver
+ if target_version is not None:
+ return target_version.to_dev(distance, sha)
+ else:
+ return new_version.to_dev(distance, sha)
- # no tags found
- revlist = _run_git_command(
- ['rev-list', '--abbrev-commit', 'HEAD'], git_dir)
- return len(revlist.splitlines())
+def _get_version_from_git(pre_version=None):
+ """Calculate a version string from git.
-def _get_version_from_git(pre_version):
- """Return a version which is equal to the tag that's on the current
- revision if there is one, or tag plus number of additional revisions
- if the current revision has no tag.
- """
+ If the revision is tagged, return that. Otherwise calculate a semantic
+ version description of the tree.
+ The number of revisions since the last tag is included in the dev counter
+ in the version for untagged versions.
+
+ :param pre_version: If supplied use this as the target version rather than
+ inferring one from the last tag + commit messages.
+ """
git_dir = _get_git_directory()
if git_dir and _git_is_installed():
- if pre_version:
- try:
- return _run_git_command(
- ['describe', '--exact-match'], git_dir,
- throw_on_error=True).replace('-', '.')
- except Exception:
- sha = _run_git_command(
- ['log', '-n1', '--pretty=format:%h'], git_dir)
- return "%s.dev%s+g%s" % (pre_version, _get_revno(git_dir), sha)
- else:
- return _run_git_command(
- ['describe', '--always'],
- git_dir).replace('-', '.').replace('.g', '+g')
+ try:
+ tagged = _run_git_command(
+ ['describe', '--exact-match'], git_dir,
+ throw_on_error=True).replace('-', '.')
+ target_version = version.SemanticVersion.from_pip_string(tagged)
+ except Exception:
+ if pre_version:
+ # not released yet - use pre_version as the target
+ target_version = version.SemanticVersion.from_pip_string(
+ pre_version)
+ else:
+ # not released yet - just calculate from git history
+ target_version = None
+ result = _get_version_from_git_target(git_dir, target_version)
+ return result.release_string()
# If we don't know the version, return an empty string so at least
# the downstream users of the value always have the same type of
# object to work with.
@@ -823,40 +998,51 @@ def _get_version_from_git(pre_version):
return ''
-def _get_version_from_pkg_info(package_name):
- """Get the version from PKG-INFO file if we can."""
- try:
- pkg_info_file = open('PKG-INFO', 'r')
- except (IOError, OSError):
- return None
- try:
- pkg_info = email.message_from_file(pkg_info_file)
- except email.MessageError:
- return None
+def _get_version_from_pkg_metadata(package_name):
+ """Get the version from package metadata if present.
+
+ This looks for PKG-INFO if present (for sdists), and if not looks
+ for METADATA (for wheels) and failing that will return None.
+ """
+ pkg_metadata_filenames = ['PKG-INFO', 'METADATA']
+ pkg_metadata = {}
+ for filename in pkg_metadata_filenames:
+ try:
+ pkg_metadata_file = open(filename, 'r')
+ except (IOError, OSError):
+ continue
+ try:
+ pkg_metadata = email.message_from_file(pkg_metadata_file)
+ except email.MessageError:
+ continue
+
# Check to make sure we're in our own dir
- if pkg_info.get('Name', None) != package_name:
+ if pkg_metadata.get('Name', None) != package_name:
return None
- return pkg_info.get('Version', None)
+ return pkg_metadata.get('Version', None)
def get_version(package_name, pre_version=None):
- """Get the version of the project. First, try getting it from PKG-INFO, if
- it exists. If it does, that means we're in a distribution tarball or that
- install has happened. Otherwise, if there is no PKG-INFO file, pull the
- version from git.
+ """Get the version of the project. First, try getting it from PKG-INFO or
+ METADATA, if it exists. If it does, that means we're in a distribution
+ tarball or that install has happened. Otherwise, if there is no PKG-INFO
+ or METADATA file, pull the version from git.
We do not support setup.py version sanity in git archive tarballs, nor do
we support packagers directly sucking our git repo into theirs. We expect
that a source tarball be made from our git repo - or that if someone wants
to make a source tarball from a fork of our repo with additional tags in it
that they understand and desire the results of doing that.
+
+ :param pre_version: The version field from setup.cfg - if set then this
+ version will be the next release.
"""
version = os.environ.get(
"PBR_VERSION",
os.environ.get("OSLO_PACKAGE_VERSION", None))
if version:
return version
- version = _get_version_from_pkg_info(package_name)
+ version = _get_version_from_pkg_metadata(package_name)
if version:
return version
version = _get_version_from_git(pre_version)
diff --git a/pbr/testr_command.py b/pbr/testr_command.py
index 34b02ed..3440f02 100644
--- a/pbr/testr_command.py
+++ b/pbr/testr_command.py
@@ -96,7 +96,7 @@ class Testr(cmd.Command):
logger.debug("finalize_options: self.__dict__ = %r", self.__dict__)
def run(self):
- """Set up testr repo, then run testr"""
+ """Set up testr repo, then run testr."""
logger.debug("run called")
if not os.path.isdir(".testrepository"):
self._run_testr("init")
diff --git a/pbr/tests/base.py b/pbr/tests/base.py
index ce20de1..6f94c89 100644
--- a/pbr/tests/base.py
+++ b/pbr/tests/base.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
@@ -98,6 +96,9 @@ class BaseTestCase(testtools.TestCase, testresources.ResourcedTestCase):
self.useFixture(fixtures.TempHomeDir())
self.useFixture(fixtures.NestedTempfile())
self.useFixture(fixtures.FakeLogger())
+ # TODO(lifeless) we should remove PBR_VERSION from the environment.
+ # rather than setting it, because thats not representative - we need to
+ # test non-preversioned codepaths too!
self.useFixture(fixtures.EnvironmentVariable('PBR_VERSION', '0.0'))
self.temp_dir = self.useFixture(fixtures.TempDir()).path
@@ -107,6 +108,16 @@ class BaseTestCase(testtools.TestCase, testresources.ResourcedTestCase):
self.addCleanup(os.chdir, os.getcwd())
os.chdir(self.package_dir)
self.addCleanup(self._discard_testpackage)
+ # Tests can opt into non-PBR_VERSION by setting preversioned=False as
+ # an attribute.
+ if not getattr(self, 'preversioned', True):
+ self.useFixture(fixtures.EnvironmentVariable('PBR_VERSION'))
+ setup_cfg_path = os.path.join(self.package_dir, 'setup.cfg')
+ with open(setup_cfg_path, 'rt') as cfg:
+ content = cfg.read()
+ content = content.replace(u'version = 0.1.dev', u'')
+ with open(setup_cfg_path, 'wt') as cfg:
+ cfg.write(content)
def _discard_testpackage(self):
# Remove pbr.testpackage from sys.modules so that it can be freshly
@@ -144,7 +155,8 @@ def _run_cmd(args, cwd):
:return: ((stdout, stderr), returncode)
"""
p = subprocess.Popen(
- args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
+ args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, cwd=cwd)
streams = tuple(s.decode('latin1').strip() for s in p.communicate())
for content in streams:
print(content)
diff --git a/pbr/tests/test_files.py b/pbr/tests/test_files.py
index d9cf3e5..e60b6ca 100644
--- a/pbr/tests/test_files.py
+++ b/pbr/tests/test_files.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
diff --git a/pbr/tests/test_packaging.py b/pbr/tests/test_packaging.py
index 46dd0d5..0fdc5bb 100644
--- a/pbr/tests/test_packaging.py
+++ b/pbr/tests/test_packaging.py
@@ -39,10 +39,13 @@
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
import os
+import sys
import tempfile
import fixtures
import mock
+import testscenarios
+from testtools import matchers
from pbr import packaging
from pbr.tests import base
@@ -66,14 +69,67 @@ class TestRepo(fixtures.Fixture):
base._run_cmd(
['git', 'config', '--global', 'user.email', 'example@example.com'],
self._basedir)
+ base._run_cmd(
+ ['git', 'config', '--global', 'user.name', 'OpenStack Developer'],
+ self._basedir)
+ base._run_cmd(
+ ['git', 'config', '--global', 'user.signingkey',
+ 'example@example.com'], self._basedir)
base._run_cmd(['git', 'add', '.'], self._basedir)
- def commit(self):
- base._run_cmd(['git', 'commit', '-m', 'test commit'], self._basedir)
+ def commit(self, message_content='test commit'):
+ files = len(os.listdir(self._basedir))
+ path = self._basedir + '/%d' % files
+ open(path, 'wt').close()
+ base._run_cmd(['git', 'add', path], self._basedir)
+ base._run_cmd(['git', 'commit', '-m', message_content], self._basedir)
+
+ def uncommit(self):
+ base._run_cmd(['git', 'reset', '--hard', 'HEAD^'], self._basedir)
+
+ def tag(self, version):
+ base._run_cmd(
+ ['git', 'tag', '-sm', 'test tag', version], self._basedir)
+
+
+class GPGKeyFixture(fixtures.Fixture):
+ """Creates a GPG key for testing.
+
+ It's recommended that this be used in concert with a unique home
+ directory.
+ """
+
+ def setUp(self):
+ super(GPGKeyFixture, self).setUp()
+ tempdir = self.useFixture(fixtures.TempDir())
+ config_file = tempdir.path + '/key-config'
+ f = open(config_file, 'wt')
+ try:
+ f.write("""
+ #%no-protection -- these would be ideal but they are documented
+ #%transient-key -- but not implemented in gnupg!
+ %no-ask-passphrase
+ Key-Type: RSA
+ Name-Real: Example Key
+ Name-Comment: N/A
+ Name-Email: example@example.com
+ Expire-Date: 2d
+ Preferences: (setpref)
+ %commit
+ """)
+ finally:
+ f.close()
+ base._run_cmd(
+ ['gpg', '--gen-key', '--batch', config_file], tempdir.path)
class TestPackagingInGitRepoWithCommit(base.BaseTestCase):
+ scenarios = [
+ ('preversioned', dict(preversioned=True)),
+ ('postversioned', dict(preversioned=False)),
+ ]
+
def setUp(self):
super(TestPackagingInGitRepoWithCommit, self).setUp()
repo = self.useFixture(TestRepo(self.package_dir))
@@ -157,3 +213,188 @@ class TestNestedRequirements(base.BaseTestCase):
f.write('pbr')
result = packaging.parse_requirements([requirements])
self.assertEqual(result, ['pbr'])
+
+
+class TestGetRequirements(base.BaseTestCase):
+
+ def test_get_requirements_file(self):
+ platforms = ['Windows', 'Freebsd', 'Darwin', 'Linux']
+ version = sys.version_info[0]
+
+ with mock.patch('platform.system') as platform_system:
+ platform_system.side_effect = platforms
+
+ for platform in map(str.lower, platforms):
+ expected = [
+ 'requirements-{plat}-py{ver}.txt'.format(
+ plat=platform, ver=version),
+ 'tools/pip-requires-{plat}-py{ver}'.format(
+ plat=platform, ver=version),
+ 'requirements-{0}.txt'.format(platform),
+ 'tools/pip-requires-{0}'.format(platform),
+ 'requirements-py{0}.txt'.format(version),
+ 'tools/pip-requires-py{0}'.format(version),
+ 'requirements.txt',
+ 'tools/pip-requires',
+ ]
+ files = packaging.get_requirements_files()
+ self.assertEqual(expected, files)
+
+
+class TestVersions(base.BaseTestCase):
+
+ scenarios = [
+ ('preversioned', dict(preversioned=True)),
+ ('postversioned', dict(preversioned=False)),
+ ]
+
+ def setUp(self):
+ super(TestVersions, self).setUp()
+ self.repo = self.useFixture(TestRepo(self.package_dir))
+ self.useFixture(GPGKeyFixture())
+ self.useFixture(base.DiveDir(self.package_dir))
+
+ def test_capitalized_headers(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('Sem-Ver: api-break')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('2.0.0.dev1.g'))
+
+ def test_capitalized_headers_partial(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('Sem-ver: api-break')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('2.0.0.dev1.g'))
+
+ def test_tagged_version_has_tag_version(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ version = packaging._get_version_from_git('1.2.3')
+ self.assertEqual('1.2.3', version)
+
+ def test_untagged_version_has_dev_version_postversion(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit()
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.2.4.dev1.g'))
+
+ def test_untagged_version_minor_bump(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('sem-ver: deprecation')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.3.0.dev1.g'))
+
+ def test_untagged_version_major_bump(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('sem-ver: api-break')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('2.0.0.dev1.g'))
+
+ def test_untagged_version_has_dev_version_preversion(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit()
+ version = packaging._get_version_from_git('1.2.5')
+ self.assertThat(version, matchers.StartsWith('1.2.5.dev1.g'))
+
+ def test_preversion_too_low_simple(self):
+ # That is, the target version is either already released or not high
+ # enough for the semver requirements given api breaks etc.
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit()
+ # Note that we can't target 1.2.3 anymore - with 1.2.3 released we
+ # need to be working on 1.2.4.
+ err = self.assertRaises(
+ ValueError, packaging._get_version_from_git, '1.2.3')
+ self.assertThat(err.args[0], matchers.StartsWith('git history'))
+
+ def test_preversion_too_low_semver_headers(self):
+ # That is, the target version is either already released or not high
+ # enough for the semver requirements given api breaks etc.
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('sem-ver: feature')
+ # Note that we can't target 1.2.4, the feature header means we need
+ # to be working on 1.3.0 or above.
+ err = self.assertRaises(
+ ValueError, packaging._get_version_from_git, '1.2.4')
+ self.assertThat(err.args[0], matchers.StartsWith('git history'))
+
+ def test_get_kwargs_corner_cases(self):
+ # No tags:
+ git_dir = self.repo._basedir + '/.git'
+ get_kwargs = lambda tag: packaging._get_increment_kwargs(git_dir, tag)
+
+ def _check_combinations(tag):
+ self.repo.commit()
+ self.assertEqual(dict(), get_kwargs(tag))
+ self.repo.commit('sem-ver: bugfix')
+ self.assertEqual(dict(), get_kwargs(tag))
+ self.repo.commit('sem-ver: feature')
+ self.assertEqual(dict(minor=True), get_kwargs(tag))
+ self.repo.uncommit()
+ self.repo.commit('sem-ver: deprecation')
+ self.assertEqual(dict(minor=True), get_kwargs(tag))
+ self.repo.uncommit()
+ self.repo.commit('sem-ver: api-break')
+ self.assertEqual(dict(major=True), get_kwargs(tag))
+ self.repo.commit('sem-ver: deprecation')
+ self.assertEqual(dict(major=True, minor=True), get_kwargs(tag))
+ _check_combinations('')
+ self.repo.tag('1.2.3')
+ _check_combinations('1.2.3')
+
+ def test_invalid_tag_ignored(self):
+ # Fix for bug 1356784 - we treated any tag as a version, not just those
+ # that are valid versions.
+ self.repo.commit()
+ self.repo.tag('1')
+ self.repo.commit()
+ # when the tree is tagged and its wrong:
+ self.repo.tag('badver')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.0.1.dev1.g'))
+ # When the tree isn't tagged, we also fall through.
+ self.repo.commit()
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.0.1.dev2.g'))
+ # We don't fall through x.y versions
+ self.repo.commit()
+ self.repo.tag('1.2')
+ self.repo.commit()
+ self.repo.tag('badver2')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.2.1.dev1.g'))
+ # Or x.y.z versions
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit()
+ self.repo.tag('badver3')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.2.4.dev1.g'))
+ # Or alpha/beta/pre versions
+ self.repo.commit()
+ self.repo.tag('1.2.4.0a1')
+ self.repo.commit()
+ self.repo.tag('badver4')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.2.4.dev1.g'))
+
+ def test_valid_tag_honoured(self):
+ # Fix for bug 1370608 - we converted any target into a 'dev version'
+ # even if there was a distance of 0 - indicating that we were on the
+ # tag itself.
+ self.repo.commit()
+ self.repo.tag('1.3.0.0a1')
+ version = packaging._get_version_from_git()
+ self.assertEqual('1.3.0.0a1', version)
+
+
+def load_tests(loader, in_tests, pattern):
+ return testscenarios.load_tests_apply_scenarios(loader, in_tests, pattern)
diff --git a/pbr/tests/test_setup.py b/pbr/tests/test_setup.py
index 44436df..2714449 100644
--- a/pbr/tests/test_setup.py
+++ b/pbr/tests/test_setup.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright (c) 2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
@@ -192,6 +190,10 @@ class BuildSphinxTest(base.BaseTestCase):
scenarios = [
('true_autodoc_caps',
dict(has_opt=True, autodoc='True', has_autodoc=True)),
+ ('true_autodoc_caps_with_excludes',
+ dict(has_opt=True, autodoc='True', has_autodoc=True,
+ excludes="fake_package.fake_private_module\n"
+ "fake_package.unknown_module")),
('true_autodoc_lower',
dict(has_opt=True, autodoc='true', has_autodoc=True)),
('false_autodoc',
@@ -211,14 +213,21 @@ class BuildSphinxTest(base.BaseTestCase):
self.distr.command_options["build_sphinx"] = {
"source_dir": ["a", "."]}
pkg_fixture = fixtures.PythonPackage(
- "fake_package", [("fake_module.py", b"")])
+ "fake_package", [("fake_module.py", b""),
+ ("fake_private_module.py", b"")])
self.useFixture(pkg_fixture)
self.useFixture(base.DiveDir(pkg_fixture.base))
+ self.distr.command_options["pbr"] = {}
+ if hasattr(self, "excludes"):
+ self.distr.command_options["pbr"]["autodoc_exclude_modules"] = (
+ 'setup.cfg',
+ "fake_package.fake_private_module\n"
+ "fake_package.unknown_module")
+ if self.has_opt:
+ options = self.distr.command_options["pbr"]
+ options["autodoc_index_modules"] = ('setup.cfg', self.autodoc)
def test_build_doc(self):
- if self.has_opt:
- self.distr.command_options["pbr"] = {
- "autodoc_index_modules": ('setup.cfg', self.autodoc)}
build_doc = packaging.LocalBuildDoc(self.distr)
build_doc.run()
@@ -227,12 +236,15 @@ class BuildSphinxTest(base.BaseTestCase):
self.assertTrue(
os.path.exists(
"api/fake_package.fake_module.rst") == self.has_autodoc)
+ if not self.has_autodoc or hasattr(self, "excludes"):
+ assertion = self.assertFalse
+ else:
+ assertion = self.assertTrue
+ assertion(
+ os.path.exists(
+ "api/fake_package.fake_private_module.rst"))
def test_builders_config(self):
- if self.has_opt:
- self.distr.command_options["pbr"] = {
- "autodoc_index_modules": ('setup.cfg', self.autodoc)}
-
build_doc = packaging.LocalBuildDoc(self.distr)
build_doc.finalize_options()
diff --git a/pbr/tests/test_version.py b/pbr/tests/test_version.py
index e220d03..0e7e90b 100644
--- a/pbr/tests/test_version.py
+++ b/pbr/tests/test_version.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2012 Red Hat, Inc.
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
#
@@ -15,17 +13,329 @@
# License for the specific language governing permissions and limitations
# under the License.
+import operator
+
+from testtools import matchers
+
from pbr.tests import base
from pbr import version
-class DeferredVersionTestCase(base.BaseTestCase):
+from_pip_string = version.SemanticVersion.from_pip_string
+
+
+class TestSemanticVersion(base.BaseTestCase):
+
+ def test_equality(self):
+ base = version.SemanticVersion(1, 2, 3)
+ base2 = version.SemanticVersion(1, 2, 3)
+ major = version.SemanticVersion(2, 2, 3)
+ minor = version.SemanticVersion(1, 3, 3)
+ patch = version.SemanticVersion(1, 2, 4)
+ pre_base = version.SemanticVersion(1, 2, 3, 'a', 4)
+ pre_base2 = version.SemanticVersion(1, 2, 3, 'a', 4)
+ pre_type = version.SemanticVersion(1, 2, 3, 'b', 4)
+ pre_serial = version.SemanticVersion(1, 2, 3, 'a', 5)
+ dev_base = version.SemanticVersion(1, 2, 3, dev_count=6, githash='6')
+ dev_base2 = version.SemanticVersion(1, 2, 3, dev_count=6, githash='6')
+ dev_count = version.SemanticVersion(1, 2, 3, dev_count=7, githash='6')
+ githash = version.SemanticVersion(1, 2, 3, dev_count=6, githash='7')
+ self.assertEqual(base, base2)
+ self.assertNotEqual(base, major)
+ self.assertNotEqual(base, minor)
+ self.assertNotEqual(base, patch)
+ self.assertNotEqual(base, pre_type)
+ self.assertNotEqual(base, pre_serial)
+ self.assertNotEqual(base, dev_count)
+ self.assertNotEqual(base, githash)
+ self.assertEqual(pre_base, pre_base2)
+ self.assertNotEqual(pre_base, pre_type)
+ self.assertNotEqual(pre_base, pre_serial)
+ self.assertNotEqual(pre_base, dev_count)
+ self.assertNotEqual(pre_base, githash)
+ self.assertEqual(dev_base, dev_base2)
+ self.assertNotEqual(dev_base, dev_count)
+ self.assertNotEqual(dev_base, githash)
+ simple = version.SemanticVersion(1)
+ explicit_minor = version.SemanticVersion(1, 0)
+ explicit_patch = version.SemanticVersion(1, 0, 0)
+ self.assertEqual(simple, explicit_minor)
+ self.assertEqual(simple, explicit_patch)
+ self.assertEqual(explicit_minor, explicit_patch)
+
+ def test_ordering(self):
+ base = version.SemanticVersion(1, 2, 3)
+ major = version.SemanticVersion(2, 2, 3)
+ minor = version.SemanticVersion(1, 3, 3)
+ patch = version.SemanticVersion(1, 2, 4)
+ pre_alpha = version.SemanticVersion(1, 2, 3, 'a', 4)
+ pre_beta = version.SemanticVersion(1, 2, 3, 'b', 3)
+ pre_rc = version.SemanticVersion(1, 2, 3, 'rc', 2)
+ pre_serial = version.SemanticVersion(1, 2, 3, 'a', 5)
+ dev_base = version.SemanticVersion(1, 2, 3, dev_count=6, githash='6')
+ dev_count = version.SemanticVersion(1, 2, 3, dev_count=7, githash='6')
+ githash = version.SemanticVersion(1, 2, 3, dev_count=6, githash='7')
+ self.assertThat(base, matchers.LessThan(major))
+ self.assertThat(major, matchers.GreaterThan(base))
+ self.assertThat(base, matchers.LessThan(minor))
+ self.assertThat(minor, matchers.GreaterThan(base))
+ self.assertThat(base, matchers.LessThan(patch))
+ self.assertThat(patch, matchers.GreaterThan(base))
+ self.assertThat(pre_alpha, matchers.LessThan(base))
+ self.assertThat(base, matchers.GreaterThan(pre_alpha))
+ self.assertThat(pre_alpha, matchers.LessThan(pre_beta))
+ self.assertThat(pre_beta, matchers.GreaterThan(pre_alpha))
+ self.assertThat(pre_beta, matchers.LessThan(pre_rc))
+ self.assertThat(pre_rc, matchers.GreaterThan(pre_beta))
+ self.assertThat(pre_alpha, matchers.LessThan(pre_serial))
+ self.assertThat(pre_serial, matchers.GreaterThan(pre_alpha))
+ self.assertThat(pre_serial, matchers.LessThan(pre_beta))
+ self.assertThat(pre_beta, matchers.GreaterThan(pre_serial))
+ self.assertThat(dev_base, matchers.LessThan(base))
+ self.assertThat(base, matchers.GreaterThan(dev_base))
+ self.assertRaises(TypeError, operator.lt, pre_alpha, dev_base)
+ self.assertRaises(TypeError, operator.lt, dev_base, pre_alpha)
+ self.assertThat(dev_base, matchers.LessThan(dev_count))
+ self.assertThat(dev_count, matchers.GreaterThan(dev_base))
+ self.assertRaises(TypeError, operator.lt, dev_base, githash)
+
+ def test_from_pip_string_legacy_alpha(self):
+ expected = version.SemanticVersion(
+ 1, 2, 0, prerelease_type='rc', prerelease=1)
+ parsed = from_pip_string('1.2.0rc1')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_nonzero_lead_in(self):
+ # reported in bug 1361251
+ expected = version.SemanticVersion(
+ 0, 0, 1, prerelease_type='a', prerelease=2)
+ parsed = from_pip_string('0.0.1a2')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_short_nonzero_lead_in(self):
+ expected = version.SemanticVersion(
+ 0, 1, 0, prerelease_type='a', prerelease=2)
+ parsed = from_pip_string('0.1a2')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_no_0_prerelease(self):
+ expected = version.SemanticVersion(
+ 2, 1, 0, prerelease_type='rc', prerelease=1)
+ parsed = from_pip_string('2.1.0.rc1')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_no_0_prerelease_2(self):
+ expected = version.SemanticVersion(
+ 2, 0, 0, prerelease_type='rc', prerelease=1)
+ parsed = from_pip_string('2.0.0.rc1')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_non_440_beta(self):
+ expected = version.SemanticVersion(
+ 2014, 2, prerelease_type='b', prerelease=2)
+ parsed = from_pip_string('2014.2.b2')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_dev(self):
+ expected = version.SemanticVersion(
+ 0, 10, 1, dev_count=3, githash='83bef74')
+ parsed = from_pip_string('0.10.1.3.g83bef74')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_corner_case_dev(self):
+ # If the last tag is missing, or if the last tag has less than 3
+ # components, we need to 0 extend on parsing.
+ expected = version.SemanticVersion(
+ 0, 0, 0, dev_count=1, githash='83bef74')
+ parsed = from_pip_string('0.0.g83bef74')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_legacy_short_dev(self):
+ # If the last tag is missing, or if the last tag has less than 3
+ # components, we need to 0 extend on parsing.
+ expected = version.SemanticVersion(
+ 0, 0, 0, dev_count=1, githash='83bef74')
+ parsed = from_pip_string('0.g83bef74')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_dev_missing_patch_version(self):
+ expected = version.SemanticVersion(
+ 2014, 2, dev_count=21, githash='c4c8d0b')
+ parsed = from_pip_string('2014.2.dev21.gc4c8d0b')
+ self.assertEqual(expected, parsed)
+
+ def test_from_pip_string_pure_git_hash(self):
+ self.assertRaises(ValueError, from_pip_string, '6eed5ae')
+
+ def test_final_version(self):
+ semver = version.SemanticVersion(1, 2, 3)
+ self.assertEqual((1, 2, 3, 'final', 0), semver.version_tuple())
+ self.assertEqual("1.2.3", semver.brief_string())
+ self.assertEqual("1.2.3", semver.debian_string())
+ self.assertEqual("1.2.3", semver.release_string())
+ self.assertEqual("1.2.3", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.3"))
+
+ def test_parsing_short_forms(self):
+ semver = version.SemanticVersion(1, 0, 0)
+ self.assertEqual(semver, from_pip_string("1"))
+ self.assertEqual(semver, from_pip_string("1.0"))
+ self.assertEqual(semver, from_pip_string("1.0.0"))
+
+ def test_dev_version(self):
+ semver = version.SemanticVersion(1, 2, 4, dev_count=5, githash='12')
+ self.assertEqual((1, 2, 4, 'dev', 4), semver.version_tuple())
+ self.assertEqual("1.2.4", semver.brief_string())
+ self.assertEqual("1.2.4~dev5+g12", semver.debian_string())
+ self.assertEqual("1.2.4.dev5.g12", semver.release_string())
+ self.assertEqual("1.2.3.dev5+g12", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.4.dev5.g12"))
+
+ def test_dev_no_git_version(self):
+ semver = version.SemanticVersion(1, 2, 4, dev_count=5)
+ self.assertEqual((1, 2, 4, 'dev', 4), semver.version_tuple())
+ self.assertEqual("1.2.4", semver.brief_string())
+ self.assertEqual("1.2.4~dev5", semver.debian_string())
+ self.assertEqual("1.2.4.dev5", semver.release_string())
+ self.assertEqual("1.2.3.dev5", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.4.dev5"))
+
+ def test_dev_zero_version(self):
+ semver = version.SemanticVersion(1, 2, 0, dev_count=5)
+ self.assertEqual((1, 2, 0, 'dev', 4), semver.version_tuple())
+ self.assertEqual("1.2.0", semver.brief_string())
+ self.assertEqual("1.2.0~dev5", semver.debian_string())
+ self.assertEqual("1.2.0.dev5", semver.release_string())
+ self.assertEqual("1.1.9999.dev5", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.0.dev5"))
+
+ def test_alpha_dev_version(self):
+ self.assertRaises(
+ ValueError, version.SemanticVersion, 1, 2, 4, 'a', 1, 5, '12')
+
+ def test_alpha_version(self):
+ semver = version.SemanticVersion(1, 2, 4, 'a', 1)
+ self.assertEqual((1, 2, 4, 'alpha', 1), semver.version_tuple())
+ self.assertEqual("1.2.4", semver.brief_string())
+ self.assertEqual("1.2.4~a1", semver.debian_string())
+ self.assertEqual("1.2.4.0a1", semver.release_string())
+ self.assertEqual("1.2.3.a1", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.4.0a1"))
+
+ def test_alpha_zero_version(self):
+ semver = version.SemanticVersion(1, 2, 0, 'a', 1)
+ self.assertEqual((1, 2, 0, 'alpha', 1), semver.version_tuple())
+ self.assertEqual("1.2.0", semver.brief_string())
+ self.assertEqual("1.2.0~a1", semver.debian_string())
+ self.assertEqual("1.2.0.0a1", semver.release_string())
+ self.assertEqual("1.1.9999.a1", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.0.0a1"))
+
+ def test_alpha_major_zero_version(self):
+ semver = version.SemanticVersion(1, 0, 0, 'a', 1)
+ self.assertEqual((1, 0, 0, 'alpha', 1), semver.version_tuple())
+ self.assertEqual("1.0.0", semver.brief_string())
+ self.assertEqual("1.0.0~a1", semver.debian_string())
+ self.assertEqual("1.0.0.0a1", semver.release_string())
+ self.assertEqual("0.9999.9999.a1", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.0.0.0a1"))
+
+ def test_alpha_default_version(self):
+ semver = version.SemanticVersion(1, 2, 4, 'a')
+ self.assertEqual((1, 2, 4, 'alpha', 0), semver.version_tuple())
+ self.assertEqual("1.2.4", semver.brief_string())
+ self.assertEqual("1.2.4~a0", semver.debian_string())
+ self.assertEqual("1.2.4.0a0", semver.release_string())
+ self.assertEqual("1.2.3.a0", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.4.0a0"))
+
+ def test_beta_dev_version(self):
+ self.assertRaises(
+ ValueError, version.SemanticVersion, 1, 2, 4, 'b', 1, 5, '12')
+
+ def test_beta_version(self):
+ semver = version.SemanticVersion(1, 2, 4, 'b', 1)
+ self.assertEqual((1, 2, 4, 'beta', 1), semver.version_tuple())
+ self.assertEqual("1.2.4", semver.brief_string())
+ self.assertEqual("1.2.4~b1", semver.debian_string())
+ self.assertEqual("1.2.4.0b1", semver.release_string())
+ self.assertEqual("1.2.3.b1", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.4.0b1"))
+
+ def test_decrement_nonrelease(self):
+ # The prior version of any non-release is a release
+ semver = version.SemanticVersion(1, 2, 4, 'b', 1)
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 3), semver.decrement())
+
+ def test_decrement_nonrelease_zero(self):
+ # We set an arbitrary max version of 9999 when decrementing versions
+ # - this is part of handling rpm support.
+ semver = version.SemanticVersion(1, 0, 0)
+ self.assertEqual(
+ version.SemanticVersion(0, 9999, 9999), semver.decrement())
+
+ def test_decrement_release(self):
+ # The next patch version of a release version requires a change to the
+ # patch level.
+ semver = version.SemanticVersion(1, 2, 5)
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 6), semver.increment())
+ self.assertEqual(
+ version.SemanticVersion(1, 3, 0), semver.increment(minor=True))
+ self.assertEqual(
+ version.SemanticVersion(2, 0, 0), semver.increment(major=True))
+
+ def test_increment_nonrelease(self):
+ # The next patch version of a non-release version is another
+ # non-release version as the next release doesn't need to be
+ # incremented.
+ semver = version.SemanticVersion(1, 2, 4, 'b', 1)
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 4, 'b', 2), semver.increment())
+ # Major and minor increments however need to bump things.
+ self.assertEqual(
+ version.SemanticVersion(1, 3, 0), semver.increment(minor=True))
+ self.assertEqual(
+ version.SemanticVersion(2, 0, 0), semver.increment(major=True))
+
+ def test_increment_release(self):
+ # The next patch version of a release version requires a change to the
+ # patch level.
+ semver = version.SemanticVersion(1, 2, 5)
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 6), semver.increment())
+ self.assertEqual(
+ version.SemanticVersion(1, 3, 0), semver.increment(minor=True))
+ self.assertEqual(
+ version.SemanticVersion(2, 0, 0), semver.increment(major=True))
+
+ def test_rc_dev_version(self):
+ self.assertRaises(
+ ValueError, version.SemanticVersion, 1, 2, 4, 'rc', 1, 5, '12')
+
+ def test_rc_version(self):
+ semver = version.SemanticVersion(1, 2, 4, 'rc', 1)
+ self.assertEqual((1, 2, 4, 'candidate', 1), semver.version_tuple())
+ self.assertEqual("1.2.4", semver.brief_string())
+ self.assertEqual("1.2.4~rc1", semver.debian_string())
+ self.assertEqual("1.2.4.0rc1", semver.release_string())
+ self.assertEqual("1.2.3.rc1", semver.rpm_string())
+ self.assertEqual(semver, from_pip_string("1.2.4.0rc1"))
- def test_cached_version(self):
- class MyVersionInfo(version.VersionInfo):
- def _get_version_from_pkg_resources(self):
- return "5.5.5.5"
+ def test_to_dev(self):
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 3, dev_count=1, githash='foo'),
+ version.SemanticVersion(1, 2, 3).to_dev(1, 'foo'))
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 3, dev_count=1, githash='foo'),
+ version.SemanticVersion(1, 2, 3, 'rc', 1).to_dev(1, 'foo'))
- deferred_string = MyVersionInfo("openstack").\
- cached_version_string()
- self.assertEqual("5.5.5.5", deferred_string)
+ def test_to_release(self):
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 3),
+ version.SemanticVersion(
+ 1, 2, 3, dev_count=1, githash='foo').to_release())
+ self.assertEqual(
+ version.SemanticVersion(1, 2, 3),
+ version.SemanticVersion(1, 2, 3, 'rc', 1).to_release())
diff --git a/pbr/tests/testpackage/pbr_testpackage/__init__.py b/pbr/tests/testpackage/pbr_testpackage/__init__.py
index e69de29..aa56dc6 100644
--- a/pbr/tests/testpackage/pbr_testpackage/__init__.py
+++ b/pbr/tests/testpackage/pbr_testpackage/__init__.py
@@ -0,0 +1,3 @@
+import pbr.version
+
+__version__ = pbr.version.VersionInfo('pbr_testpackage').version_string()
diff --git a/pbr/tests/testpackage/setup.cfg b/pbr/tests/testpackage/setup.cfg
index a410e3c..0188bd2 100644
--- a/pbr/tests/testpackage/setup.cfg
+++ b/pbr/tests/testpackage/setup.cfg
@@ -1,5 +1,7 @@
[metadata]
name = pbr_testpackage
+# TODO(lifeless) we should inject this as needed otherwise we're not truely
+# testing postversioned codepaths.
version = 0.1.dev
author = OpenStack
author-email = openstack-dev@lists.openstack.org
diff --git a/pbr/version.py b/pbr/version.py
index 3210505..9ef51fb 100644
--- a/pbr/version.py
+++ b/pbr/version.py
@@ -18,9 +18,404 @@
Utilities for consuming the version from pkg_resources.
"""
+import itertools
+import operator
+
import pkg_resources
+def _is_int(string):
+ try:
+ int(string)
+ return True
+ except ValueError:
+ return False
+
+
+class SemanticVersion(object):
+ """A pure semantic version independent of serialisation.
+
+ See the pbr doc 'semver' for details on the semantics.
+ """
+
+ def __init__(self, major, minor=0, patch=0, prerelease_type=None,
+ prerelease=None, dev_count=None, githash=None):
+ """Create a SemanticVersion.
+
+ :param major: Major component of the version.
+ :param minor: Minor component of the version. Defaults to 0.
+ :param patch: Patch level component. Defaults to 0.
+ :param prerelease_type: What sort of prerelease version this is -
+ one of a(alpha), b(beta) or rc(release candidate).
+ :param prerelease: For prerelease versions, what number prerelease.
+ Defaults to 0.
+ :param dev_count: How many commits since the last release.
+ :param githash: What tree hash is this version for.
+
+ :raises: ValueError if both a prerelease version and dev_count or
+ githash are supplied. This is because semver (see the pbr semver
+ documentation) does not permit both a prerelease version and a dev
+ marker at the same time.
+ """
+ self._major = major
+ self._minor = minor
+ self._patch = patch
+ self._prerelease_type = prerelease_type
+ self._prerelease = prerelease
+ if self._prerelease_type and not self._prerelease:
+ self._prerelease = 0
+ self._dev_count = dev_count
+ self._githash = githash
+ if prerelease_type is not None and dev_count is not None:
+ raise ValueError(
+ "invalid version: cannot have prerelease and dev strings %s %s"
+ % (prerelease_type, dev_count))
+
+ def __eq__(self, other):
+ if not isinstance(other, SemanticVersion):
+ return False
+ return self.__dict__ == other.__dict__
+
+ def __hash__(self):
+ return sum(map(hash, self.__dict__.values()))
+
+ def __lt__(self, other):
+ """Compare self and other, another Semantic Version."""
+ # NB(lifeless) this could perhaps be rewritten as
+ # lt (tuple_of_one, tuple_of_other) with a single check for
+ # the typeerror corner cases - that would likely be faster
+ # if this ever becomes performance sensitive.
+ if not isinstance(other, SemanticVersion):
+ raise TypeError("ordering to non-SemanticVersion is undefined")
+ this_tuple = (self._major, self._minor, self._patch)
+ other_tuple = (other._major, other._minor, other._patch)
+ if this_tuple < other_tuple:
+ return True
+ elif this_tuple > other_tuple:
+ return False
+ if self._prerelease_type:
+ if other._prerelease_type:
+ # Use the a < b < rc cheat
+ this_tuple = (self._prerelease_type, self._prerelease)
+ other_tuple = (other._prerelease_type, other._prerelease)
+ return this_tuple < other_tuple
+ elif other._dev_count:
+ raise TypeError(
+ "ordering pre-release with dev builds is undefined")
+ else:
+ return True
+ elif self._dev_count:
+ if other._dev_count:
+ if self._dev_count < other._dev_count:
+ return True
+ elif self._dev_count > other._dev_count:
+ return False
+ elif self._githash == other._githash:
+ # == it not <
+ return False
+ raise TypeError(
+ "same version with different hash has no defined order")
+ elif other._prerelease_type:
+ raise TypeError(
+ "ordering pre-release with dev builds is undefined")
+ else:
+ return True
+ else:
+ # This is not pre-release.
+ # If the other is pre-release or dev, we are greater, which is ! <
+ # If the other is not pre-release, we are equal, which is ! <
+ return False
+
+ def __le__(self, other):
+ return self == other or self < other
+
+ def __ge__(self, other):
+ return not self < other
+
+ def __gt__(self, other):
+ return not self <= other
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __repr__(self):
+ return "pbr.version.SemanticVersion(%s)" % self.release_string()
+
+ @classmethod
+ def from_pip_string(klass, version_string):
+ """Create a SemanticVersion from a pip version string.
+
+ This method will parse a version like 1.3.0 into a SemanticVersion.
+
+ This method is responsible for accepting any version string that any
+ older version of pbr ever created.
+
+ Therefore: versions like 1.3.0a1 versions are handled, parsed into a
+ canonical form and then output - resulting in 1.3.0.0a1.
+ Pre pbr-semver dev versions like 0.10.1.3.g83bef74 will be parsed but
+ output as 0.10.1.dev3.g83bef74.
+
+ :raises ValueError: Never tagged versions sdisted by old pbr result in
+ just the git hash, e.g. '1234567' which poses a substantial problem
+ since they collide with the semver versions when all the digits are
+ numerals. Such versions will result in a ValueError being thrown if
+ any non-numeric digits are present. They are an exception to the
+ general case of accepting anything we ever output, since they were
+ never intended and would permanently mess up versions on PyPI if
+ ever released - we're treating that as a critical bug that we ever
+ made them and have stopped doing that.
+ """
+ input_components = version_string.split('.')
+ # decimals first (keep pre-release and dev/hashes to the right)
+ components = [c for c in input_components if c.isdigit()]
+ digit_len = len(components)
+ if digit_len == 0:
+ raise ValueError("Invalid version %r" % version_string)
+ elif digit_len < 3:
+ if (digit_len < len(input_components) and
+ input_components[digit_len][0].isdigit()):
+ # Handle X.YaZ - Y is a digit not a leadin to pre-release.
+ mixed_component = input_components[digit_len]
+ last_component = ''.join(itertools.takewhile(
+ lambda x: x.isdigit(), mixed_component))
+ components.append(last_component)
+ input_components[digit_len:digit_len + 1] = [
+ last_component, mixed_component[len(last_component):]]
+ digit_len += 1
+ components.extend([0] * (3 - digit_len))
+ components.extend(input_components[digit_len:])
+ major = int(components[0])
+ minor = int(components[1])
+ dev_count = None
+ prerelease_type = None
+ prerelease = None
+ githash = None
+
+ def _parse_type(segment):
+ # Discard leading digits (the 0 in 0a1)
+ isdigit = operator.methodcaller('isdigit')
+ segment = ''.join(itertools.dropwhile(isdigit, segment))
+ isalpha = operator.methodcaller('isalpha')
+ prerelease_type = ''.join(itertools.takewhile(isalpha, segment))
+ prerelease = segment[len(prerelease_type)::]
+ return prerelease_type, int(prerelease)
+ if _is_int(components[2]):
+ patch = int(components[2])
+ else:
+ # legacy version e.g. 1.2.0a1 (canonical is 1.2.0.0a1)
+ # or 1.2.dev4.g1234 or 1.2.b4
+ patch = 0
+ components[2:2] = [0]
+ remainder = components[3:]
+ remainder_starts_with_int = False
+ try:
+ if remainder and int(remainder[0]):
+ remainder_starts_with_int = True
+ except ValueError:
+ pass
+ if remainder_starts_with_int:
+ # old dev format - 0.1.2.3.g1234
+ dev_count = int(remainder[0])
+ else:
+ if remainder and (remainder[0][0] == '0' or
+ remainder[0][0] in ('a', 'b', 'r')):
+ # Current RC/beta layout
+ prerelease_type, prerelease = _parse_type(remainder[0])
+ remainder = remainder[1:]
+ if remainder:
+ component = remainder[0]
+ if component.startswith('dev'):
+ dev_count = int(component[3:])
+ elif component.startswith('g'):
+ # git hash - so use a dev_count of 1 as we have to have one
+ dev_count = 1
+ githash = component[1:]
+ else:
+ raise ValueError(
+ 'Unknown remainder %r in %r'
+ % (remainder, version_string))
+ if len(remainder) > 1:
+ githash = remainder[1][1:]
+ return SemanticVersion(
+ major, minor, patch, prerelease_type=prerelease_type,
+ prerelease=prerelease, dev_count=dev_count, githash=githash)
+
+ def brief_string(self):
+ """Return the short version minus any alpha/beta tags."""
+ return "%s.%s.%s" % (self._major, self._minor, self._patch)
+
+ def debian_string(self):
+ """Return the version number to use when building a debian package.
+
+ This translates the PEP440/semver precedence rules into Debian version
+ sorting operators.
+ """
+ return self._long_version("~", "+g")
+
+ def decrement(self, minor=False, major=False):
+ """Return a decremented SemanticVersion.
+
+ Decrementing versions doesn't make a lot of sense - this method only
+ exists to support rendering of pre-release versions strings into
+ serialisations (such as rpm) with no sort-before operator.
+
+ The 9999 magic version component is from the spec on this - pbr-semver.
+
+ :return: A new SemanticVersion object.
+ """
+ if self._patch:
+ new_patch = self._patch - 1
+ new_minor = self._minor
+ new_major = self._major
+ else:
+ new_patch = 9999
+ if self._minor:
+ new_minor = self._minor - 1
+ new_major = self._major
+ else:
+ new_minor = 9999
+ if self._major:
+ new_major = self._major - 1
+ else:
+ new_major = 0
+ return SemanticVersion(
+ new_major, new_minor, new_patch)
+
+ def increment(self, minor=False, major=False):
+ """Return an incremented SemanticVersion.
+
+ The default behaviour is to perform a patch level increment. When
+ incrementing a prerelease version, the patch level is not changed
+ - the prerelease serial is changed (e.g. beta 0 -> beta 1).
+
+ Incrementing non-pre-release versions will not introduce pre-release
+ versions - except when doing a patch incremental to a pre-release
+ version the new version will only consist of major/minor/patch.
+
+ :param minor: Increment the minor version.
+ :param major: Increment the major version.
+ :return: A new SemanticVersion object.
+ """
+ if self._prerelease_type:
+ new_prerelease_type = self._prerelease_type
+ new_prerelease = self._prerelease + 1
+ new_patch = self._patch
+ else:
+ new_prerelease_type = None
+ new_prerelease = None
+ new_patch = self._patch + 1
+ if minor:
+ new_minor = self._minor + 1
+ new_patch = 0
+ new_prerelease_type = None
+ new_prerelease = None
+ else:
+ new_minor = self._minor
+ if major:
+ new_major = self._major + 1
+ new_minor = 0
+ new_patch = 0
+ new_prerelease_type = None
+ new_prerelease = None
+ else:
+ new_major = self._major
+ return SemanticVersion(
+ new_major, new_minor, new_patch,
+ new_prerelease_type, new_prerelease)
+
+ def _long_version(self, pre_separator, hash_separator, rc_marker=""):
+ """Construct a long string version of this semver.
+
+ :param pre_separator: What separator to use between components
+ that sort before rather than after. If None, use . and lower the
+ version number of the component to preserve sorting. (Used for
+ rpm support)
+ :param hash_separator: What separator to use to append the git hash.
+ """
+ if ((self._prerelease_type or self._dev_count)
+ and pre_separator is None):
+ segments = [self.decrement().brief_string()]
+ pre_separator = "."
+ else:
+ segments = [self.brief_string()]
+ if self._prerelease_type:
+ segments.append(
+ "%s%s%s%s" % (pre_separator, rc_marker, self._prerelease_type,
+ self._prerelease))
+ if self._dev_count:
+ segments.append(pre_separator)
+ segments.append('dev')
+ segments.append(self._dev_count)
+ if self._githash:
+ segments.append(hash_separator)
+ segments.append(self._githash)
+ return "".join(str(s) for s in segments)
+
+ def release_string(self):
+ """Return the full version of the package.
+
+ This including suffixes indicating VCS status.
+ """
+ return self._long_version(".", ".g", "0")
+
+ def rpm_string(self):
+ """Return the version number to use when building an RPM package.
+
+ This translates the PEP440/semver precedence rules into RPM version
+ sorting operators. Because RPM has no sort-before operator (such as the
+ ~ operator in dpkg), we show all prerelease versions as being versions
+ of the release before.
+ """
+ return self._long_version(None, "+g")
+
+ def to_dev(self, dev_count, githash):
+ """Return a development version of this semver.
+
+ :param dev_count: The number of commits since the last release.
+ :param githash: The git hash of the tree with this version.
+ """
+ return SemanticVersion(
+ self._major, self._minor, self._patch, dev_count=dev_count,
+ githash=githash)
+
+ def to_release(self):
+ """Discard any pre-release or dev metadata.
+
+ :return: A new SemanticVersion with major/minor/patch the same as this
+ one.
+ """
+ return SemanticVersion(self._major, self._minor, self._patch)
+
+ def version_tuple(self):
+ """Present the version as a version_info tuple.
+
+ For documentation on version_info tuples see the Python
+ documentation for sys.version_info.
+
+ Since semver and PEP-440 represent overlapping but not subsets of
+ versions, we have to have some heuristic / mapping rules:
+ - a/b/rc take precedence.
+ - if there is no pre-release version the dev version is used.
+ - serial is taken from the dev/a/b/c component.
+ - final non-dev versions never get serials.
+ """
+ segments = [self._major, self._minor, self._patch]
+ if self._prerelease_type:
+ type_map = {'a': 'alpha',
+ 'b': 'beta',
+ 'rc': 'candidate',
+ }
+ segments.append(type_map[self._prerelease_type])
+ segments.append(self._prerelease)
+ elif self._dev_count:
+ segments.append('dev')
+ segments.append(self._dev_count - 1)
+ else:
+ segments.append('final')
+ segments.append(0)
+ return tuple(segments)
+
+
class VersionInfo(object):
def __init__(self, package):
@@ -30,9 +425,9 @@ class VersionInfo(object):
python-glanceclient
"""
self.package = package
- self.release = None
self.version = None
self._cached_version = None
+ self._semantic = None
def __str__(self):
"""Make the VersionInfo object behave like a string."""
@@ -53,36 +448,31 @@ class VersionInfo(object):
try:
requirement = pkg_resources.Requirement.parse(self.package)
provider = pkg_resources.get_provider(requirement)
- return provider.version
+ result_string = provider.version
except pkg_resources.DistributionNotFound:
# The most likely cause for this is running tests in a tree
# produced from a tarball where the package itself has not been
# installed into anything. Revert to setup-time logic.
from pbr import packaging
- return packaging.get_version(self.package)
+ result_string = packaging.get_version(self.package)
+ return SemanticVersion.from_pip_string(result_string)
def release_string(self):
"""Return the full version of the package.
This including suffixes indicating VCS status.
"""
- if self.release is None:
- self.release = self._get_version_from_pkg_resources()
+ return self.semantic_version().release_string()
- return self.release
+ def semantic_version(self):
+ """Return the SemanticVersion object for this version."""
+ if self._semantic is None:
+ self._semantic = self._get_version_from_pkg_resources()
+ return self._semantic
def version_string(self):
"""Return the short version minus any alpha/beta tags."""
- if self.version is None:
- parts = []
- for part in self.release_string().split('.'):
- if part[0].isdigit():
- parts.append(part)
- else:
- break
- self.version = ".".join(parts)
-
- return self.version
+ return self.semantic_version().brief_string()
# Compatibility functions
canonical_version_string = version_string
diff --git a/setup.cfg b/setup.cfg
index 34b88a8..8115d7b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,7 +5,7 @@ author-email = openstack-dev@lists.openstack.org
summary = Python Build Reasonableness
description-file =
README.rst
-home-page = http://pypi.python.org/pypi/pbr
+home-page = https://launchpad.net/pbr
requires-python = >=2.6
classifier =
Development Status :: 5 - Production/Stable
diff --git a/test-requirements.txt b/test-requirements.txt
index 625a7e8..75ad928 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,7 +1,7 @@
coverage>=3.6
discover
fixtures>=0.3.14
-hacking>=0.8,<0.9
+hacking>=0.9.2,<0.10
mock>=1.0
python-subunit>=0.0.18
sphinx>=1.1.2,<1.2
diff --git a/tools/integration.sh b/tools/integration.sh
index 7d6038a..227aa4c 100644
--- a/tools/integration.sh
+++ b/tools/integration.sh
@@ -55,9 +55,25 @@ EOF
cat <<EOF > setup.py
import setuptools
-setuptools.setup(
- setup_requires=['pbr'],
- pbr=True)
+try:
+ from requests import Timeout
+except ImportError:
+ from pip._vendor.requests import Timeout
+
+from socket import error as SocketError
+
+# Some environments have network issues that drop connections to pypi
+# when running integration tests, so we retry here so that hour-long
+# test runs are less likely to fail randomly.
+try:
+ setuptools.setup(
+ setup_requires=['pbr'],
+ pbr=True)
+except (SocketError, Timeout):
+ setuptools.setup(
+ setup_requires=['pbr'],
+ pbr=True)
+
EOF
mkdir test_project
diff --git a/tox.ini b/tox.ini
index 51d55e2..40999f8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
[tox]
minversion = 1.6
skipsdist = True
-envlist = py26,py27,py33,pypy,pep8
+envlist = py33,py34,py26,py27,pypy,pep8,docs
[testenv]
usedevelop = True
@@ -17,10 +17,11 @@ sitepackages = True
downloadcache = ~/cache/pip
[testenv:pep8]
-deps = -r{toxinidir}/requirements.txt
- -r{toxinidir}/test-requirements.txt
commands = flake8 {posargs}
+[testenv:docs]
+commands = python setup.py build_sphinx
+
[testenv:cover]
setenv = VIRTUAL_ENV={envdir}
commands =
@@ -30,6 +31,9 @@ commands =
commands = {posargs}
[flake8]
-ignore = H803
+# H405 multi line docstring summary not separated with an empty line
+# H904 "Wrap lines in parentheses and not a backslash for line continuation
+# Removed in current hacking (https://review.openstack.org/#/c/101701/).
+ignore = H405,H803,H904
exclude = .venv,.tox,dist,doc,*.egg,build
show-source = true