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