├── .coveragerc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── kernda ├── __init__.py ├── _version.py └── cli.py ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── test └── test_cli.py └── versioneer.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source=kernda 3 | [report] 4 | omit = 5 | */python?.?/* 6 | *test* 7 | # ignore _version.py and versioneer.py 8 | .*version.* 9 | *_version.py 10 | 11 | exclude_lines = 12 | def set_default 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | kernda/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | doc/_build 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | cover/ 34 | .coverage 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | cover/ 39 | 40 | # Translations 41 | *.mo 42 | 43 | # Mr Developer 44 | .mr.developer.cfg 45 | .project 46 | .pydevproject 47 | 48 | # Rope 49 | .ropeproject 50 | 51 | # Django stuff: 52 | *.log 53 | *.pot 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | #mac 59 | .DS_Store 60 | *~ 61 | 62 | #pycharm 63 | .idea/* 64 | 65 | #Dolphin browser files 66 | .directory/ 67 | .directory 68 | 69 | #Binary data files 70 | *.volume 71 | *.am 72 | *.tiff 73 | *.tif 74 | *.dat 75 | *.DAT 76 | 77 | #generated documntation files 78 | generated/ 79 | 80 | #ipython notebook 81 | .ipynb_checkpoints/ 82 | 83 | #vim 84 | *.swp 85 | 86 | #data files 87 | *.zip 88 | *.jpg 89 | 90 | # ctags 91 | .tags* 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | matrix: 4 | include: 5 | - name: "Python 2.7 unit tests" 6 | env: PYTHON=2.7 7 | - name: "Python 3.5 unit tests" 8 | env: PYTHON=3.5 9 | - name: "Python 3.6 unit tests" 10 | env: PYTHON=3.6 11 | - name: "Python 3.7 unit tests" 12 | env: PYTHON=3.7 13 | 14 | before_install: 15 | - git clone https://github.com/ericdill/ci ~/scripts 16 | 17 | install: 18 | - . ~/scripts/install-miniconda.sh 19 | - conda create -n testenv-$PYTHON python=$PYTHON pip --file test-requirements.txt 20 | - source activate testenv-$PYTHON 21 | - pip install codecov 22 | - pip install -e . 23 | 24 | script: 25 | - kernda -h 26 | - coverage run -m pytest -vrsx 27 | 28 | after_success: 29 | - codecov 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, 2018 Valassis Digital 2 | Copyright (c) 2016, 2017 MaxPoint Interactive, Inc. 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 29 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include kernda/_version.py 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kernda 2 | 3 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 4 | [![Build Status](https://travis-ci.org/Valassis-Digital-Media/kernda.svg?branch=master)](https://travis-ci.org/Valassis-Digital-Media/kernda) 5 | [![PyPI version](https://badge.fury.io/py/kernda.svg)](https://badge.fury.io/py/kernda) 6 | [![codecov](https://codecov.io/gh/maxpoint/Valassis-Digital-Media/branch/master/graph/badge.svg)](https://codecov.io/gh/Valassis-Digital-Media/kernda) 7 | 8 | Updates an IPython or IRKernel kernel spec (i.e., kernel.json file) to activate a conda environment before launching the kernel process. 9 | 10 | ## Requirements 11 | 12 | * bash (i.e., does not yet work for kernels on Windows) 13 | 14 | ## Install 15 | 16 | `pip install kernda` 17 | 18 | ## Usage 19 | 20 | ``` 21 | usage: kernda [-h] [--display-name DISPLAY_NAME] [--overwrite] 22 | [--env-dir ENV_DIR] 23 | kernel.json 24 | 25 | positional arguments: 26 | kernel.json Path to a kernel spec 27 | 28 | optional arguments: 29 | -h, --help show this help message and exit 30 | --display-name DISPLAY_NAME 31 | New display name for the kernel (default: keep the 32 | original) 33 | --overwrite, -o Overwrite the existing kernel spec (default: False, 34 | print to stdout 35 | --env-dir ENV_DIR Path to the conda environment that should activate 36 | (default: prefix path to the kernel in the existing 37 | kernel spec file) 38 | ``` 39 | 40 | ### Examples 41 | 42 | ``` 43 | # modify the kernel spec in place so that it activates the conda 44 | # environment containing the kernel binary 45 | kernda ~/.local/share/jupyter/kernels/my_kernel/kernel.json -o 46 | 47 | # print the modified kernel spec to stdout and redirect it 48 | # to a new file 49 | kernda /usr/local/share/jupyter/kernels/my_kernel/kernel.json > other_kernel.json 50 | 51 | # modify the kernel spec in place so that it activates the 52 | # specified conda environment 53 | kernda ~/some_kernel.json -o --env-dir ~/envs/my_env 54 | ``` 55 | -------------------------------------------------------------------------------- /kernda/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from ._version import get_versions 3 | __version__ = get_versions()['version'] 4 | del get_versions 5 | -------------------------------------------------------------------------------- /kernda/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.17 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> master)" 27 | git_full = "8ac1f1cf0307d710b1a549bbfba11c31a3d2d6bd" 28 | git_date = "2019-03-11 20:05:01 -0400" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440" 44 | cfg.tag_prefix = "" 45 | cfg.parentdir_prefix = "None" 46 | cfg.versionfile_source = "kernda/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /kernda/cli.py: -------------------------------------------------------------------------------- 1 | """Adds conda environment activation to a Jupyter kernel spec.""" 2 | from __future__ import print_function 3 | 4 | import argparse 5 | import json 6 | import os 7 | import sys 8 | import subprocess 9 | from os.path import join as pjoin, dirname, isfile, expanduser, abspath 10 | try: 11 | from shlex import quote 12 | except ImportError: 13 | from pipes import quote 14 | 15 | 16 | # This is the final form the kernel start command will take 17 | # after running kernda. It's at the module-level for ease of reference only. 18 | 19 | FULL_CMD_TMPL = '{source_or_conda} "{activate_script}" "{env_dir}" && exec {start_cmd} {start_args}' 20 | 21 | 22 | def determine_conda_activate_script(env_dir): 23 | """Finds the correct path to an activate script. 24 | 25 | If no activate script exists or conda is broken / nonexistant this function will raise 26 | 27 | Parameters 28 | ---------- 29 | env_dir : str 30 | path to an environment root 31 | 32 | Returns 33 | ------- 34 | str 35 | Absolute path to a $PREFIX/bin/activate script 36 | 37 | """ 38 | in_env = pjoin(env_dir, 'bin', 'activate') 39 | # virtualenv / conda < 4.4 40 | if os.path.exists(in_env): 41 | return abspath(in_env) 42 | # conda 4.4+ when something has been activated 43 | conda_executable_from_env = os.getenv('CONDA_EXE') 44 | if conda_executable_from_env: 45 | conda_prefix = abspath(pjoin(dirname(conda_executable_from_env), '..')) 46 | else: 47 | # conda 4.4+ when nothing is activated 48 | output = subprocess.check_output(['conda', 'info', '--json']) 49 | if sys.version_info[0] >= 3: 50 | output = output.decode('utf8') 51 | 52 | conda_prefix = json.loads(output).get("conda_prefix") 53 | if not conda_prefix: 54 | raise ValueError("No conda prefix could be determined") 55 | 56 | return abspath(pjoin(conda_prefix, 'bin', 'activate')) 57 | 58 | 59 | def add_activation(args): 60 | """Add conda environment activation to a kernel spec. 61 | 62 | Parameters 63 | ---------- 64 | args: Namespace 65 | argparse command line arguments 66 | 67 | Returns 68 | ------- 69 | int 70 | Exit code 71 | """ 72 | input_fn = args.kernelspec 73 | if not isfile(input_fn): 74 | print('Error: kernel spec {} not found'.format(args.kernelspec)) 75 | return 1 76 | 77 | with open(input_fn) as f: 78 | spec = json.load(f) 79 | 80 | # Treat the path provided by the user as the conda environment we 81 | # want to activate. If the user did not provide a path, assume the 82 | # path containing the conda kernel is the desired environment. 83 | bin_dir = args.env_dir 84 | original_argv = spec.get("_kernda_original_argv") or spec['argv'] 85 | if not bin_dir: 86 | executable = original_argv[0] 87 | bin_dir = dirname(executable) 88 | elif bin_dir and not os.path.exists(bin_dir): 89 | print("Error: {} does not exist".format(bin_dir), file=sys.stderr) 90 | return 1 91 | 92 | # Add the bin subdir to the path if it's not already included. 93 | if not bin_dir.endswith('bin'): 94 | bin_dir += os.path.sep + 'bin' 95 | 96 | # Retrieve the activate script associated with this environment. 97 | # This will return {bin_dir}/activate if it exists (conda<4.4, base env or virtualenv), otherwise it will fall back 98 | # to the activate script in the current base conda environment. 99 | # 100 | # In versions of conda > 4.4 environments no longer have their own activate script and rely on the base env 101 | # In prior versions of conda this was a symlink in any case to the base env's activate script 102 | try: 103 | activate_script = determine_conda_activate_script(pjoin(bin_dir, '..')) 104 | except (subprocess.CalledProcessError, ValueError): 105 | print("Error: Could not determine the location of the activation script associated with {}".format(bin_dir), 106 | file=sys.stderr) 107 | print(" Verify that the `conda` command works in your current shell by running `conda --info`", 108 | file=sys.stderr) 109 | return 1 110 | # Use source activate or conda activate, depending on the CLI flag 111 | source_or_conda = "conda" if args.conda_activate else "source" 112 | env_dir = dirname(bin_dir) 113 | start_cmd = ' '.join(quote(x) for x in original_argv) 114 | full_cmd = FULL_CMD_TMPL.format( 115 | source_or_conda=source_or_conda, 116 | activate_script=activate_script, 117 | env_dir=env_dir, 118 | start_cmd=start_cmd, 119 | start_args=args.start_args) 120 | spec['argv'] = ['bash', '-c', full_cmd] 121 | spec['_kernda_original_argv'] = original_argv 122 | 123 | if args.display_name: 124 | spec['display_name'] = args.display_name 125 | 126 | # Print the new kernel spec JSON to stdout for redirection 127 | print(json.dumps(spec, indent=2)) 128 | 129 | # Overwrite the original if requested 130 | if args.overwrite: 131 | with open(input_fn, 'w') as f: 132 | json.dump(spec, f, indent=2) 133 | print('Wrote to {}'.format(input_fn), file=sys.stderr) 134 | 135 | return 0 136 | 137 | 138 | def cli(argv=sys.argv[1:]): 139 | """Parse command line args and execute add_activation.""" 140 | parser = argparse.ArgumentParser(description='') 141 | parser.add_argument('kernelspec', metavar='kernel.json', 142 | help='Path to a kernel spec') 143 | parser.add_argument('--display-name', dest='display_name', type=str, 144 | help='New display name for the kernel (default: keep ' 145 | 'the original)') 146 | parser.add_argument('--overwrite', '-o', dest='overwrite', 147 | action='store_const', 148 | const=True, default=False, 149 | help='Overwrite the existing kernel spec (default: ' 150 | 'False, print to stdout') 151 | parser.add_argument("--env-dir", action="store", default=None, 152 | help="Path to the conda environment that should " 153 | "activate (default: prefix path to the " 154 | "kernel in the existing kernel spec file)") 155 | parser.add_argument("--start-args", dest="start_args", type=str, 156 | default='', 157 | help="Additional arguments to append to the kernel " 158 | "start command (default: '')") 159 | parser.add_argument("--conda-activate", type=bool, 160 | default=False, 161 | help=("Use 'conda /path/to/activate' (when True) or " 162 | "'source /path/to/activate' (when False). Defaults to " 163 | "False")) 164 | 165 | args, unknown = parser.parse_known_args(argv) 166 | return add_activation(args) 167 | 168 | 169 | if __name__ == '__main__': 170 | sys.exit(cli()) 171 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440 4 | versionfile_source = kernda/_version.py 5 | versionfile_build = kernda/_version.py 6 | tag_prefix = 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import versioneer 2 | from setuptools import setup 3 | 4 | setup_args = dict( 5 | name='kernda', 6 | version=versioneer.get_version(), 7 | cmdclass=versioneer.get_cmdclass(), 8 | description='Add conda environment activation to an IPython kernel spec', 9 | url='https://github.com/Valassis-Digital-Media/kernda', 10 | author='Valassis Digital', 11 | maintainer='Valassis Digital', 12 | maintainer_email='ParenteP@valassis.com', 13 | license='BSD 3-Clause', 14 | platforms=['Linux', 'Mac OSX'], 15 | packages=['kernda'], 16 | entry_points={ 17 | 'console_scripts': ['kernda = kernda.cli:cli'] 18 | } 19 | ) 20 | 21 | if __name__ == '__main__': 22 | setup(**setup_args) 23 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | ipykernel 3 | jupyter_console 4 | pexpect 5 | pytest 6 | -------------------------------------------------------------------------------- /test/test_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import pexpect 5 | import pytest 6 | import shutil 7 | import sys 8 | from collections import namedtuple 9 | from kernda.cli import cli, determine_conda_activate_script 10 | from tempfile import gettempdir 11 | from uuid import uuid4 12 | 13 | Kernel = namedtuple('Kernel', ['name', 'spec', 'env']) 14 | 15 | @pytest.fixture(scope='function') 16 | def kernel(): 17 | """Create a ipykernel conda environment separate from this test 18 | environment where jupyter console is installed. 19 | 20 | The environments must be separate otherwise we cannot easily check 21 | if kernel start is activating the environment or if it was already 22 | active when the test suite started. 23 | """ 24 | # unique name for the kernel and environment 25 | name = str(uuid4()) 26 | env_path = '{}/kernel-env-{name}'.format(gettempdir(), name=name) 27 | stdout = subprocess.check_output( 28 | ["conda", "create", "--yes", "--quiet", "--prefix", env_path, "python=3.6", "ipykernel"]) 29 | 30 | stdout = pexpect.run('/bin/bash -c "source activate {env_path} && \ 31 | python -m ipykernel install --user \ 32 | --name {name}"'.format(env_path=env_path, name=name)) 33 | # query jupyter for the user data directory in a separate command to 34 | # make parsing easier 35 | stdout = pexpect.run('jupyter --data-dir') 36 | user_path = stdout.decode('utf-8').strip() 37 | # the kernel spec resides in the jupyter user data path 38 | spec_path = os.path.join(user_path, 'kernels', name) 39 | yield Kernel(name, os.path.join(spec_path, 'kernel.json'), env_path) 40 | shutil.rmtree(env_path) 41 | 42 | 43 | def kernel_conda(kernel): 44 | """Run which conda in the test fixture kernel using Jupyter console.""" 45 | jupyter = pexpect.spawn('jupyter', [ 46 | 'console', 47 | '--simple-prompt', 48 | '--kernel', kernel.name] 49 | ) 50 | jupyter.expect('In.*:') 51 | jupyter.sendline('json_data = !conda info --json') 52 | # input echo 53 | jupyter.readline() 54 | out = jupyter.readline() 55 | 56 | jupyter.expect('In.*:') 57 | jupyter.sendline('import json; json_data = json.loads("".join(json_data))') 58 | # input echo 59 | jupyter.readline() 60 | out = jupyter.readline() 61 | 62 | jupyter.expect('In.*:') 63 | jupyter.sendline('json_data["active_prefix"]') 64 | # input echo 65 | jupyter.readline()# path output 66 | path = jupyter.readline() 67 | 68 | jupyter.close() 69 | return path.decode('utf-8') 70 | 71 | 72 | def test_can_retrieve_activate_script(): 73 | # test with no actual environment given. This should retrieve the base environment's conda-activate by calling 74 | # conda or using the environment variables 75 | activate_script = determine_conda_activate_script('.') 76 | assert '/bin/activate' in activate_script 77 | assert os.path.exists(activate_script) 78 | 79 | # test with an actual environment 80 | activate_script = determine_conda_activate_script(sys.prefix) 81 | assert '/bin/activate' in activate_script 82 | assert os.path.exists(activate_script) 83 | 84 | 85 | def test_overwritten_spec(kernel): 86 | """The kernel should output a conda path in its own environment.""" 87 | rv = cli(['-o', kernel.spec]) 88 | assert rv == 0 89 | assert kernel.env in kernel_conda(kernel) 90 | 91 | 92 | def test_start_args(kernel): 93 | """The kernel spec should include additional arguments.""" 94 | rv = cli(['-o', kernel.spec, '--start-args=--Completer.use_jedi=False']) 95 | assert rv == 0 96 | assert kernel.env in kernel_conda(kernel) 97 | 98 | 99 | def test_idempotent(kernel): 100 | """Running kernda twice on the same kernel.json should be idempotent""" 101 | with open(kernel.spec, 'r') as fo: 102 | original_spec_json = json.load(fo) 103 | rv = cli(['-o', kernel.spec]) 104 | assert rv == 0 105 | with open(kernel.spec, 'r') as fo: 106 | once_spec_json = json.load(fo) 107 | rv = cli(['-o', kernel.spec]) 108 | assert rv == 0 109 | with open(kernel.spec, 'r') as fo: 110 | twice_spec_json = json.load(fo) 111 | 112 | assert once_spec_json['_kernda_original_argv'] == original_spec_json['argv'] 113 | assert twice_spec_json['_kernda_original_argv'] == original_spec_json['argv'] 114 | assert once_spec_json['argv'] == twice_spec_json['argv'] 115 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.17 3 | 4 | """The Versioneer - like a rocketeer, but for versions. 5 | 6 | The Versioneer 7 | ============== 8 | 9 | * like a rocketeer, but for versions! 10 | * https://github.com/warner/python-versioneer 11 | * Brian Warner 12 | * License: Public Domain 13 | * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, and pypy 14 | * [![Latest Version] 15 | (https://pypip.in/version/versioneer/badge.svg?style=flat) 16 | ](https://pypi.python.org/pypi/versioneer/) 17 | * [![Build Status] 18 | (https://travis-ci.org/warner/python-versioneer.png?branch=master) 19 | ](https://travis-ci.org/warner/python-versioneer) 20 | 21 | This is a tool for managing a recorded version number in distutils-based 22 | python projects. The goal is to remove the tedious and error-prone "update 23 | the embedded version string" step from your release process. Making a new 24 | release should be as easy as recording a new tag in your version-control 25 | system, and maybe making new tarballs. 26 | 27 | 28 | ## Quick Install 29 | 30 | * `pip install versioneer` to somewhere to your $PATH 31 | * add a `[versioneer]` section to your setup.cfg (see below) 32 | * run `versioneer install` in your source tree, commit the results 33 | 34 | ## Version Identifiers 35 | 36 | Source trees come from a variety of places: 37 | 38 | * a version-control system checkout (mostly used by developers) 39 | * a nightly tarball, produced by build automation 40 | * a snapshot tarball, produced by a web-based VCS browser, like github's 41 | "tarball from tag" feature 42 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 43 | 44 | Within each source tree, the version identifier (either a string or a number, 45 | this tool is format-agnostic) can come from a variety of places: 46 | 47 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 48 | about recent "tags" and an absolute revision-id 49 | * the name of the directory into which the tarball was unpacked 50 | * an expanded VCS keyword ($Id$, etc) 51 | * a `_version.py` created by some earlier build step 52 | 53 | For released software, the version identifier is closely related to a VCS 54 | tag. Some projects use tag names that include more than just the version 55 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 56 | needs to strip the tag prefix to extract the version identifier. For 57 | unreleased software (between tags), the version identifier should provide 58 | enough information to help developers recreate the same tree, while also 59 | giving them an idea of roughly how old the tree is (after version 1.2, before 60 | version 1.3). Many VCS systems can report a description that captures this, 61 | for example `git describe --tags --dirty --always` reports things like 62 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 63 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 64 | uncommitted changes. 65 | 66 | The version identifier is used for multiple purposes: 67 | 68 | * to allow the module to self-identify its version: `myproject.__version__` 69 | * to choose a name and prefix for a 'setup.py sdist' tarball 70 | 71 | ## Theory of Operation 72 | 73 | Versioneer works by adding a special `_version.py` file into your source 74 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 75 | dynamically ask the VCS tool for version information at import time. 76 | 77 | `_version.py` also contains `$Revision$` markers, and the installation 78 | process marks `_version.py` to have this marker rewritten with a tag name 79 | during the `git archive` command. As a result, generated tarballs will 80 | contain enough information to get the proper version. 81 | 82 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 83 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 84 | that configures it. This overrides several distutils/setuptools commands to 85 | compute the version when invoked, and changes `setup.py build` and `setup.py 86 | sdist` to replace `_version.py` with a small static file that contains just 87 | the generated version data. 88 | 89 | ## Installation 90 | 91 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 92 | 93 | ## Version-String Flavors 94 | 95 | Code which uses Versioneer can learn about its version string at runtime by 96 | importing `_version` from your main `__init__.py` file and running the 97 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 98 | import the top-level `versioneer.py` and run `get_versions()`. 99 | 100 | Both functions return a dictionary with different flavors of version 101 | information: 102 | 103 | * `['version']`: A condensed version string, rendered using the selected 104 | style. This is the most commonly used value for the project's version 105 | string. The default "pep440" style yields strings like `0.11`, 106 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 107 | below for alternative styles. 108 | 109 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 110 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 111 | 112 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 113 | commit date in ISO 8601 format. This will be None if the date is not 114 | available. 115 | 116 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 117 | this is only accurate if run in a VCS checkout, otherwise it is likely to 118 | be False or None 119 | 120 | * `['error']`: if the version string could not be computed, this will be set 121 | to a string describing the problem, otherwise it will be None. It may be 122 | useful to throw an exception in setup.py if this is set, to avoid e.g. 123 | creating tarballs with a version string of "unknown". 124 | 125 | Some variants are more useful than others. Including `full-revisionid` in a 126 | bug report should allow developers to reconstruct the exact code being tested 127 | (or indicate the presence of local changes that should be shared with the 128 | developers). `version` is suitable for display in an "about" box or a CLI 129 | `--version` output: it can be easily compared against release notes and lists 130 | of bugs fixed in various releases. 131 | 132 | The installer adds the following text to your `__init__.py` to place a basic 133 | version in `YOURPROJECT.__version__`: 134 | 135 | from ._version import get_versions 136 | __version__ = get_versions()['version'] 137 | del get_versions 138 | 139 | ## Styles 140 | 141 | The setup.cfg `style=` configuration controls how the VCS information is 142 | rendered into a version string. 143 | 144 | The default style, "pep440", produces a PEP440-compliant string, equal to the 145 | un-prefixed tag name for actual releases, and containing an additional "local 146 | version" section with more detail for in-between builds. For Git, this is 147 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 148 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 149 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 150 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 151 | software (exactly equal to a known tag), the identifier will only contain the 152 | stripped tag, e.g. "0.11". 153 | 154 | Other styles are available. See details.md in the Versioneer source tree for 155 | descriptions. 156 | 157 | ## Debugging 158 | 159 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 160 | to return a version of "0+unknown". To investigate the problem, run `setup.py 161 | version`, which will run the version-lookup code in a verbose mode, and will 162 | display the full contents of `get_versions()` (including the `error` string, 163 | which may help identify what went wrong). 164 | 165 | ## Known Limitations 166 | 167 | Some situations are known to cause problems for Versioneer. This details the 168 | most significant ones. More can be found on Github 169 | [issues page](https://github.com/warner/python-versioneer/issues). 170 | 171 | ### Subprojects 172 | 173 | Versioneer has limited support for source trees in which `setup.py` is not in 174 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 175 | two common reasons why `setup.py` might not be in the root: 176 | 177 | * Source trees which contain multiple subprojects, such as 178 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 179 | "master" and "slave" subprojects, each with their own `setup.py`, 180 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 181 | distributions (and upload multiple independently-installable tarballs). 182 | * Source trees whose main purpose is to contain a C library, but which also 183 | provide bindings to Python (and perhaps other langauges) in subdirectories. 184 | 185 | Versioneer will look for `.git` in parent directories, and most operations 186 | should get the right version string. However `pip` and `setuptools` have bugs 187 | and implementation details which frequently cause `pip install .` from a 188 | subproject directory to fail to find a correct version string (so it usually 189 | defaults to `0+unknown`). 190 | 191 | `pip install --editable .` should work correctly. `setup.py install` might 192 | work too. 193 | 194 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 195 | some later version. 196 | 197 | [Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking 198 | this issue. The discussion in 199 | [PR #61](https://github.com/warner/python-versioneer/pull/61) describes the 200 | issue from the Versioneer side in more detail. 201 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 202 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 203 | pip to let Versioneer work correctly. 204 | 205 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 206 | `setup.cfg`, so subprojects were completely unsupported with those releases. 207 | 208 | ### Editable installs with setuptools <= 18.5 209 | 210 | `setup.py develop` and `pip install --editable .` allow you to install a 211 | project into a virtualenv once, then continue editing the source code (and 212 | test) without re-installing after every change. 213 | 214 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 215 | convenient way to specify executable scripts that should be installed along 216 | with the python package. 217 | 218 | These both work as expected when using modern setuptools. When using 219 | setuptools-18.5 or earlier, however, certain operations will cause 220 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 221 | script, which must be resolved by re-installing the package. This happens 222 | when the install happens with one version, then the egg_info data is 223 | regenerated while a different version is checked out. Many setup.py commands 224 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 225 | a different virtualenv), so this can be surprising. 226 | 227 | [Bug #83](https://github.com/warner/python-versioneer/issues/83) describes 228 | this one, but upgrading to a newer version of setuptools should probably 229 | resolve it. 230 | 231 | ### Unicode version strings 232 | 233 | While Versioneer works (and is continually tested) with both Python 2 and 234 | Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. 235 | Newer releases probably generate unicode version strings on py2. It's not 236 | clear that this is wrong, but it may be surprising for applications when then 237 | write these strings to a network connection or include them in bytes-oriented 238 | APIs like cryptographic checksums. 239 | 240 | [Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates 241 | this question. 242 | 243 | 244 | ## Updating Versioneer 245 | 246 | To upgrade your project to a new release of Versioneer, do the following: 247 | 248 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 249 | * edit `setup.cfg`, if necessary, to include any new configuration settings 250 | indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. 251 | * re-run `versioneer install` in your source tree, to replace 252 | `SRC/_version.py` 253 | * commit any changed files 254 | 255 | ## Future Directions 256 | 257 | This tool is designed to make it easily extended to other version-control 258 | systems: all VCS-specific components are in separate directories like 259 | src/git/ . The top-level `versioneer.py` script is assembled from these 260 | components by running make-versioneer.py . In the future, make-versioneer.py 261 | will take a VCS name as an argument, and will construct a version of 262 | `versioneer.py` that is specific to the given VCS. It might also take the 263 | configuration arguments that are currently provided manually during 264 | installation by editing setup.py . Alternatively, it might go the other 265 | direction and include code from all supported VCS systems, reducing the 266 | number of intermediate scripts. 267 | 268 | 269 | ## License 270 | 271 | To make Versioneer easier to embed, all its code is dedicated to the public 272 | domain. The `_version.py` that it creates is also in the public domain. 273 | Specifically, both are released under the Creative Commons "Public Domain 274 | Dedication" license (CC0-1.0), as described in 275 | https://creativecommons.org/publicdomain/zero/1.0/ . 276 | 277 | """ 278 | 279 | from __future__ import print_function 280 | try: 281 | import configparser 282 | except ImportError: 283 | import ConfigParser as configparser 284 | import errno 285 | import json 286 | import os 287 | import re 288 | import subprocess 289 | import sys 290 | 291 | 292 | class VersioneerConfig: 293 | """Container for Versioneer configuration parameters.""" 294 | 295 | 296 | def get_root(): 297 | """Get the project root directory. 298 | 299 | We require that all commands are run from the project root, i.e. the 300 | directory that contains setup.py, setup.cfg, and versioneer.py . 301 | """ 302 | root = os.path.realpath(os.path.abspath(os.getcwd())) 303 | setup_py = os.path.join(root, "setup.py") 304 | versioneer_py = os.path.join(root, "versioneer.py") 305 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 306 | # allow 'python path/to/setup.py COMMAND' 307 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 308 | setup_py = os.path.join(root, "setup.py") 309 | versioneer_py = os.path.join(root, "versioneer.py") 310 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 311 | err = ("Versioneer was unable to run the project root directory. " 312 | "Versioneer requires setup.py to be executed from " 313 | "its immediate directory (like 'python setup.py COMMAND'), " 314 | "or in a way that lets it use sys.argv[0] to find the root " 315 | "(like 'python path/to/setup.py COMMAND').") 316 | raise VersioneerBadRootError(err) 317 | try: 318 | # Certain runtime workflows (setup.py install/develop in a setuptools 319 | # tree) execute all dependencies in a single python process, so 320 | # "versioneer" may be imported multiple times, and python's shared 321 | # module-import table will cache the first one. So we can't use 322 | # os.path.dirname(__file__), as that will find whichever 323 | # versioneer.py was first imported, even in later projects. 324 | me = os.path.realpath(os.path.abspath(__file__)) 325 | me_dir = os.path.normcase(os.path.splitext(me)[0]) 326 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 327 | if me_dir != vsr_dir: 328 | print("Warning: build in %s is using versioneer.py from %s" 329 | % (os.path.dirname(me), versioneer_py)) 330 | except NameError: 331 | pass 332 | return root 333 | 334 | 335 | def get_config_from_root(root): 336 | """Read the project setup.cfg file to determine Versioneer config.""" 337 | # This might raise EnvironmentError (if setup.cfg is missing), or 338 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 339 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 340 | # the top of versioneer.py for instructions on writing your setup.cfg . 341 | setup_cfg = os.path.join(root, "setup.cfg") 342 | parser = configparser.SafeConfigParser() 343 | with open(setup_cfg, "r") as f: 344 | parser.readfp(f) 345 | VCS = parser.get("versioneer", "VCS") # mandatory 346 | 347 | def get(parser, name): 348 | if parser.has_option("versioneer", name): 349 | return parser.get("versioneer", name) 350 | return None 351 | cfg = VersioneerConfig() 352 | cfg.VCS = VCS 353 | cfg.style = get(parser, "style") or "" 354 | cfg.versionfile_source = get(parser, "versionfile_source") 355 | cfg.versionfile_build = get(parser, "versionfile_build") 356 | cfg.tag_prefix = get(parser, "tag_prefix") 357 | if cfg.tag_prefix in ("''", '""'): 358 | cfg.tag_prefix = "" 359 | cfg.parentdir_prefix = get(parser, "parentdir_prefix") 360 | cfg.verbose = get(parser, "verbose") 361 | return cfg 362 | 363 | 364 | class NotThisMethod(Exception): 365 | """Exception raised if a method is not valid for the current scenario.""" 366 | 367 | # these dictionaries contain VCS-specific tools 368 | LONG_VERSION_PY = {} 369 | HANDLERS = {} 370 | 371 | 372 | def register_vcs_handler(vcs, method): # decorator 373 | """Decorator to mark a method as the handler for a particular VCS.""" 374 | def decorate(f): 375 | """Store f in HANDLERS[vcs][method].""" 376 | if vcs not in HANDLERS: 377 | HANDLERS[vcs] = {} 378 | HANDLERS[vcs][method] = f 379 | return f 380 | return decorate 381 | 382 | 383 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 384 | env=None): 385 | """Call the given command(s).""" 386 | assert isinstance(commands, list) 387 | p = None 388 | for c in commands: 389 | try: 390 | dispcmd = str([c] + args) 391 | # remember shell=False, so use git.cmd on windows, not just git 392 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 393 | stdout=subprocess.PIPE, 394 | stderr=(subprocess.PIPE if hide_stderr 395 | else None)) 396 | break 397 | except EnvironmentError: 398 | e = sys.exc_info()[1] 399 | if e.errno == errno.ENOENT: 400 | continue 401 | if verbose: 402 | print("unable to run %s" % dispcmd) 403 | print(e) 404 | return None, None 405 | else: 406 | if verbose: 407 | print("unable to find command, tried %s" % (commands,)) 408 | return None, None 409 | stdout = p.communicate()[0].strip() 410 | if sys.version_info[0] >= 3: 411 | stdout = stdout.decode() 412 | if p.returncode != 0: 413 | if verbose: 414 | print("unable to run %s (error)" % dispcmd) 415 | print("stdout was %s" % stdout) 416 | return None, p.returncode 417 | return stdout, p.returncode 418 | LONG_VERSION_PY['git'] = ''' 419 | # This file helps to compute a version number in source trees obtained from 420 | # git-archive tarball (such as those provided by githubs download-from-tag 421 | # feature). Distribution tarballs (built by setup.py sdist) and build 422 | # directories (produced by setup.py build) will contain a much shorter file 423 | # that just contains the computed version number. 424 | 425 | # This file is released into the public domain. Generated by 426 | # versioneer-0.17 (https://github.com/warner/python-versioneer) 427 | 428 | """Git implementation of _version.py.""" 429 | 430 | import errno 431 | import os 432 | import re 433 | import subprocess 434 | import sys 435 | 436 | 437 | def get_keywords(): 438 | """Get the keywords needed to look up the version information.""" 439 | # these strings will be replaced by git during git-archive. 440 | # setup.py/versioneer.py will grep for the variable names, so they must 441 | # each be defined on a line of their own. _version.py will just call 442 | # get_keywords(). 443 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 444 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 445 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 446 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 447 | return keywords 448 | 449 | 450 | class VersioneerConfig: 451 | """Container for Versioneer configuration parameters.""" 452 | 453 | 454 | def get_config(): 455 | """Create, populate and return the VersioneerConfig() object.""" 456 | # these strings are filled in when 'setup.py versioneer' creates 457 | # _version.py 458 | cfg = VersioneerConfig() 459 | cfg.VCS = "git" 460 | cfg.style = "%(STYLE)s" 461 | cfg.tag_prefix = "%(TAG_PREFIX)s" 462 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 463 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 464 | cfg.verbose = False 465 | return cfg 466 | 467 | 468 | class NotThisMethod(Exception): 469 | """Exception raised if a method is not valid for the current scenario.""" 470 | 471 | 472 | LONG_VERSION_PY = {} 473 | HANDLERS = {} 474 | 475 | 476 | def register_vcs_handler(vcs, method): # decorator 477 | """Decorator to mark a method as the handler for a particular VCS.""" 478 | def decorate(f): 479 | """Store f in HANDLERS[vcs][method].""" 480 | if vcs not in HANDLERS: 481 | HANDLERS[vcs] = {} 482 | HANDLERS[vcs][method] = f 483 | return f 484 | return decorate 485 | 486 | 487 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 488 | env=None): 489 | """Call the given command(s).""" 490 | assert isinstance(commands, list) 491 | p = None 492 | for c in commands: 493 | try: 494 | dispcmd = str([c] + args) 495 | # remember shell=False, so use git.cmd on windows, not just git 496 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 497 | stdout=subprocess.PIPE, 498 | stderr=(subprocess.PIPE if hide_stderr 499 | else None)) 500 | break 501 | except EnvironmentError: 502 | e = sys.exc_info()[1] 503 | if e.errno == errno.ENOENT: 504 | continue 505 | if verbose: 506 | print("unable to run %%s" %% dispcmd) 507 | print(e) 508 | return None, None 509 | else: 510 | if verbose: 511 | print("unable to find command, tried %%s" %% (commands,)) 512 | return None, None 513 | stdout = p.communicate()[0].strip() 514 | if sys.version_info[0] >= 3: 515 | stdout = stdout.decode() 516 | if p.returncode != 0: 517 | if verbose: 518 | print("unable to run %%s (error)" %% dispcmd) 519 | print("stdout was %%s" %% stdout) 520 | return None, p.returncode 521 | return stdout, p.returncode 522 | 523 | 524 | def versions_from_parentdir(parentdir_prefix, root, verbose): 525 | """Try to determine the version from the parent directory name. 526 | 527 | Source tarballs conventionally unpack into a directory that includes both 528 | the project name and a version string. We will also support searching up 529 | two directory levels for an appropriately named parent directory 530 | """ 531 | rootdirs = [] 532 | 533 | for i in range(3): 534 | dirname = os.path.basename(root) 535 | if dirname.startswith(parentdir_prefix): 536 | return {"version": dirname[len(parentdir_prefix):], 537 | "full-revisionid": None, 538 | "dirty": False, "error": None, "date": None} 539 | else: 540 | rootdirs.append(root) 541 | root = os.path.dirname(root) # up a level 542 | 543 | if verbose: 544 | print("Tried directories %%s but none started with prefix %%s" %% 545 | (str(rootdirs), parentdir_prefix)) 546 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 547 | 548 | 549 | @register_vcs_handler("git", "get_keywords") 550 | def git_get_keywords(versionfile_abs): 551 | """Extract version information from the given file.""" 552 | # the code embedded in _version.py can just fetch the value of these 553 | # keywords. When used from setup.py, we don't want to import _version.py, 554 | # so we do it with a regexp instead. This function is not used from 555 | # _version.py. 556 | keywords = {} 557 | try: 558 | f = open(versionfile_abs, "r") 559 | for line in f.readlines(): 560 | if line.strip().startswith("git_refnames ="): 561 | mo = re.search(r'=\s*"(.*)"', line) 562 | if mo: 563 | keywords["refnames"] = mo.group(1) 564 | if line.strip().startswith("git_full ="): 565 | mo = re.search(r'=\s*"(.*)"', line) 566 | if mo: 567 | keywords["full"] = mo.group(1) 568 | if line.strip().startswith("git_date ="): 569 | mo = re.search(r'=\s*"(.*)"', line) 570 | if mo: 571 | keywords["date"] = mo.group(1) 572 | f.close() 573 | except EnvironmentError: 574 | pass 575 | return keywords 576 | 577 | 578 | @register_vcs_handler("git", "keywords") 579 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 580 | """Get version information from git keywords.""" 581 | if not keywords: 582 | raise NotThisMethod("no keywords at all, weird") 583 | date = keywords.get("date") 584 | if date is not None: 585 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 586 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 587 | # -like" string, which we must then edit to make compliant), because 588 | # it's been around since git-1.5.3, and it's too difficult to 589 | # discover which version we're using, or to work around using an 590 | # older one. 591 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 592 | refnames = keywords["refnames"].strip() 593 | if refnames.startswith("$Format"): 594 | if verbose: 595 | print("keywords are unexpanded, not using") 596 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 597 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 598 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 599 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 600 | TAG = "tag: " 601 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 602 | if not tags: 603 | # Either we're using git < 1.8.3, or there really are no tags. We use 604 | # a heuristic: assume all version tags have a digit. The old git %%d 605 | # expansion behaves like git log --decorate=short and strips out the 606 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 607 | # between branches and tags. By ignoring refnames without digits, we 608 | # filter out many common branch names like "release" and 609 | # "stabilization", as well as "HEAD" and "master". 610 | tags = set([r for r in refs if re.search(r'\d', r)]) 611 | if verbose: 612 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 613 | if verbose: 614 | print("likely tags: %%s" %% ",".join(sorted(tags))) 615 | for ref in sorted(tags): 616 | # sorting will prefer e.g. "2.0" over "2.0rc1" 617 | if ref.startswith(tag_prefix): 618 | r = ref[len(tag_prefix):] 619 | if verbose: 620 | print("picking %%s" %% r) 621 | return {"version": r, 622 | "full-revisionid": keywords["full"].strip(), 623 | "dirty": False, "error": None, 624 | "date": date} 625 | # no suitable tags, so version is "0+unknown", but full hex is still there 626 | if verbose: 627 | print("no suitable tags, using unknown + full revision id") 628 | return {"version": "0+unknown", 629 | "full-revisionid": keywords["full"].strip(), 630 | "dirty": False, "error": "no suitable tags", "date": None} 631 | 632 | 633 | @register_vcs_handler("git", "pieces_from_vcs") 634 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 635 | """Get version from 'git describe' in the root of the source tree. 636 | 637 | This only gets called if the git-archive 'subst' keywords were *not* 638 | expanded, and _version.py hasn't already been rewritten with a short 639 | version string, meaning we're inside a checked out source tree. 640 | """ 641 | GITS = ["git"] 642 | if sys.platform == "win32": 643 | GITS = ["git.cmd", "git.exe"] 644 | 645 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 646 | hide_stderr=True) 647 | if rc != 0: 648 | if verbose: 649 | print("Directory %%s not under git control" %% root) 650 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 651 | 652 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 653 | # if there isn't one, this yields HEX[-dirty] (no NUM) 654 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 655 | "--always", "--long", 656 | "--match", "%%s*" %% tag_prefix], 657 | cwd=root) 658 | # --long was added in git-1.5.5 659 | if describe_out is None: 660 | raise NotThisMethod("'git describe' failed") 661 | describe_out = describe_out.strip() 662 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 663 | if full_out is None: 664 | raise NotThisMethod("'git rev-parse' failed") 665 | full_out = full_out.strip() 666 | 667 | pieces = {} 668 | pieces["long"] = full_out 669 | pieces["short"] = full_out[:7] # maybe improved later 670 | pieces["error"] = None 671 | 672 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 673 | # TAG might have hyphens. 674 | git_describe = describe_out 675 | 676 | # look for -dirty suffix 677 | dirty = git_describe.endswith("-dirty") 678 | pieces["dirty"] = dirty 679 | if dirty: 680 | git_describe = git_describe[:git_describe.rindex("-dirty")] 681 | 682 | # now we have TAG-NUM-gHEX or HEX 683 | 684 | if "-" in git_describe: 685 | # TAG-NUM-gHEX 686 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 687 | if not mo: 688 | # unparseable. Maybe git-describe is misbehaving? 689 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 690 | %% describe_out) 691 | return pieces 692 | 693 | # tag 694 | full_tag = mo.group(1) 695 | if not full_tag.startswith(tag_prefix): 696 | if verbose: 697 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 698 | print(fmt %% (full_tag, tag_prefix)) 699 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 700 | %% (full_tag, tag_prefix)) 701 | return pieces 702 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 703 | 704 | # distance: number of commits since tag 705 | pieces["distance"] = int(mo.group(2)) 706 | 707 | # commit: short hex revision ID 708 | pieces["short"] = mo.group(3) 709 | 710 | else: 711 | # HEX: no tags 712 | pieces["closest-tag"] = None 713 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 714 | cwd=root) 715 | pieces["distance"] = int(count_out) # total number of commits 716 | 717 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 718 | date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], 719 | cwd=root)[0].strip() 720 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 721 | 722 | return pieces 723 | 724 | 725 | def plus_or_dot(pieces): 726 | """Return a + if we don't already have one, else return a .""" 727 | if "+" in pieces.get("closest-tag", ""): 728 | return "." 729 | return "+" 730 | 731 | 732 | def render_pep440(pieces): 733 | """Build up version string, with post-release "local version identifier". 734 | 735 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 736 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 737 | 738 | Exceptions: 739 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 740 | """ 741 | if pieces["closest-tag"]: 742 | rendered = pieces["closest-tag"] 743 | if pieces["distance"] or pieces["dirty"]: 744 | rendered += plus_or_dot(pieces) 745 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 746 | if pieces["dirty"]: 747 | rendered += ".dirty" 748 | else: 749 | # exception #1 750 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 751 | pieces["short"]) 752 | if pieces["dirty"]: 753 | rendered += ".dirty" 754 | return rendered 755 | 756 | 757 | def render_pep440_pre(pieces): 758 | """TAG[.post.devDISTANCE] -- No -dirty. 759 | 760 | Exceptions: 761 | 1: no tags. 0.post.devDISTANCE 762 | """ 763 | if pieces["closest-tag"]: 764 | rendered = pieces["closest-tag"] 765 | if pieces["distance"]: 766 | rendered += ".post.dev%%d" %% pieces["distance"] 767 | else: 768 | # exception #1 769 | rendered = "0.post.dev%%d" %% pieces["distance"] 770 | return rendered 771 | 772 | 773 | def render_pep440_post(pieces): 774 | """TAG[.postDISTANCE[.dev0]+gHEX] . 775 | 776 | The ".dev0" means dirty. Note that .dev0 sorts backwards 777 | (a dirty tree will appear "older" than the corresponding clean one), 778 | but you shouldn't be releasing software with -dirty anyways. 779 | 780 | Exceptions: 781 | 1: no tags. 0.postDISTANCE[.dev0] 782 | """ 783 | if pieces["closest-tag"]: 784 | rendered = pieces["closest-tag"] 785 | if pieces["distance"] or pieces["dirty"]: 786 | rendered += ".post%%d" %% pieces["distance"] 787 | if pieces["dirty"]: 788 | rendered += ".dev0" 789 | rendered += plus_or_dot(pieces) 790 | rendered += "g%%s" %% pieces["short"] 791 | else: 792 | # exception #1 793 | rendered = "0.post%%d" %% pieces["distance"] 794 | if pieces["dirty"]: 795 | rendered += ".dev0" 796 | rendered += "+g%%s" %% pieces["short"] 797 | return rendered 798 | 799 | 800 | def render_pep440_old(pieces): 801 | """TAG[.postDISTANCE[.dev0]] . 802 | 803 | The ".dev0" means dirty. 804 | 805 | Eexceptions: 806 | 1: no tags. 0.postDISTANCE[.dev0] 807 | """ 808 | if pieces["closest-tag"]: 809 | rendered = pieces["closest-tag"] 810 | if pieces["distance"] or pieces["dirty"]: 811 | rendered += ".post%%d" %% pieces["distance"] 812 | if pieces["dirty"]: 813 | rendered += ".dev0" 814 | else: 815 | # exception #1 816 | rendered = "0.post%%d" %% pieces["distance"] 817 | if pieces["dirty"]: 818 | rendered += ".dev0" 819 | return rendered 820 | 821 | 822 | def render_git_describe(pieces): 823 | """TAG[-DISTANCE-gHEX][-dirty]. 824 | 825 | Like 'git describe --tags --dirty --always'. 826 | 827 | Exceptions: 828 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 829 | """ 830 | if pieces["closest-tag"]: 831 | rendered = pieces["closest-tag"] 832 | if pieces["distance"]: 833 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 834 | else: 835 | # exception #1 836 | rendered = pieces["short"] 837 | if pieces["dirty"]: 838 | rendered += "-dirty" 839 | return rendered 840 | 841 | 842 | def render_git_describe_long(pieces): 843 | """TAG-DISTANCE-gHEX[-dirty]. 844 | 845 | Like 'git describe --tags --dirty --always -long'. 846 | The distance/hash is unconditional. 847 | 848 | Exceptions: 849 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 850 | """ 851 | if pieces["closest-tag"]: 852 | rendered = pieces["closest-tag"] 853 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 854 | else: 855 | # exception #1 856 | rendered = pieces["short"] 857 | if pieces["dirty"]: 858 | rendered += "-dirty" 859 | return rendered 860 | 861 | 862 | def render(pieces, style): 863 | """Render the given version pieces into the requested style.""" 864 | if pieces["error"]: 865 | return {"version": "unknown", 866 | "full-revisionid": pieces.get("long"), 867 | "dirty": None, 868 | "error": pieces["error"], 869 | "date": None} 870 | 871 | if not style or style == "default": 872 | style = "pep440" # the default 873 | 874 | if style == "pep440": 875 | rendered = render_pep440(pieces) 876 | elif style == "pep440-pre": 877 | rendered = render_pep440_pre(pieces) 878 | elif style == "pep440-post": 879 | rendered = render_pep440_post(pieces) 880 | elif style == "pep440-old": 881 | rendered = render_pep440_old(pieces) 882 | elif style == "git-describe": 883 | rendered = render_git_describe(pieces) 884 | elif style == "git-describe-long": 885 | rendered = render_git_describe_long(pieces) 886 | else: 887 | raise ValueError("unknown style '%%s'" %% style) 888 | 889 | return {"version": rendered, "full-revisionid": pieces["long"], 890 | "dirty": pieces["dirty"], "error": None, 891 | "date": pieces.get("date")} 892 | 893 | 894 | def get_versions(): 895 | """Get version information or return default if unable to do so.""" 896 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 897 | # __file__, we can work backwards from there to the root. Some 898 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 899 | # case we can only use expanded keywords. 900 | 901 | cfg = get_config() 902 | verbose = cfg.verbose 903 | 904 | try: 905 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 906 | verbose) 907 | except NotThisMethod: 908 | pass 909 | 910 | try: 911 | root = os.path.realpath(__file__) 912 | # versionfile_source is the relative path from the top of the source 913 | # tree (where the .git directory might live) to this file. Invert 914 | # this to find the root from __file__. 915 | for i in cfg.versionfile_source.split('/'): 916 | root = os.path.dirname(root) 917 | except NameError: 918 | return {"version": "0+unknown", "full-revisionid": None, 919 | "dirty": None, 920 | "error": "unable to find root of source tree", 921 | "date": None} 922 | 923 | try: 924 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 925 | return render(pieces, cfg.style) 926 | except NotThisMethod: 927 | pass 928 | 929 | try: 930 | if cfg.parentdir_prefix: 931 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 932 | except NotThisMethod: 933 | pass 934 | 935 | return {"version": "0+unknown", "full-revisionid": None, 936 | "dirty": None, 937 | "error": "unable to compute version", "date": None} 938 | ''' 939 | 940 | 941 | @register_vcs_handler("git", "get_keywords") 942 | def git_get_keywords(versionfile_abs): 943 | """Extract version information from the given file.""" 944 | # the code embedded in _version.py can just fetch the value of these 945 | # keywords. When used from setup.py, we don't want to import _version.py, 946 | # so we do it with a regexp instead. This function is not used from 947 | # _version.py. 948 | keywords = {} 949 | try: 950 | f = open(versionfile_abs, "r") 951 | for line in f.readlines(): 952 | if line.strip().startswith("git_refnames ="): 953 | mo = re.search(r'=\s*"(.*)"', line) 954 | if mo: 955 | keywords["refnames"] = mo.group(1) 956 | if line.strip().startswith("git_full ="): 957 | mo = re.search(r'=\s*"(.*)"', line) 958 | if mo: 959 | keywords["full"] = mo.group(1) 960 | if line.strip().startswith("git_date ="): 961 | mo = re.search(r'=\s*"(.*)"', line) 962 | if mo: 963 | keywords["date"] = mo.group(1) 964 | f.close() 965 | except EnvironmentError: 966 | pass 967 | return keywords 968 | 969 | 970 | @register_vcs_handler("git", "keywords") 971 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 972 | """Get version information from git keywords.""" 973 | if not keywords: 974 | raise NotThisMethod("no keywords at all, weird") 975 | date = keywords.get("date") 976 | if date is not None: 977 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 978 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 979 | # -like" string, which we must then edit to make compliant), because 980 | # it's been around since git-1.5.3, and it's too difficult to 981 | # discover which version we're using, or to work around using an 982 | # older one. 983 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 984 | refnames = keywords["refnames"].strip() 985 | if refnames.startswith("$Format"): 986 | if verbose: 987 | print("keywords are unexpanded, not using") 988 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 989 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 990 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 991 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 992 | TAG = "tag: " 993 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 994 | if not tags: 995 | # Either we're using git < 1.8.3, or there really are no tags. We use 996 | # a heuristic: assume all version tags have a digit. The old git %d 997 | # expansion behaves like git log --decorate=short and strips out the 998 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 999 | # between branches and tags. By ignoring refnames without digits, we 1000 | # filter out many common branch names like "release" and 1001 | # "stabilization", as well as "HEAD" and "master". 1002 | tags = set([r for r in refs if re.search(r'\d', r)]) 1003 | if verbose: 1004 | print("discarding '%s', no digits" % ",".join(refs - tags)) 1005 | if verbose: 1006 | print("likely tags: %s" % ",".join(sorted(tags))) 1007 | for ref in sorted(tags): 1008 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1009 | if ref.startswith(tag_prefix): 1010 | r = ref[len(tag_prefix):] 1011 | if verbose: 1012 | print("picking %s" % r) 1013 | return {"version": r, 1014 | "full-revisionid": keywords["full"].strip(), 1015 | "dirty": False, "error": None, 1016 | "date": date} 1017 | # no suitable tags, so version is "0+unknown", but full hex is still there 1018 | if verbose: 1019 | print("no suitable tags, using unknown + full revision id") 1020 | return {"version": "0+unknown", 1021 | "full-revisionid": keywords["full"].strip(), 1022 | "dirty": False, "error": "no suitable tags", "date": None} 1023 | 1024 | 1025 | @register_vcs_handler("git", "pieces_from_vcs") 1026 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1027 | """Get version from 'git describe' in the root of the source tree. 1028 | 1029 | This only gets called if the git-archive 'subst' keywords were *not* 1030 | expanded, and _version.py hasn't already been rewritten with a short 1031 | version string, meaning we're inside a checked out source tree. 1032 | """ 1033 | GITS = ["git"] 1034 | if sys.platform == "win32": 1035 | GITS = ["git.cmd", "git.exe"] 1036 | 1037 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 1038 | hide_stderr=True) 1039 | if rc != 0: 1040 | if verbose: 1041 | print("Directory %s not under git control" % root) 1042 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 1043 | 1044 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1045 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1046 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 1047 | "--always", "--long", 1048 | "--match", "%s*" % tag_prefix], 1049 | cwd=root) 1050 | # --long was added in git-1.5.5 1051 | if describe_out is None: 1052 | raise NotThisMethod("'git describe' failed") 1053 | describe_out = describe_out.strip() 1054 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 1055 | if full_out is None: 1056 | raise NotThisMethod("'git rev-parse' failed") 1057 | full_out = full_out.strip() 1058 | 1059 | pieces = {} 1060 | pieces["long"] = full_out 1061 | pieces["short"] = full_out[:7] # maybe improved later 1062 | pieces["error"] = None 1063 | 1064 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1065 | # TAG might have hyphens. 1066 | git_describe = describe_out 1067 | 1068 | # look for -dirty suffix 1069 | dirty = git_describe.endswith("-dirty") 1070 | pieces["dirty"] = dirty 1071 | if dirty: 1072 | git_describe = git_describe[:git_describe.rindex("-dirty")] 1073 | 1074 | # now we have TAG-NUM-gHEX or HEX 1075 | 1076 | if "-" in git_describe: 1077 | # TAG-NUM-gHEX 1078 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1079 | if not mo: 1080 | # unparseable. Maybe git-describe is misbehaving? 1081 | pieces["error"] = ("unable to parse git-describe output: '%s'" 1082 | % describe_out) 1083 | return pieces 1084 | 1085 | # tag 1086 | full_tag = mo.group(1) 1087 | if not full_tag.startswith(tag_prefix): 1088 | if verbose: 1089 | fmt = "tag '%s' doesn't start with prefix '%s'" 1090 | print(fmt % (full_tag, tag_prefix)) 1091 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1092 | % (full_tag, tag_prefix)) 1093 | return pieces 1094 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 1095 | 1096 | # distance: number of commits since tag 1097 | pieces["distance"] = int(mo.group(2)) 1098 | 1099 | # commit: short hex revision ID 1100 | pieces["short"] = mo.group(3) 1101 | 1102 | else: 1103 | # HEX: no tags 1104 | pieces["closest-tag"] = None 1105 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 1106 | cwd=root) 1107 | pieces["distance"] = int(count_out) # total number of commits 1108 | 1109 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 1110 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 1111 | cwd=root)[0].strip() 1112 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1113 | 1114 | return pieces 1115 | 1116 | 1117 | def do_vcs_install(manifest_in, versionfile_source, ipy): 1118 | """Git-specific installation logic for Versioneer. 1119 | 1120 | For Git, this means creating/changing .gitattributes to mark _version.py 1121 | for export-subst keyword substitution. 1122 | """ 1123 | GITS = ["git"] 1124 | if sys.platform == "win32": 1125 | GITS = ["git.cmd", "git.exe"] 1126 | files = [manifest_in, versionfile_source] 1127 | if ipy: 1128 | files.append(ipy) 1129 | try: 1130 | me = __file__ 1131 | if me.endswith(".pyc") or me.endswith(".pyo"): 1132 | me = os.path.splitext(me)[0] + ".py" 1133 | versioneer_file = os.path.relpath(me) 1134 | except NameError: 1135 | versioneer_file = "versioneer.py" 1136 | files.append(versioneer_file) 1137 | present = False 1138 | try: 1139 | f = open(".gitattributes", "r") 1140 | for line in f.readlines(): 1141 | if line.strip().startswith(versionfile_source): 1142 | if "export-subst" in line.strip().split()[1:]: 1143 | present = True 1144 | f.close() 1145 | except EnvironmentError: 1146 | pass 1147 | if not present: 1148 | f = open(".gitattributes", "a+") 1149 | f.write("%s export-subst\n" % versionfile_source) 1150 | f.close() 1151 | files.append(".gitattributes") 1152 | run_command(GITS, ["add", "--"] + files) 1153 | 1154 | 1155 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1156 | """Try to determine the version from the parent directory name. 1157 | 1158 | Source tarballs conventionally unpack into a directory that includes both 1159 | the project name and a version string. We will also support searching up 1160 | two directory levels for an appropriately named parent directory 1161 | """ 1162 | rootdirs = [] 1163 | 1164 | for i in range(3): 1165 | dirname = os.path.basename(root) 1166 | if dirname.startswith(parentdir_prefix): 1167 | return {"version": dirname[len(parentdir_prefix):], 1168 | "full-revisionid": None, 1169 | "dirty": False, "error": None, "date": None} 1170 | else: 1171 | rootdirs.append(root) 1172 | root = os.path.dirname(root) # up a level 1173 | 1174 | if verbose: 1175 | print("Tried directories %s but none started with prefix %s" % 1176 | (str(rootdirs), parentdir_prefix)) 1177 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1178 | 1179 | SHORT_VERSION_PY = """ 1180 | # This file was generated by 'versioneer.py' (0.17) from 1181 | # revision-control system data, or from the parent directory name of an 1182 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1183 | # of this file. 1184 | 1185 | import json 1186 | 1187 | version_json = ''' 1188 | %s 1189 | ''' # END VERSION_JSON 1190 | 1191 | 1192 | def get_versions(): 1193 | return json.loads(version_json) 1194 | """ 1195 | 1196 | 1197 | def versions_from_file(filename): 1198 | """Try to determine the version from _version.py if present.""" 1199 | try: 1200 | with open(filename) as f: 1201 | contents = f.read() 1202 | except EnvironmentError: 1203 | raise NotThisMethod("unable to read _version.py") 1204 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1205 | contents, re.M | re.S) 1206 | if not mo: 1207 | mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1208 | contents, re.M | re.S) 1209 | if not mo: 1210 | raise NotThisMethod("no version_json in _version.py") 1211 | return json.loads(mo.group(1)) 1212 | 1213 | 1214 | def write_to_version_file(filename, versions): 1215 | """Write the given version number to the given _version.py file.""" 1216 | os.unlink(filename) 1217 | contents = json.dumps(versions, sort_keys=True, 1218 | indent=1, separators=(",", ": ")) 1219 | with open(filename, "w") as f: 1220 | f.write(SHORT_VERSION_PY % contents) 1221 | 1222 | print("set %s to '%s'" % (filename, versions["version"])) 1223 | 1224 | 1225 | def plus_or_dot(pieces): 1226 | """Return a + if we don't already have one, else return a .""" 1227 | if "+" in pieces.get("closest-tag", ""): 1228 | return "." 1229 | return "+" 1230 | 1231 | 1232 | def render_pep440(pieces): 1233 | """Build up version string, with post-release "local version identifier". 1234 | 1235 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1236 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1237 | 1238 | Exceptions: 1239 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1240 | """ 1241 | if pieces["closest-tag"]: 1242 | rendered = pieces["closest-tag"] 1243 | if pieces["distance"] or pieces["dirty"]: 1244 | rendered += plus_or_dot(pieces) 1245 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1246 | if pieces["dirty"]: 1247 | rendered += ".dirty" 1248 | else: 1249 | # exception #1 1250 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1251 | pieces["short"]) 1252 | if pieces["dirty"]: 1253 | rendered += ".dirty" 1254 | return rendered 1255 | 1256 | 1257 | def render_pep440_pre(pieces): 1258 | """TAG[.post.devDISTANCE] -- No -dirty. 1259 | 1260 | Exceptions: 1261 | 1: no tags. 0.post.devDISTANCE 1262 | """ 1263 | if pieces["closest-tag"]: 1264 | rendered = pieces["closest-tag"] 1265 | if pieces["distance"]: 1266 | rendered += ".post.dev%d" % pieces["distance"] 1267 | else: 1268 | # exception #1 1269 | rendered = "0.post.dev%d" % pieces["distance"] 1270 | return rendered 1271 | 1272 | 1273 | def render_pep440_post(pieces): 1274 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1275 | 1276 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1277 | (a dirty tree will appear "older" than the corresponding clean one), 1278 | but you shouldn't be releasing software with -dirty anyways. 1279 | 1280 | Exceptions: 1281 | 1: no tags. 0.postDISTANCE[.dev0] 1282 | """ 1283 | if pieces["closest-tag"]: 1284 | rendered = pieces["closest-tag"] 1285 | if pieces["distance"] or pieces["dirty"]: 1286 | rendered += ".post%d" % pieces["distance"] 1287 | if pieces["dirty"]: 1288 | rendered += ".dev0" 1289 | rendered += plus_or_dot(pieces) 1290 | rendered += "g%s" % pieces["short"] 1291 | else: 1292 | # exception #1 1293 | rendered = "0.post%d" % pieces["distance"] 1294 | if pieces["dirty"]: 1295 | rendered += ".dev0" 1296 | rendered += "+g%s" % pieces["short"] 1297 | return rendered 1298 | 1299 | 1300 | def render_pep440_old(pieces): 1301 | """TAG[.postDISTANCE[.dev0]] . 1302 | 1303 | The ".dev0" means dirty. 1304 | 1305 | Eexceptions: 1306 | 1: no tags. 0.postDISTANCE[.dev0] 1307 | """ 1308 | if pieces["closest-tag"]: 1309 | rendered = pieces["closest-tag"] 1310 | if pieces["distance"] or pieces["dirty"]: 1311 | rendered += ".post%d" % pieces["distance"] 1312 | if pieces["dirty"]: 1313 | rendered += ".dev0" 1314 | else: 1315 | # exception #1 1316 | rendered = "0.post%d" % pieces["distance"] 1317 | if pieces["dirty"]: 1318 | rendered += ".dev0" 1319 | return rendered 1320 | 1321 | 1322 | def render_git_describe(pieces): 1323 | """TAG[-DISTANCE-gHEX][-dirty]. 1324 | 1325 | Like 'git describe --tags --dirty --always'. 1326 | 1327 | Exceptions: 1328 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1329 | """ 1330 | if pieces["closest-tag"]: 1331 | rendered = pieces["closest-tag"] 1332 | if pieces["distance"]: 1333 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1334 | else: 1335 | # exception #1 1336 | rendered = pieces["short"] 1337 | if pieces["dirty"]: 1338 | rendered += "-dirty" 1339 | return rendered 1340 | 1341 | 1342 | def render_git_describe_long(pieces): 1343 | """TAG-DISTANCE-gHEX[-dirty]. 1344 | 1345 | Like 'git describe --tags --dirty --always -long'. 1346 | The distance/hash is unconditional. 1347 | 1348 | Exceptions: 1349 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1350 | """ 1351 | if pieces["closest-tag"]: 1352 | rendered = pieces["closest-tag"] 1353 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1354 | else: 1355 | # exception #1 1356 | rendered = pieces["short"] 1357 | if pieces["dirty"]: 1358 | rendered += "-dirty" 1359 | return rendered 1360 | 1361 | 1362 | def render(pieces, style): 1363 | """Render the given version pieces into the requested style.""" 1364 | if pieces["error"]: 1365 | return {"version": "unknown", 1366 | "full-revisionid": pieces.get("long"), 1367 | "dirty": None, 1368 | "error": pieces["error"], 1369 | "date": None} 1370 | 1371 | if not style or style == "default": 1372 | style = "pep440" # the default 1373 | 1374 | if style == "pep440": 1375 | rendered = render_pep440(pieces) 1376 | elif style == "pep440-pre": 1377 | rendered = render_pep440_pre(pieces) 1378 | elif style == "pep440-post": 1379 | rendered = render_pep440_post(pieces) 1380 | elif style == "pep440-old": 1381 | rendered = render_pep440_old(pieces) 1382 | elif style == "git-describe": 1383 | rendered = render_git_describe(pieces) 1384 | elif style == "git-describe-long": 1385 | rendered = render_git_describe_long(pieces) 1386 | else: 1387 | raise ValueError("unknown style '%s'" % style) 1388 | 1389 | return {"version": rendered, "full-revisionid": pieces["long"], 1390 | "dirty": pieces["dirty"], "error": None, 1391 | "date": pieces.get("date")} 1392 | 1393 | 1394 | class VersioneerBadRootError(Exception): 1395 | """The project root directory is unknown or missing key files.""" 1396 | 1397 | 1398 | def get_versions(verbose=False): 1399 | """Get the project version from whatever source is available. 1400 | 1401 | Returns dict with two keys: 'version' and 'full'. 1402 | """ 1403 | if "versioneer" in sys.modules: 1404 | # see the discussion in cmdclass.py:get_cmdclass() 1405 | del sys.modules["versioneer"] 1406 | 1407 | root = get_root() 1408 | cfg = get_config_from_root(root) 1409 | 1410 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1411 | handlers = HANDLERS.get(cfg.VCS) 1412 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1413 | verbose = verbose or cfg.verbose 1414 | assert cfg.versionfile_source is not None, \ 1415 | "please set versioneer.versionfile_source" 1416 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1417 | 1418 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1419 | 1420 | # extract version from first of: _version.py, VCS command (e.g. 'git 1421 | # describe'), parentdir. This is meant to work for developers using a 1422 | # source checkout, for users of a tarball created by 'setup.py sdist', 1423 | # and for users of a tarball/zipball created by 'git archive' or github's 1424 | # download-from-tag feature or the equivalent in other VCSes. 1425 | 1426 | get_keywords_f = handlers.get("get_keywords") 1427 | from_keywords_f = handlers.get("keywords") 1428 | if get_keywords_f and from_keywords_f: 1429 | try: 1430 | keywords = get_keywords_f(versionfile_abs) 1431 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1432 | if verbose: 1433 | print("got version from expanded keyword %s" % ver) 1434 | return ver 1435 | except NotThisMethod: 1436 | pass 1437 | 1438 | try: 1439 | ver = versions_from_file(versionfile_abs) 1440 | if verbose: 1441 | print("got version from file %s %s" % (versionfile_abs, ver)) 1442 | return ver 1443 | except NotThisMethod: 1444 | pass 1445 | 1446 | from_vcs_f = handlers.get("pieces_from_vcs") 1447 | if from_vcs_f: 1448 | try: 1449 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1450 | ver = render(pieces, cfg.style) 1451 | if verbose: 1452 | print("got version from VCS %s" % ver) 1453 | return ver 1454 | except NotThisMethod: 1455 | pass 1456 | 1457 | try: 1458 | if cfg.parentdir_prefix: 1459 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1460 | if verbose: 1461 | print("got version from parentdir %s" % ver) 1462 | return ver 1463 | except NotThisMethod: 1464 | pass 1465 | 1466 | if verbose: 1467 | print("unable to compute version") 1468 | 1469 | return {"version": "0+unknown", "full-revisionid": None, 1470 | "dirty": None, "error": "unable to compute version", 1471 | "date": None} 1472 | 1473 | 1474 | def get_version(): 1475 | """Get the short version string for this project.""" 1476 | return get_versions()["version"] 1477 | 1478 | 1479 | def get_cmdclass(): 1480 | """Get the custom setuptools/distutils subclasses used by Versioneer.""" 1481 | if "versioneer" in sys.modules: 1482 | del sys.modules["versioneer"] 1483 | # this fixes the "python setup.py develop" case (also 'install' and 1484 | # 'easy_install .'), in which subdependencies of the main project are 1485 | # built (using setup.py bdist_egg) in the same python process. Assume 1486 | # a main project A and a dependency B, which use different versions 1487 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1488 | # sys.modules by the time B's setup.py is executed, causing B to run 1489 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1490 | # sandbox that restores sys.modules to it's pre-build state, so the 1491 | # parent is protected against the child's "import versioneer". By 1492 | # removing ourselves from sys.modules here, before the child build 1493 | # happens, we protect the child from the parent's versioneer too. 1494 | # Also see https://github.com/warner/python-versioneer/issues/52 1495 | 1496 | cmds = {} 1497 | 1498 | # we add "version" to both distutils and setuptools 1499 | from distutils.core import Command 1500 | 1501 | class cmd_version(Command): 1502 | description = "report generated version string" 1503 | user_options = [] 1504 | boolean_options = [] 1505 | 1506 | def initialize_options(self): 1507 | pass 1508 | 1509 | def finalize_options(self): 1510 | pass 1511 | 1512 | def run(self): 1513 | vers = get_versions(verbose=True) 1514 | print("Version: %s" % vers["version"]) 1515 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1516 | print(" dirty: %s" % vers.get("dirty")) 1517 | print(" date: %s" % vers.get("date")) 1518 | if vers["error"]: 1519 | print(" error: %s" % vers["error"]) 1520 | cmds["version"] = cmd_version 1521 | 1522 | # we override "build_py" in both distutils and setuptools 1523 | # 1524 | # most invocation pathways end up running build_py: 1525 | # distutils/build -> build_py 1526 | # distutils/install -> distutils/build ->.. 1527 | # setuptools/bdist_wheel -> distutils/install ->.. 1528 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1529 | # setuptools/install -> bdist_egg ->.. 1530 | # setuptools/develop -> ? 1531 | # pip install: 1532 | # copies source tree to a tempdir before running egg_info/etc 1533 | # if .git isn't copied too, 'git describe' will fail 1534 | # then does setup.py bdist_wheel, or sometimes setup.py install 1535 | # setup.py egg_info -> ? 1536 | 1537 | # we override different "build_py" commands for both environments 1538 | if "setuptools" in sys.modules: 1539 | from setuptools.command.build_py import build_py as _build_py 1540 | else: 1541 | from distutils.command.build_py import build_py as _build_py 1542 | 1543 | class cmd_build_py(_build_py): 1544 | def run(self): 1545 | root = get_root() 1546 | cfg = get_config_from_root(root) 1547 | versions = get_versions() 1548 | _build_py.run(self) 1549 | # now locate _version.py in the new build/ directory and replace 1550 | # it with an updated value 1551 | if cfg.versionfile_build: 1552 | target_versionfile = os.path.join(self.build_lib, 1553 | cfg.versionfile_build) 1554 | print("UPDATING %s" % target_versionfile) 1555 | write_to_version_file(target_versionfile, versions) 1556 | cmds["build_py"] = cmd_build_py 1557 | 1558 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1559 | from cx_Freeze.dist import build_exe as _build_exe 1560 | # nczeczulin reports that py2exe won't like the pep440-style string 1561 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1562 | # setup(console=[{ 1563 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1564 | # "product_version": versioneer.get_version(), 1565 | # ... 1566 | 1567 | class cmd_build_exe(_build_exe): 1568 | def run(self): 1569 | root = get_root() 1570 | cfg = get_config_from_root(root) 1571 | versions = get_versions() 1572 | target_versionfile = cfg.versionfile_source 1573 | print("UPDATING %s" % target_versionfile) 1574 | write_to_version_file(target_versionfile, versions) 1575 | 1576 | _build_exe.run(self) 1577 | os.unlink(target_versionfile) 1578 | with open(cfg.versionfile_source, "w") as f: 1579 | LONG = LONG_VERSION_PY[cfg.VCS] 1580 | f.write(LONG % 1581 | {"DOLLAR": "$", 1582 | "STYLE": cfg.style, 1583 | "TAG_PREFIX": cfg.tag_prefix, 1584 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1585 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1586 | }) 1587 | cmds["build_exe"] = cmd_build_exe 1588 | del cmds["build_py"] 1589 | 1590 | if 'py2exe' in sys.modules: # py2exe enabled? 1591 | try: 1592 | from py2exe.distutils_buildexe import py2exe as _py2exe # py3 1593 | except ImportError: 1594 | from py2exe.build_exe import py2exe as _py2exe # py2 1595 | 1596 | class cmd_py2exe(_py2exe): 1597 | def run(self): 1598 | root = get_root() 1599 | cfg = get_config_from_root(root) 1600 | versions = get_versions() 1601 | target_versionfile = cfg.versionfile_source 1602 | print("UPDATING %s" % target_versionfile) 1603 | write_to_version_file(target_versionfile, versions) 1604 | 1605 | _py2exe.run(self) 1606 | os.unlink(target_versionfile) 1607 | with open(cfg.versionfile_source, "w") as f: 1608 | LONG = LONG_VERSION_PY[cfg.VCS] 1609 | f.write(LONG % 1610 | {"DOLLAR": "$", 1611 | "STYLE": cfg.style, 1612 | "TAG_PREFIX": cfg.tag_prefix, 1613 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1614 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1615 | }) 1616 | cmds["py2exe"] = cmd_py2exe 1617 | 1618 | # we override different "sdist" commands for both environments 1619 | if "setuptools" in sys.modules: 1620 | from setuptools.command.sdist import sdist as _sdist 1621 | else: 1622 | from distutils.command.sdist import sdist as _sdist 1623 | 1624 | class cmd_sdist(_sdist): 1625 | def run(self): 1626 | versions = get_versions() 1627 | self._versioneer_generated_versions = versions 1628 | # unless we update this, the command will keep using the old 1629 | # version 1630 | self.distribution.metadata.version = versions["version"] 1631 | return _sdist.run(self) 1632 | 1633 | def make_release_tree(self, base_dir, files): 1634 | root = get_root() 1635 | cfg = get_config_from_root(root) 1636 | _sdist.make_release_tree(self, base_dir, files) 1637 | # now locate _version.py in the new base_dir directory 1638 | # (remembering that it may be a hardlink) and replace it with an 1639 | # updated value 1640 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 1641 | print("UPDATING %s" % target_versionfile) 1642 | write_to_version_file(target_versionfile, 1643 | self._versioneer_generated_versions) 1644 | cmds["sdist"] = cmd_sdist 1645 | 1646 | return cmds 1647 | 1648 | 1649 | CONFIG_ERROR = """ 1650 | setup.cfg is missing the necessary Versioneer configuration. You need 1651 | a section like: 1652 | 1653 | [versioneer] 1654 | VCS = git 1655 | style = pep440 1656 | versionfile_source = src/myproject/_version.py 1657 | versionfile_build = myproject/_version.py 1658 | tag_prefix = 1659 | parentdir_prefix = myproject- 1660 | 1661 | You will also need to edit your setup.py to use the results: 1662 | 1663 | import versioneer 1664 | setup(version=versioneer.get_version(), 1665 | cmdclass=versioneer.get_cmdclass(), ...) 1666 | 1667 | Please read the docstring in ./versioneer.py for configuration instructions, 1668 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 1669 | """ 1670 | 1671 | SAMPLE_CONFIG = """ 1672 | # See the docstring in versioneer.py for instructions. Note that you must 1673 | # re-run 'versioneer.py setup' after changing this section, and commit the 1674 | # resulting files. 1675 | 1676 | [versioneer] 1677 | #VCS = git 1678 | #style = pep440 1679 | #versionfile_source = 1680 | #versionfile_build = 1681 | #tag_prefix = 1682 | #parentdir_prefix = 1683 | 1684 | """ 1685 | 1686 | INIT_PY_SNIPPET = """ 1687 | from ._version import get_versions 1688 | __version__ = get_versions()['version'] 1689 | del get_versions 1690 | """ 1691 | 1692 | 1693 | def do_setup(): 1694 | """Main VCS-independent setup function for installing Versioneer.""" 1695 | root = get_root() 1696 | try: 1697 | cfg = get_config_from_root(root) 1698 | except (EnvironmentError, configparser.NoSectionError, 1699 | configparser.NoOptionError) as e: 1700 | if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 1701 | print("Adding sample versioneer config to setup.cfg", 1702 | file=sys.stderr) 1703 | with open(os.path.join(root, "setup.cfg"), "a") as f: 1704 | f.write(SAMPLE_CONFIG) 1705 | print(CONFIG_ERROR, file=sys.stderr) 1706 | return 1 1707 | 1708 | print(" creating %s" % cfg.versionfile_source) 1709 | with open(cfg.versionfile_source, "w") as f: 1710 | LONG = LONG_VERSION_PY[cfg.VCS] 1711 | f.write(LONG % {"DOLLAR": "$", 1712 | "STYLE": cfg.style, 1713 | "TAG_PREFIX": cfg.tag_prefix, 1714 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1715 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1716 | }) 1717 | 1718 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 1719 | "__init__.py") 1720 | if os.path.exists(ipy): 1721 | try: 1722 | with open(ipy, "r") as f: 1723 | old = f.read() 1724 | except EnvironmentError: 1725 | old = "" 1726 | if INIT_PY_SNIPPET not in old: 1727 | print(" appending to %s" % ipy) 1728 | with open(ipy, "a") as f: 1729 | f.write(INIT_PY_SNIPPET) 1730 | else: 1731 | print(" %s unmodified" % ipy) 1732 | else: 1733 | print(" %s doesn't exist, ok" % ipy) 1734 | ipy = None 1735 | 1736 | # Make sure both the top-level "versioneer.py" and versionfile_source 1737 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 1738 | # they'll be copied into source distributions. Pip won't be able to 1739 | # install the package without this. 1740 | manifest_in = os.path.join(root, "MANIFEST.in") 1741 | simple_includes = set() 1742 | try: 1743 | with open(manifest_in, "r") as f: 1744 | for line in f: 1745 | if line.startswith("include "): 1746 | for include in line.split()[1:]: 1747 | simple_includes.add(include) 1748 | except EnvironmentError: 1749 | pass 1750 | # That doesn't cover everything MANIFEST.in can do 1751 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 1752 | # it might give some false negatives. Appending redundant 'include' 1753 | # lines is safe, though. 1754 | if "versioneer.py" not in simple_includes: 1755 | print(" appending 'versioneer.py' to MANIFEST.in") 1756 | with open(manifest_in, "a") as f: 1757 | f.write("include versioneer.py\n") 1758 | else: 1759 | print(" 'versioneer.py' already in MANIFEST.in") 1760 | if cfg.versionfile_source not in simple_includes: 1761 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 1762 | cfg.versionfile_source) 1763 | with open(manifest_in, "a") as f: 1764 | f.write("include %s\n" % cfg.versionfile_source) 1765 | else: 1766 | print(" versionfile_source already in MANIFEST.in") 1767 | 1768 | # Make VCS-specific changes. For git, this means creating/changing 1769 | # .gitattributes to mark _version.py for export-subst keyword 1770 | # substitution. 1771 | do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 1772 | return 0 1773 | 1774 | 1775 | def scan_setup_py(): 1776 | """Validate the contents of setup.py against Versioneer's expectations.""" 1777 | found = set() 1778 | setters = False 1779 | errors = 0 1780 | with open("setup.py", "r") as f: 1781 | for line in f.readlines(): 1782 | if "import versioneer" in line: 1783 | found.add("import") 1784 | if "versioneer.get_cmdclass()" in line: 1785 | found.add("cmdclass") 1786 | if "versioneer.get_version()" in line: 1787 | found.add("get_version") 1788 | if "versioneer.VCS" in line: 1789 | setters = True 1790 | if "versioneer.versionfile_source" in line: 1791 | setters = True 1792 | if len(found) != 3: 1793 | print("") 1794 | print("Your setup.py appears to be missing some important items") 1795 | print("(but I might be wrong). Please make sure it has something") 1796 | print("roughly like the following:") 1797 | print("") 1798 | print(" import versioneer") 1799 | print(" setup( version=versioneer.get_version(),") 1800 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 1801 | print("") 1802 | errors += 1 1803 | if setters: 1804 | print("You should remove lines like 'versioneer.VCS = ' and") 1805 | print("'versioneer.versionfile_source = ' . This configuration") 1806 | print("now lives in setup.cfg, and should be removed from setup.py") 1807 | print("") 1808 | errors += 1 1809 | return errors 1810 | 1811 | if __name__ == "__main__": 1812 | cmd = sys.argv[1] 1813 | if cmd == "setup": 1814 | errors = do_setup() 1815 | errors += scan_setup_py() 1816 | if errors: 1817 | sys.exit(1) 1818 | --------------------------------------------------------------------------------