diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8c25b2f..7c69807 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,15 +29,15 @@ jobs: if: ${{ github.event.inputs.dry-run == 'false' }} needs: commit-changelog-and-release runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write steps: - - name: Set up Python 3.11 - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 with: - python-version: 3.11 - + ref: ${{ github.event.inputs.version-tag }} + - uses: deargen/workflows/actions/setup-python-and-uv@master - name: Build and upload to PyPI run: | - python -m pip install --upgrade pip - pip3 install build twine - python -m build . --sdist - python3 -m twine upload dist/* -u __token__ -p ${{ secrets.PYPI_TOKEN }} --non-interactive + uv build + uv publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c979baa..f2e124f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: uv venv source .venv/bin/activate uv pip install -r deps/lock/x86_64-manylinux_2_28/requirements_dev.txt - uv pip install -e . + uv pip install . - name: Run pytest uses: deargen/workflows/actions/run-pytest@master with: @@ -52,6 +52,6 @@ jobs: uv venv source .venv/bin/activate uv pip install -r deps/lock/x86_64-manylinux_2_28/requirements_dev.txt - uv pip install -e . + uv pip install . - name: Run doctest uses: deargen/workflows/actions/run-doctest@master diff --git a/deps/lock/aarch64-apple-darwin/.requirements_dev.in.sha256 b/deps/lock/aarch64-apple-darwin/.requirements_dev.in.sha256 index 835d712..e458c9b 100644 --- a/deps/lock/aarch64-apple-darwin/.requirements_dev.in.sha256 +++ b/deps/lock/aarch64-apple-darwin/.requirements_dev.in.sha256 @@ -1 +1 @@ -d7314ad59261b4109ef6e9022d331c212b1cb1dc62da32163cea3cef9fc1a3f2 requirements_dev.in +425899e6ec1e329b19f09174d83b1bccec945bba515f7747009f7f494d77cd18 requirements_dev.in diff --git a/deps/lock/aarch64-apple-darwin/requirements_dev.txt b/deps/lock/aarch64-apple-darwin/requirements_dev.txt index 6a56579..35cd2ab 100644 --- a/deps/lock/aarch64-apple-darwin/requirements_dev.txt +++ b/deps/lock/aarch64-apple-darwin/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o /home/runner/work/python-import.nvim/python-import.nvim/deps/lock/aarch64-apple-darwin/requirements_dev.txt --python-platform aarch64-apple-darwin --python-version 3.9 +# uv pip compile requirements_dev.in -o /Users/kiyoon/project/python-import.nvim/deps/lock/aarch64-apple-darwin/requirements_dev.txt --python-platform aarch64-apple-darwin --python-version 3.9 click==8.1.7 # via typer coverage==7.5.3 @@ -26,7 +26,7 @@ pytest-cov==5.0.0 # via -r requirements_dev.in rich==13.7.1 # via typer -ruff==0.6.9 +ruff==0.9.6 # via -r requirements_dev.in shellingham==1.5.4 # via typer @@ -34,6 +34,7 @@ tomli==2.0.1 # via # coverage # pytest + # version-pioneer tree-sitter==0.23.0 # via -r requirements.in tree-sitter-python==0.23.0 @@ -42,3 +43,5 @@ typer==0.12.4 # via -r requirements.in typing-extensions==4.12.2 # via typer +version-pioneer==0.0.13 + # via -r requirements_dev.in diff --git a/deps/lock/x86_64-apple-darwin/.requirements_dev.in.sha256 b/deps/lock/x86_64-apple-darwin/.requirements_dev.in.sha256 index 835d712..e458c9b 100644 --- a/deps/lock/x86_64-apple-darwin/.requirements_dev.in.sha256 +++ b/deps/lock/x86_64-apple-darwin/.requirements_dev.in.sha256 @@ -1 +1 @@ -d7314ad59261b4109ef6e9022d331c212b1cb1dc62da32163cea3cef9fc1a3f2 requirements_dev.in +425899e6ec1e329b19f09174d83b1bccec945bba515f7747009f7f494d77cd18 requirements_dev.in diff --git a/deps/lock/x86_64-apple-darwin/requirements_dev.txt b/deps/lock/x86_64-apple-darwin/requirements_dev.txt index 2b5ab24..472d04a 100644 --- a/deps/lock/x86_64-apple-darwin/requirements_dev.txt +++ b/deps/lock/x86_64-apple-darwin/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o /home/runner/work/python-import.nvim/python-import.nvim/deps/lock/x86_64-apple-darwin/requirements_dev.txt --python-platform x86_64-apple-darwin --python-version 3.9 +# uv pip compile requirements_dev.in -o /Users/kiyoon/project/python-import.nvim/deps/lock/x86_64-apple-darwin/requirements_dev.txt --python-platform x86_64-apple-darwin --python-version 3.9 click==8.1.7 # via typer coverage==7.5.3 @@ -26,7 +26,7 @@ pytest-cov==5.0.0 # via -r requirements_dev.in rich==13.7.1 # via typer -ruff==0.6.9 +ruff==0.9.6 # via -r requirements_dev.in shellingham==1.5.4 # via typer @@ -34,6 +34,7 @@ tomli==2.0.1 # via # coverage # pytest + # version-pioneer tree-sitter==0.23.0 # via -r requirements.in tree-sitter-python==0.23.0 @@ -42,3 +43,5 @@ typer==0.12.4 # via -r requirements.in typing-extensions==4.12.2 # via typer +version-pioneer==0.0.13 + # via -r requirements_dev.in diff --git a/deps/lock/x86_64-manylinux_2_28/.requirements_dev.in.sha256 b/deps/lock/x86_64-manylinux_2_28/.requirements_dev.in.sha256 index 835d712..e458c9b 100644 --- a/deps/lock/x86_64-manylinux_2_28/.requirements_dev.in.sha256 +++ b/deps/lock/x86_64-manylinux_2_28/.requirements_dev.in.sha256 @@ -1 +1 @@ -d7314ad59261b4109ef6e9022d331c212b1cb1dc62da32163cea3cef9fc1a3f2 requirements_dev.in +425899e6ec1e329b19f09174d83b1bccec945bba515f7747009f7f494d77cd18 requirements_dev.in diff --git a/deps/lock/x86_64-manylinux_2_28/requirements_dev.txt b/deps/lock/x86_64-manylinux_2_28/requirements_dev.txt index a2d9d30..77d9303 100644 --- a/deps/lock/x86_64-manylinux_2_28/requirements_dev.txt +++ b/deps/lock/x86_64-manylinux_2_28/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o /home/runner/work/python-import.nvim/python-import.nvim/deps/lock/x86_64-manylinux_2_28/requirements_dev.txt --python-platform x86_64-manylinux_2_28 --python-version 3.9 +# uv pip compile requirements_dev.in -o /Users/kiyoon/project/python-import.nvim/deps/lock/x86_64-manylinux_2_28/requirements_dev.txt --python-platform x86_64-manylinux_2_28 --python-version 3.9 click==8.1.7 # via typer coverage==7.5.3 @@ -26,7 +26,7 @@ pytest-cov==5.0.0 # via -r requirements_dev.in rich==13.7.1 # via typer -ruff==0.6.9 +ruff==0.9.6 # via -r requirements_dev.in shellingham==1.5.4 # via typer @@ -34,6 +34,7 @@ tomli==2.0.1 # via # coverage # pytest + # version-pioneer tree-sitter==0.23.0 # via -r requirements.in tree-sitter-python==0.23.0 @@ -42,3 +43,5 @@ typer==0.12.4 # via -r requirements.in typing-extensions==4.12.2 # via typer +version-pioneer==0.0.13 + # via -r requirements_dev.in diff --git a/deps/lock/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 b/deps/lock/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 index 835d712..e458c9b 100644 --- a/deps/lock/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 +++ b/deps/lock/x86_64-pc-windows-msvc/.requirements_dev.in.sha256 @@ -1 +1 @@ -d7314ad59261b4109ef6e9022d331c212b1cb1dc62da32163cea3cef9fc1a3f2 requirements_dev.in +425899e6ec1e329b19f09174d83b1bccec945bba515f7747009f7f494d77cd18 requirements_dev.in diff --git a/deps/lock/x86_64-pc-windows-msvc/requirements_dev.txt b/deps/lock/x86_64-pc-windows-msvc/requirements_dev.txt index 011dd1f..caeb791 100644 --- a/deps/lock/x86_64-pc-windows-msvc/requirements_dev.txt +++ b/deps/lock/x86_64-pc-windows-msvc/requirements_dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_dev.in -o /home/runner/work/python-import.nvim/python-import.nvim/deps/lock/x86_64-pc-windows-msvc/requirements_dev.txt --python-platform x86_64-pc-windows-msvc --python-version 3.9 +# uv pip compile requirements_dev.in -o /Users/kiyoon/project/python-import.nvim/deps/lock/x86_64-pc-windows-msvc/requirements_dev.txt --python-platform x86_64-pc-windows-msvc --python-version 3.9 click==8.1.7 # via typer colorama==0.4.6 @@ -30,7 +30,7 @@ pytest-cov==5.0.0 # via -r requirements_dev.in rich==13.7.1 # via typer -ruff==0.6.9 +ruff==0.9.6 # via -r requirements_dev.in shellingham==1.5.4 # via typer @@ -38,6 +38,7 @@ tomli==2.0.1 # via # coverage # pytest + # version-pioneer tree-sitter==0.23.0 # via -r requirements.in tree-sitter-python==0.23.0 @@ -46,3 +47,5 @@ typer==0.12.4 # via -r requirements.in typing-extensions==4.12.2 # via typer +version-pioneer==0.0.13 + # via -r requirements_dev.in diff --git a/deps/requirements_dev.in b/deps/requirements_dev.in index 0d54ba2..259f064 100644 --- a/deps/requirements_dev.in +++ b/deps/requirements_dev.in @@ -1,4 +1,5 @@ -r requirements.in -ruff==0.6.9 +version-pioneer +ruff==0.9.6 pytest>=8.0.1 pytest-cov>=4.1.0 diff --git a/pyproject.toml b/pyproject.toml index 66ecdd1..b850638 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,22 @@ [build-system] -requires = ["setuptools>=60", "versioneer[toml]==0.29"] -build-backend = "setuptools.build_meta" +requires = ["hatchling", "hatch-requirements-txt", "version-pioneer"] +build-backend = "hatchling.build" -[tool.versioneer] -VCS = "git" -style = "pep440" -versionfile_source = "src/python_import/_version.py" # CHANGE -versionfile_build = "python_import/_version.py" # CHANGE -tag_prefix = "v" -parentdir_prefix = "python_import-" # CHANGE +[tool.hatch.metadata.hooks.requirements_txt] +files = ["deps/requirements.in"] + +[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] +dev = ["deps/requirements_dev.in"] + +[tool.hatch.version] +source = "version-pioneer" + +[tool.hatch.build.hooks.version-pioneer] + +[tool.version-pioneer] +versionscript = "src/python_import/_version.py" # CHANGE +versionfile-source = "src/python_import/_version.py" # CHANGE +versionfile-build = "python_import/_version.py" # CHANGE [project] name = "python-import" # CHANGE @@ -34,15 +42,15 @@ keywords = ["neovim", "nvim", "nvim-plugin", "python", "python-import", "autoimp [project.urls] "Homepage" = "https://github.com/kiyoon/python-import.nvim" # OPTIONALLY CHANGE -[tool.setuptools.dynamic] -dependencies = {file = ["deps/requirements.in"]} - -[tool.setuptools.packages.find] -where = ["src"] - [project.scripts] python-import = "python_import.cli:app" +[tool.projector.pip-compile] +# https://github.com/deargen/workflows/blob/master/python-projector +requirements-in-dir = "deps" +requirements-out-dir = "deps/lock" +python-platforms = ["x86_64-manylinux_2_28", "aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-pc-windows-msvc"] + [tool.pyright] include = ["src"] @@ -66,10 +74,10 @@ omit = [ ] [tool.ruff] -src = ["src"] # for ruff isort -namespace-packages = ["tools", "scripts", "tests"] # for INP rule, suppress on these directories +# Ignore INP001 on these directories +# The directories that do not contain s, r, and c are already ignored. +namespace-packages = ["scripts"] extend-exclude = [ - "src/python_import/_version.py", # CHANGE "tests/sample_projects/project1/*", "scripts/version_from_tag.py", ] @@ -77,52 +85,7 @@ extend-exclude = [ [tool.ruff.lint] # OPTIONALLY ADD MORE LATER select = [ - # flake8 - "E", - "F", - "W", - "B", # Bugbear - "D", # Docstring - "D213", # Multi-line docstring summary should start at the second line (replace D212) - "N", # Naming - "C4", # flake8-comprehensions - "UP", # pyupgrade - "SIM", # simplify - "RUF", # ruff-specific - "RET501", # return - "RET502", # return - "RET503", # return - "PTH", # path - "NPY", # numpy - "PD", # pandas - "PYI", # type stubs for pyright/pylance - "PT", # pytest - "PIE", # - "LOG", # logging - "COM818", # comma misplaced - "COM819", # comma - "DTZ", # datetime - "YTT", - "ASYNC", - "FBT", # boolean trap - "A", # Shadowing python builtins - "EXE", # executable (shebang) - "FA", # future annotations - "ISC", # Implicit string concatenation - "ICN", # Import convention - "INP", # Implicit namespace package (no __init__.py) - "Q", # Quotes - "RSE", # raise - "SLOT", # __slots__ - "PL", # Pylint - "TRY", # try - "FAST", # FastAPI - "AIR", # airflow - "DOC", # docstring - - # Not important - "T10", # debug statements - "T20", # print statements + "ALL", ] ignore = [ @@ -131,6 +94,7 @@ ignore = [ "W291", # Trailing whitespace "D10", # Missing docstring in public module / function / etc. "D200", # One-line docstring should fit on one line with quotes + "D205", # 1 blank line required between summary line and description "D212", # Multi-line docstring summary should start at the first line "D417", # require documentation for every function parameter. "D401", # require an imperative mood for all docstrings. @@ -142,8 +106,33 @@ ignore = [ "UP017", # datetime.timezone.utc -> datetime.UTC "SIM108", # use ternary operator instead of if-else "TRY003", # long message in except + "TRY400", # logger.exception instead of logger.error + "PLR2004", # magic value comparison + "PLW2901", # loop variable overwritten by assignment target + "COM812", # missing trailing comma + "RET504", # return with unnecessary assignment + "RET505", + "RET506", + "RET507", + "RET508", + "S", # Security issues + "ANN", # Missing type annotations + "ERA001", # commented-out code + "G", # Logging with format string + "EM", # error message has to be variable + "SLF001", # private member access + "TD", # TODO + "FIX", # TODO + "ARG", # unused argument ] +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ + "F401", # Ignore seemingly unused imports (they're meant for re-export) +] +# Directories that do not contain s, r, and c +"[!s][!r][!c]*/**" = ["INP001"] # Implicit namespace package (no __init__.py) + [tool.ruff.lint.pydocstyle] convention = "google" @@ -155,20 +144,44 @@ max-line-length = 120 [tool.ruff.lint.isort] # combine-as-imports = true known-third-party = ["wandb"] +known-first-party = [ + "rust_graph", + "bio_data_to_db", + "reduce_binary", + "apbs_binary", + "msms_binary", + "slack_helpers", + "biotest", +] ## Uncomment this if you want to use Python < 3.10 -required-imports = [ - "from __future__ import annotations", -] +# required-imports = [ +# "from __future__ import annotations", +# ] -[tool.ruff.lint.flake8-tidy-imports] +# [tool.ruff.lint.flake8-tidy-imports] # Ban certain modules from being imported at module level, instead requiring # that they're imported lazily (e.g., within a function definition, if TYPE_CHECKING, etc.) -# NOTE: Ruff code TID is currently disabled, so this settings doesn't do anything. -banned-module-level-imports = ["torch", "tensorflow"] +# banned-module-level-imports = ["torch"] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"pytorch_lightning".msg = "Use lightning.fabric instead" +"lightning.pytorch".msg = "Use lightning.fabric instead" +"lightning_fabric".msg = "Use lightning.fabric instead" +"accelerate".msg = "Use lightning.fabric instead" +"os.system".msg = "Use subprocess.run or subprocess.Popen instead" +"easydict".msg = "Use typing.TypedDict instead (also consider dataclasses and pydantic)" [tool.ruff.lint.pylint] -max-args = 10 +max-args = 15 max-bool-expr = 10 max-statements = 100 - +max-returns = 6 +max-public-methods = 30 +max-nested-blocks = 10 +max-locals = 30 +max-branches = 24 + +[tool.ruff.lint.mccabe] +# C901: limit the number of decision points in a function +max-complexity = 20 diff --git a/scripts/version_from_tag.py b/scripts/version_from_tag.py old mode 100644 new mode 100755 index 7223c5d..a76436d --- a/scripts/version_from_tag.py +++ b/scripts/version_from_tag.py @@ -1,92 +1,822 @@ -# 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.29 -# https://github.com/python-versioneer/python-versioneer -from __future__ import annotations - +#!/usr/bin/env python3 +# THIS "versionscript" IS GENERATED BY version-pioneer-0.0.13 +""" +Generate a version number from Git tags (e.g. tag "v1.2.3" and 4 commits -> "1.2.3+4.g123abcdef" in "pep440" style). + +This "versionscript" may be replaced with a much shorter "versionfile" in distribution tarballs +(built by `uv build`, `pyproject-build`) that just contains one method: +`def get_version_dict() -> VersionDict: return {"version": "0.1.0", ...}`. + +Refactored from Versioneer's _version.py. + +Note: + - Should be compatible with python 3.8+ without any dependencies. + - (For dev) Avoid importing third-party libraries, including version-pioneer itself + because this file can get vendored into other projects. + - (For user) Once the script is vendored, and you want to customise it, of course you can + import other libraries and add those in build-time dependencies. + - This file is usually located at `src/my_package/_version.py`. + - `src/my_package/__init__.py` should define `__version__ = get_version_dict()["version"]` by importing this module. + - It should also be able to be run as a script to print the version info in json format. + - It is often `exec`-uted and `get_version_dict()` is evaluated from this file. + - Using `from __future__ import ...` with dataclasses makes it hard to "exec" this file, + so you MUST NOT use both here. + - See https://github.com/mkdocs/mkdocs/issues/3141 + https://github.com/pypa/hatch/issues/1863 + https://github.com/sqlalchemy/alembic/issues/1419 + - You need to put the module in `sys.modules` before executing + because dataclass will look for the type there. + - While this can be fixed, it's a common gotcha and I expect that + some build backends or tools will be buggy. + - For now, we don't future import. In dataclass definition, + we use `typing.Optional` instead of `| None` + until we drop support for Python 3.9. +""" + +# ruff: noqa: T201 FA100 + +import contextlib import errno import functools import os import re import subprocess import sys -from collections.abc import Callable -from typing import Any - - -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 - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - VCS: str - style: str - tag_prefix: str - parentdir_prefix: str - versionfile_source: str +from collections.abc import Iterable +from dataclasses import dataclass +from email.parser import Parser +from enum import Enum +from os import PathLike +from pathlib import Path +from typing import Any, Literal, Optional, TypedDict, TypeVar, Union + + +class VersionStyle(str, Enum): + pep440 = "pep440" + pep440_master = "pep440-master" + pep440_branch = "pep440-branch" + pep440_pre = "pep440-pre" + pep440_post = "pep440-post" + pep440_post_branch = "pep440-post-branch" + git_describe = "git-describe" + git_describe_long = "git-describe-long" + digits = "digits" + + +VERSION_STYLE_TYPE = TypeVar( + "VERSION_STYLE_TYPE", + Literal[ + "pep440", + "pep440-master", + "pep440-branch", + "pep440-pre", + "pep440-post", + "pep440-post-branch", + "git-describe", + "git-describe-long", + "digits", + ], + VersionStyle, +) + + +# ┌──────────────────────────────────────────┐ +# │ Modify the configuration below. │ +# └──────────────────────────────────────────┘ +@dataclass(frozen=True) +class VersionPioneerConfig: + style: VersionStyle = VersionStyle.pep440_master + tag_prefix: str = "v" + # if there is no .git, like it's a source tarball downloaded from GitHub Releases, + # find version from the name of the parent directory. + # e.g. setting it to "github-repo-name-" will find the version from "github-repo-name-1.2.3" + # Set it to None to try to determine the prefix from pyproject.toml. + parentdir_prefix: Optional[str] = None + verbose: bool = False + + +# ┌──────────────────────────────────────────┐ +# │ Modify the configuration above. │ +# └──────────────────────────────────────────┘ + + +class VersionDict(TypedDict): + """Return type of get_version_dict().""" + + version: str + full_revisionid: "str | None" + dirty: "bool | None" + error: "str | None" + date: "str | None" + + +try: + _SCRIPT_DIR_OR_CURRENT_DIR = Path(__file__).resolve().parent +except NameError: + # NOTE: py2exe/bbfreeze/non-cpython implementations may not have __file__. + # and usually during installation when this file is evaluated, __file__ doesn't exist. + # However, once you installed in editable mode (e.g. `pip install -e .`), + # __file__ will be available and more reliable. + _SCRIPT_DIR_OR_CURRENT_DIR = Path.cwd() + + +MASTER_BRANCHES = ("master", "main") + +if sys.platform == "win32": + GIT_COMMANDS = ["git.cmd", "git.exe"] +else: + GIT_COMMANDS = ["git"] + +# 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) + + +# https://github.com/pypa/packaging/blob/24.2/src/packaging/version.py#L117-L146 +# Make parentdir-prefix only match version strings. +# Example: +# the GitHub repo can be myprogram-python and the package name is myprogram, +# leading to parse "python" as a version string. +# So we restrict it to search myprogram-python-1.0.0 styled folders only. +# Note: +# - The check passes version strings other than PEP440. This is good because there are projects other than Python. +# - Use with re.VERBOSE: `re.match(_VERSION_PATTERN, "1.0.0", re.VERBOSE)` +_VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?Palpha|a|beta|b|preview|pre|c|rc)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+
+class NotThisMethodError(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+
+class CurrentBranchIsMasterError(Exception):
+    """GitMasterDistance can't be initialised if the current branch is master."""
+
+
+@dataclass(frozen=True)
+class GitMasterDistance:
+    """
+    Compute the distance from the tag until master, and from master to current branch.
+
+    Useful if you often create a develop branch from master.
+
+    Relevant commands:
+        ```console
+        $ git branch --show-current
+        feat/branch-name
+
+        $ # use with -a to include remote branches
+        $ git branch --contains 
+        * master
+          feat/branch-name
+
+        $ # Notice that there are no master (only the origin/master) in the list
+        $ # because the master has more commits than the current branch.
+        $ # Thus we can't use the '%D' ref names.
+        $ # We instead use `git branch --contains` on each commit to find the branch.
+        $ git log v0.3.2..[BRANCH] --pretty=format:"%H,%h,%D"  # BRANCH is optional (default: current branch)
+        87e38450d1fce0398fbc9de08f2abe3e5da0431e,87e3845,trash/version
+        8a76eac0f107dd3810c3cfd5c89e92c7f5d31e50,8a76eac,
+        a855d640912f728b8946eddba41ed5b2a992f394,a855d64,origin/master, origin/HEAD
+        5cb3c6663f494b3c99dfd23b5394fa2da7f49cef,5cb3c66,
+        2c00c0ed4e46a5459bc70f47739a0c50a789d3c3,2c00c0e,
+        812d3e29666f2d75b80b4160532fa25afaab2ffd,812d3e2,tag: v0.3.3
+        2127fd373d14ed5ded497fc18ac1c1b667f93a7d,2127fd3,
+        ae7cb503342d551e2503dc0a90be656946342743,ae7cb50,
+        ```
+
+    References:
+        - https://stackoverflow.com/questions/4649356/how-do-i-run-git-log-to-see-changes-only-for-a-specific-branch
+        - https://stackoverflow.com/questions/2706797/finding-what-branch-a-git-commit-came-from
+        - https://stackoverflow.com/questions/3998883/git-how-to-find-commit-hash-where-branch-originated-from
+            - The reason we can't use reflog..
+    """
+
+    current_branch: str
+    distance_from_tag_to_master: int
+    distance_from_master: int
+    master_commit: str
+
+    @property
+    def master_commit_short(self) -> "str | None":
+        """Return the short commit hash of the master commit."""
+        if self.master_commit is not None:
+            return self.master_commit[:7]
+        return None
+
+    @classmethod
+    def from_git(
+        cls: "type[GitMasterDistance]",
+        tag_of_interest: "str | None",
+        *,
+        cwd: "str | PathLike",
+        verbose: bool = False,
+    ) -> "GitMasterDistance":
+        git_runner = functools.partial(
+            _run_git_command_or_error, env=env, verbose=verbose
+        )
+
+        # Get the current branch name
+        current_branch, rc = git_runner(
+            ["branch", "--show-current"],
+            cwd=cwd,
+        )
+        current_branch = current_branch.strip()
+
+        if current_branch in MASTER_BRANCHES:
+            raise CurrentBranchIsMasterError(
+                "Current branch is master. Can't compute distance to/from master."
+            )
+
+        if tag_of_interest is None:
+            # Get entire history (commits) from the current branch
+            out, rc = git_runner(
+                ["log", "--pretty=format:%H"],
+                cwd=cwd,
+            )
+        else:
+            # Get history from the tag to the current branch
+            out, rc = git_runner(
+                ["log", f"{tag_of_interest}..", "--pretty=format:%H"],
+                cwd=cwd,
+            )
+
+        all_commits = out.splitlines()
+
+        # NOTE: all commits could be no output if the tag is the same as the current branch.
+
+        # Search from the top, and find the first commit that shares the master branch
+        distance_from_master = 0
+        master_commit = None
+        for commit in all_commits:
+            out, rc = git_runner(
+                ["branch", "--contains", commit],
+                cwd=cwd,
+            )
+            branches = out.splitlines()
+            # Strip off the leading "* " from the list of branches.
+            branches = [branch.lstrip("* ") for branch in branches]
+            if any(
+                master_branch_name in branches for master_branch_name in MASTER_BRANCHES
+            ):
+                master_commit = commit
+                break
+
+            distance_from_master += 1
+
+        if master_commit is None:
+            # Can't find master? We assume that it's the tag. It may not always be. (like release branch)
+            if tag_of_interest is None:
+                if verbose:
+                    print(
+                        "No tag found and none of the commit history points to master/main."
+                    )
+                    print("Maybe detached head or you don't use master?")
+                raise NotThisMethodError(
+                    "No tag found and none of the commit history points to master/main. "
+                    "Maybe detached head or you don't use master?"
+                )
+
+            out, re = git_runner(["rev-list", "-1", tag_of_interest], cwd=cwd)
+            master_commit = out.strip()
+            if len(master_commit) != 40:
+                raise NotThisMethodError("Something is strange in you git commit hash")
+
+        return cls(
+            current_branch=current_branch,
+            distance_from_tag_to_master=len(all_commits) - distance_from_master,
+            distance_from_master=distance_from_master,
+            master_commit=master_commit,
+        )
+
+
+@dataclass(frozen=True)
+class GitPieces:
+    """
+    Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+
+    long: str
+    short: str
+    branch: str
+    dirty: bool
+
+    # options to pass to GitMasterDistance
+    cwd: Union[str, PathLike]
     verbose: bool
 
+    error: Optional[str] = None
+    distance: Optional[int] = None
+    closest_fulltag: Optional[str] = None  # include tag_prefix (v1.0.0)
+    closest_tag: Optional[str] = None  # strip tag_prefix (1.0.0)
+    date: Optional[str] = None
+
+    @classmethod
+    def from_git(
+        cls: "type[GitPieces]",
+        tag_prefix: str,
+        *,
+        cwd: "str | PathLike",
+        verbose: bool = False,
+    ) -> "GitPieces":
+        runner = functools.partial(_run_command, env=env, verbose=verbose)
+
+        _, rc = runner(
+            GIT_COMMANDS, ["rev-parse", "--git-dir"], cwd=cwd, hide_stderr=not verbose
+        )
+        if rc != 0:
+            if verbose:
+                print(f"Directory {cwd} not under git control")
+            raise NotThisMethodError("'git rev-parse --git-dir' returned error")
+
+        # 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 = runner(
+            GIT_COMMANDS,
+            [
+                "describe",
+                "--tags",
+                "--dirty",
+                "--always",
+                "--long",
+                "--match",
+                f"{tag_prefix}[[:digit:]]*",
+            ],
+            cwd=cwd,
+        )
+        # --long was added in git-1.5.5
+        if describe_out is None:
+            raise NotThisMethodError("'git describe' failed")
+        describe_out = describe_out.strip()
+        full_out, rc = runner(GIT_COMMANDS, ["rev-parse", "HEAD"], cwd=cwd)
+        if full_out is None:
+            raise NotThisMethodError("'git rev-parse' failed")
+        full_out = full_out.strip()
+
+        pieces: dict[str, Any] = {}
+        pieces["long"] = full_out
+        pieces["short"] = full_out[:7]  # maybe improved later
+        pieces["error"] = None
+
+        branch_name, rc = runner(
+            GIT_COMMANDS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd
+        )
+        # --abbrev-ref was added in git-1.6.3
+        if rc != 0 or branch_name is None:
+            raise NotThisMethodError("'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(GIT_COMMANDS, ["branch", "--contains"], cwd=cwd)
+            # --contains was added in git-1.5.4
+            if rc != 0 or branches is None:
+                raise NotThisMethodError("'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.lstrip("* ") for branch in branches]
+            if not branches:
+                branch_name = None
+            else:
+                for master_branch_name in MASTER_BRANCHES:
+                    if master_branch_name in branches:
+                        branch_name = master_branch_name
+                        break
+                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
+
+        # look for -dirty suffix
+        dirty = git_describe.endswith("-dirty")
+        pieces["dirty"] = dirty
+        if 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)
+            if not mo:
+                # unparsable. Maybe git-describe is misbehaving?
+                pieces["error"] = (
+                    f"unable to parse git-describe output: '{describe_out}'"
+                )
+                return cls(**pieces, cwd=cwd, verbose=verbose)
+
+            # tag
+            full_tag = mo.group(1)
+            if not full_tag.startswith(tag_prefix):
+                if verbose:
+                    print(f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'")
+                pieces["error"] = (
+                    f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'"
+                )
+                return cls(**pieces, cwd=cwd, verbose=verbose)
+            pieces["closest_fulltag"] = full_tag
+            pieces["closest_tag"] = full_tag[len(tag_prefix) :]
+
+            # distance: number of commits since tag
+            pieces["distance"] = int(mo.group(2))
+
+            # commit: short hex revision ID
+            pieces["short"] = mo.group(3)
 
-def get_config() -> VersioneerConfig:
-    """Create, populate and return the VersioneerConfig() object."""
-    # these strings are filled in when 'setup.py versioneer' creates
-    # _version.py
-    cfg = VersioneerConfig()
-    cfg.VCS = "git"
-    cfg.style = "pep440"
-    cfg.tag_prefix = "v"
-    cfg.parentdir_prefix = "mlproject-"
-    cfg.versionfile_source = "version.py"
-    cfg.verbose = False
+        else:
+            # HEX: no tags
+            pieces["closest_fulltag"] = None
+            pieces["closest_tag"] = None
+            out, rc = runner(
+                GIT_COMMANDS, ["rev-list", "HEAD", "--left-right"], cwd=cwd
+            )
+            assert out is not None
+            pieces["distance"] = len(out.split())  # total number of commits
 
-    return cfg
+        # commit date: see ISO-8601 comment in git_versions_from_keywords()
+        out, rc = runner(GIT_COMMANDS, ["show", "-s", "--format=%ci", "HEAD"], cwd=cwd)
+        assert out is not None
+        date = out.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 cls(**pieces, cwd=cwd, verbose=verbose)
+
+    @property
+    def _plus_or_dot(self) -> str:
+        """Return a + if we don't already have one, else return a ."""
+        if self.closest_tag is None:
+            return "+"
+        elif "+" in self.closest_tag:
+            return "."
+        return "+"
+
+    def _render_pep440(self) -> str:
+        """
+        Build up version string, with post-release "local version identifier".
+
+        Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+        get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+        Exceptions:
+        1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+        """
+        if self.closest_tag:
+            rendered = self.closest_tag
+            if self.distance or self.dirty:
+                rendered += self._plus_or_dot
+                rendered += f"{self.distance}.g{self.short}"
+                if self.dirty:
+                    rendered += ".dirty"
+        else:
+            # exception #1
+            rendered = f"0+untagged.{self.distance}.g{self.short}"
+            if self.dirty:
+                rendered += ".dirty"
+        return rendered
 
+    def _render_pep440_master(self) -> str:
+        """
+        Render including master branch distance and commit like TAG[+MASTERDIST.gMASTERHEX.BRANCHDIST.gHEX[.dirty]].
 
-class NotThisMethod(Exception):
-    """Exception raised if a method is not valid for the current scenario."""
+        For example, 'v1.2.3+4.g1abcdef.5.g2345678'
+        meaning 4 commits from v1.2.3 to master, and 5 commits from master to the current branch.
 
+        Exceptions:
+            1. no tags. git_describe was just HEX. 0+untagged.MASTERDISTANCE.gMASTERHEX.BRANCHDIST.gHEX[.dirty]
+            2. current branch is master. Just like PEP440 style.
+            3. if no master is found after the tag, we assume tag = master. (TAG+0.gTAGHEX.BRANCHDIST.gHEX[.dirty])
+                - the logic is in GitMasterDistance.from_git()
 
-LONG_VERSION_PY: dict[str, str] = {}
-HANDLERS: dict[str, dict[str, Callable]] = {}
+        Note:
+            - New in Version-Pioneer.
+        """
+        try:
+            master_info = GitMasterDistance.from_git(
+                tag_of_interest=self.closest_fulltag, cwd=self.cwd, verbose=self.verbose
+            )
+        except CurrentBranchIsMasterError:
+            # exception #2
+            return self._render_pep440()
+
+        if (
+            master_info.distance_from_tag_to_master == 0
+            and master_info.distance_from_master == 0
+        ):
+            # Just the tag
+            return self.closest_tag or "0+untagged"
+
+        if self.closest_tag:
+            rendered = self.closest_tag
+            rendered += self._plus_or_dot
+        else:
+            # exception #1
+            rendered = "0+untagged."
 
+        rendered += (
+            f"{master_info.distance_from_tag_to_master}.g{master_info.master_commit_short}"
+            f".{master_info.distance_from_master}.g{self.short}"
+        )
 
-def register_vcs_handler(vcs: str, method: str) -> Callable:  # decorator
-    """Create decorator to mark a method as the handler of a VCS."""
+        if self.dirty:
+            rendered += ".dirty"
 
-    def decorate(f: Callable) -> Callable:
-        """Store f in HANDLERS[vcs][method]."""
-        if vcs not in HANDLERS:
-            HANDLERS[vcs] = {}
-        HANDLERS[vcs][method] = f
-        return f
+        return rendered
+
+    def _render_pep440_branch(self) -> 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[.dev0]+untagged.DISTANCE.gHEX[.dirty]
+        """
+        if self.closest_tag:
+            rendered = self.closest_tag
+            if self.distance or self.dirty:
+                if self.branch not in MASTER_BRANCHES:
+                    rendered += ".dev0"
+                rendered += self._plus_or_dot
+                rendered += f"{self.distance}.g{self.short}"
+                if self.dirty:
+                    rendered += ".dirty"
+        else:
+            # exception #1
+            rendered = "0"
+            if self.branch not in MASTER_BRANCHES:
+                rendered += ".dev0"
+            rendered += f"+untagged.{self.distance}.g{self.short}"
+            if self.dirty:
+                rendered += ".dirty"
+        return rendered
+
+    @staticmethod
+    def _pep440_split_post(ver: str) -> "tuple[str, int | None]":
+        """
+        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(self) -> str:
+        """
+        TAG[.postN.devDISTANCE] -- No -dirty.
+
+        Exceptions:
+            1: no tags. 0.post0.devDISTANCE
+        """
+        if self.closest_tag:
+            if self.distance:
+                # update the post release segment
+                tag_version, post_version = self._pep440_split_post(self.closest_tag)
+                rendered = tag_version
+                if post_version is not None:
+                    rendered += f".post{post_version}.dev{self.distance}"
+                else:
+                    rendered += f".post0.dev{self.distance}"
+            else:
+                # no commits, use the tag as the version
+                rendered = self.closest_tag
+        else:
+            # exception #1
+            rendered = f"0.post0.dev{self.distance}"
+        return rendered
+
+    def _render_pep440_post(self) -> str:
+        """
+        TAG[.postDISTANCE+gHEX[.dirty]] .
+
+        Note:
+            Difference from versioneer is that .dev0 used to be used for .dirty. Their note:
+
+            > TAG[.postDISTANCE[.dev0]+gHEX] .
+            >
+            > The ".dev0" means dirty. Note that .dev0 sorts backwards
+            > (a dirty tree will appear "older" than the corresponding clean one),
+            > but you shouldn't be releasing software with -dirty anyways.
+
+        Exceptions:
+            When no tags: 0.postDISTANCE+gHEX[.dirty]
+        """
+        if self.closest_tag:
+            rendered = self.closest_tag
+            if self.distance or self.dirty:
+                rendered += f".post{self.distance}"
+                rendered += self._plus_or_dot
+                rendered += f"g{self.short}"
+                if self.dirty:
+                    rendered += ".dirty"
+        else:
+            # exception
+            rendered = f"0.post{self.distance}"
+            rendered += f"+g{self.short}"
+            if self.dirty:
+                rendered += ".dirty"
+        return rendered
+
+    def _render_pep440_post_branch(self) -> str:
+        """
+        TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
+
+        The ".dev0" means not master branch.
+
+        Exceptions:
+        1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
+        """
+        if self.closest_tag:
+            rendered = self.closest_tag
+            if self.distance or self.dirty:
+                rendered += f".post{self.distance}"
+                if self.branch not in MASTER_BRANCHES:
+                    rendered += ".dev0"
+                rendered += self._plus_or_dot
+                rendered += f"g{self.short}"
+                if self.dirty:
+                    rendered += ".dirty"
+        else:
+            # exception #1
+            rendered = f"0.post{self.distance}"
+            if self.branch not in MASTER_BRANCHES:
+                rendered += ".dev0"
+            rendered += f"+g{self.short}"
+            if self.dirty:
+                rendered += ".dirty"
+        return rendered
 
-    return decorate
+    def _render_git_describe(self) -> str:
+        """
+        TAG[-DISTANCE-gHEX][-dirty].
 
+        Like 'git describe --tags --dirty --always'.
 
-def run_command(
-    commands: list[str],
-    args: list[str],
-    cwd: str | None = None,
-    verbose: bool = False,
+        Exceptions:
+        1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+        """
+        if self.closest_tag:
+            rendered = self.closest_tag
+            if self.distance:
+                rendered += f"-{self.distance}-g{self.short}"
+        else:
+            # exception #1
+            rendered = self.short
+        if self.dirty:
+            rendered += "-dirty"
+        return rendered
+
+    def _render_git_describe_long(self) -> str:
+        """
+        TAG-DISTANCE-gHEX[-dirty].
+
+        Like 'git describe --tags --dirty --always -long'.
+        The distance/hash is unconditional.
+
+        Exceptions:
+        1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+        """
+        if self.closest_tag:
+            rendered = self.closest_tag
+            rendered += f"-{self.distance}-g{self.short}"
+        else:
+            # exception #1
+            rendered = self.short
+        if self.dirty:
+            rendered += "-dirty"
+        return rendered
+
+    def _render_digits(self) -> str:
+        """
+        TAG.DISTANCE.
+
+        For example, 'v1.2.3+4.g1abcdef' -> '1.2.3.4' and
+        'v1.2.3+4.g1abcdef.dirty' -> '1.2.3.5' (dirty counts as 1 commit further).
+
+        Digit-only version string that is compatible with most package managers.
+
+        Note:
+            - New in Version-Pioneer.
+            - Compatible with Chrome extension version format.
+                - Chrome extension version should not have more than 4 segments,
+                  so make sure the tags are up to 3 segments.
+        """
+        if self.error:
+            raise ValueError("Unable to render version")
+
+        if self.closest_tag:
+            closest_tag: str = self.closest_tag
+        else:
+            closest_tag = "0"
+
+        version = closest_tag
+        if self.distance or self.dirty:
+            if self.dirty:
+                version += f".{self.distance + 1}"
+            else:
+                version += f".{self.distance}"
+
+        return version
+
+    def render(self, style: VersionStyle = VersionStyle.pep440) -> VersionDict:
+        """Render the given version pieces into the requested style."""
+        if self.error:
+            return {
+                "version": "unknown",
+                "full_revisionid": self.long,
+                "dirty": None,
+                "error": self.error,
+                "date": None,
+            }
+
+        if style == "pep440":
+            rendered = self._render_pep440()
+        elif style == "pep440-master":
+            rendered = self._render_pep440_master()
+        elif style == "pep440-branch":
+            rendered = self._render_pep440_branch()
+        elif style == "pep440-pre":
+            rendered = self._render_pep440_pre()
+        elif style == "pep440-post":
+            rendered = self._render_pep440_post()
+        elif style == "pep440-post-branch":
+            rendered = self._render_pep440_post_branch()
+        elif style == "git-describe":
+            rendered = self._render_git_describe()
+        elif style == "git-describe-long":
+            rendered = self._render_git_describe_long()
+        elif style == "digits":
+            rendered = self._render_digits()
+        else:
+            raise ValueError(f"unknown style '{style}'")
+
+        return {
+            "version": rendered,
+            "full_revisionid": self.long,
+            "dirty": self.dirty,
+            "error": None,
+            "date": self.date,
+        }
+
+
+def _run_command(
+    commands: "list[str]",
+    args: "list[str | PathLike]",
+    *,
+    cwd: "str | PathLike | None" = None,
     hide_stderr: bool = False,
-    env: dict[str, str] | None = None,
-) -> tuple[str | None, int | None]:
+    env: "dict[str, str] | None" = None,
+    verbose: bool = False,
+) -> "tuple[str | None, int | None]":
     """Call the given command(s)."""
     assert isinstance(commands, list)
     process = None
