diff --git a/.bzrignore b/.bzrignore index 83eeb73..5e65185 100644 --- a/.bzrignore +++ b/.bzrignore @@ -1,5 +1,5 @@ -build -dist *.egg-info +.tox __pycache__ -.tox \ No newline at end of file +build +dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25168e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.egg-info +*.pyc +.tox +__pycache__ +build +dist diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..32160ed --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +install: + sudo apt-get install git bzr +python: + - "2.6" + - "2.7" + - "3.2" + - "3.3" + - "3.4" +script: python setup.py test diff --git a/doc/changes.rst b/doc/changes.rst index 6a847c4..278a1c6 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -1,6 +1,37 @@ Release history *************** +.. _version_1_10: + +Version 1.10 +============ + +* Add support for python3.2, python3.3 and python3.4. This represents the + current Ubuntu LTS release (12.04), current Debian testing release and + upcoming Ubuntu LTS release (14.04). This is where the development happens now + so support for 3.x is now first-class and should be promptly fixed, if any + bugs are discovered. +* Drop support for python2.4, python2.5, python3.0 and python3.1. Those + versions are either very old and not actively used anymore, except for legacy + systems or represent the early, virtually unused python3 versions. Support + for python2.7 will be retained indefinitely. The status of 2.6 is uncertain + as it's not used by any systems I'm interested in anymore and I cannot test + it easily. +* Split off :class:`versiontools.VersionBase` from + :class:`versiontools.Version` so that it can be used outside of the VCS + context. This allows for derivative versions classes to easily reuse the + common parts. The normal Version class is now just focused on discovering + version control system. +* Add :class:`versiontools.git_support.GitShellIntegration` that does not + depend on python git classes and instead parses git output. It is therefore + more likely to just work out of the box on otherwise empty virtualenv. It + also provides good support for python3.x without being bound to python-git. +* Add :class:`versiontools.bzr_support.BzrShellIntegration` that does not + depend on python2.x bzr classes and instead parses bzr output. It is + therefore more likely to just work out of the box on otherwise empty + virtualenv. It also provides good support for python3.x without being bound + to bzrlib that is likely going to stay on python2.7 forever. + .. _version_1_9_1: Version 1.9.1 diff --git a/doc/index.rst b/doc/index.rst index fc5ac6e..a81f424 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,7 +2,7 @@ Version Tools Documentation =========================== .. seealso:: To get started quickly see :ref:`usage` -.. seealso:: See what's new in :ref:`version_1_9_1` +.. seealso:: See what's new in :ref:`version_1_10` .. note:: This document may be out of date, the bleeding edge version is always diff --git a/setup.py b/setup.py index 8475d49..db80e18 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,9 @@ entry_points=""" [versiontools.vcs_integration] bzr=versiontools.bzr_support:BzrIntegration + bzr_sh=versiontools.bzr_support:BzrShellIntegration git=versiontools.git_support:GitIntegration + git_sh=versiontools.git_support:GitShellIntegration hg=versiontools.hg_support:HgIntegration [distutils.setup_keywords] version=versiontools.setuptools_hooks:version @@ -47,13 +49,11 @@ ("License :: OSI Approved :: GNU Library or Lesser General Public" " License (LGPL)"), "Operating System :: OS Independent", - "Programming Language :: Python :: 2.4", - "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.0", - "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", "Topic :: Software Development :: Version Control", ], zip_safe=True) diff --git a/tox.ini b/tox.ini index da52744..d6183fe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py24, py25, py26, py27, py31, py32 +envlist = py26, py27, py32, py33, py34 [testenv] commands = {envpython} setup.py test diff --git a/versiontools/__init__.py b/versiontools/__init__.py index ab586f4..7a91644 100644 --- a/versiontools/__init__.py +++ b/versiontools/__init__.py @@ -25,7 +25,8 @@ .. note: Since version 1.1 we should conform to PEP 386 """ -__version__ = (1, 9, 1, "final", 0) +__version__ = (1, 10, 0, "dev", 0) +__all__ = ["VersionBase", "Version", "format_version", "handle_version"] import inspect @@ -34,15 +35,15 @@ import sys -class Version(tuple): +class VersionBase(tuple): """ - Smart version class. - - Version class is a tuple of five elements and has the same logical - components as :data:`sys.version_info`. + Base class for Version class. - In addition to the tuple elements there is a special :attr:`vcs` attribute - that has all of the data exported by the version control system. + Keep the core logic without any VCS awareness or support. The VersionBase + class is a tuple of five elements and has the same logical components as + :data:`sys.version_info`. + + .. versionadded:: 1.10 """ _RELEASELEVEL_TO_TOKEN = { @@ -59,7 +60,7 @@ def __new__(cls, major, minor, micro=0, releaselevel="final", serial=0): variables except for releaselevel are silently converted to integers That is:: - >>> Version("1.2.3.dev".split(".")) + >>> VersionBase("1.2.3.dev".split(".")) (1, 2, 3, "dev", 0) :param major: @@ -124,8 +125,6 @@ def to_int(v): ("serial must be greater than zero for" " %s releases") % releaselevel) obj = tuple.__new__(cls, (major, minor, micro, releaselevel, serial)) - object.__setattr__(obj, '_source_tree', cls._find_source_tree()) - object.__setattr__(obj, '_vcs', None) return obj major = property( @@ -148,6 +147,116 @@ def to_int(v): operator.itemgetter(4), doc="Serial number") + def __str__(self): + """ + Return a string representation of the version tuple. + + The string is not a direct concatenation of all version components. + Instead it's a more natural 'human friendly' version where components + with certain values are left out. + + The following table shows how a version tuple gets converted to a + version string. + + +-------------------------------+-------------------+ + | __version__ | Formatter version | + +===============================+===================+ + | ``(1, 2, 0, "final", 0)`` | ``"1.2"`` | + +-------------------------------+-------------------+ + | ``(1, 2, 3, "final", 0)`` | ``"1.2.3"`` | + +-------------------------------+-------------------+ + | ``(1, 3, 0, "alpha", 1)`` | ``"1.3a1"`` | + +-------------------------------+-------------------+ + | ``(1, 3, 0, "beta", 1)`` | ``"1.3b1"`` | + +-------------------------------+-------------------+ + | ``(1, 3, 0, "candidate", 1)`` | ``"1.3c1"`` | + +-------------------------------+-------------------+ + | ``(1, 3, 0, "dev", 0)`` | ``"1.3.dev"`` | + +-------------------------------+-------------------+ + """ + version = "%s.%s" % (self.major, self.minor) + if self.micro != 0: + version += ".%s" % self.micro + token = self._RELEASELEVEL_TO_TOKEN.get(self.releaselevel) + if token: + version += "%s%d" % (token, self.serial) + if self.releaselevel == "dev": + version += ".dev" + return version + + +class Version(VersionBase): + """ + Smart version class. + + Version class is a tuple of five elements and has the same logical + components as :data:`sys.version_info`. + + In addition to the tuple elements there is a special :attr:`vcs` attribute + that has all of the data exported by the version control system. + """ + + def __new__(cls, major, minor, micro=0, releaselevel="final", serial=0): + """ + Construct a new version tuple. + + There is some extra logic when initializing tuple elements. All + variables except for releaselevel are silently converted to integers + That is:: + + >>> Version("1.2.3.dev".split(".")) + (1, 2, 3, "dev", 0) + + :param major: + Major version number + + :type major: + :class:`int` or :class:`str` + + :param minor: + Minor version number + + :type minor: + :class:`int` or :class:`str` + + :param micro: + Micro version number, defaults to ``0``. + + :type micro: + :class:`int` or :class:`str` + + :param releaselevel: + Release level name. + + There is a constraint on allowed values of releaselevel. Only the + following values are permitted: + + * 'dev' + * 'alpha' + * 'beta' + * 'candidate' + * 'final' + + :type releaselevel: + :class:`str` + + :param serial: + Serial number, usually zero, only used for alpha, beta and + candidate versions where it must be greater than zero. + + :type micro: + :class:`int` or :class:`str` + + :raises ValueError: + If releaselevel is incorrect, a version component is negative or + serial is 0 and releaselevel is alpha, beta or candidate. + """ + obj = super(Version, cls).__new__( + cls, major, minor, micro, releaselevel, serial) + object.__setattr__(obj, '_source_tree', cls._find_source_tree()) + object.__setattr__(obj, '_vcs', None) + return obj + @property def vcs(self): """ @@ -211,7 +320,7 @@ def from_expression(cls, pkg_expression): a variable that holds the actual version. The version cannot be a plain string and instead must be a tuple of five elements as described by the :class:`~versiontools.Version` class. - + The variable that holds the version should be called ``__version__``. If it is called something else the actual name has to be specified explicitly in ``pkg_expression`` by appending a colon (``:``) and the @@ -225,7 +334,7 @@ def from_expression(cls, pkg_expression): else: # Allow people not to include the identifier separator module_or_package = pkg_expression - identifier = "" + identifier = "" # Use __version__ unless specified otherwise if identifier == "": identifier = "__version__" @@ -295,17 +404,9 @@ def __str__(self): | Mercurial | Tip revision number, e.g. ``54`` | +-----------+------------------------------------------------+ """ - version = "%s.%s" % (self.major, self.minor) - if self.micro != 0: - version += ".%s" % self.micro - token = self._RELEASELEVEL_TO_TOKEN.get(self.releaselevel) - if token: - version += "%s%d" % (token, self.serial) - if self.releaselevel == "dev": - if self.vcs is not None: - version += ".dev%s" % self.vcs.revno - else: - version += ".dev" + version = super(Version, self).__str__() + if self.releaselevel == "dev" and self.vcs is not None: + version += str(self.vcs.revno) return version @classmethod @@ -320,8 +421,8 @@ def _find_source_tree(cls): frame, filename, lineno, func_name, context, context_index = record if context is None or context_index >= len(context): continue - if (func_name == "" and "__version__" in - context[context_index]): + if (func_name == "" + and "__version__" in context[context_index]): caller = frame break else: @@ -347,7 +448,7 @@ def _query_vcs(self): if self._source_tree is None: return for entrypoint in pkg_resources.iter_entry_points( - "versiontools.vcs_integration"): + "versiontools.vcs_integration"): try: integration_cls = entrypoint.load() integration = integration_cls.from_source_tree( diff --git a/versiontools/bzr_support.py b/versiontools/bzr_support.py index bd3c975..5dd67ec 100644 --- a/versiontools/bzr_support.py +++ b/versiontools/bzr_support.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010 -2012 Linaro Limited +# Copyright (C) 2010-2012 Linaro Limited # # Author: Zygmunt Krynicki # @@ -29,7 +29,7 @@ To work with Bazaar repositories you will need bzrlib. You can install it with pip or from the ``bzr`` package on Ubuntu. -.. warning:: +.. warning:: On Windows the typical Bazaar installation bundles both the python interpreter and a host of libraries and those libraries are not accessible @@ -95,3 +95,58 @@ def from_source_tree(cls, source_tree): (source_tree, message)) if branch: return cls(branch) + + +class BzrShellIntegration(object): + """ + Bazaar (shell version) integration for versiontools + + .. versionadded:: 1.10 + """ + def __init__(self, revno, branch_nick): + self._revno = revno + self._branch_nick = branch_nick + + @property + def revno(self): + """ + Revision number of the branch + """ + return self._revno + + @property + def branch_nick(self): + """ + Nickname of the branch + """ + return self._branch_nick + + @classmethod + def from_source_tree(cls, source_tree): + """ + Initialize :class:`~versiontools.bzr_support.BzrShellIntegration` by + pointing at the source tree. Any file or directory inside the + source tree may be used. + """ + import subprocess + try: + revno = subprocess.check_output( + ['bzr', 'revno'], + cwd=source_tree, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + revno = revno.strip() + except (OSError, subprocess.CalledProcessError): + return + try: + branch_nick = subprocess.check_output( + ['bzr', 'nick'], + cwd=source_tree, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + branch_nick = branch_nick.strip() + except (OSError, subprocess.CalledProcessError): + branch_nick = None + return cls(revno, branch_nick) diff --git a/versiontools/git_support.py b/versiontools/git_support.py index edce6be..e2adb38 100644 --- a/versiontools/git_support.py +++ b/versiontools/git_support.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*-" # Copyright (C) 2011 enn.io UG (haftungsbeschränkt) # Copyright (C) 2011-2012 Linaro Limited +# Copyright (C) 2012 Canonical +# Copyright (C) 2014 Canonical # # Author: Jannis Leidel # Author: Zygmunt Krynicki @@ -31,7 +33,8 @@ To work with Git repositories you will need `GitPython `_. Version 0.1.6 is sufficient to - run the code. You can install it with pip. + run the code. You can install it with pip. Alternatively, if you have + git(1) in your PATH you don't need any additional python modules. """ import logging @@ -53,7 +56,8 @@ def __init__(self, repo): pass try: # This is for python-git 0.1.6 (that is in debian and ubuntu) - head = [head for head in repo.heads if head.name==repo.active_branch][0] + head = [a_head for a_head in repo.heads + if a_head.name == repo.active_branch][0] self._branch_nick = head.name self._commit_id = head.commit.id except (IndexError, KeyError): @@ -111,3 +115,75 @@ def from_source_tree(cls, source_tree): (source_tree, message)) if repo: return cls(repo) + + +class GitShellIntegration(object): + """ + Git (shell version) integration for versiontools + """ + def __init__(self, commit_id, branch_nick=None): + self._commit_id = commit_id + self._branch_nick = branch_nick + + @property + def revno(self): + """ + Same as + :attr:`~versiontools.git_support.GitShellIntegration.commit_id_abbrev` + """ + return self.commit_id_abbrev + + @property + def commit_id(self): + """ + The full commit id + """ + return self._commit_id + + @property + def commit_id_abbrev(self): + """ + The abbreviated, 7 character commit id + """ + return self._commit_id[:7] + + @property + def branch_nick(self): + """ + Nickname of the branch + + .. versionadded:: 1.0.4 + """ + return self._branch_nick + + @classmethod + def from_source_tree(cls, source_tree): + """ + Initialize :class:`~versiontools.git_support.GitShellIntegration` by + pointing at the source tree. Any file or directory inside the + source tree may be used. + """ + import subprocess + try: + commit_id = subprocess.check_output( + ['git', 'show', '-s', '--format=%H', 'HEAD'], + cwd=source_tree, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + commit_id = commit_id.strip() + except (OSError, subprocess.CalledProcessError): + return + try: + branch_name = subprocess.check_output( + ['git', 'symbolic-ref', '--quiet', 'HEAD'], + cwd=source_tree, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + branch_name = branch_name.strip() + if branch_name.startswith("refs/heads/"): + branch_name = branch_name[len("refs/heads"):] + except (OSError, subprocess.CalledProcessError): + branch_name = None + return cls(commit_id, branch_name) diff --git a/versiontools/hg_support.py b/versiontools/hg_support.py index fa32e9b..567e7e0 100644 --- a/versiontools/hg_support.py +++ b/versiontools/hg_support.py @@ -31,7 +31,7 @@ To work with Mercurial repositories you will need `Mercurial `_. You can install it with pip or from the - `mercurial` package on Ubuntu. + `mercurial` package on Ubuntu. """ import logging import sys diff --git a/versiontools/setuptools_hooks.py b/versiontools/setuptools_hooks.py index 822ccad..990edc9 100644 --- a/versiontools/setuptools_hooks.py +++ b/versiontools/setuptools_hooks.py @@ -63,7 +63,7 @@ def version(dist, attr, value): # Lookup the version object version = Version.from_expression(value) # Update distribution metadata - dist.metadata.version = str(version) + dist.metadata.version = str(version) except ValueError: message = _get_exception_message(*sys.exc_info()) if message.startswith(": "): diff --git a/versiontools/tests.py b/versiontools/tests.py index f30068e..10efb14 100644 --- a/versiontools/tests.py +++ b/versiontools/tests.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010, 2011 Linaro Limited +# Copyright (C) 2010-2011 Linaro Limited # # Author: Zygmunt Krynicki # @@ -23,7 +23,7 @@ from unittest import TestCase from versiontools import Version -from versiontools.setuptools_hooks import version as handle_version +from versiontools.setuptools_hooks import version as handle_version class VersionFormattingTests(TestCase): @@ -134,8 +134,14 @@ def test_cant_import(self): except Exception: e = sys.exc_info()[1] self.assertTrue(isinstance(e, DistutilsSetupError)) - self.assertEqual(str(e), "Unable to import 'nonexisting': " - "No module named nonexisting") + if sys.version_info[0:2] >= (3, 3): + self.assertEqual(str(e), ( + "Unable to import 'nonexisting': " + "No module named 'nonexisting'")) + else: + self.assertEqual(str(e), ( + "Unable to import 'nonexisting': " + "No module named nonexisting")) def test_not_found(self): version = ':versiontools:versiontools:__nonexisting__' @@ -144,6 +150,7 @@ def test_not_found(self): except Exception: e = sys.exc_info()[1] self.assertTrue(isinstance(e, DistutilsSetupError)) - self.assertEqual(str(e), "Unable to access '__nonexisting__' in " - "'versiontools': 'module' object has " - "no attribute '__nonexisting__'") + self.assertEqual(str(e), ( + "Unable to access '__nonexisting__' in " + "'versiontools': 'module' object has " + "no attribute '__nonexisting__'")) diff --git a/versiontools/versiontools_support.py b/versiontools/versiontools_support.py index 5135b70..3f9bd79 100644 --- a/versiontools/versiontools_support.py +++ b/versiontools/versiontools_support.py @@ -61,7 +61,8 @@ import distutils.errors -class VersiontoolsEnchancedDistributionMetadata(distutils.dist.DistributionMetadata): +class VersiontoolsEnchancedDistributionMetadata( + distutils.dist.DistributionMetadata): """ A subclass of distutils.dist.DistributionMetadata that uses versiontools @@ -75,7 +76,7 @@ class VersiontoolsEnchancedDistributionMetadata(distutils.dist.DistributionMetad # was created before the introduction of new-style classes to python. __base = distutils.dist.DistributionMetadata - def get_version(self): + def get_version(self): """ Get distribution version. @@ -92,7 +93,7 @@ def get_version(self): ``setup_requires`` feature of ``setuptools``. """ if (self.name is not None and self.version is not None - and self.version.startswith(":versiontools:")): + and self.version.startswith(":versiontools:")): return (self.__get_live_version() or self.__get_frozen_version() or self.__fail_to_get_any_version()) else: @@ -141,5 +142,6 @@ def __fail_to_get_any_version(self): # prevent a (odd) case of multiple imports of this module. if not issubclass( distutils.dist.DistributionMetadata, - VersiontoolsEnchancedDistributionMetadata): - distutils.dist.DistributionMetadata = VersiontoolsEnchancedDistributionMetadata + VersiontoolsEnchancedDistributionMetadata): + distutils.dist.DistributionMetadata = ( + VersiontoolsEnchancedDistributionMetadata)