1From 5b5436f11d01e66505bb4c148304c2eb49346529 Mon Sep 17 00:00:00 2001 2From: Adam Duskett <adam.duskett@amarulasolutions.com> 3Date: Tue, 24 Oct 2023 09:56:57 +0200 4Subject: [PATCH] Update versioneer to 0.29 5 6Fixes builds against Python 3.12.0 7 8Upstream: https://github.com/warner/python-spake2/pull/15 9 10Signed-off-by: Adam Duskett <adam.duskett@amarulasolutions.com> 11--- 12 versioneer.py | 1350 ++++++++++++++++++++++++++++++++++--------------- 13 1 file changed, 931 insertions(+), 419 deletions(-) 14 15diff --git a/versioneer.py b/versioneer.py 16index 64fea1c..de97d90 100644 17--- a/versioneer.py 18+++ b/versioneer.py 19@@ -1,5 +1,4 @@ 20- 21-# Version: 0.18 22+# Version: 0.29 23 24 """The Versioneer - like a rocketeer, but for versions. 25 26@@ -7,18 +6,14 @@ The Versioneer 27 ============== 28 29 * like a rocketeer, but for versions! 30-* https://github.com/warner/python-versioneer 31+* https://github.com/python-versioneer/python-versioneer 32 * Brian Warner 33-* License: Public Domain 34-* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy 35-* [![Latest Version] 36-(https://pypip.in/version/versioneer/badge.svg?style=flat) 37-](https://pypi.python.org/pypi/versioneer/) 38-* [![Build Status] 39-(https://travis-ci.org/warner/python-versioneer.png?branch=master) 40-](https://travis-ci.org/warner/python-versioneer) 41- 42-This is a tool for managing a recorded version number in distutils-based 43+* License: Public Domain (Unlicense) 44+* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 45+* [![Latest Version][pypi-image]][pypi-url] 46+* [![Build Status][travis-image]][travis-url] 47+ 48+This is a tool for managing a recorded version number in setuptools-based 49 python projects. The goal is to remove the tedious and error-prone "update 50 the embedded version string" step from your release process. Making a new 51 release should be as easy as recording a new tag in your version-control 52@@ -27,9 +22,38 @@ system, and maybe making new tarballs. 53 54 ## Quick Install 55 56-* `pip install versioneer` to somewhere to your $PATH 57-* add a `[versioneer]` section to your setup.cfg (see below) 58-* run `versioneer install` in your source tree, commit the results 59+Versioneer provides two installation modes. The "classic" vendored mode installs 60+a copy of versioneer into your repository. The experimental build-time dependency mode 61+is intended to allow you to skip this step and simplify the process of upgrading. 62+ 63+### Vendored mode 64+ 65+* `pip install versioneer` to somewhere in your $PATH 66+ * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is 67+ available, so you can also use `conda install -c conda-forge versioneer` 68+* add a `[tool.versioneer]` section to your `pyproject.toml` or a 69+ `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) 70+ * Note that you will need to add `tomli; python_version < "3.11"` to your 71+ build-time dependencies if you use `pyproject.toml` 72+* run `versioneer install --vendor` in your source tree, commit the results 73+* verify version information with `python setup.py version` 74+ 75+### Build-time dependency mode 76+ 77+* `pip install versioneer` to somewhere in your $PATH 78+ * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is 79+ available, so you can also use `conda install -c conda-forge versioneer` 80+* add a `[tool.versioneer]` section to your `pyproject.toml` or a 81+ `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) 82+* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) 83+ to the `requires` key of the `build-system` table in `pyproject.toml`: 84+ ```toml 85+ [build-system] 86+ requires = ["setuptools", "versioneer[toml]"] 87+ build-backend = "setuptools.build_meta" 88+ ``` 89+* run `versioneer install --no-vendor` in your source tree, commit the results 90+* verify version information with `python setup.py version` 91 92 ## Version Identifiers 93 94@@ -61,7 +85,7 @@ version 1.3). Many VCS systems can report a description that captures this, 95 for example `git describe --tags --dirty --always` reports things like 96 "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 97 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 98-uncommitted changes. 99+uncommitted changes). 100 101 The version identifier is used for multiple purposes: 102 103@@ -166,7 +190,7 @@ which may help identify what went wrong). 104 105 Some situations are known to cause problems for Versioneer. This details the 106 most significant ones. More can be found on Github 107-[issues page](https://github.com/warner/python-versioneer/issues). 108+[issues page](https://github.com/python-versioneer/python-versioneer/issues). 109 110 ### Subprojects 111 112@@ -180,7 +204,7 @@ two common reasons why `setup.py` might not be in the root: 113 `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 114 distributions (and upload multiple independently-installable tarballs). 115 * Source trees whose main purpose is to contain a C library, but which also 116- provide bindings to Python (and perhaps other langauges) in subdirectories. 117+ provide bindings to Python (and perhaps other languages) in subdirectories. 118 119 Versioneer will look for `.git` in parent directories, and most operations 120 should get the right version string. However `pip` and `setuptools` have bugs 121@@ -194,9 +218,9 @@ work too. 122 Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 123 some later version. 124 125-[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking 126+[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking 127 this issue. The discussion in 128-[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the 129+[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the 130 issue from the Versioneer side in more detail. 131 [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 132 [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 133@@ -224,31 +248,20 @@ regenerated while a different version is checked out. Many setup.py commands 134 cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 135 a different virtualenv), so this can be surprising. 136 137-[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes 138+[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes 139 this one, but upgrading to a newer version of setuptools should probably 140 resolve it. 141 142-### Unicode version strings 143- 144-While Versioneer works (and is continually tested) with both Python 2 and 145-Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. 146-Newer releases probably generate unicode version strings on py2. It's not 147-clear that this is wrong, but it may be surprising for applications when then 148-write these strings to a network connection or include them in bytes-oriented 149-APIs like cryptographic checksums. 150- 151-[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates 152-this question. 153- 154 155 ## Updating Versioneer 156 157 To upgrade your project to a new release of Versioneer, do the following: 158 159 * install the new Versioneer (`pip install -U versioneer` or equivalent) 160-* edit `setup.cfg`, if necessary, to include any new configuration settings 161- indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. 162-* re-run `versioneer install` in your source tree, to replace 163+* edit `setup.cfg` and `pyproject.toml`, if necessary, 164+ to include any new configuration settings indicated by the release notes. 165+ See [UPGRADING](./UPGRADING.md) for details. 166+* re-run `versioneer install --[no-]vendor` in your source tree, to replace 167 `SRC/_version.py` 168 * commit any changed files 169 170@@ -265,35 +278,70 @@ installation by editing setup.py . Alternatively, it might go the other 171 direction and include code from all supported VCS systems, reducing the 172 number of intermediate scripts. 173 174+## Similar projects 175+ 176+* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time 177+ dependency 178+* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of 179+ versioneer 180+* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools 181+ plugin 182 183 ## License 184 185 To make Versioneer easier to embed, all its code is dedicated to the public 186 domain. The `_version.py` that it creates is also in the public domain. 187-Specifically, both are released under the Creative Commons "Public Domain 188-Dedication" license (CC0-1.0), as described in 189-https://creativecommons.org/publicdomain/zero/1.0/ . 190+Specifically, both are released under the "Unlicense", as described in 191+https://unlicense.org/. 192+ 193+[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg 194+[pypi-url]: https://pypi.python.org/pypi/versioneer/ 195+[travis-image]: 196+https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg 197+[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer 198 199 """ 200+# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring 201+# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements 202+# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error 203+# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with 204+# pylint:disable=attribute-defined-outside-init,too-many-arguments 205 206-from __future__ import print_function 207-try: 208- import configparser 209-except ImportError: 210- import ConfigParser as configparser 211+import configparser 212 import errno 213 import json 214 import os 215 import re 216 import subprocess 217 import sys 218+from pathlib import Path 219+from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union 220+from typing import NoReturn 221+import functools 222+ 223+have_tomllib = True 224+if sys.version_info >= (3, 11): 225+ import tomllib 226+else: 227+ try: 228+ import tomli as tomllib 229+ except ImportError: 230+ have_tomllib = False 231 232 233 class VersioneerConfig: 234 """Container for Versioneer configuration parameters.""" 235 236+ VCS: str 237+ style: str 238+ tag_prefix: str 239+ versionfile_source: str 240+ versionfile_build: Optional[str] 241+ parentdir_prefix: Optional[str] 242+ verbose: Optional[bool] 243+ 244 245-def get_root(): 246+def get_root() -> str: 247 """Get the project root directory. 248 249 We require that all commands are run from the project root, i.e. the 250@@ -301,18 +349,30 @@ def get_root(): 251 """ 252 root = os.path.realpath(os.path.abspath(os.getcwd())) 253 setup_py = os.path.join(root, "setup.py") 254+ pyproject_toml = os.path.join(root, "pyproject.toml") 255 versioneer_py = os.path.join(root, "versioneer.py") 256- if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 257+ if not ( 258+ os.path.exists(setup_py) 259+ or os.path.exists(pyproject_toml) 260+ or os.path.exists(versioneer_py) 261+ ): 262 # allow 'python path/to/setup.py COMMAND' 263 root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 264 setup_py = os.path.join(root, "setup.py") 265+ pyproject_toml = os.path.join(root, "pyproject.toml") 266 versioneer_py = os.path.join(root, "versioneer.py") 267- if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 268- err = ("Versioneer was unable to run the project root directory. " 269- "Versioneer requires setup.py to be executed from " 270- "its immediate directory (like 'python setup.py COMMAND'), " 271- "or in a way that lets it use sys.argv[0] to find the root " 272- "(like 'python path/to/setup.py COMMAND').") 273+ if not ( 274+ os.path.exists(setup_py) 275+ or os.path.exists(pyproject_toml) 276+ or os.path.exists(versioneer_py) 277+ ): 278+ err = ( 279+ "Versioneer was unable to run the project root directory. " 280+ "Versioneer requires setup.py to be executed from " 281+ "its immediate directory (like 'python setup.py COMMAND'), " 282+ "or in a way that lets it use sys.argv[0] to find the root " 283+ "(like 'python path/to/setup.py COMMAND')." 284+ ) 285 raise VersioneerBadRootError(err) 286 try: 287 # Certain runtime workflows (setup.py install/develop in a setuptools 288@@ -321,43 +381,64 @@ def get_root(): 289 # module-import table will cache the first one. So we can't use 290 # os.path.dirname(__file__), as that will find whichever 291 # versioneer.py was first imported, even in later projects. 292- me = os.path.realpath(os.path.abspath(__file__)) 293- me_dir = os.path.normcase(os.path.splitext(me)[0]) 294+ my_path = os.path.realpath(os.path.abspath(__file__)) 295+ me_dir = os.path.normcase(os.path.splitext(my_path)[0]) 296 vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 297- if me_dir != vsr_dir: 298- print("Warning: build in %s is using versioneer.py from %s" 299- % (os.path.dirname(me), versioneer_py)) 300+ if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): 301+ print( 302+ "Warning: build in %s is using versioneer.py from %s" 303+ % (os.path.dirname(my_path), versioneer_py) 304+ ) 305 except NameError: 306 pass 307 return root 308 309 310-def get_config_from_root(root): 311+def get_config_from_root(root: str) -> VersioneerConfig: 312 """Read the project setup.cfg file to determine Versioneer config.""" 313- # This might raise EnvironmentError (if setup.cfg is missing), or 314+ # This might raise OSError (if setup.cfg is missing), or 315 # configparser.NoSectionError (if it lacks a [versioneer] section), or 316 # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 317 # the top of versioneer.py for instructions on writing your setup.cfg . 318- setup_cfg = os.path.join(root, "setup.cfg") 319- parser = configparser.SafeConfigParser() 320- with open(setup_cfg, "r") as f: 321- parser.readfp(f) 322- VCS = parser.get("versioneer", "VCS") # mandatory 323- 324- def get(parser, name): 325- if parser.has_option("versioneer", name): 326- return parser.get("versioneer", name) 327- return None 328+ root_pth = Path(root) 329+ pyproject_toml = root_pth / "pyproject.toml" 330+ setup_cfg = root_pth / "setup.cfg" 331+ section: Union[Dict[str, Any], configparser.SectionProxy, None] = None 332+ if pyproject_toml.exists() and have_tomllib: 333+ try: 334+ with open(pyproject_toml, "rb") as fobj: 335+ pp = tomllib.load(fobj) 336+ section = pp["tool"]["versioneer"] 337+ except (tomllib.TOMLDecodeError, KeyError) as e: 338+ print(f"Failed to load config from {pyproject_toml}: {e}") 339+ print("Try to load it from setup.cfg") 340+ if not section: 341+ parser = configparser.ConfigParser() 342+ with open(setup_cfg) as cfg_file: 343+ parser.read_file(cfg_file) 344+ parser.get("versioneer", "VCS") # raise error if missing 345+ 346+ section = parser["versioneer"] 347+ 348+ # `cast`` really shouldn't be used, but its simplest for the 349+ # common VersioneerConfig users at the moment. We verify against 350+ # `None` values elsewhere where it matters 351+ 352 cfg = VersioneerConfig() 353- cfg.VCS = VCS 354- cfg.style = get(parser, "style") or "" 355- cfg.versionfile_source = get(parser, "versionfile_source") 356- cfg.versionfile_build = get(parser, "versionfile_build") 357- cfg.tag_prefix = get(parser, "tag_prefix") 358- if cfg.tag_prefix in ("''", '""'): 359+ cfg.VCS = section["VCS"] 360+ cfg.style = section.get("style", "") 361+ cfg.versionfile_source = cast(str, section.get("versionfile_source")) 362+ cfg.versionfile_build = section.get("versionfile_build") 363+ cfg.tag_prefix = cast(str, section.get("tag_prefix")) 364+ if cfg.tag_prefix in ("''", '""', None): 365 cfg.tag_prefix = "" 366- cfg.parentdir_prefix = get(parser, "parentdir_prefix") 367- cfg.verbose = get(parser, "verbose") 368+ cfg.parentdir_prefix = section.get("parentdir_prefix") 369+ if isinstance(section, configparser.SectionProxy): 370+ # Make sure configparser translates to bool 371+ cfg.verbose = section.getboolean("verbose") 372+ else: 373+ cfg.verbose = section.get("verbose") 374+ 375 return cfg 376 377 378@@ -366,37 +447,54 @@ class NotThisMethod(Exception): 379 380 381 # these dictionaries contain VCS-specific tools 382-LONG_VERSION_PY = {} 383-HANDLERS = {} 384+LONG_VERSION_PY: Dict[str, str] = {} 385+HANDLERS: Dict[str, Dict[str, Callable]] = {} 386 387 388-def register_vcs_handler(vcs, method): # decorator 389- """Decorator to mark a method as the handler for a particular VCS.""" 390- def decorate(f): 391+def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator 392+ """Create decorator to mark a method as the handler of a VCS.""" 393+ 394+ def decorate(f: Callable) -> Callable: 395 """Store f in HANDLERS[vcs][method].""" 396- if vcs not in HANDLERS: 397- HANDLERS[vcs] = {} 398- HANDLERS[vcs][method] = f 399+ HANDLERS.setdefault(vcs, {})[method] = f 400 return f 401+ 402 return decorate 403 404 405-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 406- env=None): 407+def run_command( 408+ commands: List[str], 409+ args: List[str], 410+ cwd: Optional[str] = None, 411+ verbose: bool = False, 412+ hide_stderr: bool = False, 413+ env: Optional[Dict[str, str]] = None, 414+) -> Tuple[Optional[str], Optional[int]]: 415 """Call the given command(s).""" 416 assert isinstance(commands, list) 417- p = None 418- for c in commands: 419+ process = None 420+ 421+ popen_kwargs: Dict[str, Any] = {} 422+ if sys.platform == "win32": 423+ # This hides the console window if pythonw.exe is used 424+ startupinfo = subprocess.STARTUPINFO() 425+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 426+ popen_kwargs["startupinfo"] = startupinfo 427+ 428+ for command in commands: 429 try: 430- dispcmd = str([c] + args) 431+ dispcmd = str([command] + args) 432 # remember shell=False, so use git.cmd on windows, not just git 433- p = subprocess.Popen([c] + args, cwd=cwd, env=env, 434- stdout=subprocess.PIPE, 435- stderr=(subprocess.PIPE if hide_stderr 436- else None)) 437+ process = subprocess.Popen( 438+ [command] + args, 439+ cwd=cwd, 440+ env=env, 441+ stdout=subprocess.PIPE, 442+ stderr=(subprocess.PIPE if hide_stderr else None), 443+ **popen_kwargs, 444+ ) 445 break 446- except EnvironmentError: 447- e = sys.exc_info()[1] 448+ except OSError as e: 449 if e.errno == errno.ENOENT: 450 continue 451 if verbose: 452@@ -407,26 +505,27 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 453 if verbose: 454 print("unable to find command, tried %s" % (commands,)) 455 return None, None 456- stdout = p.communicate()[0].strip() 457- if sys.version_info[0] >= 3: 458- stdout = stdout.decode() 459- if p.returncode != 0: 460+ stdout = process.communicate()[0].strip().decode() 461+ if process.returncode != 0: 462 if verbose: 463 print("unable to run %s (error)" % dispcmd) 464 print("stdout was %s" % stdout) 465- return None, p.returncode 466- return stdout, p.returncode 467+ return None, process.returncode 468+ return stdout, process.returncode 469 470 471-LONG_VERSION_PY['git'] = ''' 472+LONG_VERSION_PY[ 473+ "git" 474+] = r''' 475 # This file helps to compute a version number in source trees obtained from 476 # git-archive tarball (such as those provided by githubs download-from-tag 477 # feature). Distribution tarballs (built by setup.py sdist) and build 478 # directories (produced by setup.py build) will contain a much shorter file 479 # that just contains the computed version number. 480 481-# This file is released into the public domain. Generated by 482-# versioneer-0.18 (https://github.com/warner/python-versioneer) 483+# This file is released into the public domain. 484+# Generated by versioneer-0.29 485+# https://github.com/python-versioneer/python-versioneer 486 487 """Git implementation of _version.py.""" 488 489@@ -435,9 +534,11 @@ import os 490 import re 491 import subprocess 492 import sys 493+from typing import Any, Callable, Dict, List, Optional, Tuple 494+import functools 495 496 497-def get_keywords(): 498+def get_keywords() -> Dict[str, str]: 499 """Get the keywords needed to look up the version information.""" 500 # these strings will be replaced by git during git-archive. 501 # setup.py/versioneer.py will grep for the variable names, so they must 502@@ -453,8 +554,15 @@ def get_keywords(): 503 class VersioneerConfig: 504 """Container for Versioneer configuration parameters.""" 505 506+ VCS: str 507+ style: str 508+ tag_prefix: str 509+ parentdir_prefix: str 510+ versionfile_source: str 511+ verbose: bool 512+ 513 514-def get_config(): 515+def get_config() -> VersioneerConfig: 516 """Create, populate and return the VersioneerConfig() object.""" 517 # these strings are filled in when 'setup.py versioneer' creates 518 # _version.py 519@@ -472,13 +580,13 @@ class NotThisMethod(Exception): 520 """Exception raised if a method is not valid for the current scenario.""" 521 522 523-LONG_VERSION_PY = {} 524-HANDLERS = {} 525+LONG_VERSION_PY: Dict[str, str] = {} 526+HANDLERS: Dict[str, Dict[str, Callable]] = {} 527 528 529-def register_vcs_handler(vcs, method): # decorator 530- """Decorator to mark a method as the handler for a particular VCS.""" 531- def decorate(f): 532+def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator 533+ """Create decorator to mark a method as the handler of a VCS.""" 534+ def decorate(f: Callable) -> Callable: 535 """Store f in HANDLERS[vcs][method].""" 536 if vcs not in HANDLERS: 537 HANDLERS[vcs] = {} 538@@ -487,22 +595,35 @@ def register_vcs_handler(vcs, method): # decorator 539 return decorate 540 541 542-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 543- env=None): 544+def run_command( 545+ commands: List[str], 546+ args: List[str], 547+ cwd: Optional[str] = None, 548+ verbose: bool = False, 549+ hide_stderr: bool = False, 550+ env: Optional[Dict[str, str]] = None, 551+) -> Tuple[Optional[str], Optional[int]]: 552 """Call the given command(s).""" 553 assert isinstance(commands, list) 554- p = None 555- for c in commands: 556+ process = None 557+ 558+ popen_kwargs: Dict[str, Any] = {} 559+ if sys.platform == "win32": 560+ # This hides the console window if pythonw.exe is used 561+ startupinfo = subprocess.STARTUPINFO() 562+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 563+ popen_kwargs["startupinfo"] = startupinfo 564+ 565+ for command in commands: 566 try: 567- dispcmd = str([c] + args) 568+ dispcmd = str([command] + args) 569 # remember shell=False, so use git.cmd on windows, not just git 570- p = subprocess.Popen([c] + args, cwd=cwd, env=env, 571- stdout=subprocess.PIPE, 572- stderr=(subprocess.PIPE if hide_stderr 573- else None)) 574+ process = subprocess.Popen([command] + args, cwd=cwd, env=env, 575+ stdout=subprocess.PIPE, 576+ stderr=(subprocess.PIPE if hide_stderr 577+ else None), **popen_kwargs) 578 break 579- except EnvironmentError: 580- e = sys.exc_info()[1] 581+ except OSError as e: 582 if e.errno == errno.ENOENT: 583 continue 584 if verbose: 585@@ -513,18 +634,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 586 if verbose: 587 print("unable to find command, tried %%s" %% (commands,)) 588 return None, None 589- stdout = p.communicate()[0].strip() 590- if sys.version_info[0] >= 3: 591- stdout = stdout.decode() 592- if p.returncode != 0: 593+ stdout = process.communicate()[0].strip().decode() 594+ if process.returncode != 0: 595 if verbose: 596 print("unable to run %%s (error)" %% dispcmd) 597 print("stdout was %%s" %% stdout) 598- return None, p.returncode 599- return stdout, p.returncode 600+ return None, process.returncode 601+ return stdout, process.returncode 602 603 604-def versions_from_parentdir(parentdir_prefix, root, verbose): 605+def versions_from_parentdir( 606+ parentdir_prefix: str, 607+ root: str, 608+ verbose: bool, 609+) -> Dict[str, Any]: 610 """Try to determine the version from the parent directory name. 611 612 Source tarballs conventionally unpack into a directory that includes both 613@@ -533,15 +656,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): 614 """ 615 rootdirs = [] 616 617- for i in range(3): 618+ for _ in range(3): 619 dirname = os.path.basename(root) 620 if dirname.startswith(parentdir_prefix): 621 return {"version": dirname[len(parentdir_prefix):], 622 "full-revisionid": None, 623 "dirty": False, "error": None, "date": None} 624- else: 625- rootdirs.append(root) 626- root = os.path.dirname(root) # up a level 627+ rootdirs.append(root) 628+ root = os.path.dirname(root) # up a level 629 630 if verbose: 631 print("Tried directories %%s but none started with prefix %%s" %% 632@@ -550,41 +672,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): 633 634 635 @register_vcs_handler("git", "get_keywords") 636-def git_get_keywords(versionfile_abs): 637+def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: 638 """Extract version information from the given file.""" 639 # the code embedded in _version.py can just fetch the value of these 640 # keywords. When used from setup.py, we don't want to import _version.py, 641 # so we do it with a regexp instead. This function is not used from 642 # _version.py. 643- keywords = {} 644+ keywords: Dict[str, str] = {} 645 try: 646- f = open(versionfile_abs, "r") 647- for line in f.readlines(): 648- if line.strip().startswith("git_refnames ="): 649- mo = re.search(r'=\s*"(.*)"', line) 650- if mo: 651- keywords["refnames"] = mo.group(1) 652- if line.strip().startswith("git_full ="): 653- mo = re.search(r'=\s*"(.*)"', line) 654- if mo: 655- keywords["full"] = mo.group(1) 656- if line.strip().startswith("git_date ="): 657- mo = re.search(r'=\s*"(.*)"', line) 658- if mo: 659- keywords["date"] = mo.group(1) 660- f.close() 661- except EnvironmentError: 662+ with open(versionfile_abs, "r") as fobj: 663+ for line in fobj: 664+ if line.strip().startswith("git_refnames ="): 665+ mo = re.search(r'=\s*"(.*)"', line) 666+ if mo: 667+ keywords["refnames"] = mo.group(1) 668+ if line.strip().startswith("git_full ="): 669+ mo = re.search(r'=\s*"(.*)"', line) 670+ if mo: 671+ keywords["full"] = mo.group(1) 672+ if line.strip().startswith("git_date ="): 673+ mo = re.search(r'=\s*"(.*)"', line) 674+ if mo: 675+ keywords["date"] = mo.group(1) 676+ except OSError: 677 pass 678 return keywords 679 680 681 @register_vcs_handler("git", "keywords") 682-def git_versions_from_keywords(keywords, tag_prefix, verbose): 683+def git_versions_from_keywords( 684+ keywords: Dict[str, str], 685+ tag_prefix: str, 686+ verbose: bool, 687+) -> Dict[str, Any]: 688 """Get version information from git keywords.""" 689- if not keywords: 690- raise NotThisMethod("no keywords at all, weird") 691+ if "refnames" not in keywords: 692+ raise NotThisMethod("Short version file found") 693 date = keywords.get("date") 694 if date is not None: 695+ # Use only the last line. Previous lines may contain GPG signature 696+ # information. 697+ date = date.splitlines()[-1] 698+ 699 # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 700 # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 701 # -like" string, which we must then edit to make compliant), because 702@@ -597,11 +726,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 703 if verbose: 704 print("keywords are unexpanded, not using") 705 raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 706- refs = set([r.strip() for r in refnames.strip("()").split(",")]) 707+ refs = {r.strip() for r in refnames.strip("()").split(",")} 708 # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 709 # just "foo-1.0". If we see a "tag: " prefix, prefer those. 710 TAG = "tag: " 711- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 712+ tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 713 if not tags: 714 # Either we're using git < 1.8.3, or there really are no tags. We use 715 # a heuristic: assume all version tags have a digit. The old git %%d 716@@ -610,7 +739,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 717 # between branches and tags. By ignoring refnames without digits, we 718 # filter out many common branch names like "release" and 719 # "stabilization", as well as "HEAD" and "master". 720- tags = set([r for r in refs if re.search(r'\d', r)]) 721+ tags = {r for r in refs if re.search(r'\d', r)} 722 if verbose: 723 print("discarding '%%s', no digits" %% ",".join(refs - tags)) 724 if verbose: 725@@ -619,6 +748,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 726 # sorting will prefer e.g. "2.0" over "2.0rc1" 727 if ref.startswith(tag_prefix): 728 r = ref[len(tag_prefix):] 729+ # Filter out refs that exactly match prefix or that don't start 730+ # with a number once the prefix is stripped (mostly a concern 731+ # when prefix is '') 732+ if not re.match(r'\d', r): 733+ continue 734 if verbose: 735 print("picking %%s" %% r) 736 return {"version": r, 737@@ -634,7 +768,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 738 739 740 @register_vcs_handler("git", "pieces_from_vcs") 741-def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 742+def git_pieces_from_vcs( 743+ tag_prefix: str, 744+ root: str, 745+ verbose: bool, 746+ runner: Callable = run_command 747+) -> Dict[str, Any]: 748 """Get version from 'git describe' in the root of the source tree. 749 750 This only gets called if the git-archive 'subst' keywords were *not* 751@@ -645,8 +784,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 752 if sys.platform == "win32": 753 GITS = ["git.cmd", "git.exe"] 754 755- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 756- hide_stderr=True) 757+ # GIT_DIR can interfere with correct operation of Versioneer. 758+ # It may be intended to be passed to the Versioneer-versioned project, 759+ # but that should not change where we get our version from. 760+ env = os.environ.copy() 761+ env.pop("GIT_DIR", None) 762+ runner = functools.partial(runner, env=env) 763+ 764+ _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 765+ hide_stderr=not verbose) 766 if rc != 0: 767 if verbose: 768 print("Directory %%s not under git control" %% root) 769@@ -654,24 +800,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 770 771 # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 772 # if there isn't one, this yields HEX[-dirty] (no NUM) 773- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 774- "--always", "--long", 775- "--match", "%%s*" %% tag_prefix], 776- cwd=root) 777+ describe_out, rc = runner(GITS, [ 778+ "describe", "--tags", "--dirty", "--always", "--long", 779+ "--match", f"{tag_prefix}[[:digit:]]*" 780+ ], cwd=root) 781 # --long was added in git-1.5.5 782 if describe_out is None: 783 raise NotThisMethod("'git describe' failed") 784 describe_out = describe_out.strip() 785- full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 786+ full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 787 if full_out is None: 788 raise NotThisMethod("'git rev-parse' failed") 789 full_out = full_out.strip() 790 791- pieces = {} 792+ pieces: Dict[str, Any] = {} 793 pieces["long"] = full_out 794 pieces["short"] = full_out[:7] # maybe improved later 795 pieces["error"] = None 796 797+ branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 798+ cwd=root) 799+ # --abbrev-ref was added in git-1.6.3 800+ if rc != 0 or branch_name is None: 801+ raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 802+ branch_name = branch_name.strip() 803+ 804+ if branch_name == "HEAD": 805+ # If we aren't exactly on a branch, pick a branch which represents 806+ # the current commit. If all else fails, we are on a branchless 807+ # commit. 808+ branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 809+ # --contains was added in git-1.5.4 810+ if rc != 0 or branches is None: 811+ raise NotThisMethod("'git branch --contains' returned error") 812+ branches = branches.split("\n") 813+ 814+ # Remove the first line if we're running detached 815+ if "(" in branches[0]: 816+ branches.pop(0) 817+ 818+ # Strip off the leading "* " from the list of branches. 819+ branches = [branch[2:] for branch in branches] 820+ if "master" in branches: 821+ branch_name = "master" 822+ elif not branches: 823+ branch_name = None 824+ else: 825+ # Pick the first branch that is returned. Good or bad. 826+ branch_name = branches[0] 827+ 828+ pieces["branch"] = branch_name 829+ 830 # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 831 # TAG might have hyphens. 832 git_describe = describe_out 833@@ -688,7 +867,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 834 # TAG-NUM-gHEX 835 mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 836 if not mo: 837- # unparseable. Maybe git-describe is misbehaving? 838+ # unparsable. Maybe git-describe is misbehaving? 839 pieces["error"] = ("unable to parse git-describe output: '%%s'" 840 %% describe_out) 841 return pieces 842@@ -713,26 +892,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 843 else: 844 # HEX: no tags 845 pieces["closest-tag"] = None 846- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 847- cwd=root) 848- pieces["distance"] = int(count_out) # total number of commits 849+ out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 850+ pieces["distance"] = len(out.split()) # total number of commits 851 852 # commit date: see ISO-8601 comment in git_versions_from_keywords() 853- date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], 854- cwd=root)[0].strip() 855+ date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() 856+ # Use only the last line. Previous lines may contain GPG signature 857+ # information. 858+ date = date.splitlines()[-1] 859 pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 860 861 return pieces 862 863 864-def plus_or_dot(pieces): 865+def plus_or_dot(pieces: Dict[str, Any]) -> str: 866 """Return a + if we don't already have one, else return a .""" 867 if "+" in pieces.get("closest-tag", ""): 868 return "." 869 return "+" 870 871 872-def render_pep440(pieces): 873+def render_pep440(pieces: Dict[str, Any]) -> str: 874 """Build up version string, with post-release "local version identifier". 875 876 Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 877@@ -757,23 +937,71 @@ def render_pep440(pieces): 878 return rendered 879 880 881-def render_pep440_pre(pieces): 882- """TAG[.post.devDISTANCE] -- No -dirty. 883+def render_pep440_branch(pieces: Dict[str, Any]) -> str: 884+ """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 885+ 886+ The ".dev0" means not master branch. Note that .dev0 sorts backwards 887+ (a feature branch will appear "older" than the master branch). 888 889 Exceptions: 890- 1: no tags. 0.post.devDISTANCE 891+ 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 892 """ 893 if pieces["closest-tag"]: 894 rendered = pieces["closest-tag"] 895+ if pieces["distance"] or pieces["dirty"]: 896+ if pieces["branch"] != "master": 897+ rendered += ".dev0" 898+ rendered += plus_or_dot(pieces) 899+ rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 900+ if pieces["dirty"]: 901+ rendered += ".dirty" 902+ else: 903+ # exception #1 904+ rendered = "0" 905+ if pieces["branch"] != "master": 906+ rendered += ".dev0" 907+ rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], 908+ pieces["short"]) 909+ if pieces["dirty"]: 910+ rendered += ".dirty" 911+ return rendered 912+ 913+ 914+def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: 915+ """Split pep440 version string at the post-release segment. 916+ 917+ Returns the release segments before the post-release and the 918+ post-release version number (or -1 if no post-release segment is present). 919+ """ 920+ vc = str.split(ver, ".post") 921+ return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 922+ 923+ 924+def render_pep440_pre(pieces: Dict[str, Any]) -> str: 925+ """TAG[.postN.devDISTANCE] -- No -dirty. 926+ 927+ Exceptions: 928+ 1: no tags. 0.post0.devDISTANCE 929+ """ 930+ if pieces["closest-tag"]: 931 if pieces["distance"]: 932- rendered += ".post.dev%%d" %% pieces["distance"] 933+ # update the post release segment 934+ tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 935+ rendered = tag_version 936+ if post_version is not None: 937+ rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) 938+ else: 939+ rendered += ".post0.dev%%d" %% (pieces["distance"]) 940+ else: 941+ # no commits, use the tag as the version 942+ rendered = pieces["closest-tag"] 943 else: 944 # exception #1 945- rendered = "0.post.dev%%d" %% pieces["distance"] 946+ rendered = "0.post0.dev%%d" %% pieces["distance"] 947 return rendered 948 949 950-def render_pep440_post(pieces): 951+def render_pep440_post(pieces: Dict[str, Any]) -> str: 952 """TAG[.postDISTANCE[.dev0]+gHEX] . 953 954 The ".dev0" means dirty. Note that .dev0 sorts backwards 955@@ -800,12 +1028,41 @@ def render_pep440_post(pieces): 956 return rendered 957 958 959-def render_pep440_old(pieces): 960+def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: 961+ """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 962+ 963+ The ".dev0" means not master branch. 964+ 965+ Exceptions: 966+ 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 967+ """ 968+ if pieces["closest-tag"]: 969+ rendered = pieces["closest-tag"] 970+ if pieces["distance"] or pieces["dirty"]: 971+ rendered += ".post%%d" %% pieces["distance"] 972+ if pieces["branch"] != "master": 973+ rendered += ".dev0" 974+ rendered += plus_or_dot(pieces) 975+ rendered += "g%%s" %% pieces["short"] 976+ if pieces["dirty"]: 977+ rendered += ".dirty" 978+ else: 979+ # exception #1 980+ rendered = "0.post%%d" %% pieces["distance"] 981+ if pieces["branch"] != "master": 982+ rendered += ".dev0" 983+ rendered += "+g%%s" %% pieces["short"] 984+ if pieces["dirty"]: 985+ rendered += ".dirty" 986+ return rendered 987+ 988+ 989+def render_pep440_old(pieces: Dict[str, Any]) -> str: 990 """TAG[.postDISTANCE[.dev0]] . 991 992 The ".dev0" means dirty. 993 994- Eexceptions: 995+ Exceptions: 996 1: no tags. 0.postDISTANCE[.dev0] 997 """ 998 if pieces["closest-tag"]: 999@@ -822,7 +1079,7 @@ def render_pep440_old(pieces): 1000 return rendered 1001 1002 1003-def render_git_describe(pieces): 1004+def render_git_describe(pieces: Dict[str, Any]) -> str: 1005 """TAG[-DISTANCE-gHEX][-dirty]. 1006 1007 Like 'git describe --tags --dirty --always'. 1008@@ -842,7 +1099,7 @@ def render_git_describe(pieces): 1009 return rendered 1010 1011 1012-def render_git_describe_long(pieces): 1013+def render_git_describe_long(pieces: Dict[str, Any]) -> str: 1014 """TAG-DISTANCE-gHEX[-dirty]. 1015 1016 Like 'git describe --tags --dirty --always -long'. 1017@@ -862,7 +1119,7 @@ def render_git_describe_long(pieces): 1018 return rendered 1019 1020 1021-def render(pieces, style): 1022+def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: 1023 """Render the given version pieces into the requested style.""" 1024 if pieces["error"]: 1025 return {"version": "unknown", 1026@@ -876,10 +1133,14 @@ def render(pieces, style): 1027 1028 if style == "pep440": 1029 rendered = render_pep440(pieces) 1030+ elif style == "pep440-branch": 1031+ rendered = render_pep440_branch(pieces) 1032 elif style == "pep440-pre": 1033 rendered = render_pep440_pre(pieces) 1034 elif style == "pep440-post": 1035 rendered = render_pep440_post(pieces) 1036+ elif style == "pep440-post-branch": 1037+ rendered = render_pep440_post_branch(pieces) 1038 elif style == "pep440-old": 1039 rendered = render_pep440_old(pieces) 1040 elif style == "git-describe": 1041@@ -894,7 +1155,7 @@ def render(pieces, style): 1042 "date": pieces.get("date")} 1043 1044 1045-def get_versions(): 1046+def get_versions() -> Dict[str, Any]: 1047 """Get version information or return default if unable to do so.""" 1048 # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 1049 # __file__, we can work backwards from there to the root. Some 1050@@ -915,7 +1176,7 @@ def get_versions(): 1051 # versionfile_source is the relative path from the top of the source 1052 # tree (where the .git directory might live) to this file. Invert 1053 # this to find the root from __file__. 1054- for i in cfg.versionfile_source.split('/'): 1055+ for _ in cfg.versionfile_source.split('/'): 1056 root = os.path.dirname(root) 1057 except NameError: 1058 return {"version": "0+unknown", "full-revisionid": None, 1059@@ -942,41 +1203,48 @@ def get_versions(): 1060 1061 1062 @register_vcs_handler("git", "get_keywords") 1063-def git_get_keywords(versionfile_abs): 1064+def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: 1065 """Extract version information from the given file.""" 1066 # the code embedded in _version.py can just fetch the value of these 1067 # keywords. When used from setup.py, we don't want to import _version.py, 1068 # so we do it with a regexp instead. This function is not used from 1069 # _version.py. 1070- keywords = {} 1071+ keywords: Dict[str, str] = {} 1072 try: 1073- f = open(versionfile_abs, "r") 1074- for line in f.readlines(): 1075- if line.strip().startswith("git_refnames ="): 1076- mo = re.search(r'=\s*"(.*)"', line) 1077- if mo: 1078- keywords["refnames"] = mo.group(1) 1079- if line.strip().startswith("git_full ="): 1080- mo = re.search(r'=\s*"(.*)"', line) 1081- if mo: 1082- keywords["full"] = mo.group(1) 1083- if line.strip().startswith("git_date ="): 1084- mo = re.search(r'=\s*"(.*)"', line) 1085- if mo: 1086- keywords["date"] = mo.group(1) 1087- f.close() 1088- except EnvironmentError: 1089+ with open(versionfile_abs, "r") as fobj: 1090+ for line in fobj: 1091+ if line.strip().startswith("git_refnames ="): 1092+ mo = re.search(r'=\s*"(.*)"', line) 1093+ if mo: 1094+ keywords["refnames"] = mo.group(1) 1095+ if line.strip().startswith("git_full ="): 1096+ mo = re.search(r'=\s*"(.*)"', line) 1097+ if mo: 1098+ keywords["full"] = mo.group(1) 1099+ if line.strip().startswith("git_date ="): 1100+ mo = re.search(r'=\s*"(.*)"', line) 1101+ if mo: 1102+ keywords["date"] = mo.group(1) 1103+ except OSError: 1104 pass 1105 return keywords 1106 1107 1108 @register_vcs_handler("git", "keywords") 1109-def git_versions_from_keywords(keywords, tag_prefix, verbose): 1110+def git_versions_from_keywords( 1111+ keywords: Dict[str, str], 1112+ tag_prefix: str, 1113+ verbose: bool, 1114+) -> Dict[str, Any]: 1115 """Get version information from git keywords.""" 1116- if not keywords: 1117- raise NotThisMethod("no keywords at all, weird") 1118+ if "refnames" not in keywords: 1119+ raise NotThisMethod("Short version file found") 1120 date = keywords.get("date") 1121 if date is not None: 1122+ # Use only the last line. Previous lines may contain GPG signature 1123+ # information. 1124+ date = date.splitlines()[-1] 1125+ 1126 # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 1127 # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 1128 # -like" string, which we must then edit to make compliant), because 1129@@ -989,11 +1257,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 1130 if verbose: 1131 print("keywords are unexpanded, not using") 1132 raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 1133- refs = set([r.strip() for r in refnames.strip("()").split(",")]) 1134+ refs = {r.strip() for r in refnames.strip("()").split(",")} 1135 # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 1136 # just "foo-1.0". If we see a "tag: " prefix, prefer those. 1137 TAG = "tag: " 1138- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 1139+ tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} 1140 if not tags: 1141 # Either we're using git < 1.8.3, or there really are no tags. We use 1142 # a heuristic: assume all version tags have a digit. The old git %d 1143@@ -1002,7 +1270,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 1144 # between branches and tags. By ignoring refnames without digits, we 1145 # filter out many common branch names like "release" and 1146 # "stabilization", as well as "HEAD" and "master". 1147- tags = set([r for r in refs if re.search(r'\d', r)]) 1148+ tags = {r for r in refs if re.search(r"\d", r)} 1149 if verbose: 1150 print("discarding '%s', no digits" % ",".join(refs - tags)) 1151 if verbose: 1152@@ -1010,23 +1278,37 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 1153 for ref in sorted(tags): 1154 # sorting will prefer e.g. "2.0" over "2.0rc1" 1155 if ref.startswith(tag_prefix): 1156- r = ref[len(tag_prefix):] 1157+ r = ref[len(tag_prefix) :] 1158+ # Filter out refs that exactly match prefix or that don't start 1159+ # with a number once the prefix is stripped (mostly a concern 1160+ # when prefix is '') 1161+ if not re.match(r"\d", r): 1162+ continue 1163 if verbose: 1164 print("picking %s" % r) 1165- return {"version": r, 1166- "full-revisionid": keywords["full"].strip(), 1167- "dirty": False, "error": None, 1168- "date": date} 1169+ return { 1170+ "version": r, 1171+ "full-revisionid": keywords["full"].strip(), 1172+ "dirty": False, 1173+ "error": None, 1174+ "date": date, 1175+ } 1176 # no suitable tags, so version is "0+unknown", but full hex is still there 1177 if verbose: 1178 print("no suitable tags, using unknown + full revision id") 1179- return {"version": "0+unknown", 1180- "full-revisionid": keywords["full"].strip(), 1181- "dirty": False, "error": "no suitable tags", "date": None} 1182+ return { 1183+ "version": "0+unknown", 1184+ "full-revisionid": keywords["full"].strip(), 1185+ "dirty": False, 1186+ "error": "no suitable tags", 1187+ "date": None, 1188+ } 1189 1190 1191 @register_vcs_handler("git", "pieces_from_vcs") 1192-def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1193+def git_pieces_from_vcs( 1194+ tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command 1195+) -> Dict[str, Any]: 1196 """Get version from 'git describe' in the root of the source tree. 1197 1198 This only gets called if the git-archive 'subst' keywords were *not* 1199@@ -1037,8 +1319,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1200 if sys.platform == "win32": 1201 GITS = ["git.cmd", "git.exe"] 1202 1203- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 1204- hide_stderr=True) 1205+ # GIT_DIR can interfere with correct operation of Versioneer. 1206+ # It may be intended to be passed to the Versioneer-versioned project, 1207+ # but that should not change where we get our version from. 1208+ env = os.environ.copy() 1209+ env.pop("GIT_DIR", None) 1210+ runner = functools.partial(runner, env=env) 1211+ 1212+ _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) 1213 if rc != 0: 1214 if verbose: 1215 print("Directory %s not under git control" % root) 1216@@ -1046,24 +1334,65 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1217 1218 # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1219 # if there isn't one, this yields HEX[-dirty] (no NUM) 1220- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 1221- "--always", "--long", 1222- "--match", "%s*" % tag_prefix], 1223- cwd=root) 1224+ describe_out, rc = runner( 1225+ GITS, 1226+ [ 1227+ "describe", 1228+ "--tags", 1229+ "--dirty", 1230+ "--always", 1231+ "--long", 1232+ "--match", 1233+ f"{tag_prefix}[[:digit:]]*", 1234+ ], 1235+ cwd=root, 1236+ ) 1237 # --long was added in git-1.5.5 1238 if describe_out is None: 1239 raise NotThisMethod("'git describe' failed") 1240 describe_out = describe_out.strip() 1241- full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 1242+ full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 1243 if full_out is None: 1244 raise NotThisMethod("'git rev-parse' failed") 1245 full_out = full_out.strip() 1246 1247- pieces = {} 1248+ pieces: Dict[str, Any] = {} 1249 pieces["long"] = full_out 1250 pieces["short"] = full_out[:7] # maybe improved later 1251 pieces["error"] = None 1252 1253+ branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) 1254+ # --abbrev-ref was added in git-1.6.3 1255+ if rc != 0 or branch_name is None: 1256+ raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 1257+ branch_name = branch_name.strip() 1258+ 1259+ if branch_name == "HEAD": 1260+ # If we aren't exactly on a branch, pick a branch which represents 1261+ # the current commit. If all else fails, we are on a branchless 1262+ # commit. 1263+ branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 1264+ # --contains was added in git-1.5.4 1265+ if rc != 0 or branches is None: 1266+ raise NotThisMethod("'git branch --contains' returned error") 1267+ branches = branches.split("\n") 1268+ 1269+ # Remove the first line if we're running detached 1270+ if "(" in branches[0]: 1271+ branches.pop(0) 1272+ 1273+ # Strip off the leading "* " from the list of branches. 1274+ branches = [branch[2:] for branch in branches] 1275+ if "master" in branches: 1276+ branch_name = "master" 1277+ elif not branches: 1278+ branch_name = None 1279+ else: 1280+ # Pick the first branch that is returned. Good or bad. 1281+ branch_name = branches[0] 1282+ 1283+ pieces["branch"] = branch_name 1284+ 1285 # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1286 # TAG might have hyphens. 1287 git_describe = describe_out 1288@@ -1072,17 +1401,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1289 dirty = git_describe.endswith("-dirty") 1290 pieces["dirty"] = dirty 1291 if dirty: 1292- git_describe = git_describe[:git_describe.rindex("-dirty")] 1293+ git_describe = git_describe[: git_describe.rindex("-dirty")] 1294 1295 # now we have TAG-NUM-gHEX or HEX 1296 1297 if "-" in git_describe: 1298 # TAG-NUM-gHEX 1299- mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1300+ mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 1301 if not mo: 1302- # unparseable. Maybe git-describe is misbehaving? 1303- pieces["error"] = ("unable to parse git-describe output: '%s'" 1304- % describe_out) 1305+ # unparsable. Maybe git-describe is misbehaving? 1306+ pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out 1307 return pieces 1308 1309 # tag 1310@@ -1091,10 +1419,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1311 if verbose: 1312 fmt = "tag '%s' doesn't start with prefix '%s'" 1313 print(fmt % (full_tag, tag_prefix)) 1314- pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1315- % (full_tag, tag_prefix)) 1316+ pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( 1317+ full_tag, 1318+ tag_prefix, 1319+ ) 1320 return pieces 1321- pieces["closest-tag"] = full_tag[len(tag_prefix):] 1322+ pieces["closest-tag"] = full_tag[len(tag_prefix) :] 1323 1324 # distance: number of commits since tag 1325 pieces["distance"] = int(mo.group(2)) 1326@@ -1105,19 +1435,20 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1327 else: 1328 # HEX: no tags 1329 pieces["closest-tag"] = None 1330- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 1331- cwd=root) 1332- pieces["distance"] = int(count_out) # total number of commits 1333+ out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 1334+ pieces["distance"] = len(out.split()) # total number of commits 1335 1336 # commit date: see ISO-8601 comment in git_versions_from_keywords() 1337- date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 1338- cwd=root)[0].strip() 1339+ date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 1340+ # Use only the last line. Previous lines may contain GPG signature 1341+ # information. 1342+ date = date.splitlines()[-1] 1343 pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1344 1345 return pieces 1346 1347 1348-def do_vcs_install(manifest_in, versionfile_source, ipy): 1349+def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: 1350 """Git-specific installation logic for Versioneer. 1351 1352 For Git, this means creating/changing .gitattributes to mark _version.py 1353@@ -1126,36 +1457,40 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): 1354 GITS = ["git"] 1355 if sys.platform == "win32": 1356 GITS = ["git.cmd", "git.exe"] 1357- files = [manifest_in, versionfile_source] 1358+ files = [versionfile_source] 1359 if ipy: 1360 files.append(ipy) 1361- try: 1362- me = __file__ 1363- if me.endswith(".pyc") or me.endswith(".pyo"): 1364- me = os.path.splitext(me)[0] + ".py" 1365- versioneer_file = os.path.relpath(me) 1366- except NameError: 1367- versioneer_file = "versioneer.py" 1368- files.append(versioneer_file) 1369+ if "VERSIONEER_PEP518" not in globals(): 1370+ try: 1371+ my_path = __file__ 1372+ if my_path.endswith((".pyc", ".pyo")): 1373+ my_path = os.path.splitext(my_path)[0] + ".py" 1374+ versioneer_file = os.path.relpath(my_path) 1375+ except NameError: 1376+ versioneer_file = "versioneer.py" 1377+ files.append(versioneer_file) 1378 present = False 1379 try: 1380- f = open(".gitattributes", "r") 1381- for line in f.readlines(): 1382- if line.strip().startswith(versionfile_source): 1383- if "export-subst" in line.strip().split()[1:]: 1384- present = True 1385- f.close() 1386- except EnvironmentError: 1387+ with open(".gitattributes", "r") as fobj: 1388+ for line in fobj: 1389+ if line.strip().startswith(versionfile_source): 1390+ if "export-subst" in line.strip().split()[1:]: 1391+ present = True 1392+ break 1393+ except OSError: 1394 pass 1395 if not present: 1396- f = open(".gitattributes", "a+") 1397- f.write("%s export-subst\n" % versionfile_source) 1398- f.close() 1399+ with open(".gitattributes", "a+") as fobj: 1400+ fobj.write(f"{versionfile_source} export-subst\n") 1401 files.append(".gitattributes") 1402 run_command(GITS, ["add", "--"] + files) 1403 1404 1405-def versions_from_parentdir(parentdir_prefix, root, verbose): 1406+def versions_from_parentdir( 1407+ parentdir_prefix: str, 1408+ root: str, 1409+ verbose: bool, 1410+) -> Dict[str, Any]: 1411 """Try to determine the version from the parent directory name. 1412 1413 Source tarballs conventionally unpack into a directory that includes both 1414@@ -1164,24 +1499,29 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): 1415 """ 1416 rootdirs = [] 1417 1418- for i in range(3): 1419+ for _ in range(3): 1420 dirname = os.path.basename(root) 1421 if dirname.startswith(parentdir_prefix): 1422- return {"version": dirname[len(parentdir_prefix):], 1423- "full-revisionid": None, 1424- "dirty": False, "error": None, "date": None} 1425- else: 1426- rootdirs.append(root) 1427- root = os.path.dirname(root) # up a level 1428+ return { 1429+ "version": dirname[len(parentdir_prefix) :], 1430+ "full-revisionid": None, 1431+ "dirty": False, 1432+ "error": None, 1433+ "date": None, 1434+ } 1435+ rootdirs.append(root) 1436+ root = os.path.dirname(root) # up a level 1437 1438 if verbose: 1439- print("Tried directories %s but none started with prefix %s" % 1440- (str(rootdirs), parentdir_prefix)) 1441+ print( 1442+ "Tried directories %s but none started with prefix %s" 1443+ % (str(rootdirs), parentdir_prefix) 1444+ ) 1445 raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1446 1447 1448 SHORT_VERSION_PY = """ 1449-# This file was generated by 'versioneer.py' (0.18) from 1450+# This file was generated by 'versioneer.py' (0.29) from 1451 # revision-control system data, or from the parent directory name of an 1452 # unpacked source archive. Distribution tarballs contain a pre-generated copy 1453 # of this file. 1454@@ -1198,42 +1538,42 @@ def get_versions(): 1455 """ 1456 1457 1458-def versions_from_file(filename): 1459+def versions_from_file(filename: str) -> Dict[str, Any]: 1460 """Try to determine the version from _version.py if present.""" 1461 try: 1462 with open(filename) as f: 1463 contents = f.read() 1464- except EnvironmentError: 1465+ except OSError: 1466 raise NotThisMethod("unable to read _version.py") 1467- mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1468- contents, re.M | re.S) 1469+ mo = re.search( 1470+ r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S 1471+ ) 1472 if not mo: 1473- mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1474- contents, re.M | re.S) 1475+ mo = re.search( 1476+ r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S 1477+ ) 1478 if not mo: 1479 raise NotThisMethod("no version_json in _version.py") 1480 return json.loads(mo.group(1)) 1481 1482 1483-def write_to_version_file(filename, versions): 1484+def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: 1485 """Write the given version number to the given _version.py file.""" 1486- os.unlink(filename) 1487- contents = json.dumps(versions, sort_keys=True, 1488- indent=1, separators=(",", ": ")) 1489+ contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) 1490 with open(filename, "w") as f: 1491 f.write(SHORT_VERSION_PY % contents) 1492 1493 print("set %s to '%s'" % (filename, versions["version"])) 1494 1495 1496-def plus_or_dot(pieces): 1497+def plus_or_dot(pieces: Dict[str, Any]) -> str: 1498 """Return a + if we don't already have one, else return a .""" 1499 if "+" in pieces.get("closest-tag", ""): 1500 return "." 1501 return "+" 1502 1503 1504-def render_pep440(pieces): 1505+def render_pep440(pieces: Dict[str, Any]) -> str: 1506 """Build up version string, with post-release "local version identifier". 1507 1508 Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1509@@ -1251,30 +1591,76 @@ def render_pep440(pieces): 1510 rendered += ".dirty" 1511 else: 1512 # exception #1 1513- rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1514- pieces["short"]) 1515+ rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 1516 if pieces["dirty"]: 1517 rendered += ".dirty" 1518 return rendered 1519 1520 1521-def render_pep440_pre(pieces): 1522- """TAG[.post.devDISTANCE] -- No -dirty. 1523+def render_pep440_branch(pieces: Dict[str, Any]) -> str: 1524+ """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 1525+ 1526+ The ".dev0" means not master branch. Note that .dev0 sorts backwards 1527+ (a feature branch will appear "older" than the master branch). 1528 1529 Exceptions: 1530- 1: no tags. 0.post.devDISTANCE 1531+ 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 1532 """ 1533 if pieces["closest-tag"]: 1534 rendered = pieces["closest-tag"] 1535+ if pieces["distance"] or pieces["dirty"]: 1536+ if pieces["branch"] != "master": 1537+ rendered += ".dev0" 1538+ rendered += plus_or_dot(pieces) 1539+ rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1540+ if pieces["dirty"]: 1541+ rendered += ".dirty" 1542+ else: 1543+ # exception #1 1544+ rendered = "0" 1545+ if pieces["branch"] != "master": 1546+ rendered += ".dev0" 1547+ rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 1548+ if pieces["dirty"]: 1549+ rendered += ".dirty" 1550+ return rendered 1551+ 1552+ 1553+def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: 1554+ """Split pep440 version string at the post-release segment. 1555+ 1556+ Returns the release segments before the post-release and the 1557+ post-release version number (or -1 if no post-release segment is present). 1558+ """ 1559+ vc = str.split(ver, ".post") 1560+ return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 1561+ 1562+ 1563+def render_pep440_pre(pieces: Dict[str, Any]) -> str: 1564+ """TAG[.postN.devDISTANCE] -- No -dirty. 1565+ 1566+ Exceptions: 1567+ 1: no tags. 0.post0.devDISTANCE 1568+ """ 1569+ if pieces["closest-tag"]: 1570 if pieces["distance"]: 1571- rendered += ".post.dev%d" % pieces["distance"] 1572+ # update the post release segment 1573+ tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 1574+ rendered = tag_version 1575+ if post_version is not None: 1576+ rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 1577+ else: 1578+ rendered += ".post0.dev%d" % (pieces["distance"]) 1579+ else: 1580+ # no commits, use the tag as the version 1581+ rendered = pieces["closest-tag"] 1582 else: 1583 # exception #1 1584- rendered = "0.post.dev%d" % pieces["distance"] 1585+ rendered = "0.post0.dev%d" % pieces["distance"] 1586 return rendered 1587 1588 1589-def render_pep440_post(pieces): 1590+def render_pep440_post(pieces: Dict[str, Any]) -> str: 1591 """TAG[.postDISTANCE[.dev0]+gHEX] . 1592 1593 The ".dev0" means dirty. Note that .dev0 sorts backwards 1594@@ -1301,12 +1687,41 @@ def render_pep440_post(pieces): 1595 return rendered 1596 1597 1598-def render_pep440_old(pieces): 1599+def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: 1600+ """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 1601+ 1602+ The ".dev0" means not master branch. 1603+ 1604+ Exceptions: 1605+ 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 1606+ """ 1607+ if pieces["closest-tag"]: 1608+ rendered = pieces["closest-tag"] 1609+ if pieces["distance"] or pieces["dirty"]: 1610+ rendered += ".post%d" % pieces["distance"] 1611+ if pieces["branch"] != "master": 1612+ rendered += ".dev0" 1613+ rendered += plus_or_dot(pieces) 1614+ rendered += "g%s" % pieces["short"] 1615+ if pieces["dirty"]: 1616+ rendered += ".dirty" 1617+ else: 1618+ # exception #1 1619+ rendered = "0.post%d" % pieces["distance"] 1620+ if pieces["branch"] != "master": 1621+ rendered += ".dev0" 1622+ rendered += "+g%s" % pieces["short"] 1623+ if pieces["dirty"]: 1624+ rendered += ".dirty" 1625+ return rendered 1626+ 1627+ 1628+def render_pep440_old(pieces: Dict[str, Any]) -> str: 1629 """TAG[.postDISTANCE[.dev0]] . 1630 1631 The ".dev0" means dirty. 1632 1633- Eexceptions: 1634+ Exceptions: 1635 1: no tags. 0.postDISTANCE[.dev0] 1636 """ 1637 if pieces["closest-tag"]: 1638@@ -1323,7 +1738,7 @@ def render_pep440_old(pieces): 1639 return rendered 1640 1641 1642-def render_git_describe(pieces): 1643+def render_git_describe(pieces: Dict[str, Any]) -> str: 1644 """TAG[-DISTANCE-gHEX][-dirty]. 1645 1646 Like 'git describe --tags --dirty --always'. 1647@@ -1343,7 +1758,7 @@ def render_git_describe(pieces): 1648 return rendered 1649 1650 1651-def render_git_describe_long(pieces): 1652+def render_git_describe_long(pieces: Dict[str, Any]) -> str: 1653 """TAG-DISTANCE-gHEX[-dirty]. 1654 1655 Like 'git describe --tags --dirty --always -long'. 1656@@ -1363,24 +1778,30 @@ def render_git_describe_long(pieces): 1657 return rendered 1658 1659 1660-def render(pieces, style): 1661+def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: 1662 """Render the given version pieces into the requested style.""" 1663 if pieces["error"]: 1664- return {"version": "unknown", 1665- "full-revisionid": pieces.get("long"), 1666- "dirty": None, 1667- "error": pieces["error"], 1668- "date": None} 1669+ return { 1670+ "version": "unknown", 1671+ "full-revisionid": pieces.get("long"), 1672+ "dirty": None, 1673+ "error": pieces["error"], 1674+ "date": None, 1675+ } 1676 1677 if not style or style == "default": 1678 style = "pep440" # the default 1679 1680 if style == "pep440": 1681 rendered = render_pep440(pieces) 1682+ elif style == "pep440-branch": 1683+ rendered = render_pep440_branch(pieces) 1684 elif style == "pep440-pre": 1685 rendered = render_pep440_pre(pieces) 1686 elif style == "pep440-post": 1687 rendered = render_pep440_post(pieces) 1688+ elif style == "pep440-post-branch": 1689+ rendered = render_pep440_post_branch(pieces) 1690 elif style == "pep440-old": 1691 rendered = render_pep440_old(pieces) 1692 elif style == "git-describe": 1693@@ -1390,16 +1811,20 @@ def render(pieces, style): 1694 else: 1695 raise ValueError("unknown style '%s'" % style) 1696 1697- return {"version": rendered, "full-revisionid": pieces["long"], 1698- "dirty": pieces["dirty"], "error": None, 1699- "date": pieces.get("date")} 1700+ return { 1701+ "version": rendered, 1702+ "full-revisionid": pieces["long"], 1703+ "dirty": pieces["dirty"], 1704+ "error": None, 1705+ "date": pieces.get("date"), 1706+ } 1707 1708 1709 class VersioneerBadRootError(Exception): 1710 """The project root directory is unknown or missing key files.""" 1711 1712 1713-def get_versions(verbose=False): 1714+def get_versions(verbose: bool = False) -> Dict[str, Any]: 1715 """Get the project version from whatever source is available. 1716 1717 Returns dict with two keys: 'version' and 'full'. 1718@@ -1414,9 +1839,10 @@ def get_versions(verbose=False): 1719 assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1720 handlers = HANDLERS.get(cfg.VCS) 1721 assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1722- verbose = verbose or cfg.verbose 1723- assert cfg.versionfile_source is not None, \ 1724- "please set versioneer.versionfile_source" 1725+ verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` 1726+ assert ( 1727+ cfg.versionfile_source is not None 1728+ ), "please set versioneer.versionfile_source" 1729 assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1730 1731 versionfile_abs = os.path.join(root, cfg.versionfile_source) 1732@@ -1470,18 +1896,26 @@ def get_versions(verbose=False): 1733 if verbose: 1734 print("unable to compute version") 1735 1736- return {"version": "0+unknown", "full-revisionid": None, 1737- "dirty": None, "error": "unable to compute version", 1738- "date": None} 1739+ return { 1740+ "version": "0+unknown", 1741+ "full-revisionid": None, 1742+ "dirty": None, 1743+ "error": "unable to compute version", 1744+ "date": None, 1745+ } 1746 1747 1748-def get_version(): 1749+def get_version() -> str: 1750 """Get the short version string for this project.""" 1751 return get_versions()["version"] 1752 1753 1754-def get_cmdclass(): 1755- """Get the custom setuptools/distutils subclasses used by Versioneer.""" 1756+def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): 1757+ """Get the custom setuptools subclasses used by Versioneer. 1758+ 1759+ If the package uses a different cmdclass (e.g. one from numpy), it 1760+ should be provide as an argument. 1761+ """ 1762 if "versioneer" in sys.modules: 1763 del sys.modules["versioneer"] 1764 # this fixes the "python setup.py develop" case (also 'install' and 1765@@ -1495,25 +1929,25 @@ def get_cmdclass(): 1766 # parent is protected against the child's "import versioneer". By 1767 # removing ourselves from sys.modules here, before the child build 1768 # happens, we protect the child from the parent's versioneer too. 1769- # Also see https://github.com/warner/python-versioneer/issues/52 1770+ # Also see https://github.com/python-versioneer/python-versioneer/issues/52 1771 1772- cmds = {} 1773+ cmds = {} if cmdclass is None else cmdclass.copy() 1774 1775- # we add "version" to both distutils and setuptools 1776- from distutils.core import Command 1777+ # we add "version" to setuptools 1778+ from setuptools import Command 1779 1780 class cmd_version(Command): 1781 description = "report generated version string" 1782- user_options = [] 1783- boolean_options = [] 1784+ user_options: List[Tuple[str, str, str]] = [] 1785+ boolean_options: List[str] = [] 1786 1787- def initialize_options(self): 1788+ def initialize_options(self) -> None: 1789 pass 1790 1791- def finalize_options(self): 1792+ def finalize_options(self) -> None: 1793 pass 1794 1795- def run(self): 1796+ def run(self) -> None: 1797 vers = get_versions(verbose=True) 1798 print("Version: %s" % vers["version"]) 1799 print(" full-revisionid: %s" % vers.get("full-revisionid")) 1800@@ -1521,9 +1955,10 @@ def get_cmdclass(): 1801 print(" date: %s" % vers.get("date")) 1802 if vers["error"]: 1803 print(" error: %s" % vers["error"]) 1804+ 1805 cmds["version"] = cmd_version 1806 1807- # we override "build_py" in both distutils and setuptools 1808+ # we override "build_py" in setuptools 1809 # 1810 # most invocation pathways end up running build_py: 1811 # distutils/build -> build_py 1812@@ -1538,29 +1973,71 @@ def get_cmdclass(): 1813 # then does setup.py bdist_wheel, or sometimes setup.py install 1814 # setup.py egg_info -> ? 1815 1816+ # pip install -e . and setuptool/editable_wheel will invoke build_py 1817+ # but the build_py command is not expected to copy any files. 1818+ 1819 # we override different "build_py" commands for both environments 1820- if "setuptools" in sys.modules: 1821- from setuptools.command.build_py import build_py as _build_py 1822+ if "build_py" in cmds: 1823+ _build_py: Any = cmds["build_py"] 1824 else: 1825- from distutils.command.build_py import build_py as _build_py 1826+ from setuptools.command.build_py import build_py as _build_py 1827 1828 class cmd_build_py(_build_py): 1829- def run(self): 1830+ def run(self) -> None: 1831 root = get_root() 1832 cfg = get_config_from_root(root) 1833 versions = get_versions() 1834 _build_py.run(self) 1835+ if getattr(self, "editable_mode", False): 1836+ # During editable installs `.py` and data files are 1837+ # not copied to build_lib 1838+ return 1839 # now locate _version.py in the new build/ directory and replace 1840 # it with an updated value 1841 if cfg.versionfile_build: 1842- target_versionfile = os.path.join(self.build_lib, 1843- cfg.versionfile_build) 1844+ target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) 1845 print("UPDATING %s" % target_versionfile) 1846 write_to_version_file(target_versionfile, versions) 1847+ 1848 cmds["build_py"] = cmd_build_py 1849 1850+ if "build_ext" in cmds: 1851+ _build_ext: Any = cmds["build_ext"] 1852+ else: 1853+ from setuptools.command.build_ext import build_ext as _build_ext 1854+ 1855+ class cmd_build_ext(_build_ext): 1856+ def run(self) -> None: 1857+ root = get_root() 1858+ cfg = get_config_from_root(root) 1859+ versions = get_versions() 1860+ _build_ext.run(self) 1861+ if self.inplace: 1862+ # build_ext --inplace will only build extensions in 1863+ # build/lib<..> dir with no _version.py to write to. 1864+ # As in place builds will already have a _version.py 1865+ # in the module dir, we do not need to write one. 1866+ return 1867+ # now locate _version.py in the new build/ directory and replace 1868+ # it with an updated value 1869+ if not cfg.versionfile_build: 1870+ return 1871+ target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) 1872+ if not os.path.exists(target_versionfile): 1873+ print( 1874+ f"Warning: {target_versionfile} does not exist, skipping " 1875+ "version update. This can happen if you are running build_ext " 1876+ "without first running build_py." 1877+ ) 1878+ return 1879+ print("UPDATING %s" % target_versionfile) 1880+ write_to_version_file(target_versionfile, versions) 1881+ 1882+ cmds["build_ext"] = cmd_build_ext 1883+ 1884 if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1885- from cx_Freeze.dist import build_exe as _build_exe 1886+ from cx_Freeze.dist import build_exe as _build_exe # type: ignore 1887+ 1888 # nczeczulin reports that py2exe won't like the pep440-style string 1889 # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1890 # setup(console=[{ 1891@@ -1569,7 +2046,7 @@ def get_cmdclass(): 1892 # ... 1893 1894 class cmd_build_exe(_build_exe): 1895- def run(self): 1896+ def run(self) -> None: 1897 root = get_root() 1898 cfg = get_config_from_root(root) 1899 versions = get_versions() 1900@@ -1581,24 +2058,28 @@ def get_cmdclass(): 1901 os.unlink(target_versionfile) 1902 with open(cfg.versionfile_source, "w") as f: 1903 LONG = LONG_VERSION_PY[cfg.VCS] 1904- f.write(LONG % 1905- {"DOLLAR": "$", 1906- "STYLE": cfg.style, 1907- "TAG_PREFIX": cfg.tag_prefix, 1908- "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1909- "VERSIONFILE_SOURCE": cfg.versionfile_source, 1910- }) 1911+ f.write( 1912+ LONG 1913+ % { 1914+ "DOLLAR": "$", 1915+ "STYLE": cfg.style, 1916+ "TAG_PREFIX": cfg.tag_prefix, 1917+ "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1918+ "VERSIONFILE_SOURCE": cfg.versionfile_source, 1919+ } 1920+ ) 1921+ 1922 cmds["build_exe"] = cmd_build_exe 1923 del cmds["build_py"] 1924 1925- if 'py2exe' in sys.modules: # py2exe enabled? 1926+ if "py2exe" in sys.modules: # py2exe enabled? 1927 try: 1928- from py2exe.distutils_buildexe import py2exe as _py2exe # py3 1929+ from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore 1930 except ImportError: 1931- from py2exe.build_exe import py2exe as _py2exe # py2 1932+ from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore 1933 1934 class cmd_py2exe(_py2exe): 1935- def run(self): 1936+ def run(self) -> None: 1937 root = get_root() 1938 cfg = get_config_from_root(root) 1939 versions = get_versions() 1940@@ -1610,23 +2091,67 @@ def get_cmdclass(): 1941 os.unlink(target_versionfile) 1942 with open(cfg.versionfile_source, "w") as f: 1943 LONG = LONG_VERSION_PY[cfg.VCS] 1944- f.write(LONG % 1945- {"DOLLAR": "$", 1946- "STYLE": cfg.style, 1947- "TAG_PREFIX": cfg.tag_prefix, 1948- "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1949- "VERSIONFILE_SOURCE": cfg.versionfile_source, 1950- }) 1951+ f.write( 1952+ LONG 1953+ % { 1954+ "DOLLAR": "$", 1955+ "STYLE": cfg.style, 1956+ "TAG_PREFIX": cfg.tag_prefix, 1957+ "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1958+ "VERSIONFILE_SOURCE": cfg.versionfile_source, 1959+ } 1960+ ) 1961+ 1962 cmds["py2exe"] = cmd_py2exe 1963 1964+ # sdist farms its file list building out to egg_info 1965+ if "egg_info" in cmds: 1966+ _egg_info: Any = cmds["egg_info"] 1967+ else: 1968+ from setuptools.command.egg_info import egg_info as _egg_info 1969+ 1970+ class cmd_egg_info(_egg_info): 1971+ def find_sources(self) -> None: 1972+ # egg_info.find_sources builds the manifest list and writes it 1973+ # in one shot 1974+ super().find_sources() 1975+ 1976+ # Modify the filelist and normalize it 1977+ root = get_root() 1978+ cfg = get_config_from_root(root) 1979+ self.filelist.append("versioneer.py") 1980+ if cfg.versionfile_source: 1981+ # There are rare cases where versionfile_source might not be 1982+ # included by default, so we must be explicit 1983+ self.filelist.append(cfg.versionfile_source) 1984+ self.filelist.sort() 1985+ self.filelist.remove_duplicates() 1986+ 1987+ # The write method is hidden in the manifest_maker instance that 1988+ # generated the filelist and was thrown away 1989+ # We will instead replicate their final normalization (to unicode, 1990+ # and POSIX-style paths) 1991+ from setuptools import unicode_utils 1992+ 1993+ normalized = [ 1994+ unicode_utils.filesys_decode(f).replace(os.sep, "/") 1995+ for f in self.filelist.files 1996+ ] 1997+ 1998+ manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") 1999+ with open(manifest_filename, "w") as fobj: 2000+ fobj.write("\n".join(normalized)) 2001+ 2002+ cmds["egg_info"] = cmd_egg_info 2003+ 2004 # we override different "sdist" commands for both environments 2005- if "setuptools" in sys.modules: 2006- from setuptools.command.sdist import sdist as _sdist 2007+ if "sdist" in cmds: 2008+ _sdist: Any = cmds["sdist"] 2009 else: 2010- from distutils.command.sdist import sdist as _sdist 2011+ from setuptools.command.sdist import sdist as _sdist 2012 2013 class cmd_sdist(_sdist): 2014- def run(self): 2015+ def run(self) -> None: 2016 versions = get_versions() 2017 self._versioneer_generated_versions = versions 2018 # unless we update this, the command will keep using the old 2019@@ -1634,7 +2159,7 @@ def get_cmdclass(): 2020 self.distribution.metadata.version = versions["version"] 2021 return _sdist.run(self) 2022 2023- def make_release_tree(self, base_dir, files): 2024+ def make_release_tree(self, base_dir: str, files: List[str]) -> None: 2025 root = get_root() 2026 cfg = get_config_from_root(root) 2027 _sdist.make_release_tree(self, base_dir, files) 2028@@ -1643,8 +2168,10 @@ def get_cmdclass(): 2029 # updated value 2030 target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 2031 print("UPDATING %s" % target_versionfile) 2032- write_to_version_file(target_versionfile, 2033- self._versioneer_generated_versions) 2034+ write_to_version_file( 2035+ target_versionfile, self._versioneer_generated_versions 2036+ ) 2037+ 2038 cmds["sdist"] = cmd_sdist 2039 2040 return cmds 2041@@ -1687,23 +2214,26 @@ SAMPLE_CONFIG = """ 2042 2043 """ 2044 2045-INIT_PY_SNIPPET = """ 2046+OLD_SNIPPET = """ 2047 from ._version import get_versions 2048 __version__ = get_versions()['version'] 2049 del get_versions 2050 """ 2051 2052+INIT_PY_SNIPPET = """ 2053+from . import {0} 2054+__version__ = {0}.get_versions()['version'] 2055+""" 2056+ 2057 2058-def do_setup(): 2059- """Main VCS-independent setup function for installing Versioneer.""" 2060+def do_setup() -> int: 2061+ """Do main VCS-independent setup function for installing Versioneer.""" 2062 root = get_root() 2063 try: 2064 cfg = get_config_from_root(root) 2065- except (EnvironmentError, configparser.NoSectionError, 2066- configparser.NoOptionError) as e: 2067- if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 2068- print("Adding sample versioneer config to setup.cfg", 2069- file=sys.stderr) 2070+ except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: 2071+ if isinstance(e, (OSError, configparser.NoSectionError)): 2072+ print("Adding sample versioneer config to setup.cfg", file=sys.stderr) 2073 with open(os.path.join(root, "setup.cfg"), "a") as f: 2074 f.write(SAMPLE_CONFIG) 2075 print(CONFIG_ERROR, file=sys.stderr) 2076@@ -1712,71 +2242,49 @@ def do_setup(): 2077 print(" creating %s" % cfg.versionfile_source) 2078 with open(cfg.versionfile_source, "w") as f: 2079 LONG = LONG_VERSION_PY[cfg.VCS] 2080- f.write(LONG % {"DOLLAR": "$", 2081- "STYLE": cfg.style, 2082- "TAG_PREFIX": cfg.tag_prefix, 2083- "PARENTDIR_PREFIX": cfg.parentdir_prefix, 2084- "VERSIONFILE_SOURCE": cfg.versionfile_source, 2085- }) 2086- 2087- ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 2088- "__init__.py") 2089+ f.write( 2090+ LONG 2091+ % { 2092+ "DOLLAR": "$", 2093+ "STYLE": cfg.style, 2094+ "TAG_PREFIX": cfg.tag_prefix, 2095+ "PARENTDIR_PREFIX": cfg.parentdir_prefix, 2096+ "VERSIONFILE_SOURCE": cfg.versionfile_source, 2097+ } 2098+ ) 2099+ 2100+ ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") 2101+ maybe_ipy: Optional[str] = ipy 2102 if os.path.exists(ipy): 2103 try: 2104 with open(ipy, "r") as f: 2105 old = f.read() 2106- except EnvironmentError: 2107+ except OSError: 2108 old = "" 2109- if INIT_PY_SNIPPET not in old: 2110+ module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] 2111+ snippet = INIT_PY_SNIPPET.format(module) 2112+ if OLD_SNIPPET in old: 2113+ print(" replacing boilerplate in %s" % ipy) 2114+ with open(ipy, "w") as f: 2115+ f.write(old.replace(OLD_SNIPPET, snippet)) 2116+ elif snippet not in old: 2117 print(" appending to %s" % ipy) 2118 with open(ipy, "a") as f: 2119- f.write(INIT_PY_SNIPPET) 2120+ f.write(snippet) 2121 else: 2122 print(" %s unmodified" % ipy) 2123 else: 2124 print(" %s doesn't exist, ok" % ipy) 2125- ipy = None 2126- 2127- # Make sure both the top-level "versioneer.py" and versionfile_source 2128- # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 2129- # they'll be copied into source distributions. Pip won't be able to 2130- # install the package without this. 2131- manifest_in = os.path.join(root, "MANIFEST.in") 2132- simple_includes = set() 2133- try: 2134- with open(manifest_in, "r") as f: 2135- for line in f: 2136- if line.startswith("include "): 2137- for include in line.split()[1:]: 2138- simple_includes.add(include) 2139- except EnvironmentError: 2140- pass 2141- # That doesn't cover everything MANIFEST.in can do 2142- # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 2143- # it might give some false negatives. Appending redundant 'include' 2144- # lines is safe, though. 2145- if "versioneer.py" not in simple_includes: 2146- print(" appending 'versioneer.py' to MANIFEST.in") 2147- with open(manifest_in, "a") as f: 2148- f.write("include versioneer.py\n") 2149- else: 2150- print(" 'versioneer.py' already in MANIFEST.in") 2151- if cfg.versionfile_source not in simple_includes: 2152- print(" appending versionfile_source ('%s') to MANIFEST.in" % 2153- cfg.versionfile_source) 2154- with open(manifest_in, "a") as f: 2155- f.write("include %s\n" % cfg.versionfile_source) 2156- else: 2157- print(" versionfile_source already in MANIFEST.in") 2158+ maybe_ipy = None 2159 2160 # Make VCS-specific changes. For git, this means creating/changing 2161 # .gitattributes to mark _version.py for export-subst keyword 2162 # substitution. 2163- do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 2164+ do_vcs_install(cfg.versionfile_source, maybe_ipy) 2165 return 0 2166 2167 2168-def scan_setup_py(): 2169+def scan_setup_py() -> int: 2170 """Validate the contents of setup.py against Versioneer's expectations.""" 2171 found = set() 2172 setters = False 2173@@ -1813,10 +2321,14 @@ def scan_setup_py(): 2174 return errors 2175 2176 2177+def setup_command() -> NoReturn: 2178+ """Set up Versioneer and exit with appropriate error code.""" 2179+ errors = do_setup() 2180+ errors += scan_setup_py() 2181+ sys.exit(1 if errors else 0) 2182+ 2183+ 2184 if __name__ == "__main__": 2185 cmd = sys.argv[1] 2186 if cmd == "setup": 2187- errors = do_setup() 2188- errors += scan_setup_py() 2189- if errors: 2190- sys.exit(1) 2191+ setup_command() 2192-- 21932.41.0 2194 2195