diff --git a/components/polylith/distributions/collect.py b/components/polylith/distributions/collect.py index c4c95f32..42c3d68d 100644 --- a/components/polylith/distributions/collect.py +++ b/components/polylith/distributions/collect.py @@ -1,10 +1,11 @@ -import importlib.metadata from typing import Set from polylith import alias from polylith.distributions.core import ( distributions_packages, distributions_sub_packages, + get_distributions, + get_packages_distributions, ) @@ -12,13 +13,13 @@ def known_aliases_and_sub_dependencies(deps: dict, library_alias: list) -> Set[s """Collect known aliases (packages) for third-party libraries. When the library origin is not from a lock-file: - collect sub-dependencies for each library, and append to the result. + collect sub-dependencies and distribution top-namespace for each library, and append to the result. """ third_party_libs = {k for k, _v in deps["items"].items()} lock_file = str.endswith(deps["source"], ".lock") - dists = list(importlib.metadata.distributions()) + dists = get_distributions() dist_packages = distributions_packages(dists) custom_aliases = alias.parse(library_alias) @@ -28,4 +29,6 @@ def known_aliases_and_sub_dependencies(deps: dict, library_alias: list) -> Set[s b = alias.pick(custom_aliases, third_party_libs) c = alias.pick(sub_deps, third_party_libs) - return third_party_libs.union(a, b, c) + d = get_packages_distributions(third_party_libs) if not lock_file else set() + + return third_party_libs.union(a, b, c, d) diff --git a/components/polylith/distributions/core.py b/components/polylith/distributions/core.py index 29e71e9b..6886d540 100644 --- a/components/polylith/distributions/core.py +++ b/components/polylith/distributions/core.py @@ -3,7 +3,6 @@ from functools import reduce from typing import Dict, List - SUB_DEP_SEPARATORS = r"[\s!=;><\^~]" @@ -53,7 +52,25 @@ def distributions_sub_packages(dists) -> Dict[str, List[str]]: return reduce(map_sub_packages, dists, {}) -def get_distributions(project_dependencies: set) -> list: - dists = importlib.metadata.distributions() +def get_distributions() -> list: + return list(importlib.metadata.distributions()) + + +def get_packages_distributions(project_dependencies: set) -> set: + """Return the mapped top namespace from an import + + Example: + A third-party library, such as opentelemetry-instrumentation-fastapi. + The return value would be the mapped top namespace: opentelemetry + + Note: available for Python >= 3.10 + """ + + # added in Python 3.10 + fn = getattr(importlib.metadata, "packages_distributions", None) + + dists = fn() if fn else {} + + common = {k for k, v in dists.items() if project_dependencies.intersection(set(v))} - return [dist for dist in dists if dist.metadata["name"] in project_dependencies] + return common.difference(project_dependencies) diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml index 288a25a6..cc96cf56 100644 --- a/projects/poetry_polylith_plugin/pyproject.toml +++ b/projects/poetry_polylith_plugin/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry-polylith-plugin" -version = "1.20.1" +version = "1.21.0" description = "A Poetry plugin that adds tooling support for the Polylith Architecture" authors = ["David Vujic"] homepage = "https://davidvujic.github.io/python-polylith-docs/" diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml index b4274971..afa4c055 100644 --- a/projects/polylith_cli/pyproject.toml +++ b/projects/polylith_cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polylith-cli" -version = "1.7.1" +version = "1.8.0" description = "Python tooling support for the Polylith Architecture" authors = ['David Vujic'] homepage = "https://davidvujic.github.io/python-polylith-docs/" diff --git a/test/components/polylith/distributions/test_core.py b/test/components/polylith/distributions/test_core.py index fb02b896..a0e0a3f5 100644 --- a/test/components/polylith/distributions/test_core.py +++ b/test/components/polylith/distributions/test_core.py @@ -1,5 +1,7 @@ import importlib.metadata +import sys +import pytest from polylith import distributions @@ -40,3 +42,39 @@ def test_distribution_sub_packages(): assert res.get(expected_dist) is not None assert expected_sub_package in res[expected_dist] + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") +def test_package_distributions_returning_top_namespace(monkeypatch): + fake_dists = { + "something": ["something-subnamespace"], + "opentelemetry": ["opentelemetry-instrumentation-fastapi"], + "google": ["google-cloud-storage", "google-api-core"], + "other": ["other-sub-ns"], + } + + fake_project_deps = { + "opentelemetry-instrumentation-fastapi", + "fastapi", + "something-subnamespace", + "google-cloud-storage", + } + + monkeypatch.setattr( + distributions.core.importlib.metadata, + "packages_distributions", + lambda: fake_dists, + ) + + res = distributions.core.get_packages_distributions(fake_project_deps) + + assert res == {"google", "opentelemetry", "something"} + + +@pytest.mark.skipif(sys.version_info > (3, 9), reason="asserting python3.9 and lower") +def test_package_distributions_returning_empty_set(): + fake_project_deps = {"something-subnamespace"} + + res = distributions.core.get_packages_distributions(fake_project_deps) + + assert res == set()