@@ -99,11 +829,11 @@ def run_command(
         popen_kwargs["startupinfo"] = startupinfo
 
     for command in commands:
+        dispcmd = str([command, *args])
         try:
-            dispcmd = str([command] + args)
             # remember shell=False, so use git.cmd on windows, not just git
             process = subprocess.Popen(
-                [command] + args,
+                [command, *args],
                 cwd=cwd,
                 env=env,
                 stdout=subprocess.PIPE,
@@ -115,618 +845,306 @@ def run_command(
             if e.errno == errno.ENOENT:
                 continue
             if verbose:
-                print("unable to run %s" % dispcmd)
+                print(f"unable to run {dispcmd}")
                 print(e)
             return None, None
     else:
         if verbose:
-            print("unable to find command, tried %s" % (commands,))
+            print(f"unable to find command, tried {commands}")
         return None, None
     stdout = process.communicate()[0].strip().decode()
     if process.returncode != 0:
         if verbose:
-            print("unable to run %s (error)" % dispcmd)
-            print("stdout was %s" % stdout)
+            print(f"unable to run {dispcmd} (error)")
+            print(f"stdout was {stdout}")
         return None, process.returncode
     return stdout, process.returncode
 
 
-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
-    the project name and a version string. We will also support searching up
-    two directory levels for an appropriately named parent directory
-    """
-    rootdirs = []
-
-    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,
-            }
-        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)
-        )
-    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-@register_vcs_handler("git", "get_keywords")
-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: dict[str, str] = {}
-    try:
-        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: dict[str, str],
-    tag_prefix: str,
-    verbose: bool,
-) -> dict[str, Any]:
-    """Get version information from git keywords."""
-    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
-        # it's been around since git-1.5.3, and it's too difficult to
-        # discover which version we're using, or to work around using an
-        # older one.
-        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-    refnames = keywords["refnames"].strip()
-    if refnames.startswith("$Format"):
-        if verbose:
-            print("keywords are unexpanded, not using")
-        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
-    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 = {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
-        # expansion behaves like git log --decorate=short and strips out the
-        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
-        # between branches and tags. By ignoring refnames without digits, we
-        # filter out many common branch names like "release" and
-        # "stabilization", as well as "HEAD" and "master".
-        tags = {r for r in refs if re.search(r"\d", r)}
-        if verbose:
-            print("discarding '%s', no digits" % ",".join(refs - tags))
-    if verbose:
-        print("likely tags: %s" % ",".join(sorted(tags)))
-    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) :]
-            # 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,
-            }
-    # 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,
-    }
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-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*
-    expanded, and _version.py hasn't already been rewritten with a short
-    version string, meaning we're inside a checked out source tree.
-    """
-    GITS = ["git"]
-    if sys.platform == "win32":
-        GITS = ["git.cmd", "git.exe"]
-
-    # 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)
-        raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
-    # 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 = runner(
-        GITS,
-        [
-            "describe",
-            "--tags",
-            "--dirty",
-            "--always",
-            "--long",
-            "--match",
-            f"{tag_prefix}[[:digit:]]*",
-        ],
-        cwd=root,
+def _run_git_command_or_error(
+    args: "list[str | PathLike]",
+    *,
+    cwd: "str | PathLike | None" = None,
+    env: "dict[str, str] | None" = None,
+    verbose: bool = False,
+):
+    """Run a git command or raise an error."""
+    out, rc = _run_command(
+        GIT_COMMANDS, args, cwd=cwd, hide_stderr=not verbose, env=env, verbose=verbose
     )
-    # --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 = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
-    if full_out is None:
-        raise NotThisMethod("'git rev-parse' failed")
-    full_out = full_out.strip()
-
-    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
-
-    # look for -dirty suffix
-    dirty = git_describe.endswith("-dirty")
-    pieces["dirty"] = dirty
-    if 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)
-        if not mo:
-            # unparsable. Maybe git-describe is misbehaving?
-            pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
-            return pieces
-
-        # tag
-        full_tag = mo.group(1)
-        if not full_tag.startswith(tag_prefix):
-            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,
-            )
-            return pieces
-        pieces["closest-tag"] = full_tag[len(tag_prefix) :]
-
-        # distance: number of commits since tag
-        pieces["distance"] = int(mo.group(2))
-
-        # commit: short hex revision ID
-        pieces["short"] = mo.group(3)
-
-    else:
-        # HEX: no tags
-        pieces["closest-tag"] = None
-        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 = 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)
+    if rc != 0 or out is None:
+        raise NotThisMethodError(f"'git {' '.join(map(str, args))}' returned error")
+    return out, rc
 
