From 7b63fa514c45d9cbdc3e75cc23b71cf82ff4944e Mon Sep 17 00:00:00 2001 From: David Robertson Date: Thu, 10 Feb 2022 18:39:48 +0000 Subject: [PATCH 01/10] Pull in versionstring from Synapse --- matrix_common/versionstring.py | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 matrix_common/versionstring.py diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py new file mode 100644 index 0000000..c144ff6 --- /dev/null +++ b/matrix_common/versionstring.py @@ -0,0 +1,85 @@ +# Copyright 2016 OpenMarket Ltd +# Copyright 2021 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 logging +import os +import subprocess +from types import ModuleType +from typing import Dict + +logger = logging.getLogger(__name__) + +version_cache: Dict[ModuleType, str] = {} + + +def get_version_string(module: ModuleType) -> str: + """Given a module calculate a git-aware version string for it. + + If called on a module not in a git checkout will return `__version__`. + + Args: + module: The module to check the version of. Must declare a __version__ + attribute. + + Returns: + The module version (as a string). + """ + + cached_version = version_cache.get(module) + if cached_version is not None: + return cached_version + + # We want this to fail loudly with an AttributeError. Type-ignore this so + # mypy only considers the happy path. + version_string = module.__version__ # type: ignore[attr-defined] + + try: + cwd = os.path.dirname(os.path.abspath(module.__file__)) + + 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) + + version_cache[module] = version_string + + return version_string From 6628d07127bdc67465378d140a6f41e04f4a900a Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 18:45:27 +0000 Subject: [PATCH 02/10] Use importlib.metadata and clarify that we're looking for the version of a _distribution_ package. --- matrix_common/versionstring.py | 49 +++++++++++++++++++++++----------- setup.cfg | 1 + 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py index c144ff6..3bfac19 100644 --- a/matrix_common/versionstring.py +++ b/matrix_common/versionstring.py @@ -1,5 +1,5 @@ # Copyright 2016 OpenMarket Ltd -# Copyright 2021 The Matrix.org Foundation C.I.C. +# 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. @@ -14,39 +14,56 @@ # limitations under the License. import logging -import os import subprocess -from types import ModuleType from typing import Dict +try: + from importlib.metadata import Distribution +except ImportError: + from importlib_metadata import Distribution # type: ignore[misc] + +__all__ = ["get_distribution_version_string"] + logger = logging.getLogger(__name__) -version_cache: Dict[ModuleType, str] = {} +version_cache: Dict[str, str] = {} + +def get_distribution_version_string(distribution_name: str) -> str: + """Calculate a git-aware version string for a distribution package. -def get_version_string(module: ModuleType) -> str: - """Given a module calculate a git-aware version string for it. + 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 - If called on a module not in a git checkout will return `__version__`. + 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: - module: The module to check the version of. Must declare a __version__ - attribute. + 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 (as a string). + The module version, possibly with git version information included. """ - cached_version = version_cache.get(module) + # TODO: let's just replace this with @functools.lrucache. + cached_version = version_cache.get(distribution_name) if cached_version is not None: return cached_version - # We want this to fail loudly with an AttributeError. Type-ignore this so - # mypy only considers the happy path. - version_string = module.__version__ # type: ignore[attr-defined] + distribution = Distribution.from_name(distribution_name) + version_string = distribution.version + cwd = distribution.locate_file(".") try: - cwd = os.path.dirname(os.path.abspath(module.__file__)) def _run_git_command(prefix: str, *params: str) -> str: try: @@ -80,6 +97,6 @@ def _run_git_command(prefix: str, *params: str) -> str: except Exception as e: logger.info("Failed to check for git repository: %s", e) - version_cache[module] = version_string + version_cache[distribution_name] = version_string return version_string 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] From 8420421c5ff02ad3b4c4a2419b0a6e39b42bd2c8 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 18:48:46 +0000 Subject: [PATCH 03/10] Use functools.lru_cache rather than our own cache --- matrix_common/versionstring.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py index 3bfac19..550b733 100644 --- a/matrix_common/versionstring.py +++ b/matrix_common/versionstring.py @@ -12,7 +12,7 @@ # 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 from typing import Dict @@ -29,6 +29,7 @@ version_cache: Dict[str, str] = {} +@functools.lru_cache def get_distribution_version_string(distribution_name: str) -> str: """Calculate a git-aware version string for a distribution package. @@ -54,11 +55,6 @@ def get_distribution_version_string(distribution_name: str) -> str: The module version, possibly with git version information included. """ - # TODO: let's just replace this with @functools.lrucache. - cached_version = version_cache.get(distribution_name) - if cached_version is not None: - return cached_version - distribution = Distribution.from_name(distribution_name) version_string = distribution.version cwd = distribution.locate_file(".") @@ -97,6 +93,4 @@ def _run_git_command(prefix: str, *params: str) -> str: except Exception as e: logger.info("Failed to check for git repository: %s", e) - version_cache[distribution_name] = version_string - return version_string From ae21a18ef4a11a1fe00459890d52fd2198f9dbf8 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 19:08:28 +0000 Subject: [PATCH 04/10] Shut up mypy --- mypy.ini | 1 + 1 file changed, 1 insertion(+) 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 From 69b92db43c8a20c72c47ef81c529982efd294a75 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 19:15:00 +0000 Subject: [PATCH 05/10] Add quick unit test --- tests/test_versionstring.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_versionstring.py diff --git a/tests/test_versionstring.py b/tests/test_versionstring.py new file mode 100644 index 0000000..248e588 --- /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.assertIs(type(version), str) + self.assertTrue(version) From f6858f2696b9b63a7637881631ccb045611712f5 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 19:23:10 +0000 Subject: [PATCH 06/10] Use old-style invocation of `lru_cache` --- matrix_common/versionstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py index 550b733..a39d896 100644 --- a/matrix_common/versionstring.py +++ b/matrix_common/versionstring.py @@ -29,7 +29,7 @@ version_cache: Dict[str, str] = {} -@functools.lru_cache +@functools.lru_cache() def get_distribution_version_string(distribution_name: str) -> str: """Calculate a git-aware version string for a distribution package. From 405ee26214a5a7243e732541c91028866e2daff4 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 19:30:02 +0000 Subject: [PATCH 07/10] Use `distribution` instead of `Distribution` the former is more explicitly a public API --- matrix_common/versionstring.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py index a39d896..79bd679 100644 --- a/matrix_common/versionstring.py +++ b/matrix_common/versionstring.py @@ -18,9 +18,9 @@ from typing import Dict try: - from importlib.metadata import Distribution + from importlib.metadata import distribution except ImportError: - from importlib_metadata import Distribution # type: ignore[misc] + from importlib_metadata import distribution # type: ignore[misc] __all__ = ["get_distribution_version_string"] @@ -55,9 +55,9 @@ def get_distribution_version_string(distribution_name: str) -> str: The module version, possibly with git version information included. """ - distribution = Distribution.from_name(distribution_name) - version_string = distribution.version - cwd = distribution.locate_file(".") + dist = distribution(distribution_name) + version_string = dist.version + cwd = dist.locate_file(".") try: From f21a34caf38e593e6be02c0c0efae8524627256a Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 19:37:49 +0000 Subject: [PATCH 08/10] Update tests/test_versionstring.py Co-authored-by: Patrick Cloke --- tests/test_versionstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_versionstring.py b/tests/test_versionstring.py index 248e588..a30ae0a 100644 --- a/tests/test_versionstring.py +++ b/tests/test_versionstring.py @@ -23,5 +23,5 @@ def test_our_own_version_string(self) -> None: Check that it's a nonempty string. """ version = get_distribution_version_string("matrix-common") - self.assertIs(type(version), str) + self.assertIsInstance(version, str) self.assertTrue(version) From 59956d693148d33f8bf820069eb2994574adebc9 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 19:41:27 +0000 Subject: [PATCH 09/10] Try a less specific ignore --- matrix_common/versionstring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py index 79bd679..3adf7c3 100644 --- a/matrix_common/versionstring.py +++ b/matrix_common/versionstring.py @@ -20,7 +20,7 @@ try: from importlib.metadata import distribution except ImportError: - from importlib_metadata import distribution # type: ignore[misc] + from importlib_metadata import distribution # type: ignore __all__ = ["get_distribution_version_string"] From 41f561e37a5810e6f5c8d8ab98cd349e45ab6ff9 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Fri, 11 Feb 2022 20:56:25 +0000 Subject: [PATCH 10/10] Remove dead cache --- matrix_common/versionstring.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/matrix_common/versionstring.py b/matrix_common/versionstring.py index 3adf7c3..31bc56c 100644 --- a/matrix_common/versionstring.py +++ b/matrix_common/versionstring.py @@ -15,7 +15,6 @@ import functools import logging import subprocess -from typing import Dict try: from importlib.metadata import distribution @@ -26,8 +25,6 @@ logger = logging.getLogger(__name__) -version_cache: Dict[str, str] = {} - @functools.lru_cache() def get_distribution_version_string(distribution_name: str) -> str: