From 8ddd9b07a51b0a5864d3b0381dcef9a583fd89f8 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:54:07 +0300 Subject: [PATCH] Improve: Use distribution package names in version strings (#462) --- src/mdformat/_cli.py | 54 ++++++++++++++++++++++++++++---------------- tests/test_cli.py | 14 +++++++++++- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/mdformat/_cli.py b/src/mdformat/_cli.py index a4f3aca..0230ad8 100644 --- a/src/mdformat/_cli.py +++ b/src/mdformat/_cli.py @@ -3,6 +3,7 @@ import argparse from collections.abc import Callable, Generator, Iterable, Mapping, Sequence import contextlib +import inspect import itertools import logging import os.path @@ -303,30 +304,45 @@ def log_handler_applied( logger.removeHandler(handler) -def get_package_name(obj: object) -> str: - # Packages and modules should have `__package__` - if hasattr(obj, "__package__"): - package_name = obj.__package__ - else: # class or function - module_name = obj.__module__ - package_name = module_name.split(".", maxsplit=1)[0] - return package_name +def get_package_name(obj: object) -> str | None: + """Return top level module name, or None if not found.""" + module = inspect.getmodule(obj) + return module.__name__.split(".", maxsplit=1)[0] if module else None def get_plugin_versions( parser_extensions: Mapping[str, mdformat.plugins.ParserExtensionInterface], codeformatters: Mapping[str, Callable[[str, str], str]], -) -> dict[str, str]: - versions = {} +) -> list[tuple[str, str]]: + """Return a list of (plugin_distro, plugin_version) tuples. + + If many plugins come from the same distribution package, only return + the version of that distribution once. If we have no reliable way to + one-to-one map a plugin to a distribution package, use the top level + module name and set version to "unknown". If we don't even know the + top level module name, return the tuple ("unknown", "unknown"). + """ + problematic_versions = [] + # Use a dict for successful version lookups so that if more than one plugin + # originates from the same distribution, it only shows up once in a version + # string. + success_versions = {} + import_package_to_distro = importlib_metadata.packages_distributions() for iface in itertools.chain(parser_extensions.values(), codeformatters.values()): - package_name = get_package_name(iface) - try: - package_version = importlib_metadata.version(package_name) - except importlib_metadata.PackageNotFoundError: - # In test scenarios the package may not exist - package_version = "unknown" - versions[package_name] = package_version - return versions + import_package = get_package_name(iface) + if import_package is None: + problematic_versions.append(("unknown", "unknown")) + continue + distro_list = import_package_to_distro.get(import_package) + if ( + not distro_list # No distribution package found + or len(distro_list) > 1 # Don't make any guesses with namespace packages + ): + problematic_versions.append((import_package, "unknown")) + continue + distro_name = distro_list[0] + success_versions[distro_name] = importlib_metadata.version(distro_name) + return [(k, v) for k, v in success_versions.items()] + problematic_versions def get_plugin_versions_str( @@ -334,4 +350,4 @@ def get_plugin_versions_str( codeformatters: Mapping[str, Callable[[str, str], str]], ) -> str: plugin_versions = get_plugin_versions(parser_extensions, codeformatters) - return ", ".join(f"{name}: {version}" for name, version in plugin_versions.items()) + return ", ".join(f"{name}: {version}" for name, version in plugin_versions) diff --git a/tests/test_cli.py b/tests/test_cli.py index 2bdf668..4df9140 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,7 @@ import pytest import mdformat -from mdformat._cli import get_package_name, run, wrap_paragraphs +from mdformat._cli import get_package_name, get_plugin_versions, run, wrap_paragraphs from mdformat.plugins import CODEFORMATTERS UNFORMATTED_MARKDOWN = "\n\n# A header\n\n" @@ -350,6 +350,18 @@ def test_get_package_name(): assert get_package_name(mdformat) == "mdformat" +def test_get_plugin_versions(): + # Pretend that "pytest" and "unittest.mock.patch" are plugins + versions = get_plugin_versions({"p1": pytest}, {"f1": patch}) # type: ignore[dict-item] # noqa: E501 + assert versions[0][0] == "pytest" + assert versions[0][1] != "unknown" + assert versions[1] == ("unittest", "unknown") + + with patch("mdformat._cli.inspect.getmodule", return_value=None): + versions = get_plugin_versions({"p1": pytest}, {}) # type: ignore[dict-item] + assert versions[0] == ("unknown", "unknown") + + def test_no_timestamp_modify(tmp_path): file_path = tmp_path / "test.md"