-    return 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: dict[str, Any]) -> str:
+def _find_root_dir_with_file(
+    source: "str | PathLike", marker: "str | Iterable[str]"
+) -> Path:
     """
-    Build up version string, with post-release "local version identifier".
-
-    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
-    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
-    Exceptions:
-    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    Find the first parent directory containing a specific "marker", relative to a file path.
     """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += plus_or_dot(pieces)
-            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
+    source = Path(source).resolve()
+    if isinstance(marker, str):
+        marker = {marker}
 
-def render_pep440_branch(pieces: dict[str, Any]) -> str:
-    """
-    TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
+    while source != source.parent:
+        if any((source / m).exists() for m in marker):
+            return source
 
-    The ".dev0" means not master branch. Note that .dev0 sorts backwards
-    (a feature branch will appear "older" than the master branch).
+        source = source.parent
 
-    Exceptions:
-    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, int | None]:
-    """
-    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"]:
-            # 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.post0.dev%d" % pieces["distance"]
-    return rendered
+    raise FileNotFoundError(f"File {marker} not found in any parent directory")
 
 
-def render_pep440_post(pieces: dict[str, Any]) -> str:
+def get_version_from_parentdir(
+    parentdir_prefix: "str | None", cwd: "str | PathLike", *, verbose: bool = False
+) -> VersionDict:
     """
-    TAG[.postDISTANCE[.dev0]+gHEX] .
+    Try to determine the version from the parent directory name.
 
-    The ".dev0" means dirty. Note that .dev0 sorts backwards
-    (a dirty tree will appear "older" than the corresponding clean one),
-    but you shouldn't be releasing software with -dirty anyways.
+    Source tarballs conventionally unpack into a directory that includes both the project name and a version string.
 
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
+    If parentdir_prefix is None, it will try to determine the parentdir_prefix from pyproject.toml.
     """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%s" % pieces["short"]
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-        rendered += "+g%s" % pieces["short"]
-    return rendered
-
+    rootdirs = []
 
