Skip to content

Commit

Permalink
check: Add function for finding import names provided by local packages
Browse files Browse the repository at this point in the history
This adds find_import_names_from_package_name() which will use
importlib.metadata to look up a package name (e.g. a dependency name)
in the current Python environment, and attempt to find the corresponding
import names that this package provides.

If the package does not exist in the local environment, or if
importlib.metadata is not able to find any provided import names, we
return `None`, and expect the caller to fall back to some other
mechanism to map the package name into import name(s).
  • Loading branch information
jherland committed Feb 14, 2023
1 parent f56a8bf commit 86568cd
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 3 deletions.
34 changes: 33 additions & 1 deletion fawltydeps/check.py
@@ -1,7 +1,9 @@
"Compare imports and dependencies"

import logging
import sys
from itertools import groupby
from typing import List, Tuple
from typing import List, Optional, Tuple

from fawltydeps.types import (
DeclaredDependency,
Expand All @@ -10,6 +12,36 @@
UnusedDependency,
)

# importlib.metadata.packages_distributions() was introduced in v3.10, but it
# is not able to infer import names for modules lacking a top_level.txt until
# v3.11. Hence we prefer importlib_metadata in v3.10 as well as pre-v3.10.
if sys.version_info >= (3, 11):
from importlib.metadata import packages_distributions
else:
from importlib_metadata import packages_distributions

logger = logging.getLogger(__name__)


def find_import_names_from_package_name(package: str) -> Optional[List[str]]:
"""Convert a package name to provided import names.
Use importlib.metadata to look up the mapping between packages and their
provided import names, and return the import names associated with the given
package/distribution name in the current Python environment. This obviously
depends on which Python environment (e.g. virtualenv) we're calling from.
Return None if we're unable to find any import names for the given package.
This is typically because the package is missing from the current
environment, or because it fails to declare its importable modules.
"""
ret = [
import_name
for import_name, packages in packages_distributions().items()
if package in packages
]
return None if not ret else ret


def compare_imports_to_dependencies(
imports: List[ParsedImport], dependencies: List[DeclaredDependency]
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Expand Up @@ -32,7 +32,7 @@ fawltydeps = "fawltydeps.main:main"
# These are the main dependencies for fawltydeps at runtime.
# Do not add anything here that is only needed by CI/tests/linters/developers
python = "^3.7.2"
importlib_metadata = {version = "^5.0.0", python = "<3.8"}
importlib_metadata = {version = "^5.0.0", python = "<3.11"}
isort = "^5.10"
tomli = {version = "^2.0.1", python = "<3.11"}
typing-extensions = {version = "^4.4.0", python = "<3.8"}
Expand Down
41 changes: 41 additions & 0 deletions tests/test_map_dep_name_to_import_names.py
@@ -0,0 +1,41 @@
"""Test the mapping of dependency names to import names."""

import pytest

from fawltydeps.check import find_import_names_from_package_name

# TODO: These tests are not fully isolated, i.e. they do not control the
# virtualenv in which they run. For now, we assume that we are running in an
# environment where at least these packages are available:
# - setuptools (provides multiple names, including pkg_resources)
# - pip (provides a single name: pip)
# - isort (provides no top_level.txt, but 'isort' import name can be inferred)


@pytest.mark.parametrize(
"dep_name,expect_import_names",
[
pytest.param(
"NOT_A_PACKAGE",
None,
id="missing_package__returns_None",
),
pytest.param(
"isort",
["isort"],
id="missing_top_level_txt__can_still_infer_import_name",
),
pytest.param(
"pip",
["pip"],
id="top_level_txt_w_one_entry__returns_entry",
),
pytest.param(
"setuptools",
["_distutils_hack", "pkg_resources", "setuptools"],
id="top_level_txt_w_many_entries__returns_all_entries",
),
],
)
def test_find_import_names_from_package_name(dep_name, expect_import_names):
assert find_import_names_from_package_name(dep_name) == expect_import_names

0 comments on commit 86568cd

Please sign in to comment.