Repository: cocotb/cocotb

Issue:
Thoughts on adding git rev to dev version numbers?
How would everyone feel about adding `git rev-parse --short HEAD` to `__version__`?  Currently it is `2.0.0.dev0` and I'm not sure what the final `0` is for, but I'm guessing it's to manually increment as one feels like?  What if instead it were `2.0.0.dev-d379318e`?  I'd propose that we build the file on the fly in `setup.py` but only add the git rev when the non-suffixed version (stored as a variable in `setup.py`) includes `dev`.

I can take a stab at this if people are onboard.


Discussion:
In case it's not clear, the motivation for this would be to be able to determine what exact version of cocotb one is running when using non-released versions.
setuptools-scm did this. But it did a lot more than that, poorly. Maybe you can extract the logic it uses from it. Technically I think the correct version we should be using is `2.0.devN`, but I'd have to check the PEP. ~~I'd like the N incremented for each commit as well.~~ We could then just set the next release version in a Python variable and this would do the rest, rather than what setuptools-scm did just guessing the next version.

And ofc, this should be totally optional so people can still install release versions from source without needing more dependencies.
I would like this as well.
I should have known there was a PEP, there's always a PEP.  It's PEP 440, but that says it's historical and points to this:
https://packaging.python.org/en/latest/specifications/version-specifiers/

Apparently `devN` is for developmental releases, not necessarily individual commits.  Regardless, that's orthogonal to what I'm suggesting as just incrementing some number won't be unique across Git branches.

Also this:
> DVCS based version labels
Many build tools integrate with distributed version control systems like Git and Mercurial in order to add an identifying hash to the version identifier. As hashes cannot be ordered reliably such versions are not permitted in the public version field.
>
> As with semantic versioning, the public .devN suffix may be used to uniquely identify such releases for publication, while the original DVCS based label can be stored in the project metadata.
>
> Identifying hash information may also be included in local version labels.

If I'm reading this correctly `__version__` would look like `2.0.0.dev0+d379318e`.  Or something.  But the rev would have to go after the `+`.

Also:
> Local version identifiers SHOULD NOT be used when publishing upstream projects to a public index server

So is it fine to always add the git rev when there is a `dev` in the version?  Do `dev` versions ever get published?

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -44,6 +44,7 @@
     raise SystemExit("\n".join(msg))
 
 import logging
+import subprocess
 from io import StringIO
 from os import path, walk
 
@@ -69,8 +70,26 @@ def package_files(directory):
     return paths
 
 
-# this sets the __version__ variable
-exec(read_file(path.join("src", "cocotb", "_version.py")))
+version_file_path = path.join("src", "cocotb", "_version.py")
+__version__ = "2.0.0.dev0"
+if "dev" in __version__:
+    try:
+        rev = subprocess.check_output(
+            ["git", "rev-parse", "--short", "HEAD"], universal_newlines=True
+        ).strip()
+        __version__ += f"+{rev}"
+    except Exception as e:
+        # if this is not a git repository and _version.py already exists,
+        # we are probably installing from an sdist, so use the existing _version.py
+        if path.exists(version_file_path):
+            exec(read_file(version_file_path))
+        else:
+            print(e, file=sys.stderr)
+with open(version_file_path, "w") as f:
+    f.write("# Package version\n")
+    f.write("# Generated by setup.py -- do not modify directly\n\n")
+    f.write(f'__version__ = "{__version__}"')
+
 
 # store log from build_libs and display at the end in verbose mode
 # see https://github.com/pypa/pip/issues/6634
@@ -83,7 +102,7 @@ def package_files(directory):
 setup(
     name="cocotb",
     cmdclass={"build_ext": build_ext},
-    version=__version__,  # noqa: F821
+    version=__version__,
     description="cocotb is a coroutine based cosimulation library for writing VHDL and Verilog testbenches in Python.",
     url="https://www.cocotb.org",
     license="BSD",
diff --git a/src/cocotb/_version.py b/src/cocotb/_version.py
deleted file mode 100644
--- a/src/cocotb/_version.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Package versioning solution originally found here:
-# http://stackoverflow.com/q/458550
-
-# Store the version here so:
-# 1) we don't load dependencies by storing it in __init__.py
-# 2) we can import it in setup.py for the same reason
-# 3) we can import it into your module
-__version__ = "2.0.0.dev0"


Test patch:
diff --git a/tests/pytest/test_version.py b/tests/pytest/test_version.py
new file mode 100644
--- /dev/null
+++ b/tests/pytest/test_version.py
@@ -0,0 +1,19 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+import os
+import shutil
+import subprocess
+
+import cocotb
+import pytest
+
+
+def test_version():
+    if "dev" in cocotb.__version__ and os.path.exists(".git") and shutil.which("git"):
+        assert "+" in cocotb.__version__
+        parts = cocotb.__version__.split("+")
+        rev = subprocess.check_output(
+            ["git", "rev-parse", "--short", "HEAD"], universal_newlines=True
+        ).strip()
+        assert parts[1] == rev