-def render_pep440_post_branch(pieces: dict[str, Any]) -> str:
-    """
-    TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
+    # First find a directory with `pyproject.toml`, `setup.cfg`, or `setup.py`
 
-    The ".dev0" means not master branch.
+    try:
+        root = _find_root_dir_with_file(
+            cwd, ["pyproject.toml", "setup.cfg", "setup.py"]
+        )
+    except FileNotFoundError as e:
+        raise NotThisMethodError from e
+
+    def try_parentdir(
+        project_root: Path, parentdir_prefix: str
+    ) -> "VersionDict | None":
+        # It's likely that the root is the parent directory of the package,
+        # but in some cases like multiple languages, mono-repo, etc. it may not be.
+        for _ in range(3):
+            dirname = project_root.name
+            if dirname.startswith(parentdir_prefix):
+                version_candidate = dirname[len(parentdir_prefix) :]
+                if re.match(_VERSION_PATTERN, version_candidate, re.VERBOSE):
+                    return {
+                        "version": version_candidate,
+                        "full_revisionid": None,
+                        "dirty": False,
+                        "error": None,
+                        "date": None,
+                    }
+            rootdirs.append(project_root)
+
+            if project_root.parent.samefile(project_root):
+                break
+            project_root = project_root.parent
+
+        return None
+
+    def get_prefix_from_source_url(source_url: str) -> "str | None":
+        if not source_url.startswith(
+            "https://github.com/"
+        ) and not source_url.startswith("https://gitlab.com/"):
+            return None
+
+        # Remove trailing .git
+        if source_url.endswith(".git"):
+            source_url = source_url[:-4]
+
+        # Last part of the URL plus a hyphen
+        return source_url.split("/")[-1] + "-"
+
+    def try_parentdir_from_source_url(source_url: str) -> "VersionDict | None":
+        prefix = get_prefix_from_source_url(source_url)
+        if prefix:
+            version_dict = try_parentdir(root, prefix)
+            if version_dict:
+                return version_dict
+        return None
+
+    def try_all_parentdir_in_pyproject_toml(pyproject: dict) -> "VersionDict | None":
+        with contextlib.suppress(KeyError):
+            version_dict = try_parentdir_from_source_url(
+                pyproject["project"]["urls"]["homepage"]
+            )
+            if version_dict:
+                return version_dict
+        with contextlib.suppress(KeyError):
+            version_dict = try_parentdir_from_source_url(
+                pyproject["project"]["urls"]["Homepage"]
+            )
+            if version_dict:
+                return version_dict
+        with contextlib.suppress(KeyError):
+            version_dict = try_parentdir_from_source_url(
+                pyproject["project"]["urls"]["source"]
+            )
+            if version_dict:
+                return version_dict
+        with contextlib.suppress(KeyError):
+            version_dict = try_parentdir_from_source_url(
+                pyproject["project"]["urls"]["Source"]
+            )
+            if version_dict:
+                return version_dict
+            return None
+        with contextlib.suppress(KeyError):
+            return try_parentdir(root, pyproject["project"]["name"] + "-")
+
+    if parentdir_prefix is None:
+        # NOTE: New in Version-Pioneer
+        # Automatically determine the parentdir_prefix from pyproject.toml
+        # 1. project.urls.Homepage
+        # homepage / Homepage / source / Source -> if https://github.com/ -> remove trailing .git
+        """
+        # numpy example
+        [project.urls]
+        homepage = "https://numpy.org"
+        documentation = "https://numpy.org/doc/"
+        source = "https://github.com/numpy/numpy"
+        download = "https://pypi.org/project/numpy/#files"
+        tracker = "https://github.com/numpy/numpy/issues"
+        "release notes" = "https://numpy.org/doc/stable/release"
+        """
+        # NOTE: 2. project.name
+        """
+        [project]
+        name = "version-pioneer"
+        """
 
-    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"
+        try:
+            if sys.version_info >= (3, 11):
+                import tomllib
+            else:
+                import tomli as tomllib
+        except ModuleNotFoundError as e:
+            if verbose:
+                print(
+                    "tomli not found. Please install tomli or use Python 3.11 to "
+                    "automatically determine the parentdir_prefix from pyproject.toml"
+                )
+            raise NotThisMethodError(
+                "tomli not found. Please install tomli or use Python 3.11 to "
+                "automatically determine the parentdir_prefix from pyproject.toml"
+            ) from e
+        with open(root / "pyproject.toml", "rb") as f:
+            pyproject = tomllib.load(f)
+        version_dict = try_all_parentdir_in_pyproject_toml(pyproject)
     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
+        version_dict = try_parentdir(root, parentdir_prefix)
 
+    if version_dict:
+        return version_dict
 
-def render_pep440_old(pieces: dict[str, Any]) -> str:
-    """
-    TAG[.postDISTANCE[.dev0]] .
+    if verbose:
+        print(
+            f"Tried directories {rootdirs!s} but none started with prefix {parentdir_prefix or ''}"
+        )
+    raise NotThisMethodError("rootdir doesn't start with parentdir_prefix")
 
