2
1
Эх сурвалжийг харах

package/python-spake2: update versioneer to 0.29

Versioneer < 0.21 is incompatible with Python 3.12.0. Use the latest version
which is 0.29 as of this commit.

Signed-off-by: Adam Duskett <adam.duskett@amarulasolutions.com>
Signed-off-by: Arnout Vandecappelle <arnout@mind.be>
Adam Duskett 1 жил өмнө
parent
commit
0bf73cce2b

+ 2194 - 0
package/python-spake2/0001-Update-versioneer-to-0.29.patch

@@ -0,0 +1,2194 @@
+From 5b5436f11d01e66505bb4c148304c2eb49346529 Mon Sep 17 00:00:00 2001
+From: Adam Duskett <adam.duskett@amarulasolutions.com>
+Date: Tue, 24 Oct 2023 09:56:57 +0200
+Subject: [PATCH] Update versioneer to 0.29
+
+Fixes builds against Python 3.12.0
+
+Upstream: https://github.com/warner/python-spake2/pull/15
+
+Signed-off-by: Adam Duskett <adam.duskett@amarulasolutions.com>
+---
+ versioneer.py | 1350 ++++++++++++++++++++++++++++++++++---------------
+ 1 file changed, 931 insertions(+), 419 deletions(-)
+
+diff --git a/versioneer.py b/versioneer.py
+index 64fea1c..de97d90 100644
+--- a/versioneer.py
++++ b/versioneer.py
+@@ -1,5 +1,4 @@
+-
+-# Version: 0.18
++# Version: 0.29
+ 
+ """The Versioneer - like a rocketeer, but for versions.
+ 
+@@ -7,18 +6,14 @@ The Versioneer
+ ==============
+ 
+ * like a rocketeer, but for versions!
+-* https://github.com/warner/python-versioneer
++* https://github.com/python-versioneer/python-versioneer
+ * Brian Warner
+-* License: Public Domain
+-* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy
+-* [![Latest Version]
+-(https://pypip.in/version/versioneer/badge.svg?style=flat)
+-](https://pypi.python.org/pypi/versioneer/)
+-* [![Build Status]
+-(https://travis-ci.org/warner/python-versioneer.png?branch=master)
+-](https://travis-ci.org/warner/python-versioneer)
+-
+-This is a tool for managing a recorded version number in distutils-based
++* License: Public Domain (Unlicense)
++* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3
++* [![Latest Version][pypi-image]][pypi-url]
++* [![Build Status][travis-image]][travis-url]
++
++This is a tool for managing a recorded version number in setuptools-based
+ python projects. The goal is to remove the tedious and error-prone "update
+ the embedded version string" step from your release process. Making a new
+ release should be as easy as recording a new tag in your version-control
+@@ -27,9 +22,38 @@ system, and maybe making new tarballs.
+ 
+ ## Quick Install
+ 
+-* `pip install versioneer` to somewhere to your $PATH
+-* add a `[versioneer]` section to your setup.cfg (see below)
+-* run `versioneer install` in your source tree, commit the results
++Versioneer provides two installation modes. The "classic" vendored mode installs
++a copy of versioneer into your repository. The experimental build-time dependency mode
++is intended to allow you to skip this step and simplify the process of upgrading.
++
++### Vendored mode
++
++* `pip install versioneer` to somewhere in your $PATH
++   * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is
++     available, so you can also use `conda install -c conda-forge versioneer`
++* add a `[tool.versioneer]` section to your `pyproject.toml` or a
++  `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md))
++   * Note that you will need to add `tomli; python_version < "3.11"` to your
++     build-time dependencies if you use `pyproject.toml`
++* run `versioneer install --vendor` in your source tree, commit the results
++* verify version information with `python setup.py version`
++
++### Build-time dependency mode
++
++* `pip install versioneer` to somewhere in your $PATH
++   * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is
++     available, so you can also use `conda install -c conda-forge versioneer`
++* add a `[tool.versioneer]` section to your `pyproject.toml` or a
++  `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md))
++* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`)
++  to the `requires` key of the `build-system` table in `pyproject.toml`:
++  ```toml
++  [build-system]
++  requires = ["setuptools", "versioneer[toml]"]
++  build-backend = "setuptools.build_meta"
++  ```
++* run `versioneer install --no-vendor` in your source tree, commit the results
++* verify version information with `python setup.py version`
+ 
+ ## Version Identifiers
+ 
+@@ -61,7 +85,7 @@ version 1.3). Many VCS systems can report a description that captures this,
+ for example `git describe --tags --dirty --always` reports things like
+ "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the
+ 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has
+-uncommitted changes.
++uncommitted changes).
+ 
+ The version identifier is used for multiple purposes:
+ 
+@@ -166,7 +190,7 @@ which may help identify what went wrong).
+ 
+ Some situations are known to cause problems for Versioneer. This details the
+ most significant ones. More can be found on Github
+-[issues page](https://github.com/warner/python-versioneer/issues).
++[issues page](https://github.com/python-versioneer/python-versioneer/issues).
+ 
+ ### Subprojects
+ 
+@@ -180,7 +204,7 @@ two common reasons why `setup.py` might not be in the root:
+   `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI
+   distributions (and upload multiple independently-installable tarballs).
+ * Source trees whose main purpose is to contain a C library, but which also
+-  provide bindings to Python (and perhaps other langauges) in subdirectories.
++  provide bindings to Python (and perhaps other languages) in subdirectories.
+ 
+ Versioneer will look for `.git` in parent directories, and most operations
+ should get the right version string. However `pip` and `setuptools` have bugs
+@@ -194,9 +218,9 @@ work too.
+ Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in
+ some later version.
+ 
+-[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking
++[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking
+ this issue. The discussion in
+-[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the
++[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the
+ issue from the Versioneer side in more detail.
+ [pip PR#3176](https://github.com/pypa/pip/pull/3176) and
+ [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve
+@@ -224,31 +248,20 @@ regenerated while a different version is checked out. Many setup.py commands
+ cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into
+ a different virtualenv), so this can be surprising.
+ 
+-[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes
++[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes
+ this one, but upgrading to a newer version of setuptools should probably
+ resolve it.
+ 
+-### Unicode version strings
+-
+-While Versioneer works (and is continually tested) with both Python 2 and
+-Python 3, it is not entirely consistent with bytes-vs-unicode distinctions.
+-Newer releases probably generate unicode version strings on py2. It's not
+-clear that this is wrong, but it may be surprising for applications when then
+-write these strings to a network connection or include them in bytes-oriented
+-APIs like cryptographic checksums.
+-
+-[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates
+-this question.
+-
+ 
+ ## Updating Versioneer
+ 
+ To upgrade your project to a new release of Versioneer, do the following:
+ 
+ * install the new Versioneer (`pip install -U versioneer` or equivalent)
+-* edit `setup.cfg`, if necessary, to include any new configuration settings
+-  indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details.
+-* re-run `versioneer install` in your source tree, to replace
++* edit `setup.cfg` and `pyproject.toml`, if necessary,
++  to include any new configuration settings indicated by the release notes.
++  See [UPGRADING](./UPGRADING.md) for details.
++* re-run `versioneer install --[no-]vendor` in your source tree, to replace
+   `SRC/_version.py`
+ * commit any changed files
+ 
+@@ -265,35 +278,70 @@ installation by editing setup.py . Alternatively, it might go the other
+ direction and include code from all supported VCS systems, reducing the
+ number of intermediate scripts.
+ 
++## Similar projects
++
++* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time
++  dependency
++* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of
++  versioneer
++* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools
++  plugin
+ 
+ ## License
+ 
+ To make Versioneer easier to embed, all its code is dedicated to the public
+ domain. The `_version.py` that it creates is also in the public domain.
+-Specifically, both are released under the Creative Commons "Public Domain
+-Dedication" license (CC0-1.0), as described in
+-https://creativecommons.org/publicdomain/zero/1.0/ .
++Specifically, both are released under the "Unlicense", as described in
++https://unlicense.org/.
++
++[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg
++[pypi-url]: https://pypi.python.org/pypi/versioneer/
++[travis-image]:
++https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg
++[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer
+ 
+ """
++# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring
++# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements
++# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error
++# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with
++# pylint:disable=attribute-defined-outside-init,too-many-arguments
+ 
+-from __future__ import print_function
+-try:
+-    import configparser
+-except ImportError:
+-    import ConfigParser as configparser
++import configparser
+ import errno
+ import json
+ import os
+ import re
+ import subprocess
+ import sys
++from pathlib import Path
++from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union
++from typing import NoReturn
++import functools
++
++have_tomllib = True
++if sys.version_info >= (3, 11):
++    import tomllib
++else:
++    try:
++        import tomli as tomllib
++    except ImportError:
++        have_tomllib = False
+ 
+ 
+ class VersioneerConfig:
+     """Container for Versioneer configuration parameters."""
+ 
++    VCS: str
++    style: str
++    tag_prefix: str
++    versionfile_source: str
++    versionfile_build: Optional[str]
++    parentdir_prefix: Optional[str]
++    verbose: Optional[bool]
++
+ 
+-def get_root():
++def get_root() -> str:
+     """Get the project root directory.
+ 
+     We require that all commands are run from the project root, i.e. the
+@@ -301,18 +349,30 @@ def get_root():
+     """
+     root = os.path.realpath(os.path.abspath(os.getcwd()))
+     setup_py = os.path.join(root, "setup.py")
++    pyproject_toml = os.path.join(root, "pyproject.toml")
+     versioneer_py = os.path.join(root, "versioneer.py")
+-    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
++    if not (
++        os.path.exists(setup_py)
++        or os.path.exists(pyproject_toml)
++        or os.path.exists(versioneer_py)
++    ):
+         # allow 'python path/to/setup.py COMMAND'
+         root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
+         setup_py = os.path.join(root, "setup.py")
++        pyproject_toml = os.path.join(root, "pyproject.toml")
+         versioneer_py = os.path.join(root, "versioneer.py")
+-    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+-        err = ("Versioneer was unable to run the project root directory. "
+-               "Versioneer requires setup.py to be executed from "
+-               "its immediate directory (like 'python setup.py COMMAND'), "
+-               "or in a way that lets it use sys.argv[0] to find the root "
+-               "(like 'python path/to/setup.py COMMAND').")
++    if not (
++        os.path.exists(setup_py)
++        or os.path.exists(pyproject_toml)
++        or os.path.exists(versioneer_py)
++    ):
++        err = (
++            "Versioneer was unable to run the project root directory. "
++            "Versioneer requires setup.py to be executed from "
++            "its immediate directory (like 'python setup.py COMMAND'), "
++            "or in a way that lets it use sys.argv[0] to find the root "
++            "(like 'python path/to/setup.py COMMAND')."
++        )
+         raise VersioneerBadRootError(err)
+     try:
+         # Certain runtime workflows (setup.py install/develop in a setuptools
+@@ -321,43 +381,64 @@ def get_root():
+         # module-import table will cache the first one. So we can't use
+         # os.path.dirname(__file__), as that will find whichever
+         # versioneer.py was first imported, even in later projects.
+-        me = os.path.realpath(os.path.abspath(__file__))
+-        me_dir = os.path.normcase(os.path.splitext(me)[0])
++        my_path = os.path.realpath(os.path.abspath(__file__))
++        me_dir = os.path.normcase(os.path.splitext(my_path)[0])
+         vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0])
+-        if me_dir != vsr_dir:
+-            print("Warning: build in %s is using versioneer.py from %s"
+-                  % (os.path.dirname(me), versioneer_py))
++        if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals():
++            print(
++                "Warning: build in %s is using versioneer.py from %s"
++                % (os.path.dirname(my_path), versioneer_py)
++            )
+     except NameError:
+         pass
+     return root
+ 
+ 
+-def get_config_from_root(root):
++def get_config_from_root(root: str) -> VersioneerConfig:
+     """Read the project setup.cfg file to determine Versioneer config."""
+-    # This might raise EnvironmentError (if setup.cfg is missing), or
++    # This might raise OSError (if setup.cfg is missing), or
+     # configparser.NoSectionError (if it lacks a [versioneer] section), or
+     # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
+     # the top of versioneer.py for instructions on writing your setup.cfg .
+-    setup_cfg = os.path.join(root, "setup.cfg")
+-    parser = configparser.SafeConfigParser()
+-    with open(setup_cfg, "r") as f:
+-        parser.readfp(f)
+-    VCS = parser.get("versioneer", "VCS")  # mandatory
+-
+-    def get(parser, name):
+-        if parser.has_option("versioneer", name):
+-            return parser.get("versioneer", name)
+-        return None
++    root_pth = Path(root)
++    pyproject_toml = root_pth / "pyproject.toml"
++    setup_cfg = root_pth / "setup.cfg"
++    section: Union[Dict[str, Any], configparser.SectionProxy, None] = None
++    if pyproject_toml.exists() and have_tomllib:
++        try:
++            with open(pyproject_toml, "rb") as fobj:
++                pp = tomllib.load(fobj)
++            section = pp["tool"]["versioneer"]
++        except (tomllib.TOMLDecodeError, KeyError) as e:
++            print(f"Failed to load config from {pyproject_toml}: {e}")
++            print("Try to load it from setup.cfg")
++    if not section:
++        parser = configparser.ConfigParser()
++        with open(setup_cfg) as cfg_file:
++            parser.read_file(cfg_file)
++        parser.get("versioneer", "VCS")  # raise error if missing
++
++        section = parser["versioneer"]
++
++    # `cast`` really shouldn't be used, but its simplest for the
++    # common VersioneerConfig users at the moment. We verify against
++    # `None` values elsewhere where it matters
++
+     cfg = VersioneerConfig()
+-    cfg.VCS = VCS
+-    cfg.style = get(parser, "style") or ""
+-    cfg.versionfile_source = get(parser, "versionfile_source")
+-    cfg.versionfile_build = get(parser, "versionfile_build")
+-    cfg.tag_prefix = get(parser, "tag_prefix")
+-    if cfg.tag_prefix in ("''", '""'):
++    cfg.VCS = section["VCS"]
++    cfg.style = section.get("style", "")
++    cfg.versionfile_source = cast(str, section.get("versionfile_source"))
++    cfg.versionfile_build = section.get("versionfile_build")
++    cfg.tag_prefix = cast(str, section.get("tag_prefix"))
++    if cfg.tag_prefix in ("''", '""', None):
+         cfg.tag_prefix = ""
+-    cfg.parentdir_prefix = get(parser, "parentdir_prefix")
+-    cfg.verbose = get(parser, "verbose")
++    cfg.parentdir_prefix = section.get("parentdir_prefix")
++    if isinstance(section, configparser.SectionProxy):
++        # Make sure configparser translates to bool
++        cfg.verbose = section.getboolean("verbose")
++    else:
++        cfg.verbose = section.get("verbose")
++
+     return cfg
+ 
+ 
+@@ -366,37 +447,54 @@ class NotThisMethod(Exception):
+ 
+ 
+ # these dictionaries contain VCS-specific tools
+-LONG_VERSION_PY = {}
+-HANDLERS = {}
++LONG_VERSION_PY: Dict[str, str] = {}
++HANDLERS: Dict[str, Dict[str, Callable]] = {}
+ 
+ 
+-def register_vcs_handler(vcs, method):  # decorator
+-    """Decorator to mark a method as the handler for a particular VCS."""
+-    def decorate(f):
++def register_vcs_handler(vcs: str, method: str) -> Callable:  # decorator
++    """Create decorator to mark a method as the handler of a VCS."""
++
++    def decorate(f: Callable) -> Callable:
+         """Store f in HANDLERS[vcs][method]."""
+-        if vcs not in HANDLERS:
+-            HANDLERS[vcs] = {}
+-        HANDLERS[vcs][method] = f
++        HANDLERS.setdefault(vcs, {})[method] = f
+         return f
++
+     return decorate
+ 
+ 
+-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+-                env=None):
++def run_command(
++    commands: List[str],
++    args: List[str],
++    cwd: Optional[str] = None,
++    verbose: bool = False,
++    hide_stderr: bool = False,
++    env: Optional[Dict[str, str]] = None,
++) -> Tuple[Optional[str], Optional[int]]:
+     """Call the given command(s)."""
+     assert isinstance(commands, list)
+-    p = None
+-    for c in commands:
++    process = None
++
++    popen_kwargs: Dict[str, Any] = {}
++    if sys.platform == "win32":
++        # This hides the console window if pythonw.exe is used
++        startupinfo = subprocess.STARTUPINFO()
++        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
++        popen_kwargs["startupinfo"] = startupinfo
++
++    for command in commands:
+         try:
+-            dispcmd = str([c] + args)
++            dispcmd = str([command] + args)
+             # remember shell=False, so use git.cmd on windows, not just git
+-            p = subprocess.Popen([c] + args, cwd=cwd, env=env,
+-                                 stdout=subprocess.PIPE,
+-                                 stderr=(subprocess.PIPE if hide_stderr
+-                                         else None))
++            process = subprocess.Popen(
++                [command] + args,
++                cwd=cwd,
++                env=env,
++                stdout=subprocess.PIPE,
++                stderr=(subprocess.PIPE if hide_stderr else None),
++                **popen_kwargs,
++            )
+             break
+-        except EnvironmentError:
+-            e = sys.exc_info()[1]
++        except OSError as e:
+             if e.errno == errno.ENOENT:
+                 continue
+             if verbose:
+@@ -407,26 +505,27 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+         if verbose:
+             print("unable to find command, tried %s" % (commands,))
+         return None, None
+-    stdout = p.communicate()[0].strip()
+-    if sys.version_info[0] >= 3:
+-        stdout = stdout.decode()
+-    if p.returncode != 0:
++    stdout = process.communicate()[0].strip().decode()
++    if process.returncode != 0:
+         if verbose:
+             print("unable to run %s (error)" % dispcmd)
+             print("stdout was %s" % stdout)
+-        return None, p.returncode
+-    return stdout, p.returncode
++        return None, process.returncode
++    return stdout, process.returncode
+ 
+ 
+-LONG_VERSION_PY['git'] = '''
++LONG_VERSION_PY[
++    "git"
++] = r'''
+ # This file helps to compute a version number in source trees obtained from
+ # git-archive tarball (such as those provided by githubs download-from-tag
+ # feature). Distribution tarballs (built by setup.py sdist) and build
+ # directories (produced by setup.py build) will contain a much shorter file
+ # that just contains the computed version number.
+ 
+-# This file is released into the public domain. Generated by
+-# versioneer-0.18 (https://github.com/warner/python-versioneer)
++# This file is released into the public domain.
++# Generated by versioneer-0.29
++# https://github.com/python-versioneer/python-versioneer
+ 
+ """Git implementation of _version.py."""
+ 
+@@ -435,9 +534,11 @@ import os
+ import re
+ import subprocess
+ import sys
++from typing import Any, Callable, Dict, List, Optional, Tuple
++import functools
+ 
+ 
+-def get_keywords():
++def get_keywords() -> Dict[str, str]:
+     """Get the keywords needed to look up the version information."""
+     # these strings will be replaced by git during git-archive.
+     # setup.py/versioneer.py will grep for the variable names, so they must
+@@ -453,8 +554,15 @@ def get_keywords():
+ class VersioneerConfig:
+     """Container for Versioneer configuration parameters."""
+ 
++    VCS: str
++    style: str
++    tag_prefix: str
++    parentdir_prefix: str
++    versionfile_source: str
++    verbose: bool
++
+ 
+-def get_config():
++def get_config() -> VersioneerConfig:
+     """Create, populate and return the VersioneerConfig() object."""
+     # these strings are filled in when 'setup.py versioneer' creates
+     # _version.py
+@@ -472,13 +580,13 @@ class NotThisMethod(Exception):
+     """Exception raised if a method is not valid for the current scenario."""
+ 
+ 
+-LONG_VERSION_PY = {}
+-HANDLERS = {}
++LONG_VERSION_PY: Dict[str, str] = {}
++HANDLERS: Dict[str, Dict[str, Callable]] = {}
+ 
+ 
+-def register_vcs_handler(vcs, method):  # decorator
+-    """Decorator to mark a method as the handler for a particular VCS."""
+-    def decorate(f):
++def register_vcs_handler(vcs: str, method: str) -> Callable:  # decorator
++    """Create decorator to mark a method as the handler of a VCS."""
++    def decorate(f: Callable) -> Callable:
+         """Store f in HANDLERS[vcs][method]."""
+         if vcs not in HANDLERS:
+             HANDLERS[vcs] = {}
+@@ -487,22 +595,35 @@ def register_vcs_handler(vcs, method):  # decorator
+     return decorate
+ 
+ 
+-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+-                env=None):
++def run_command(
++    commands: List[str],
++    args: List[str],
++    cwd: Optional[str] = None,
++    verbose: bool = False,
++    hide_stderr: bool = False,
++    env: Optional[Dict[str, str]] = None,
++) -> Tuple[Optional[str], Optional[int]]:
+     """Call the given command(s)."""
+     assert isinstance(commands, list)
+-    p = None
+-    for c in commands:
++    process = None
++
++    popen_kwargs: Dict[str, Any] = {}
++    if sys.platform == "win32":
++        # This hides the console window if pythonw.exe is used
++        startupinfo = subprocess.STARTUPINFO()
++        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
++        popen_kwargs["startupinfo"] = startupinfo
++
++    for command in commands:
+         try:
+-            dispcmd = str([c] + args)
++            dispcmd = str([command] + args)
+             # remember shell=False, so use git.cmd on windows, not just git
+-            p = subprocess.Popen([c] + args, cwd=cwd, env=env,
+-                                 stdout=subprocess.PIPE,
+-                                 stderr=(subprocess.PIPE if hide_stderr
+-                                         else None))
++            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
++                                       stdout=subprocess.PIPE,
++                                       stderr=(subprocess.PIPE if hide_stderr
++                                               else None), **popen_kwargs)
+             break
+-        except EnvironmentError:
+-            e = sys.exc_info()[1]
++        except OSError as e:
+             if e.errno == errno.ENOENT:
+                 continue
+             if verbose:
+@@ -513,18 +634,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+         if verbose:
+             print("unable to find command, tried %%s" %% (commands,))
+         return None, None
+-    stdout = p.communicate()[0].strip()
+-    if sys.version_info[0] >= 3:
+-        stdout = stdout.decode()
+-    if p.returncode != 0:
++    stdout = process.communicate()[0].strip().decode()
++    if process.returncode != 0:
+         if verbose:
+             print("unable to run %%s (error)" %% dispcmd)
+             print("stdout was %%s" %% stdout)
+-        return None, p.returncode
+-    return stdout, p.returncode
++        return None, process.returncode
++    return stdout, process.returncode
+ 
+ 
+-def versions_from_parentdir(parentdir_prefix, root, verbose):
++def versions_from_parentdir(
++    parentdir_prefix: str,
++    root: str,
++    verbose: bool,
++) -> Dict[str, Any]:
+     """Try to determine the version from the parent directory name.
+ 
+     Source tarballs conventionally unpack into a directory that includes both
+@@ -533,15 +656,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
+     """
+     rootdirs = []
+ 
+-    for i in range(3):
++    for _ in range(3):
+         dirname = os.path.basename(root)
+         if dirname.startswith(parentdir_prefix):
+             return {"version": dirname[len(parentdir_prefix):],
+                     "full-revisionid": None,
+                     "dirty": False, "error": None, "date": None}
+-        else:
+-            rootdirs.append(root)
+-            root = os.path.dirname(root)  # up a level
++        rootdirs.append(root)
++        root = os.path.dirname(root)  # up a level
+ 
+     if verbose:
+         print("Tried directories %%s but none started with prefix %%s" %%
+@@ -550,41 +672,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
+ 
+ 
+ @register_vcs_handler("git", "get_keywords")
+-def git_get_keywords(versionfile_abs):
++def git_get_keywords(versionfile_abs: str) -> Dict[str, str]:
+     """Extract version information from the given file."""
+     # the code embedded in _version.py can just fetch the value of these
+     # keywords. When used from setup.py, we don't want to import _version.py,
+     # so we do it with a regexp instead. This function is not used from
+     # _version.py.
+-    keywords = {}
++    keywords: Dict[str, str] = {}
+     try:
+-        f = open(versionfile_abs, "r")
+-        for line in f.readlines():
+-            if line.strip().startswith("git_refnames ="):
+-                mo = re.search(r'=\s*"(.*)"', line)
+-                if mo:
+-                    keywords["refnames"] = mo.group(1)
+-            if line.strip().startswith("git_full ="):
+-                mo = re.search(r'=\s*"(.*)"', line)
+-                if mo:
+-                    keywords["full"] = mo.group(1)
+-            if line.strip().startswith("git_date ="):
+-                mo = re.search(r'=\s*"(.*)"', line)
+-                if mo:
+-                    keywords["date"] = mo.group(1)
+-        f.close()
+-    except EnvironmentError:
++        with open(versionfile_abs, "r") as fobj:
++            for line in fobj:
++                if line.strip().startswith("git_refnames ="):
++                    mo = re.search(r'=\s*"(.*)"', line)
++                    if mo:
++                        keywords["refnames"] = mo.group(1)
++                if line.strip().startswith("git_full ="):
++                    mo = re.search(r'=\s*"(.*)"', line)
++                    if mo:
++                        keywords["full"] = mo.group(1)
++                if line.strip().startswith("git_date ="):
++                    mo = re.search(r'=\s*"(.*)"', line)
++                    if mo:
++                        keywords["date"] = mo.group(1)
++    except OSError:
+         pass
+     return keywords
+ 
+ 
+ @register_vcs_handler("git", "keywords")
+-def git_versions_from_keywords(keywords, tag_prefix, verbose):
++def git_versions_from_keywords(
++    keywords: Dict[str, str],
++    tag_prefix: str,
++    verbose: bool,
++) -> Dict[str, Any]:
+     """Get version information from git keywords."""
+-    if not keywords:
+-        raise NotThisMethod("no keywords at all, weird")
++    if "refnames" not in keywords:
++        raise NotThisMethod("Short version file found")
+     date = keywords.get("date")
+     if date is not None:
++        # Use only the last line.  Previous lines may contain GPG signature
++        # information.
++        date = date.splitlines()[-1]
++
+         # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant
+         # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601
+         # -like" string, which we must then edit to make compliant), because
+@@ -597,11 +726,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+         if verbose:
+             print("keywords are unexpanded, not using")
+         raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+-    refs = set([r.strip() for r in refnames.strip("()").split(",")])
++    refs = {r.strip() for r in refnames.strip("()").split(",")}
+     # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+     # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+     TAG = "tag: "
+-    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
++    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
+     if not tags:
+         # Either we're using git < 1.8.3, or there really are no tags. We use
+         # a heuristic: assume all version tags have a digit. The old git %%d
+@@ -610,7 +739,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+         # between branches and tags. By ignoring refnames without digits, we
+         # filter out many common branch names like "release" and
+         # "stabilization", as well as "HEAD" and "master".
+-        tags = set([r for r in refs if re.search(r'\d', r)])
++        tags = {r for r in refs if re.search(r'\d', r)}
+         if verbose:
+             print("discarding '%%s', no digits" %% ",".join(refs - tags))
+     if verbose:
+@@ -619,6 +748,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+         # sorting will prefer e.g. "2.0" over "2.0rc1"
+         if ref.startswith(tag_prefix):
+             r = ref[len(tag_prefix):]
++            # Filter out refs that exactly match prefix or that don't start
++            # with a number once the prefix is stripped (mostly a concern
++            # when prefix is '')
++            if not re.match(r'\d', r):
++                continue
+             if verbose:
+                 print("picking %%s" %% r)
+             return {"version": r,
+@@ -634,7 +768,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+ 
+ 
+ @register_vcs_handler("git", "pieces_from_vcs")
+-def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
++def git_pieces_from_vcs(
++    tag_prefix: str,
++    root: str,
++    verbose: bool,
++    runner: Callable = run_command
++) -> Dict[str, Any]:
+     """Get version from 'git describe' in the root of the source tree.
+ 
+     This only gets called if the git-archive 'subst' keywords were *not*
+@@ -645,8 +784,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+     if sys.platform == "win32":
+         GITS = ["git.cmd", "git.exe"]
+ 
+-    out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
+-                          hide_stderr=True)
++    # GIT_DIR can interfere with correct operation of Versioneer.
++    # It may be intended to be passed to the Versioneer-versioned project,
++    # but that should not change where we get our version from.
++    env = os.environ.copy()
++    env.pop("GIT_DIR", None)
++    runner = functools.partial(runner, env=env)
++
++    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
++                   hide_stderr=not verbose)
+     if rc != 0:
+         if verbose:
+             print("Directory %%s not under git control" %% root)
+@@ -654,24 +800,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+ 
+     # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+     # if there isn't one, this yields HEX[-dirty] (no NUM)
+-    describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
+-                                          "--always", "--long",
+-                                          "--match", "%%s*" %% tag_prefix],
+-                                   cwd=root)
++    describe_out, rc = runner(GITS, [
++        "describe", "--tags", "--dirty", "--always", "--long",
++        "--match", f"{tag_prefix}[[:digit:]]*"
++    ], cwd=root)
+     # --long was added in git-1.5.5
+     if describe_out is None:
+         raise NotThisMethod("'git describe' failed")
+     describe_out = describe_out.strip()
+-    full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
++    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
+     if full_out is None:
+         raise NotThisMethod("'git rev-parse' failed")
+     full_out = full_out.strip()
+ 
+-    pieces = {}
++    pieces: Dict[str, Any] = {}
+     pieces["long"] = full_out
+     pieces["short"] = full_out[:7]  # maybe improved later
+     pieces["error"] = None
+ 
++    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
++                             cwd=root)
++    # --abbrev-ref was added in git-1.6.3
++    if rc != 0 or branch_name is None:
++        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
++    branch_name = branch_name.strip()
++
++    if branch_name == "HEAD":
++        # If we aren't exactly on a branch, pick a branch which represents
++        # the current commit. If all else fails, we are on a branchless
++        # commit.
++        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
++        # --contains was added in git-1.5.4
++        if rc != 0 or branches is None:
++            raise NotThisMethod("'git branch --contains' returned error")
++        branches = branches.split("\n")
++
++        # Remove the first line if we're running detached
++        if "(" in branches[0]:
++            branches.pop(0)
++
++        # Strip off the leading "* " from the list of branches.
++        branches = [branch[2:] for branch in branches]
++        if "master" in branches:
++            branch_name = "master"
++        elif not branches:
++            branch_name = None
++        else:
++            # Pick the first branch that is returned. Good or bad.
++            branch_name = branches[0]
++
++    pieces["branch"] = branch_name
++
+     # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+     # TAG might have hyphens.
+     git_describe = describe_out
+@@ -688,7 +867,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+         # TAG-NUM-gHEX
+         mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+         if not mo:
+-            # unparseable. Maybe git-describe is misbehaving?
++            # unparsable. Maybe git-describe is misbehaving?
+             pieces["error"] = ("unable to parse git-describe output: '%%s'"
+                                %% describe_out)
+             return pieces
+@@ -713,26 +892,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+     else:
+         # HEX: no tags
+         pieces["closest-tag"] = None
+-        count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
+-                                    cwd=root)
+-        pieces["distance"] = int(count_out)  # total number of commits
++        out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root)
++        pieces["distance"] = len(out.split())  # total number of commits
+ 
+     # commit date: see ISO-8601 comment in git_versions_from_keywords()
+-    date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"],
+-                       cwd=root)[0].strip()
++    date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip()
++    # Use only the last line.  Previous lines may contain GPG signature
++    # information.
++    date = date.splitlines()[-1]
+     pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+ 
+     return pieces
+ 
+ 
+-def plus_or_dot(pieces):
++def plus_or_dot(pieces: Dict[str, Any]) -> str:
+     """Return a + if we don't already have one, else return a ."""
+     if "+" in pieces.get("closest-tag", ""):
+         return "."
+     return "+"
+ 
+ 
+-def render_pep440(pieces):
++def render_pep440(pieces: Dict[str, Any]) -> str:
+     """Build up version string, with post-release "local version identifier".
+ 
+     Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+@@ -757,23 +937,71 @@ def render_pep440(pieces):
+     return rendered
+ 
+ 
+-def render_pep440_pre(pieces):
+-    """TAG[.post.devDISTANCE] -- No -dirty.
++def render_pep440_branch(pieces: Dict[str, Any]) -> str:
++    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
++
++    The ".dev0" means not master branch. Note that .dev0 sorts backwards
++    (a feature branch will appear "older" than the master branch).
+ 
+     Exceptions:
+-    1: no tags. 0.post.devDISTANCE
++    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
+     """
+     if pieces["closest-tag"]:
+         rendered = pieces["closest-tag"]
++        if pieces["distance"] or pieces["dirty"]:
++            if pieces["branch"] != "master":
++                rendered += ".dev0"
++            rendered += plus_or_dot(pieces)
++            rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
++            if pieces["dirty"]:
++                rendered += ".dirty"
++    else:
++        # exception #1
++        rendered = "0"
++        if pieces["branch"] != "master":
++            rendered += ".dev0"
++        rendered += "+untagged.%%d.g%%s" %% (pieces["distance"],
++                                          pieces["short"])
++        if pieces["dirty"]:
++            rendered += ".dirty"
++    return rendered
++
++
++def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]:
++    """Split pep440 version string at the post-release segment.
++
++    Returns the release segments before the post-release and the
++    post-release version number (or -1 if no post-release segment is present).
++    """
++    vc = str.split(ver, ".post")
++    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
++
++
++def render_pep440_pre(pieces: Dict[str, Any]) -> str:
++    """TAG[.postN.devDISTANCE] -- No -dirty.
++
++    Exceptions:
++    1: no tags. 0.post0.devDISTANCE
++    """
++    if pieces["closest-tag"]:
+         if pieces["distance"]:
+-            rendered += ".post.dev%%d" %% pieces["distance"]
++            # update the post release segment
++            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
++            rendered = tag_version
++            if post_version is not None:
++                rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"])
++            else:
++                rendered += ".post0.dev%%d" %% (pieces["distance"])
++        else:
++            # no commits, use the tag as the version
++            rendered = pieces["closest-tag"]
+     else:
+         # exception #1
+-        rendered = "0.post.dev%%d" %% pieces["distance"]
++        rendered = "0.post0.dev%%d" %% pieces["distance"]
+     return rendered
+ 
+ 
+-def render_pep440_post(pieces):
++def render_pep440_post(pieces: Dict[str, Any]) -> str:
+     """TAG[.postDISTANCE[.dev0]+gHEX] .
+ 
+     The ".dev0" means dirty. Note that .dev0 sorts backwards
+@@ -800,12 +1028,41 @@ def render_pep440_post(pieces):
+     return rendered
+ 
+ 
+-def render_pep440_old(pieces):
++def render_pep440_post_branch(pieces: Dict[str, Any]) -> str:
++    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
++
++    The ".dev0" means not master branch.
++
++    Exceptions:
++    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
++    """
++    if pieces["closest-tag"]:
++        rendered = pieces["closest-tag"]
++        if pieces["distance"] or pieces["dirty"]:
++            rendered += ".post%%d" %% pieces["distance"]
++            if pieces["branch"] != "master":
++                rendered += ".dev0"
++            rendered += plus_or_dot(pieces)
++            rendered += "g%%s" %% pieces["short"]
++            if pieces["dirty"]:
++                rendered += ".dirty"
++    else:
++        # exception #1
++        rendered = "0.post%%d" %% pieces["distance"]
++        if pieces["branch"] != "master":
++            rendered += ".dev0"
++        rendered += "+g%%s" %% pieces["short"]
++        if pieces["dirty"]:
++            rendered += ".dirty"
++    return rendered
++
++
++def render_pep440_old(pieces: Dict[str, Any]) -> str:
+     """TAG[.postDISTANCE[.dev0]] .
+ 
+     The ".dev0" means dirty.
+ 
+-    Eexceptions:
++    Exceptions:
+     1: no tags. 0.postDISTANCE[.dev0]
+     """
+     if pieces["closest-tag"]:
+@@ -822,7 +1079,7 @@ def render_pep440_old(pieces):
+     return rendered
+ 
+ 
+-def render_git_describe(pieces):
++def render_git_describe(pieces: Dict[str, Any]) -> str:
+     """TAG[-DISTANCE-gHEX][-dirty].
+ 
+     Like 'git describe --tags --dirty --always'.
+@@ -842,7 +1099,7 @@ def render_git_describe(pieces):
+     return rendered
+ 
+ 
+-def render_git_describe_long(pieces):
++def render_git_describe_long(pieces: Dict[str, Any]) -> str:
+     """TAG-DISTANCE-gHEX[-dirty].
+ 
+     Like 'git describe --tags --dirty --always -long'.
+@@ -862,7 +1119,7 @@ def render_git_describe_long(pieces):
+     return rendered
+ 
+ 
+-def render(pieces, style):
++def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]:
+     """Render the given version pieces into the requested style."""
+     if pieces["error"]:
+         return {"version": "unknown",
+@@ -876,10 +1133,14 @@ def render(pieces, style):
+ 
+     if style == "pep440":
+         rendered = render_pep440(pieces)
++    elif style == "pep440-branch":
++        rendered = render_pep440_branch(pieces)
+     elif style == "pep440-pre":
+         rendered = render_pep440_pre(pieces)
+     elif style == "pep440-post":
+         rendered = render_pep440_post(pieces)
++    elif style == "pep440-post-branch":
++        rendered = render_pep440_post_branch(pieces)
+     elif style == "pep440-old":
+         rendered = render_pep440_old(pieces)
+     elif style == "git-describe":
+@@ -894,7 +1155,7 @@ def render(pieces, style):
+             "date": pieces.get("date")}
+ 
+ 
+-def get_versions():
++def get_versions() -> Dict[str, Any]:
+     """Get version information or return default if unable to do so."""
+     # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+     # __file__, we can work backwards from there to the root. Some
+@@ -915,7 +1176,7 @@ def get_versions():
+         # versionfile_source is the relative path from the top of the source
+         # tree (where the .git directory might live) to this file. Invert
+         # this to find the root from __file__.
+-        for i in cfg.versionfile_source.split('/'):
++        for _ in cfg.versionfile_source.split('/'):
+             root = os.path.dirname(root)
+     except NameError:
+         return {"version": "0+unknown", "full-revisionid": None,
+@@ -942,41 +1203,48 @@ def get_versions():
+ 
+ 
+ @register_vcs_handler("git", "get_keywords")
+-def git_get_keywords(versionfile_abs):
++def git_get_keywords(versionfile_abs: str) -> Dict[str, str]:
+     """Extract version information from the given file."""
+     # the code embedded in _version.py can just fetch the value of these
+     # keywords. When used from setup.py, we don't want to import _version.py,
+     # so we do it with a regexp instead. This function is not used from
+     # _version.py.
+-    keywords = {}
++    keywords: Dict[str, str] = {}
+     try:
+-        f = open(versionfile_abs, "r")
+-        for line in f.readlines():
+-            if line.strip().startswith("git_refnames ="):
+-                mo = re.search(r'=\s*"(.*)"', line)
+-                if mo:
+-                    keywords["refnames"] = mo.group(1)
+-            if line.strip().startswith("git_full ="):
+-                mo = re.search(r'=\s*"(.*)"', line)
+-                if mo:
+-                    keywords["full"] = mo.group(1)
+-            if line.strip().startswith("git_date ="):
+-                mo = re.search(r'=\s*"(.*)"', line)
+-                if mo:
+-                    keywords["date"] = mo.group(1)
+-        f.close()
+-    except EnvironmentError:
++        with open(versionfile_abs, "r") as fobj:
++            for line in fobj:
++                if line.strip().startswith("git_refnames ="):
++                    mo = re.search(r'=\s*"(.*)"', line)
++                    if mo:
++                        keywords["refnames"] = mo.group(1)
++                if line.strip().startswith("git_full ="):
++                    mo = re.search(r'=\s*"(.*)"', line)
++                    if mo:
++                        keywords["full"] = mo.group(1)
++                if line.strip().startswith("git_date ="):
++                    mo = re.search(r'=\s*"(.*)"', line)
++                    if mo:
++                        keywords["date"] = mo.group(1)
++    except OSError:
+         pass
+     return keywords
+ 
+ 
+ @register_vcs_handler("git", "keywords")
+-def git_versions_from_keywords(keywords, tag_prefix, verbose):
++def git_versions_from_keywords(
++    keywords: Dict[str, str],
++    tag_prefix: str,
++    verbose: bool,
++) -> Dict[str, Any]:
+     """Get version information from git keywords."""
+-    if not keywords:
+-        raise NotThisMethod("no keywords at all, weird")
++    if "refnames" not in keywords:
++        raise NotThisMethod("Short version file found")
+     date = keywords.get("date")
+     if date is not None:
++        # Use only the last line.  Previous lines may contain GPG signature
++        # information.
++        date = date.splitlines()[-1]
++
+         # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
+         # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
+         # -like" string, which we must then edit to make compliant), because
+@@ -989,11 +1257,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+         if verbose:
+             print("keywords are unexpanded, not using")
+         raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+-    refs = set([r.strip() for r in refnames.strip("()").split(",")])
++    refs = {r.strip() for r in refnames.strip("()").split(",")}
+     # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+     # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+     TAG = "tag: "
+-    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
++    tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)}
+     if not tags:
+         # Either we're using git < 1.8.3, or there really are no tags. We use
+         # a heuristic: assume all version tags have a digit. The old git %d
+@@ -1002,7 +1270,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+         # between branches and tags. By ignoring refnames without digits, we
+         # filter out many common branch names like "release" and
+         # "stabilization", as well as "HEAD" and "master".
+-        tags = set([r for r in refs if re.search(r'\d', r)])
++        tags = {r for r in refs if re.search(r"\d", r)}
+         if verbose:
+             print("discarding '%s', no digits" % ",".join(refs - tags))
+     if verbose:
+@@ -1010,23 +1278,37 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
+     for ref in sorted(tags):
+         # sorting will prefer e.g. "2.0" over "2.0rc1"
+         if ref.startswith(tag_prefix):
+-            r = ref[len(tag_prefix):]
++            r = ref[len(tag_prefix) :]
++            # Filter out refs that exactly match prefix or that don't start
++            # with a number once the prefix is stripped (mostly a concern
++            # when prefix is '')
++            if not re.match(r"\d", r):
++                continue
+             if verbose:
+                 print("picking %s" % r)
+-            return {"version": r,
+-                    "full-revisionid": keywords["full"].strip(),
+-                    "dirty": False, "error": None,
+-                    "date": date}
++            return {
++                "version": r,
++                "full-revisionid": keywords["full"].strip(),
++                "dirty": False,
++                "error": None,
++                "date": date,
++            }
+     # no suitable tags, so version is "0+unknown", but full hex is still there
+     if verbose:
+         print("no suitable tags, using unknown + full revision id")
+-    return {"version": "0+unknown",
+-            "full-revisionid": keywords["full"].strip(),
+-            "dirty": False, "error": "no suitable tags", "date": None}
++    return {
++        "version": "0+unknown",
++        "full-revisionid": keywords["full"].strip(),
++        "dirty": False,
++        "error": "no suitable tags",
++        "date": None,
++    }
+ 
+ 
+ @register_vcs_handler("git", "pieces_from_vcs")
+-def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
++def git_pieces_from_vcs(
++    tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command
++) -> Dict[str, Any]:
+     """Get version from 'git describe' in the root of the source tree.
+ 
+     This only gets called if the git-archive 'subst' keywords were *not*
+@@ -1037,8 +1319,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+     if sys.platform == "win32":
+         GITS = ["git.cmd", "git.exe"]
+ 
+-    out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
+-                          hide_stderr=True)
++    # GIT_DIR can interfere with correct operation of Versioneer.
++    # It may be intended to be passed to the Versioneer-versioned project,
++    # but that should not change where we get our version from.
++    env = os.environ.copy()
++    env.pop("GIT_DIR", None)
++    runner = functools.partial(runner, env=env)
++
++    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose)
+     if rc != 0:
+         if verbose:
+             print("Directory %s not under git control" % root)
+@@ -1046,24 +1334,65 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+ 
+     # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+     # if there isn't one, this yields HEX[-dirty] (no NUM)
+-    describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
+-                                          "--always", "--long",
+-                                          "--match", "%s*" % tag_prefix],
+-                                   cwd=root)
++    describe_out, rc = runner(
++        GITS,
++        [
++            "describe",
++            "--tags",
++            "--dirty",
++            "--always",
++            "--long",
++            "--match",
++            f"{tag_prefix}[[:digit:]]*",
++        ],
++        cwd=root,
++    )
+     # --long was added in git-1.5.5
+     if describe_out is None:
+         raise NotThisMethod("'git describe' failed")
+     describe_out = describe_out.strip()
+-    full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
++    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
+     if full_out is None:
+         raise NotThisMethod("'git rev-parse' failed")
+     full_out = full_out.strip()
+ 
+-    pieces = {}
++    pieces: Dict[str, Any] = {}
+     pieces["long"] = full_out
+     pieces["short"] = full_out[:7]  # maybe improved later
+     pieces["error"] = None
+ 
++    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
++    # --abbrev-ref was added in git-1.6.3
++    if rc != 0 or branch_name is None:
++        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
++    branch_name = branch_name.strip()
++
++    if branch_name == "HEAD":
++        # If we aren't exactly on a branch, pick a branch which represents
++        # the current commit. If all else fails, we are on a branchless
++        # commit.
++        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
++        # --contains was added in git-1.5.4
++        if rc != 0 or branches is None:
++            raise NotThisMethod("'git branch --contains' returned error")
++        branches = branches.split("\n")
++
++        # Remove the first line if we're running detached
++        if "(" in branches[0]:
++            branches.pop(0)
++
++        # Strip off the leading "* " from the list of branches.
++        branches = [branch[2:] for branch in branches]
++        if "master" in branches:
++            branch_name = "master"
++        elif not branches:
++            branch_name = None
++        else:
++            # Pick the first branch that is returned. Good or bad.
++            branch_name = branches[0]
++
++    pieces["branch"] = branch_name
++
+     # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+     # TAG might have hyphens.
+     git_describe = describe_out
+@@ -1072,17 +1401,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+     dirty = git_describe.endswith("-dirty")
+     pieces["dirty"] = dirty
+     if dirty:
+-        git_describe = git_describe[:git_describe.rindex("-dirty")]
++        git_describe = git_describe[: git_describe.rindex("-dirty")]
+ 
+     # now we have TAG-NUM-gHEX or HEX
+ 
+     if "-" in git_describe:
+         # TAG-NUM-gHEX
+-        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
++        mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe)
+         if not mo:
+-            # unparseable. Maybe git-describe is misbehaving?
+-            pieces["error"] = ("unable to parse git-describe output: '%s'"
+-                               % describe_out)
++            # unparsable. Maybe git-describe is misbehaving?
++            pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
+             return pieces
+ 
+         # tag
+@@ -1091,10 +1419,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+             if verbose:
+                 fmt = "tag '%s' doesn't start with prefix '%s'"
+                 print(fmt % (full_tag, tag_prefix))
+-            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+-                               % (full_tag, tag_prefix))
++            pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (
++                full_tag,
++                tag_prefix,
++            )
+             return pieces
+-        pieces["closest-tag"] = full_tag[len(tag_prefix):]
++        pieces["closest-tag"] = full_tag[len(tag_prefix) :]
+ 
+         # distance: number of commits since tag
+         pieces["distance"] = int(mo.group(2))
+@@ -1105,19 +1435,20 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
+     else:
+         # HEX: no tags
+         pieces["closest-tag"] = None
+-        count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
+-                                    cwd=root)
+-        pieces["distance"] = int(count_out)  # total number of commits
++        out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root)
++        pieces["distance"] = len(out.split())  # total number of commits
+ 
+     # commit date: see ISO-8601 comment in git_versions_from_keywords()
+-    date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
+-                       cwd=root)[0].strip()
++    date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
++    # Use only the last line.  Previous lines may contain GPG signature
++    # information.
++    date = date.splitlines()[-1]
+     pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+ 
+     return pieces
+ 
+ 
+-def do_vcs_install(manifest_in, versionfile_source, ipy):
++def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None:
+     """Git-specific installation logic for Versioneer.
+ 
+     For Git, this means creating/changing .gitattributes to mark _version.py
+@@ -1126,36 +1457,40 @@ def do_vcs_install(manifest_in, versionfile_source, ipy):
+     GITS = ["git"]
+     if sys.platform == "win32":
+         GITS = ["git.cmd", "git.exe"]
+-    files = [manifest_in, versionfile_source]
++    files = [versionfile_source]
+     if ipy:
+         files.append(ipy)
+-    try:
+-        me = __file__
+-        if me.endswith(".pyc") or me.endswith(".pyo"):
+-            me = os.path.splitext(me)[0] + ".py"
+-        versioneer_file = os.path.relpath(me)
+-    except NameError:
+-        versioneer_file = "versioneer.py"
+-    files.append(versioneer_file)
++    if "VERSIONEER_PEP518" not in globals():
++        try:
++            my_path = __file__
++            if my_path.endswith((".pyc", ".pyo")):
++                my_path = os.path.splitext(my_path)[0] + ".py"
++            versioneer_file = os.path.relpath(my_path)
++        except NameError:
++            versioneer_file = "versioneer.py"
++        files.append(versioneer_file)
+     present = False
+     try:
+-        f = open(".gitattributes", "r")
+-        for line in f.readlines():
+-            if line.strip().startswith(versionfile_source):
+-                if "export-subst" in line.strip().split()[1:]:
+-                    present = True
+-        f.close()
+-    except EnvironmentError:
++        with open(".gitattributes", "r") as fobj:
++            for line in fobj:
++                if line.strip().startswith(versionfile_source):
++                    if "export-subst" in line.strip().split()[1:]:
++                        present = True
++                        break
++    except OSError:
+         pass
+     if not present:
+-        f = open(".gitattributes", "a+")
+-        f.write("%s export-subst\n" % versionfile_source)
+-        f.close()
++        with open(".gitattributes", "a+") as fobj:
++            fobj.write(f"{versionfile_source} export-subst\n")
+         files.append(".gitattributes")
+     run_command(GITS, ["add", "--"] + files)
+ 
+ 
+-def versions_from_parentdir(parentdir_prefix, root, verbose):
++def versions_from_parentdir(
++    parentdir_prefix: str,
++    root: str,
++    verbose: bool,
++) -> Dict[str, Any]:
+     """Try to determine the version from the parent directory name.
+ 
+     Source tarballs conventionally unpack into a directory that includes both
+@@ -1164,24 +1499,29 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
+     """
+     rootdirs = []
+ 
+-    for i in range(3):
++    for _ in range(3):
+         dirname = os.path.basename(root)
+         if dirname.startswith(parentdir_prefix):
+-            return {"version": dirname[len(parentdir_prefix):],
+-                    "full-revisionid": None,
+-                    "dirty": False, "error": None, "date": None}
+-        else:
+-            rootdirs.append(root)
+-            root = os.path.dirname(root)  # up a level
++            return {
++                "version": dirname[len(parentdir_prefix) :],
++                "full-revisionid": None,
++                "dirty": False,
++                "error": None,
++                "date": None,
++            }
++        rootdirs.append(root)
++        root = os.path.dirname(root)  # up a level
+ 
+     if verbose:
+-        print("Tried directories %s but none started with prefix %s" %
+-              (str(rootdirs), parentdir_prefix))
++        print(
++            "Tried directories %s but none started with prefix %s"
++            % (str(rootdirs), parentdir_prefix)
++        )
+     raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+ 
+ 
+ SHORT_VERSION_PY = """
+-# This file was generated by 'versioneer.py' (0.18) from
++# This file was generated by 'versioneer.py' (0.29) from
+ # revision-control system data, or from the parent directory name of an
+ # unpacked source archive. Distribution tarballs contain a pre-generated copy
+ # of this file.
+@@ -1198,42 +1538,42 @@ def get_versions():
+ """
+ 
+ 
+-def versions_from_file(filename):
++def versions_from_file(filename: str) -> Dict[str, Any]:
+     """Try to determine the version from _version.py if present."""
+     try:
+         with open(filename) as f:
+             contents = f.read()
+-    except EnvironmentError:
++    except OSError:
+         raise NotThisMethod("unable to read _version.py")
+-    mo = re.search(r"version_json = '''\n(.*)'''  # END VERSION_JSON",
+-                   contents, re.M | re.S)
++    mo = re.search(
++        r"version_json = '''\n(.*)'''  # END VERSION_JSON", contents, re.M | re.S
++    )
+     if not mo:
+-        mo = re.search(r"version_json = '''\r\n(.*)'''  # END VERSION_JSON",
+-                       contents, re.M | re.S)
++        mo = re.search(
++            r"version_json = '''\r\n(.*)'''  # END VERSION_JSON", contents, re.M | re.S
++        )
+     if not mo:
+         raise NotThisMethod("no version_json in _version.py")
+     return json.loads(mo.group(1))
+ 
+ 
+-def write_to_version_file(filename, versions):
++def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None:
+     """Write the given version number to the given _version.py file."""
+-    os.unlink(filename)
+-    contents = json.dumps(versions, sort_keys=True,
+-                          indent=1, separators=(",", ": "))
++    contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": "))
+     with open(filename, "w") as f:
+         f.write(SHORT_VERSION_PY % contents)
+ 
+     print("set %s to '%s'" % (filename, versions["version"]))
+ 
+ 
+-def plus_or_dot(pieces):
++def plus_or_dot(pieces: Dict[str, Any]) -> str:
+     """Return a + if we don't already have one, else return a ."""
+     if "+" in pieces.get("closest-tag", ""):
+         return "."
+     return "+"
+ 
+ 
+-def render_pep440(pieces):
++def render_pep440(pieces: Dict[str, Any]) -> str:
+     """Build up version string, with post-release "local version identifier".
+ 
+     Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+@@ -1251,30 +1591,76 @@ def render_pep440(pieces):
+                 rendered += ".dirty"
+     else:
+         # exception #1
+-        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+-                                          pieces["short"])
++        rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
+         if pieces["dirty"]:
+             rendered += ".dirty"
+     return rendered
+ 
+ 
+-def render_pep440_pre(pieces):
+-    """TAG[.post.devDISTANCE] -- No -dirty.
++def render_pep440_branch(pieces: Dict[str, Any]) -> str:
++    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
++
++    The ".dev0" means not master branch. Note that .dev0 sorts backwards
++    (a feature branch will appear "older" than the master branch).
+ 
+     Exceptions:
+-    1: no tags. 0.post.devDISTANCE
++    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
+     """
+     if pieces["closest-tag"]:
+         rendered = pieces["closest-tag"]
++        if pieces["distance"] or pieces["dirty"]:
++            if pieces["branch"] != "master":
++                rendered += ".dev0"
++            rendered += plus_or_dot(pieces)
++            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
++            if pieces["dirty"]:
++                rendered += ".dirty"
++    else:
++        # exception #1
++        rendered = "0"
++        if pieces["branch"] != "master":
++            rendered += ".dev0"
++        rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
++        if pieces["dirty"]:
++            rendered += ".dirty"
++    return rendered
++
++
++def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]:
++    """Split pep440 version string at the post-release segment.
++
++    Returns the release segments before the post-release and the
++    post-release version number (or -1 if no post-release segment is present).
++    """
++    vc = str.split(ver, ".post")
++    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
++
++
++def render_pep440_pre(pieces: Dict[str, Any]) -> str:
++    """TAG[.postN.devDISTANCE] -- No -dirty.
++
++    Exceptions:
++    1: no tags. 0.post0.devDISTANCE
++    """
++    if pieces["closest-tag"]:
+         if pieces["distance"]:
+-            rendered += ".post.dev%d" % pieces["distance"]
++            # update the post release segment
++            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
++            rendered = tag_version
++            if post_version is not None:
++                rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"])
++            else:
++                rendered += ".post0.dev%d" % (pieces["distance"])
++        else:
++            # no commits, use the tag as the version
++            rendered = pieces["closest-tag"]
+     else:
+         # exception #1
+-        rendered = "0.post.dev%d" % pieces["distance"]
++        rendered = "0.post0.dev%d" % pieces["distance"]
+     return rendered
+ 
+ 
+-def render_pep440_post(pieces):
++def render_pep440_post(pieces: Dict[str, Any]) -> str:
+     """TAG[.postDISTANCE[.dev0]+gHEX] .
+ 
+     The ".dev0" means dirty. Note that .dev0 sorts backwards
+@@ -1301,12 +1687,41 @@ def render_pep440_post(pieces):
+     return rendered
+ 
+ 
+-def render_pep440_old(pieces):
++def render_pep440_post_branch(pieces: Dict[str, Any]) -> str:
++    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
++
++    The ".dev0" means not master branch.
++
++    Exceptions:
++    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
++    """
++    if pieces["closest-tag"]:
++        rendered = pieces["closest-tag"]
++        if pieces["distance"] or pieces["dirty"]:
++            rendered += ".post%d" % pieces["distance"]
++            if pieces["branch"] != "master":
++                rendered += ".dev0"
++            rendered += plus_or_dot(pieces)
++            rendered += "g%s" % pieces["short"]
++            if pieces["dirty"]:
++                rendered += ".dirty"
++    else:
++        # exception #1
++        rendered = "0.post%d" % pieces["distance"]
++        if pieces["branch"] != "master":
++            rendered += ".dev0"
++        rendered += "+g%s" % pieces["short"]
++        if pieces["dirty"]:
++            rendered += ".dirty"
++    return rendered
++
++
++def render_pep440_old(pieces: Dict[str, Any]) -> str:
+     """TAG[.postDISTANCE[.dev0]] .
+ 
+     The ".dev0" means dirty.
+ 
+-    Eexceptions:
++    Exceptions:
+     1: no tags. 0.postDISTANCE[.dev0]
+     """
+     if pieces["closest-tag"]:
+@@ -1323,7 +1738,7 @@ def render_pep440_old(pieces):
+     return rendered
+ 
+ 
+-def render_git_describe(pieces):
++def render_git_describe(pieces: Dict[str, Any]) -> str:
+     """TAG[-DISTANCE-gHEX][-dirty].
+ 
+     Like 'git describe --tags --dirty --always'.
+@@ -1343,7 +1758,7 @@ def render_git_describe(pieces):
+     return rendered
+ 
+ 
+-def render_git_describe_long(pieces):
++def render_git_describe_long(pieces: Dict[str, Any]) -> str:
+     """TAG-DISTANCE-gHEX[-dirty].
+ 
+     Like 'git describe --tags --dirty --always -long'.
+@@ -1363,24 +1778,30 @@ def render_git_describe_long(pieces):
+     return rendered
+ 
+ 
+-def render(pieces, style):
++def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]:
+     """Render the given version pieces into the requested style."""
+     if pieces["error"]:
+-        return {"version": "unknown",
+-                "full-revisionid": pieces.get("long"),
+-                "dirty": None,
+-                "error": pieces["error"],
+-                "date": None}
++        return {
++            "version": "unknown",
++            "full-revisionid": pieces.get("long"),
++            "dirty": None,
++            "error": pieces["error"],
++            "date": None,
++        }
+ 
+     if not style or style == "default":
+         style = "pep440"  # the default
+ 
+     if style == "pep440":
+         rendered = render_pep440(pieces)
++    elif style == "pep440-branch":
++        rendered = render_pep440_branch(pieces)
+     elif style == "pep440-pre":
+         rendered = render_pep440_pre(pieces)
+     elif style == "pep440-post":
+         rendered = render_pep440_post(pieces)
++    elif style == "pep440-post-branch":
++        rendered = render_pep440_post_branch(pieces)
+     elif style == "pep440-old":
+         rendered = render_pep440_old(pieces)
+     elif style == "git-describe":
+@@ -1390,16 +1811,20 @@ def render(pieces, style):
+     else:
+         raise ValueError("unknown style '%s'" % style)
+ 
+-    return {"version": rendered, "full-revisionid": pieces["long"],
+-            "dirty": pieces["dirty"], "error": None,
+-            "date": pieces.get("date")}
++    return {
++        "version": rendered,
++        "full-revisionid": pieces["long"],
++        "dirty": pieces["dirty"],
++        "error": None,
++        "date": pieces.get("date"),
++    }
+ 
+ 
+ class VersioneerBadRootError(Exception):
+     """The project root directory is unknown or missing key files."""
+ 
+ 
+-def get_versions(verbose=False):
++def get_versions(verbose: bool = False) -> Dict[str, Any]:
+     """Get the project version from whatever source is available.
+ 
+     Returns dict with two keys: 'version' and 'full'.
+@@ -1414,9 +1839,10 @@ def get_versions(verbose=False):
+     assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg"
+     handlers = HANDLERS.get(cfg.VCS)
+     assert handlers, "unrecognized VCS '%s'" % cfg.VCS
+-    verbose = verbose or cfg.verbose
+-    assert cfg.versionfile_source is not None, \
+-        "please set versioneer.versionfile_source"
++    verbose = verbose or bool(cfg.verbose)  # `bool()` used to avoid `None`
++    assert (
++        cfg.versionfile_source is not None
++    ), "please set versioneer.versionfile_source"
+     assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
+ 
+     versionfile_abs = os.path.join(root, cfg.versionfile_source)
+@@ -1470,18 +1896,26 @@ def get_versions(verbose=False):
+     if verbose:
+         print("unable to compute version")
+ 
+-    return {"version": "0+unknown", "full-revisionid": None,
+-            "dirty": None, "error": "unable to compute version",
+-            "date": None}
++    return {
++        "version": "0+unknown",
++        "full-revisionid": None,
++        "dirty": None,
++        "error": "unable to compute version",
++        "date": None,
++    }
+ 
+ 
+-def get_version():
++def get_version() -> str:
+     """Get the short version string for this project."""
+     return get_versions()["version"]
+ 
+ 
+-def get_cmdclass():
+-    """Get the custom setuptools/distutils subclasses used by Versioneer."""
++def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None):
++    """Get the custom setuptools subclasses used by Versioneer.
++
++    If the package uses a different cmdclass (e.g. one from numpy), it
++    should be provide as an argument.
++    """
+     if "versioneer" in sys.modules:
+         del sys.modules["versioneer"]
+         # this fixes the "python setup.py develop" case (also 'install' and
+@@ -1495,25 +1929,25 @@ def get_cmdclass():
+         # parent is protected against the child's "import versioneer". By
+         # removing ourselves from sys.modules here, before the child build
+         # happens, we protect the child from the parent's versioneer too.
+-        # Also see https://github.com/warner/python-versioneer/issues/52
++        # Also see https://github.com/python-versioneer/python-versioneer/issues/52
+ 
+-    cmds = {}
++    cmds = {} if cmdclass is None else cmdclass.copy()
+ 
+-    # we add "version" to both distutils and setuptools
+-    from distutils.core import Command
++    # we add "version" to setuptools
++    from setuptools import Command
+ 
+     class cmd_version(Command):
+         description = "report generated version string"
+-        user_options = []
+-        boolean_options = []
++        user_options: List[Tuple[str, str, str]] = []
++        boolean_options: List[str] = []
+ 
+-        def initialize_options(self):
++        def initialize_options(self) -> None:
+             pass
+ 
+-        def finalize_options(self):
++        def finalize_options(self) -> None:
+             pass
+ 
+-        def run(self):
++        def run(self) -> None:
+             vers = get_versions(verbose=True)
+             print("Version: %s" % vers["version"])
+             print(" full-revisionid: %s" % vers.get("full-revisionid"))
+@@ -1521,9 +1955,10 @@ def get_cmdclass():
+             print(" date: %s" % vers.get("date"))
+             if vers["error"]:
+                 print(" error: %s" % vers["error"])
++
+     cmds["version"] = cmd_version
+ 
+-    # we override "build_py" in both distutils and setuptools
++    # we override "build_py" in setuptools
+     #
+     # most invocation pathways end up running build_py:
+     #  distutils/build -> build_py
+@@ -1538,29 +1973,71 @@ def get_cmdclass():
+     #   then does setup.py bdist_wheel, or sometimes setup.py install
+     #  setup.py egg_info -> ?
+ 
++    # pip install -e . and setuptool/editable_wheel will invoke build_py
++    # but the build_py command is not expected to copy any files.
++
+     # we override different "build_py" commands for both environments
+-    if "setuptools" in sys.modules:
+-        from setuptools.command.build_py import build_py as _build_py
++    if "build_py" in cmds:
++        _build_py: Any = cmds["build_py"]
+     else:
+-        from distutils.command.build_py import build_py as _build_py
++        from setuptools.command.build_py import build_py as _build_py
+ 
+     class cmd_build_py(_build_py):
+-        def run(self):
++        def run(self) -> None:
+             root = get_root()
+             cfg = get_config_from_root(root)
+             versions = get_versions()
+             _build_py.run(self)
++            if getattr(self, "editable_mode", False):
++                # During editable installs `.py` and data files are
++                # not copied to build_lib
++                return
+             # now locate _version.py in the new build/ directory and replace
+             # it with an updated value
+             if cfg.versionfile_build:
+-                target_versionfile = os.path.join(self.build_lib,
+-                                                  cfg.versionfile_build)
++                target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build)
+                 print("UPDATING %s" % target_versionfile)
+                 write_to_version_file(target_versionfile, versions)
++
+     cmds["build_py"] = cmd_build_py
+ 
++    if "build_ext" in cmds:
++        _build_ext: Any = cmds["build_ext"]
++    else:
++        from setuptools.command.build_ext import build_ext as _build_ext
++
++    class cmd_build_ext(_build_ext):
++        def run(self) -> None:
++            root = get_root()
++            cfg = get_config_from_root(root)
++            versions = get_versions()
++            _build_ext.run(self)
++            if self.inplace:
++                # build_ext --inplace will only build extensions in
++                # build/lib<..> dir with no _version.py to write to.
++                # As in place builds will already have a _version.py
++                # in the module dir, we do not need to write one.
++                return
++            # now locate _version.py in the new build/ directory and replace
++            # it with an updated value
++            if not cfg.versionfile_build:
++                return
++            target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build)
++            if not os.path.exists(target_versionfile):
++                print(
++                    f"Warning: {target_versionfile} does not exist, skipping "
++                    "version update. This can happen if you are running build_ext "
++                    "without first running build_py."
++                )
++                return
++            print("UPDATING %s" % target_versionfile)
++            write_to_version_file(target_versionfile, versions)
++
++    cmds["build_ext"] = cmd_build_ext
++
+     if "cx_Freeze" in sys.modules:  # cx_freeze enabled?
+-        from cx_Freeze.dist import build_exe as _build_exe
++        from cx_Freeze.dist import build_exe as _build_exe  # type: ignore
++
+         # nczeczulin reports that py2exe won't like the pep440-style string
+         # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g.
+         # setup(console=[{
+@@ -1569,7 +2046,7 @@ def get_cmdclass():
+         #   ...
+ 
+         class cmd_build_exe(_build_exe):
+-            def run(self):
++            def run(self) -> None:
+                 root = get_root()
+                 cfg = get_config_from_root(root)
+                 versions = get_versions()
+@@ -1581,24 +2058,28 @@ def get_cmdclass():
+                 os.unlink(target_versionfile)
+                 with open(cfg.versionfile_source, "w") as f:
+                     LONG = LONG_VERSION_PY[cfg.VCS]
+-                    f.write(LONG %
+-                            {"DOLLAR": "$",
+-                             "STYLE": cfg.style,
+-                             "TAG_PREFIX": cfg.tag_prefix,
+-                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+-                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
+-                             })
++                    f.write(
++                        LONG
++                        % {
++                            "DOLLAR": "$",
++                            "STYLE": cfg.style,
++                            "TAG_PREFIX": cfg.tag_prefix,
++                            "PARENTDIR_PREFIX": cfg.parentdir_prefix,
++                            "VERSIONFILE_SOURCE": cfg.versionfile_source,
++                        }
++                    )
++
+         cmds["build_exe"] = cmd_build_exe
+         del cmds["build_py"]
+ 
+-    if 'py2exe' in sys.modules:  # py2exe enabled?
++    if "py2exe" in sys.modules:  # py2exe enabled?
+         try:
+-            from py2exe.distutils_buildexe import py2exe as _py2exe  # py3
++            from py2exe.setuptools_buildexe import py2exe as _py2exe  # type: ignore
+         except ImportError:
+-            from py2exe.build_exe import py2exe as _py2exe  # py2
++            from py2exe.distutils_buildexe import py2exe as _py2exe  # type: ignore
+ 
+         class cmd_py2exe(_py2exe):
+-            def run(self):
++            def run(self) -> None:
+                 root = get_root()
+                 cfg = get_config_from_root(root)
+                 versions = get_versions()
+@@ -1610,23 +2091,67 @@ def get_cmdclass():
+                 os.unlink(target_versionfile)
+                 with open(cfg.versionfile_source, "w") as f:
+                     LONG = LONG_VERSION_PY[cfg.VCS]
+-                    f.write(LONG %
+-                            {"DOLLAR": "$",
+-                             "STYLE": cfg.style,
+-                             "TAG_PREFIX": cfg.tag_prefix,
+-                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+-                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
+-                             })
++                    f.write(
++                        LONG
++                        % {
++                            "DOLLAR": "$",
++                            "STYLE": cfg.style,
++                            "TAG_PREFIX": cfg.tag_prefix,
++                            "PARENTDIR_PREFIX": cfg.parentdir_prefix,
++                            "VERSIONFILE_SOURCE": cfg.versionfile_source,
++                        }
++                    )
++
+         cmds["py2exe"] = cmd_py2exe
+ 
++    # sdist farms its file list building out to egg_info
++    if "egg_info" in cmds:
++        _egg_info: Any = cmds["egg_info"]
++    else:
++        from setuptools.command.egg_info import egg_info as _egg_info
++
++    class cmd_egg_info(_egg_info):
++        def find_sources(self) -> None:
++            # egg_info.find_sources builds the manifest list and writes it
++            # in one shot
++            super().find_sources()
++
++            # Modify the filelist and normalize it
++            root = get_root()
++            cfg = get_config_from_root(root)
++            self.filelist.append("versioneer.py")
++            if cfg.versionfile_source:
++                # There are rare cases where versionfile_source might not be
++                # included by default, so we must be explicit
++                self.filelist.append(cfg.versionfile_source)
++            self.filelist.sort()
++            self.filelist.remove_duplicates()
++
++            # The write method is hidden in the manifest_maker instance that
++            # generated the filelist and was thrown away
++            # We will instead replicate their final normalization (to unicode,
++            # and POSIX-style paths)
++            from setuptools import unicode_utils
++
++            normalized = [
++                unicode_utils.filesys_decode(f).replace(os.sep, "/")
++                for f in self.filelist.files
++            ]
++
++            manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")
++            with open(manifest_filename, "w") as fobj:
++                fobj.write("\n".join(normalized))
++
++    cmds["egg_info"] = cmd_egg_info
++
+     # we override different "sdist" commands for both environments
+-    if "setuptools" in sys.modules:
+-        from setuptools.command.sdist import sdist as _sdist
++    if "sdist" in cmds:
++        _sdist: Any = cmds["sdist"]
+     else:
+-        from distutils.command.sdist import sdist as _sdist
++        from setuptools.command.sdist import sdist as _sdist
+ 
+     class cmd_sdist(_sdist):
+-        def run(self):
++        def run(self) -> None:
+             versions = get_versions()
+             self._versioneer_generated_versions = versions
+             # unless we update this, the command will keep using the old
+@@ -1634,7 +2159,7 @@ def get_cmdclass():
+             self.distribution.metadata.version = versions["version"]
+             return _sdist.run(self)
+ 
+-        def make_release_tree(self, base_dir, files):
++        def make_release_tree(self, base_dir: str, files: List[str]) -> None:
+             root = get_root()
+             cfg = get_config_from_root(root)
+             _sdist.make_release_tree(self, base_dir, files)
+@@ -1643,8 +2168,10 @@ def get_cmdclass():
+             # updated value
+             target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
+             print("UPDATING %s" % target_versionfile)
+-            write_to_version_file(target_versionfile,
+-                                  self._versioneer_generated_versions)
++            write_to_version_file(
++                target_versionfile, self._versioneer_generated_versions
++            )
++
+     cmds["sdist"] = cmd_sdist
+ 
+     return cmds
+@@ -1687,23 +2214,26 @@ SAMPLE_CONFIG = """
+ 
+ """
+ 
+-INIT_PY_SNIPPET = """
++OLD_SNIPPET = """
+ from ._version import get_versions
+ __version__ = get_versions()['version']
+ del get_versions
+ """
+ 
++INIT_PY_SNIPPET = """
++from . import {0}
++__version__ = {0}.get_versions()['version']
++"""
++
+ 
+-def do_setup():
+-    """Main VCS-independent setup function for installing Versioneer."""
++def do_setup() -> int:
++    """Do main VCS-independent setup function for installing Versioneer."""
+     root = get_root()
+     try:
+         cfg = get_config_from_root(root)
+-    except (EnvironmentError, configparser.NoSectionError,
+-            configparser.NoOptionError) as e:
+-        if isinstance(e, (EnvironmentError, configparser.NoSectionError)):
+-            print("Adding sample versioneer config to setup.cfg",
+-                  file=sys.stderr)
++    except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e:
++        if isinstance(e, (OSError, configparser.NoSectionError)):
++            print("Adding sample versioneer config to setup.cfg", file=sys.stderr)
+             with open(os.path.join(root, "setup.cfg"), "a") as f:
+                 f.write(SAMPLE_CONFIG)
+         print(CONFIG_ERROR, file=sys.stderr)
+@@ -1712,71 +2242,49 @@ def do_setup():
+     print(" creating %s" % cfg.versionfile_source)
+     with open(cfg.versionfile_source, "w") as f:
+         LONG = LONG_VERSION_PY[cfg.VCS]
+-        f.write(LONG % {"DOLLAR": "$",
+-                        "STYLE": cfg.style,
+-                        "TAG_PREFIX": cfg.tag_prefix,
+-                        "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+-                        "VERSIONFILE_SOURCE": cfg.versionfile_source,
+-                        })
+-
+-    ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
+-                       "__init__.py")
++        f.write(
++            LONG
++            % {
++                "DOLLAR": "$",
++                "STYLE": cfg.style,
++                "TAG_PREFIX": cfg.tag_prefix,
++                "PARENTDIR_PREFIX": cfg.parentdir_prefix,
++                "VERSIONFILE_SOURCE": cfg.versionfile_source,
++            }
++        )
++
++    ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py")
++    maybe_ipy: Optional[str] = ipy
+     if os.path.exists(ipy):
+         try:
+             with open(ipy, "r") as f:
+                 old = f.read()
+-        except EnvironmentError:
++        except OSError:
+             old = ""
+-        if INIT_PY_SNIPPET not in old:
++        module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0]
++        snippet = INIT_PY_SNIPPET.format(module)
++        if OLD_SNIPPET in old:
++            print(" replacing boilerplate in %s" % ipy)
++            with open(ipy, "w") as f:
++                f.write(old.replace(OLD_SNIPPET, snippet))
++        elif snippet not in old:
+             print(" appending to %s" % ipy)
+             with open(ipy, "a") as f:
+-                f.write(INIT_PY_SNIPPET)
++                f.write(snippet)
+         else:
+             print(" %s unmodified" % ipy)
+     else:
+         print(" %s doesn't exist, ok" % ipy)
+-        ipy = None
+-
+-    # Make sure both the top-level "versioneer.py" and versionfile_source
+-    # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so
+-    # they'll be copied into source distributions. Pip won't be able to
+-    # install the package without this.
+-    manifest_in = os.path.join(root, "MANIFEST.in")
+-    simple_includes = set()
+-    try:
+-        with open(manifest_in, "r") as f:
+-            for line in f:
+-                if line.startswith("include "):
+-                    for include in line.split()[1:]:
+-                        simple_includes.add(include)
+-    except EnvironmentError:
+-        pass
+-    # That doesn't cover everything MANIFEST.in can do
+-    # (http://docs.python.org/2/distutils/sourcedist.html#commands), so
+-    # it might give some false negatives. Appending redundant 'include'
+-    # lines is safe, though.
+-    if "versioneer.py" not in simple_includes:
+-        print(" appending 'versioneer.py' to MANIFEST.in")
+-        with open(manifest_in, "a") as f:
+-            f.write("include versioneer.py\n")
+-    else:
+-        print(" 'versioneer.py' already in MANIFEST.in")
+-    if cfg.versionfile_source not in simple_includes:
+-        print(" appending versionfile_source ('%s') to MANIFEST.in" %
+-              cfg.versionfile_source)
+-        with open(manifest_in, "a") as f:
+-            f.write("include %s\n" % cfg.versionfile_source)
+-    else:
+-        print(" versionfile_source already in MANIFEST.in")
++        maybe_ipy = None
+ 
+     # Make VCS-specific changes. For git, this means creating/changing
+     # .gitattributes to mark _version.py for export-subst keyword
+     # substitution.
+-    do_vcs_install(manifest_in, cfg.versionfile_source, ipy)
++    do_vcs_install(cfg.versionfile_source, maybe_ipy)
+     return 0
+ 
+ 
+-def scan_setup_py():
++def scan_setup_py() -> int:
+     """Validate the contents of setup.py against Versioneer's expectations."""
+     found = set()
+     setters = False
+@@ -1813,10 +2321,14 @@ def scan_setup_py():
+     return errors
+ 
+ 
++def setup_command() -> NoReturn:
++    """Set up Versioneer and exit with appropriate error code."""
++    errors = do_setup()
++    errors += scan_setup_py()
++    sys.exit(1 if errors else 0)
++
++
+ if __name__ == "__main__":
+     cmd = sys.argv[1]
+     if cmd == "setup":
+-        errors = do_setup()
+-        errors += scan_setup_py()
+-        if errors:
+-            sys.exit(1)
++        setup_command()
+-- 
+2.41.0
+