From c97b51fec5a597b0b708241ddd1ec1e0948d68d3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 12 May 2013 09:32:46 +0000 Subject: [PATCH] Add versioneer and restructure the repo into a src/ dir. --- Makefile | 2 +- gnupg/__init__.py | 19 - gnupg/tests/__init__.py | 0 report-a-bug | 260 ------- setup.py | 15 +- src/__init__.py | 23 + src/_version.py | 197 ++++++ {gnupg => src}/copyleft.py | 0 {gnupg => src}/gnupg.py | 5 - {gnupg => src}/parsers.py | 3 +- {gnupg => src}/util.py | 3 +- .../files/cypherpunk_manifesto | 0 {gnupg/tests => tests}/test_gnupg.py | 5 +- versioneer.py | 656 ++++++++++++++++++ 14 files changed, 890 insertions(+), 298 deletions(-) delete mode 100644 gnupg/__init__.py delete mode 100644 gnupg/tests/__init__.py delete mode 100644 report-a-bug create mode 100644 src/__init__.py create mode 100644 src/_version.py rename {gnupg => src}/copyleft.py (100%) rename {gnupg => src}/gnupg.py (99%) rename {gnupg => src}/parsers.py (99%) rename {gnupg => src}/util.py (99%) rename {gnupg/tests => tests}/files/cypherpunk_manifesto (100%) rename {gnupg/tests => tests}/test_gnupg.py (99%) create mode 100644 versioneer.py diff --git a/Makefile b/Makefile index 69283ae..4c562fd 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ uninstall: cat installed-files.txt | sudo xargs rm -rf cleandocs: - sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs gnupg/ tests/ + sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs src/ tests/ docs: cd docs diff --git a/gnupg/__init__.py b/gnupg/__init__.py deleted file mode 100644 index 726038e..0000000 --- a/gnupg/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ - -__author__ = 'Isis Agora Lovecruft' -__contact__ = 'isis@leap.se' -__date__ = '1 April 2013' -__url__ = 'https://github.com/isislovecruft/python-gnupg' -__version__ = '0.4.0' -__license__ = 'AGPLv3' - -from copyleft import disclaimer as copyright -from copyleft import txcopyright - -import gnupg -from parsers import Crypt, DeleteResult, ListKeys -from parsers import GenKey, Sign, ImportResult, Verify -from gnupg import GPG - -__all__ = ["gnupg", "copyright", - "Crypt", "DeleteResult", "ListKeys", - "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] diff --git a/gnupg/tests/__init__.py b/gnupg/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/report-a-bug b/report-a-bug deleted file mode 100644 index 73e7b98..0000000 --- a/report-a-bug +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/env python -#-*- coding: utf-8 -*- -''' - report-a-bug - ------------ - File a bug against python-gnupg. - - :authors: Wade Leftwich, - Will Holcomb, - Isis Lovecruft, - :license: AGPLv3, see LICENSE and COPYRIGHT files - :copyright: © 2002-2013 Wade Leftwich - © 2006 Will Holcomb - © 2013 Isis Agora Lovecruft - :date: 11 April 2013 - :version: 0.0.1 - -''' - -from __future__ import print_function -from cStringIO import StringIO -from datetime import datetime - -import cookielib -import mimetools -import mimetypes -#import httplib -import os -import stat -import tempfile -import urllib -import urllib2 - -# Controls how sequences are uncoded. If true, elements may be given multiple -# values by assigning a sequence. -doseq = 1 - -def _has_py3k(): - """Check if we're running on Python>=3.0.""" - try: - unicode - return False - except NameError: - return True - -## patch the stupid Python2.x input() problem: -if not _has_py3k(): - input = raw_input - -def _today(): - """Get the current date as a string in the form %Y-%m-%d.""" - now_string = datetime.now().__str__() - return now_string.split(' ', 1)[0] - -def _create_upload_list(): - """Create a dictionary containing information about files to upload.""" - - upload_list = list() - - WANT_UPLOAD = True - FILE_NUMBER = 1 - FILENAME_FIELD = 'attachments[' + str(FILE_NUMBER) + '][file]' - FILEDESC_FIELD = 'attachments[' + str(FILE_NUMBER) + '][description]' - - while WANT_UPLOAD: - do_upload = input("Would you like to attach a file to this ticket? " - + "(y/N) ") - if do_upload.strip() == "": - WANT_UPLOAD = False - break - else: - WANT_UPLOAD = True - - upload = input("Please specify the file to upload, as a filesystem " - + "absolute path or as relative to this directory (%s): " - % os.getcwd()).strip() - - if len(upload) > 0: - if upload.startswith('~'): - upload = os.path.expanduser(upload) - if not os.path.isabs(upload): - upload = os.path.abspath(upload) - try: - assert os.path.isfile(upload), "is not a file" - except AssertionError as ae: - print("Skipping: '%s' %s" % (upload, ae.message)) - else: - upload_fields = {'fields': [FILENAME_FIELD, FILEDESC_FIELD], - 'filepath': upload,} - upload_list.append(upload_fields) - FILE_NUMBER += 1 - return upload_list - -def _create_fields_and_headers(host, url, assign_to=None, - category=None, target_version=None): - REPO_NAME = os.getcwd().rsplit(os.path.sep, 1)[1] - - subject = input("Please provide a brief subject line for the ticket: ") - subject = REPO_NAME + ": " + subject - descript = input("Ticket description:\n ") - whatisit = input("Is this a feature request or a bug report? " - + "(1=feature, 2=bug) ") - if whatisit not in ['1', '2']: - whatisit = '2' - serious = input("How important is this? (1=important, 2=normal, 3=trivial) ") - if serious not in ['1', '2', '3']: - serious = '2' - - headers = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'en-us,en-gb;q=0.9,en;', - 'Connection': 'keep-alive', - 'DNT': '1', - 'Host': host, - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:10.0) Gecko/20100101 Firefox/10.0', } - - fields = { - 'issue[tracker_id]': whatisit, - 'issue[subject]': subject, - 'issue[description]': descript, - 'issue[status_id]': '2', - 'issue[priority_id]': serious, - 'issue[assigned_to_id]': assign_to, - 'issue[category_id]': category, - 'issue[fixed_version_id]': target_version, - 'issue[start_date]': _today(), - 'issue[due_date]': '', - 'issue[estimated_hours]': '', - 'issue[done_ratio]': '0', - 'issue[custom_field_values][3]': '', - 'issue[custom_field_values][4]': '', - 'issue[custom_field_values][5]': '0', - 'send_notification': '0', - 'send_notification': '1', - 'commit': 'Create', } - - return fields, headers - - -class Callable: - def __init__(self, anycallable): - self.__call__ = anycallable - -class MultipartPostHandler(urllib2.BaseHandler): - handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first - - def http_request(self, request): - data = request.get_data() - if data is not None and type(data) != str: - v_files = [] - v_vars = [] - try: - for (key, value) in data.items(): - if type(value) == file: - v_files.append((key, value)) - else: - v_vars.append((key, value)) - except TypeError: - systype, value, traceback = sys.exc_info() - raise TypeError("not non-string sequence or mapping object\n%s" - % traceback) - - if len(v_files) == 0: - data = urllib.urlencode(v_vars, doseq) - else: - boundary, data = self.multipart_encode(v_vars, v_files) - contenttype = 'multipart/form-data; boundary=%s' % boundary - if (request.has_header('Content-Type') and request.get_header( - 'Content-Type').find('multipart/form-data') != 0): - print("Replacing %s with %s" - % (request.get_header('content-type'), - 'multipart/form-data')) - request.add_unredirected_header('Content-Type', contenttype) - request.add_data(data) - return request - - def multipart_encode(self, fields=None, upload_list=None, - boundary=None, buf=None): - if fields is None: - fields = self.fields - if upload_list is None: - upload_list = self.upload_list - if boundary is None: - boundary = mimetools.choose_boundary() - if buf is None: - buf = StringIO() - - for (key, value) in fields: - buf.write('--%s\r\n' % boundary) - buf.write('Content-Disposition: form-data; name="%s"' % key) - buf.write('\r\n\r\n' + value + '\r\n') - - if isinstance(upload_list, list) and len(upload_list) > 0: - for upload in upload_list: - for (name, filepath) in upload: - with open(filepath) as fd: - file_size = os.fstat(fd.fileno())[stat.ST_SIZE] - filename = fd.name.split('/')[-1] - contenttype = mimetypes.guess_type(filename)[0] \ - or 'application/octet-stream' - buf.write('--%s\r\n' % boundary) - buf.write('Content-Disposition: form-data; ') - buf.write('name="%s"; filename="%s"' - % (name[0], filename)) - buf.write('Content-Type: %s\r\n' % contenttype) - # buf.write('Content-Length: %s\r\n' % file_size) - fd.seek(0) - buf.write('\r\n' + fd.read() + '\r\n') - buf.write('--' + boundary + '--\r\n\r\n') - buf.write('--%s\r\n' % boundary) - buf.write('Content-Disposition: form-data; ') - buf.write('name="%s"; filename="%s"' - % (name[1], filename)) - buf.write('\r\n\r\n') - buf.write('--' + boundary + '--\r\n\r\n') - - buf = buf.getvalue() - return boundary, buf - multipart_encode = Callable(multipart_encode) - - https_request = http_request - - -if __name__ == "__main__": - - raise SystemExit("Please fix me! This script needs a login handler.\n" - + "Everything else is finished.") - - ## if you're reusing this script please change these! - host = 'leap.se' - selector = '/code/projects/eip-server/issues/new' - assign_to = '30' ## isis - category = '26' ## email - target_version = '29' ## the close future - - url = 'https://' + host + selector - fields, headers = _create_fields_and_headers(host, selector, assign_to, - category, target_version) - upload_list = _create_upload_list() - cookies = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), - MultipartPostHandler()) - urllib2.install_opener(opener) - - temp = tempfile.mkstemp(suffix=".html") - temp1 = tempfile.mkstemp(suffix=".html") - os.write(temp[0], opener.open('https://'+host+'/code/login', - {'username': 'cypherpunks', - 'password': 'writecode'}).read()) - - for index,upload in enumerate(upload_list): - for (field, filepath) in upload: - fields['file'+'-'+index] = open(filepath, 'rb') - res = opener.open(url, fields) - print("Posted to: %s" % res.geturl()) - print("Server response: %s " % res.code) - print(res.info()) - print(res.read()) - os.remove(temp[1]) diff --git a/setup.py b/setup.py index 03a62f1..036d57b 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,14 @@ from distutils.core import setup -__module__ = 'gnupg' -__version__ = "0.4.0" +import versioneer +versioneer.versionfile_source = 'src/_version.py' +versioneer.versionfile_build = 'gnupg/_version.py' +versioneer.tag_prefix = 'python-gnupg-' +versioneer.parentdir_prefix = 'python-gnupg-' + __author__ = "Isis Agora Lovecruft" __contact__ = 'isis@leap.se' -__date__ = "1 April 2013" setup(name = "python-gnupg", description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", @@ -15,13 +18,15 @@ setup(name = "python-gnupg", management, encryption and signature functionality from Python programs. \ It is intended for use with Python 2.6 or greater.", license="""Copyright © 2013 Isis Lovecruft, et.al. see LICENSE file.""", - version=__version__, + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), author=__author__, author_email=__contact__, maintainer=__author__, maintainer_email=__contact__, url="https://github.com/isislovecruft/python-gnupg", - packages=['gnupg', 'gnupg.tests'], + package_dir={'gnupg': 'src'}, + packages=['gnupg'], platforms="Linux, BSD, OSX, Windows", download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", classifiers=[ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..92998b8 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,23 @@ +#-*- encoding: utf-8 -*- + +from .copyleft import disclaimer as copyright +from .copyleft import txcopyright + +import gnupg +from parsers import Crypt, DeleteResult, ListKeys +from parsers import GenKey, Sign, ImportResult, Verify +from gnupg import GPG + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions + +gnupg.__version__ = __version__ +gnupg.__author__ = 'Isis Agora Lovecruft' +gnupg.__contact__ = 'isis@leap.se' +gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg' +gnupg.__license__ = copyright + +__all__ = ["gnupg", "copyright", + "Crypt", "DeleteResult", "ListKeys", + "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] diff --git a/src/_version.py b/src/_version.py new file mode 100644 index 0000000..42d41a2 --- /dev/null +++ b/src/_version.py @@ -0,0 +1,197 @@ + +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%s', no digits" % ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %s" % ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "python-gnupg-" +parentdir_prefix = "python-gnupg-" +versionfile_source = "src/_version.py" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver + diff --git a/gnupg/copyleft.py b/src/copyleft.py similarity index 100% rename from gnupg/copyleft.py rename to src/copyleft.py diff --git a/gnupg/gnupg.py b/src/gnupg.py similarity index 99% rename from gnupg/gnupg.py rename to src/gnupg.py index d2ee5c6..9d73511 100644 --- a/gnupg/gnupg.py +++ b/src/gnupg.py @@ -75,11 +75,6 @@ Vinay Sajip's documentation: """ -__author__ = "Isis Agora Lovecruft" -__module__ = 'gnupg' -__version__ = "0.4.0" - - try: from io import StringIO from io import BytesIO diff --git a/gnupg/parsers.py b/src/parsers.py similarity index 99% rename from gnupg/parsers.py rename to src/parsers.py index 4ddd686..4477708 100644 --- a/gnupg/parsers.py +++ b/src/parsers.py @@ -22,10 +22,9 @@ parsers.py Classes for parsing GnuPG status messages and sanitising commandline options. ''' -from gnupg import __author__ -from gnupg import __version__ __module__ = 'gnupg.parsers' + import logging import re diff --git a/gnupg/util.py b/src/util.py similarity index 99% rename from gnupg/util.py rename to src/util.py index f07026b..4ddbc65 100644 --- a/gnupg/util.py +++ b/src/util.py @@ -22,10 +22,9 @@ utils.py Extra utilities for python-gnupg. ''' -from gnupg import __author__ -from gnupg import __version__ __module__ = 'gnupg.util' + from datetime import datetime import logging diff --git a/gnupg/tests/files/cypherpunk_manifesto b/tests/files/cypherpunk_manifesto similarity index 100% rename from gnupg/tests/files/cypherpunk_manifesto rename to tests/files/cypherpunk_manifesto diff --git a/gnupg/tests/test_gnupg.py b/tests/test_gnupg.py similarity index 99% rename from gnupg/tests/test_gnupg.py rename to tests/test_gnupg.py index 17068ab..53f5b51 100644 --- a/gnupg/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -28,12 +28,9 @@ import gnupg from gnupg import parsers from gnupg import util -__author__ = gnupg.__author__ -__version__ = gnupg.__version__ - logger = logging.getLogger('gnupg') -_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') +_here = os.path.join(os.getcwd(), 'tests') _files = os.path.join(_here, 'files') _tempd = os.path.join(_here, 'tmp') diff --git a/versioneer.py b/versioneer.py new file mode 100644 index 0000000..57d9941 --- /dev/null +++ b/versioneer.py @@ -0,0 +1,656 @@ +#! /usr/bin/python + +"""versioneer.py + +(like a rocketeer, but for versions) + +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Version: 0.7+ + +This file helps distutils-based projects manage their version number by just +creating version-control tags. + +For developers who work from a VCS-generated tree (e.g. 'git clone' etc), +each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a +version number by asking your version-control tool about the current +checkout. The version number will be written into a generated _version.py +file of your choosing, where it can be included by your __init__.py + +For users who work from a VCS-generated tarball (e.g. 'git archive'), it will +compute a version number by looking at the name of the directory created when +te tarball is unpacked. This conventionally includes both the name of the +project and a version number. + +For users who work from a tarball built by 'setup.py sdist', it will get a +version number from a previously-generated _version.py file. + +As a result, loading code directly from the source tree will not result in a +real version. If you want real versions from VCS trees (where you frequently +update from the upstream repository, or do new development), you will need to +do a 'setup.py version' after each update, and load code from the build/ +directory. + +You need to provide this code with a few configuration values: + + versionfile_source: + A project-relative pathname into which the generated version strings + should be written. This is usually a _version.py next to your project's + main __init__.py file. If your project uses src/myproject/__init__.py, + this should be 'src/myproject/_version.py'. This file should be checked + in to your VCS as usual: the copy created below by 'setup.py + update_files' will include code that parses expanded VCS keywords in + generated tarballs. The 'build' and 'sdist' commands will replace it with + a copy that has just the calculated version string. + + versionfile_build: + Like versionfile_source, but relative to the build directory instead of + the source directory. These will differ when your setup.py uses + 'package_dir='. If you have package_dir={'myproject': 'src/myproject'}, + then you will probably have versionfile_build='myproject/_version.py' and + versionfile_source='src/myproject/_version.py'. + + tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all + VCS tags. If your tags look like 'myproject-1.2.0', then you + should use tag_prefix='myproject-'. If you use unprefixed tags + like '1.2.0', this should be an empty string. + + parentdir_prefix: a string, frequently the same as tag_prefix, which + appears at the start of all unpacked tarball filenames. If + your tarball unpacks into 'myproject-1.2.0', this should + be 'myproject-'. + +To use it: + + 1: include this file in the top level of your project + 2: make the following changes to the top of your setup.py: + import versioneer + versioneer.versionfile_source = 'src/myproject/_version.py' + versioneer.versionfile_build = 'myproject/_version.py' + versioneer.tag_prefix = '' # tags are like 1.2.0 + versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' + 3: add the following arguments to the setup() call in your setup.py: + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + 4: run 'setup.py update_files', which will create _version.py, and will + append the following to your __init__.py: + from _version import __version__ + 5: modify your MANIFEST.in to include versioneer.py + 6: add both versioneer.py and the generated _version.py to your VCS +""" + +import os, sys, re +from distutils.core import Command +from distutils.command.sdist import sdist as _sdist +from distutils.command.build import build as _build + +versionfile_source = None +versionfile_build = None +tag_prefix = None +parentdir_prefix = None + +VCS = "git" +IN_LONG_VERSION_PY = False + + +LONG_VERSION_PY = ''' +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" +git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %%s" %% args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%%s', no digits" %% ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %%d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %%s" %% ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %%s" %% root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %% + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "%(TAG_PREFIX)s" +parentdir_prefix = "%(PARENTDIR_PREFIX)s" +versionfile_source = "%(VERSIONFILE_SOURCE)s" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver + +''' + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%s', no digits" % ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %s" % ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +import sys + +def do_vcs_install(versionfile_source, ipy): + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + run_command([GIT, "add", "versioneer.py"]) + run_command([GIT, "add", versionfile_source]) + run_command([GIT, "add", ipy]) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + run_command([GIT, "add", ".gitattributes"]) + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.7+) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +version_version = '%(version)s' +version_full = '%(full)s' +def get_versions(default={}, verbose=False): + return {'version': version_version, 'full': version_full} + +""" + +DEFAULT = {"version": "unknown", "full": "unknown"} + +def versions_from_file(filename): + versions = {} + try: + f = open(filename) + except EnvironmentError: + return versions + for line in f.readlines(): + mo = re.match("version_version = '([^']+)'", line) + if mo: + versions["version"] = mo.group(1) + mo = re.match("version_full = '([^']+)'", line) + if mo: + versions["full"] = mo.group(1) + return versions + +def write_to_version_file(filename, versions): + f = open(filename, "w") + f.write(SHORT_VERSION_PY % versions) + f.close() + print("set %s to '%s'" % (filename, versions["version"])) + + +def get_best_versions(versionfile, tag_prefix, parentdir_prefix, + default=DEFAULT, verbose=False): + # returns dict with two keys: 'version' and 'full' + # + # extract version from first of _version.py, 'git describe', parentdir. + # This is meant to work for developers using a source checkout, for users + # of a tarball created by 'setup.py sdist', and for users of a + # tarball/zipball created by 'git archive' or github's download-from-tag + # feature. + + variables = get_expanded_variables(versionfile_source) + if variables: + ver = versions_from_expanded_variables(variables, tag_prefix) + if ver: + if verbose: print("got version from expanded variable %s" % ver) + return ver + + ver = versions_from_file(versionfile) + if ver: + if verbose: print("got version from file %s %s" % (versionfile, ver)) + return ver + + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if ver: + if verbose: print("got version from git %s" % ver) + return ver + + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, verbose) + if ver: + if verbose: print("got version from parentdir %s" % ver) + return ver + + if verbose: print("got version from default %s" % ver) + return default + +def get_versions(default=DEFAULT, verbose=False): + assert versionfile_source is not None, "please set versioneer.versionfile_source" + assert tag_prefix is not None, "please set versioneer.tag_prefix" + assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" + return get_best_versions(versionfile_source, tag_prefix, parentdir_prefix, + default=default, verbose=verbose) +def get_version(verbose=False): + return get_versions(verbose=verbose)["version"] + +class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + ver = get_version(verbose=True) + print("Version is currently: %s" % ver) + + +class cmd_build(_build): + def run(self): + versions = get_versions(verbose=True) + _build.run(self) + # now locate _version.py in the new build/ directory and replace it + # with an updated value + target_versionfile = os.path.join(self.build_lib, versionfile_build) + print("UPDATING %s" % target_versionfile) + os.unlink(target_versionfile) + f = open(target_versionfile, "w") + f.write(SHORT_VERSION_PY % versions) + f.close() + +class cmd_sdist(_sdist): + def run(self): + versions = get_versions(verbose=True) + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory (remembering + # that it may be a hardlink) and replace it with an updated value + target_versionfile = os.path.join(base_dir, versionfile_source) + print("UPDATING %s" % target_versionfile) + os.unlink(target_versionfile) + f = open(target_versionfile, "w") + f.write(SHORT_VERSION_PY % self._versioneer_generated_versions) + f.close() + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + +class cmd_update_files(Command): + description = "modify __init__.py and create _version.py" + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py") + print(" creating %s" % versionfile_source) + f = open(versionfile_source, "w") + f.write(LONG_VERSION_PY % {"DOLLAR": "$", + "TAG_PREFIX": tag_prefix, + "PARENTDIR_PREFIX": parentdir_prefix, + "VERSIONFILE_SOURCE": versionfile_source, + }) + f.close() + try: + old = open(ipy, "r").read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + f = open(ipy, "a") + f.write(INIT_PY_SNIPPET) + f.close() + else: + print(" %s unmodified" % ipy) + do_vcs_install(versionfile_source, ipy) + +def get_cmdclass(): + return {'version': cmd_version, + 'update_files': cmd_update_files, + 'build': cmd_build, + 'sdist': cmd_sdist, + }