-    The ".dev0" means dirty.
 
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
+def get_version_from_pkg_info(cwd: "str | PathLike") -> VersionDict:
     """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-    return rendered
-
+    Parse PKG-INFO file if it exists, because it's the most reliable way
+    to get the version from an sdist (`build --sdist`)
+    since sdist would not have a git information.
 
-def render_git_describe(pieces: dict[str, Any]) -> str:
-    """
-    TAG[-DISTANCE-gHEX][-dirty].
+    This matters if you choose to "NOT" write the versionfile.
 
-    Like 'git describe --tags --dirty --always'.
+    i.e. [tool.version-pioneer]
+         versionscript = src/my_package/_version.py
+         # versionfile-sdist = NOT DEFINED
 
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    Note:
+        - New in Version-Pioneer
+        - Hatchling's Version Source Plugin is deactivated when PKG-INFO is present, so this method would not matter.
+        - Only for other backends like setuptools.
     """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"]:
-            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    try:
+        project_root = _find_root_dir_with_file(cwd, "PKG-INFO")
+    except FileNotFoundError:
+        raise NotThisMethodError("PKG-INFO not found")  # noqa: B904
     else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render_git_describe_long(pieces: dict[str, Any]) -> str:
-    """
-    TAG-DISTANCE-gHEX[-dirty].
-
-    Like 'git describe --tags --dirty --always -long'.
-    The distance/hash is unconditional.
+        pyproject_toml = project_root / "pyproject.toml"
+        if not pyproject_toml.exists():
+            raise NotThisMethodError(
+                "PKG-INFO found but no pyproject.toml found in the project root"
+            )
 
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
+        # Confirm [tool.version-pioneer] section exists in pyproject.toml
+        with open(pyproject_toml) as f:
+            lines = f.readlines()
+        for line in lines:
+            if "[tool.version-pioneer]" in line:
+                break
+        else:
+            raise NotThisMethodError(
+                "[tool.version-pioneer] section not found in pyproject.toml"
+            )
 
