diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py new file mode 100644 index 0000000..31bc56c --- /dev/null +++ b/matrix_common/versionstring.py @@ -0,0 +1,93 @@ +# Copyright 2016 OpenMarket Ltd +# Copyright 2021-2022 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools +import logging +import subprocess + +try: + from importlib.metadata import distribution +except ImportError: + from importlib_metadata import distribution # type: ignore + +__all__ = ["get_distribution_version_string"] + +logger = logging.getLogger(__name__) + + +@functools.lru_cache() +def get_distribution_version_string(distribution_name: str) -> str: + """Calculate a git-aware version string for a distribution package. + + A "distribution package" is a thing that you can e.g. install and manage with pip. + It can contain modules, an "import package" of multiple modules, and arbitrary + resource data. See the glossary at + + https://packaging.python.org/en/latest/glossary/#term-Distribution-Package + + for all your taxonomic needs. Often a distribution package contains exactly import + package---possibly with _different_ names. For example, one can install the + "matrix-sydent" distribution package from PyPI using pip, and doing so makes the + "sydent" import package available to import. + + Args: + distribution_name: The name of the distribution package to check the version of + + Raises: + importlib.metadata.PackageNotFoundError if the given distribution name doesn't + exist. + + Returns: + The module version, possibly with git version information included. + """ + + dist = distribution(distribution_name) + version_string = dist.version + cwd = dist.locate_file(".") + + try: + + def _run_git_command(prefix: str, *params: str) -> str: + try: + result = ( + subprocess.check_output( + ["git", *params], stderr=subprocess.DEVNULL, cwd=cwd + ) + .strip() + .decode("ascii") + ) + return prefix + result + except (subprocess.CalledProcessError, FileNotFoundError): + return "" + + git_branch = _run_git_command("b=", "rev-parse", "--abbrev-ref", "HEAD") + git_tag = _run_git_command("t=", "describe", "--exact-match") + git_commit = _run_git_command("", "rev-parse", "--short", "HEAD") + + dirty_string = "-this_is_a_dirty_checkout" + is_dirty = _run_git_command("", "describe", "--dirty=" + dirty_string).endswith( + dirty_string + ) + git_dirty = "dirty" if is_dirty else "" + + if git_branch or git_tag or git_commit or git_dirty: + git_version = ",".join( + s for s in (git_branch, git_tag, git_commit, git_dirty) if s + ) + + version_string = f"{version_string} ({git_version})" + except Exception as e: + logger.info("Failed to check for git repository: %s", e) + + return version_string diff --git a/mypy.ini b/mypy.ini index be0671c..b76feba 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,3 @@ [mypy] strict = true +warn_unused_ignores = false \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index d54b7a4..2e64e90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ packages = python_requires = >= 3.6 install_requires = attrs + importlib_metadata >= 1.4; python_version < '3.8' [options.package_data] diff --git a/tests/test_versionstring.py b/tests/test_versionstring.py new file mode 100644 index 0000000..a30ae0a --- /dev/null +++ b/tests/test_versionstring.py @@ -0,0 +1,27 @@ +# Copyright 2022 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest import TestCase + +from matrix_common.versionstring import get_distribution_version_string + + +class TestVersionString(TestCase): + def test_our_own_version_string(self) -> None: + """Sanity check that we get the version string for our own package. + + Check that it's a nonempty string. + """ + version = get_distribution_version_string("matrix-common") + self.assertIsInstance(version, str) + self.assertTrue(version)