+        # Read PKG-INFO file
+        with open(project_root / "PKG-INFO") as f:
+            pkg_info = Parser().parse(f)
+        pkg_version = pkg_info.get("Version")
+        if not pkg_version:
+            raise NotThisMethodError("Version not found in PKG-INFO")
 
-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"],
+            "version": pkg_version,
+            "full_revisionid": None,
+            "dirty": False,
+            "error": None,
             "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":
-        rendered = render_git_describe(pieces)
-    elif style == "git-describe-long":
-        rendered = render_git_describe_long(pieces)
-    else:
-        raise ValueError("unknown style '%s'" % style)
-
-    return {
-        "version": rendered,
-        "full-revisionid": pieces["long"],
-        "dirty": pieces["dirty"],
-        "error": None,
-        "date": pieces.get("date"),
-    }
 
+def get_version_dict_with_all_methods(
+    cfg: "VersionPioneerConfig | None" = None, *, cwd: "str | PathLike | None" = None
+) -> VersionDict:
+    """
+    Get version information from PKG-INFO, Git tags or parent directory as a fallback.
+    """
+    if cfg is None:
+        cfg = VersionPioneerConfig()
 
-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
-    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
-    # case we can only use expanded keywords.
-
-    cfg = get_config()
-    verbose = cfg.verbose
+    if cwd is None:
+        cwd = _SCRIPT_DIR_OR_CURRENT_DIR
 
     try:
-        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose)
-    except NotThisMethod:
+        return get_version_from_pkg_info(cwd)
+    except NotThisMethodError:
         pass
 
     try:
-        root = os.path.realpath(__file__)
-        # 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 _ in cfg.versionfile_source.split("/"):
-            root = os.path.dirname(root)
-    except NameError:
-        return {
-            "version": "0+unknown",
-            "full-revisionid": None,
-            "dirty": None,
-            "error": "unable to find root of source tree",
-            "date": None,
-        }
-
-    try:
-        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
-        return render(pieces, cfg.style)
-    except NotThisMethod:
+        return GitPieces.from_git(cfg.tag_prefix, cwd=cwd, verbose=cfg.verbose).render(
+            cfg.style
+        )
+    except NotThisMethodError:
         pass
 
     try:
-        if cfg.parentdir_prefix:
-            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
-    except NotThisMethod:
+        return get_version_from_parentdir(
+            cfg.parentdir_prefix, cwd, verbose=cfg.verbose
+        )
+    except NotThisMethodError:
         pass
 
     return {
         "version": "0+unknown",
-        "full-revisionid": None,
+        "full_revisionid": None,
         "dirty": None,
         "error": "unable to compute version",
         "date": None,
     }
 
 
+# IMPORTANT: However you customise the file, make sure the following function is defined!
+def get_version_dict() -> VersionDict:
+    return get_version_dict_with_all_methods()
+
+
 if __name__ == "__main__":
-    print(get_versions()["version"])
+    print(get_version_dict()["version"])
+
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 4fedbb9..0000000
--- a/setup.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from __future__ import annotations
-
-import versioneer
-from setuptools import setup
-
-setup(
-    version=versioneer.get_version(),
-    cmdclass=versioneer.get_cmdclass(),
-)
diff --git a/src/python_import/__init__.py b/src/python_import/__init__.py
index c14378b..8dbe4d7 100644
--- a/src/python_import/__init__.py
+++ b/src/python_import/__init__.py
@@ -1,5 +1,5 @@
 from __future__ import annotations
 
-from . import _version
+from ._version import get_version_dict
 
-__version__ = _version.get_versions()["version"]
+__version__ = get_version_dict()["version"]
diff --git a/src/python_import/_version.py b/src/python_import/_version.py
old mode 100644
new mode 100755
index bb53777..575b3f9
--- a/src/python_import/_version.py
+++ b/src/python_import/_version.py
@@ -1,712 +1,26 @@
-# 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.
+#!/usr/bin/env python3
+from pathlib import Path
 
-# This file is released into the public domain.
-# Generated by versioneer-0.29
-# https://github.com/python-versioneer/python-versioneer
+from version_pioneer.api import get_version_dict_wo_exec
 
-"""Git implementation of _version.py."""
-from __future__ import annotations
 
-import errno
-import functools
-import os
-import re
-import subprocess
-import sys
-from typing import Any, Callable, Dict, List, Optional, Tuple
-
-
-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
-    # each be defined on a line of their own. _version.py will just call
-    # get_keywords().
-    git_refnames = "$Format:%d$"
-    git_full = "$Format:%H$"
-    git_date = "$Format:%ci$"
-    keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
-    return 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() -> VersioneerConfig:
-    """Create, populate and return the VersioneerConfig() object."""
-    # these strings are filled in when 'setup.py versioneer' creates
-    # _version.py
-    cfg = VersioneerConfig()
-    cfg.VCS = "git"
-    cfg.style = "pep440"
-    cfg.tag_prefix = "v"
-    cfg.parentdir_prefix = "python-import-"
-    cfg.versionfile_source = "src/python_import/_version.py"
-    cfg.verbose = False
-    return cfg
-
-
-class NotThisMethod(Exception):
-    """Exception raised if a method is not valid for the current scenario."""
-
-
-LONG_VERSION_PY: dict[str, str] = {}
-HANDLERS: dict[str, dict[str, Callable]] = {}
-
-
-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
-        return f
-
-    return decorate
-
-
-def run_command(
-    commands: list[str],
-    args: list[str],
-    cwd: str | None = None,
-    verbose: bool = False,
-    hide_stderr: bool = False,
-    env: dict[str, str] | None = None,
-) -> tuple[str | None, int | None]:
-    """Call the given command(s)."""
-    assert isinstance(commands, list)
-    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([command] + args)
-            # remember shell=False, so use git.cmd on windows, not just git
-            process = subprocess.Popen(
-                [command] + args,
-                cwd=cwd,
-                env=env,
-                stdout=subprocess.PIPE,
-                stderr=(subprocess.PIPE if hide_stderr else None),
-                **popen_kwargs,
-            )
-            break
-        except OSError as e:
-            if e.errno == errno.ENOENT:
-                continue
-            if verbose:
-                print("unable to run %s" % dispcmd)
-                print(e)
-            return None, None
+def get_version_dict():
+    # NOTE: during installation, __file__ is not defined
+    # When installed in editable mode, __file__ is defined
+    # When installed in standard mode (when built), this file is replaced to a compiled versionfile.
+    if "__file__" in globals():
+        cwd = Path(__file__).parent
     else:
-        if verbose:
-            print("unable to find command, tried %s" % (commands,))
-        return None, None
-    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, process.returncode
-    return stdout, process.returncode
-
-
-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
-    the project name and a version string. We will also support searching up
-    two directory levels for an appropriately named parent directory
-    """
-    rootdirs = []
-
-    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,
-            }
-        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))
-    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-@register_vcs_handler("git", "get_keywords")
-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: dict[str, str] = {}
-    try:
-        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
-
+        cwd = Path.cwd()
 
-@register_vcs_handler("git", "keywords")
-def git_versions_from_keywords(
-    keywords: dict[str, str],
-    tag_prefix: str,
-    verbose: bool,
-) -> dict[str, Any]:
-    """Get version information from git keywords."""
-    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
-        # it's been around since git-1.5.3, and it's too difficult to
-        # discover which version we're using, or to work around using an
-        # older one.
-        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-    refnames = keywords["refnames"].strip()
-    if refnames.startswith("$Format"):
-        if verbose:
-            print("keywords are unexpanded, not using")
-        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
-    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 = {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
-        # expansion behaves like git log --decorate=short and strips out the
-        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
-        # between branches and tags. By ignoring refnames without digits, we
-        # filter out many common branch names like "release" and
-        # "stabilization", as well as "HEAD" and "master".
-        tags = {r for r in refs if re.search(r"\d", r)}
-        if verbose:
-            print("discarding '%s', no digits" % ",".join(refs - tags))
-    if verbose:
-        print("likely tags: %s" % ",".join(sorted(tags)))
-    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) :]
-            # 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,
-            }
-    # 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,
-    }
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-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*
-    expanded, and _version.py hasn't already been rewritten with a short
-    version string, meaning we're inside a checked out source tree.
-    """
-    GITS = ["git"]
-    if sys.platform == "win32":
-        GITS = ["git.cmd", "git.exe"]
-
-    # 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)
-        raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
-    # 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 = runner(
-        GITS,
-        [
-            "describe",
-            "--tags",
-            "--dirty",
-            "--always",
-            "--long",
-            "--match",
-            f"{tag_prefix}[[:digit:]]*",
-        ],
-        cwd=root,
+    return get_version_dict_wo_exec(
+        cwd=cwd,
+        style="pep440-master",
+        tag_prefix="v",
     )
-    # --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 = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
-    if full_out is None:
-        raise NotThisMethod("'git rev-parse' failed")
-    full_out = full_out.strip()
-
-    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
-
-    # look for -dirty suffix
-    dirty = git_describe.endswith("-dirty")
-    pieces["dirty"] = dirty
-    if 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)
-        if not mo:
-            # unparsable. Maybe git-describe is misbehaving?
-            pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
-            return pieces
-
-        # tag
-        full_tag = mo.group(1)
-        if not full_tag.startswith(tag_prefix):
-            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,
-            )
-            return pieces
-        pieces["closest-tag"] = full_tag[len(tag_prefix) :]
-
-        # distance: number of commits since tag
-        pieces["distance"] = int(mo.group(2))
-
-        # commit: short hex revision ID
-        pieces["short"] = mo.group(3)
-
-    else:
-        # HEX: no tags
-        pieces["closest-tag"] = None
-        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 = 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: 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: dict[str, Any]) -> str:
-    """Build up version string, with post-release "local version identifier".
-
-    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
-    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
-    Exceptions:
-    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += plus_or_dot(pieces)
-            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-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[.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, int | None]:
-    """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"]:
-            # 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.post0.dev%d" % pieces["distance"]
-    return rendered
-
-
-def render_pep440_post(pieces: dict[str, Any]) -> str:
-    """TAG[.postDISTANCE[.dev0]+gHEX] .
-
-    The ".dev0" means dirty. Note that .dev0 sorts backwards
-    (a dirty tree will appear "older" than the corresponding clean one),
-    but you shouldn't be releasing software with -dirty anyways.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%s" % pieces["short"]
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-        rendered += "+g%s" % pieces["short"]
-    return rendered
-
-
-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.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-    return rendered
-
-
-def render_git_describe(pieces: dict[str, Any]) -> str:
-    """TAG[-DISTANCE-gHEX][-dirty].
-
-    Like 'git describe --tags --dirty --always'.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"]:
-            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render_git_describe_long(pieces: dict[str, Any]) -> str:
-    """TAG-DISTANCE-gHEX[-dirty].
-
-    Like 'git describe --tags --dirty --always -long'.
-    The distance/hash is unconditional.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-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,
-        }
-
-    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":
-        rendered = render_git_describe(pieces)
-    elif style == "git-describe-long":
-        rendered = render_git_describe_long(pieces)
-    else:
-        raise ValueError("unknown style '%s'" % style)
-
-    return {
-        "version": rendered,
-        "full-revisionid": pieces["long"],
-        "dirty": pieces["dirty"],
-        "error": None,
-        "date": pieces.get("date"),
-    }
-
-
-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
-    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
-    # case we can only use expanded keywords.
-
-    cfg = get_config()
-    verbose = cfg.verbose
-
-    try:
-        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose)
-    except NotThisMethod:
-        pass
-
-    try:
-        root = os.path.realpath(__file__)
-        # 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 _ in cfg.versionfile_source.split("/"):
-            root = os.path.dirname(root)
-    except NameError:
-        return {
-            "version": "0+unknown",
-            "full-revisionid": None,
-            "dirty": None,
-            "error": "unable to find root of source tree",
-            "date": None,
-        }
 
-    try:
-        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
-        return render(pieces, cfg.style)
-    except NotThisMethod:
-        pass
 
-    try:
-        if cfg.parentdir_prefix:
-            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
-    except NotThisMethod:
-        pass
+if __name__ == "__main__":
+    import json
 
-    return {
-        "version": "0+unknown",
-        "full-revisionid": None,
-        "dirty": None,
-        "error": "unable to compute version",
-        "date": None,
-    }
+    print(json.dumps(get_version_dict()))  # noqa: T201
diff --git a/src/python_import/cli/main.py b/src/python_import/cli/main.py
index 8c47811..a678944 100644
--- a/src/python_import/cli/main.py
+++ b/src/python_import/cli/main.py
@@ -4,7 +4,7 @@
 import json
 import subprocess
 from collections import defaultdict
-from pathlib import Path
+from typing import TYPE_CHECKING
 
 import tree_sitter_python as tspython
 import typer
@@ -13,6 +13,9 @@
 import python_import
 from python_import.utils import get_all_imports_in_file_as_absolute
 
+if TYPE_CHECKING:
+    from pathlib import Path
+
 PY_LANGUAGE = Language(tspython.language())
 
 app = typer.Typer(
diff --git a/src/python_import/ts_utils.py b/src/python_import/ts_utils.py
index 0c3a2a4..cb4f639 100644
--- a/src/python_import/ts_utils.py
+++ b/src/python_import/ts_utils.py
@@ -1,6 +1,9 @@
 from __future__ import annotations
 
-import tree_sitter
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    import tree_sitter
 
 
 def get_node(tree: tree_sitter.Tree, row_col: tuple[int, int]):
diff --git a/src/python_import/utils.py b/src/python_import/utils.py
index e4fc372..0badadd 100644
--- a/src/python_import/utils.py
+++ b/src/python_import/utils.py
@@ -3,13 +3,16 @@
 import json
 import subprocess
 from collections import defaultdict
-from os import PathLike
 from pathlib import Path
-
-from tree_sitter import Parser
+from typing import TYPE_CHECKING
 
 from .ts_utils import get_node
 
+if TYPE_CHECKING:
+    from os import PathLike
+
+    from tree_sitter import Parser
+
 
 def relative_import_to_absolute_import(
     project_root: str | PathLike,