packagelister.packagelister

 1import ast
 2import importlib.metadata
 3import sys
 4
 5from pathier import Pathier, Pathish
 6from printbuddies import ProgBar
 7
 8
 9def get_packages_from_source(source: str) -> list[str]:
10    """Scan `source` and extract the names of imported packages/modules."""
11    tree = ast.parse(source)
12    packages = []
13    for node in ast.walk(tree):
14        type_ = type(node)
15        package = ""
16        if type_ == ast.Import:
17            package = node.names[0].name
18        elif type_ == ast.ImportFrom:
19            package = node.module
20        if package:
21            if "." in package:
22                package = package[: package.find(".")]
23            packages.append(package)
24    return sorted(list(set(packages)))
25
26
27def remove_builtins(packages: list[str]) -> list[str]:
28    """Remove built in packages/modules from a list of package names."""
29    builtins = list(sys.stdlib_module_names)
30    return filter(lambda x: x not in builtins, packages)
31
32
33def scan(project_dir: Pathish = None, include_builtins: bool = False) -> dict:
34    """Recursively scans a directory for python files to determine
35    what packages are in use, as well as the version number if applicable.
36
37    Returns a dictionary where the keys are package names and
38    the values are dictionaries with the keys `version` for the version number of the package
39    if there is one (None if there isn't) and `files` for a list of the files that import the package.
40
41    :param project_dir: Can be an absolute or relative path to a directory or a single file (.py).
42    If it is relative, it will be assumed to be relative to the current working directory.
43    If an argument isn't given, the current working directory will be scanned.
44    If the path doesn't exist, an empty dictionary is returned."""
45    if not project_dir:
46        project_dir = Pathier.cwd()
47    elif type(project_dir) is str or project_dir.is_file():
48        project_dir = Pathier(project_dir)
49    if not project_dir.is_absolute():
50        project_dir = project_dir.absolute()
51
52    # Raise error if project_dir doesn't exist
53    if not project_dir.exists():
54        raise FileNotFoundError(
55            f"Can't scan directory that doesn't exist: {project_dir}"
56        )
57    # You can scan a non python file one at a time if you reeeally want to.
58    if project_dir.is_file():
59        files = [project_dir]
60    else:
61        files = list(project_dir.rglob("*.py"))
62
63    bar = ProgBar(len(files), width_ratio=0.33)
64    used_packages = {}
65    for file in files:
66        bar.display(suffix=f"Scanning {file.name}")
67        source = file.read_text(encoding="utf-8")
68        packages = get_packages_from_source(source)
69        if not include_builtins:
70            packages = remove_builtins(packages)
71        for package in packages:
72            if file.with_stem(package) not in files:
73                if (
74                    package in used_packages
75                    and str(file) not in used_packages[package]["files"]
76                ):
77                    used_packages[package]["files"].append(str(file))
78                else:
79                    try:
80                        package_version = importlib.metadata.version(package)
81                    except ModuleNotFoundError:
82                        package_version = None
83                    except Exception as e:
84                        print(e)
85                        package_version = None
86                    used_packages[package] = {
87                        "files": [str(file)],
88                        "version": package_version,
89                    }
90    return used_packages
def get_packages_from_source(source: str) -> list[str]:
10def get_packages_from_source(source: str) -> list[str]:
11    """Scan `source` and extract the names of imported packages/modules."""
12    tree = ast.parse(source)
13    packages = []
14    for node in ast.walk(tree):
15        type_ = type(node)
16        package = ""
17        if type_ == ast.Import:
18            package = node.names[0].name
19        elif type_ == ast.ImportFrom:
20            package = node.module
21        if package:
22            if "." in package:
23                package = package[: package.find(".")]
24            packages.append(package)
25    return sorted(list(set(packages)))

Scan source and extract the names of imported packages/modules.

def remove_builtins(packages: list[str]) -> list[str]:
28def remove_builtins(packages: list[str]) -> list[str]:
29    """Remove built in packages/modules from a list of package names."""
30    builtins = list(sys.stdlib_module_names)
31    return filter(lambda x: x not in builtins, packages)

Remove built in packages/modules from a list of package names.

def scan( project_dir: pathier.pathier.Pathier | pathlib.Path | str = None, include_builtins: bool = False) -> dict:
34def scan(project_dir: Pathish = None, include_builtins: bool = False) -> dict:
35    """Recursively scans a directory for python files to determine
36    what packages are in use, as well as the version number if applicable.
37
38    Returns a dictionary where the keys are package names and
39    the values are dictionaries with the keys `version` for the version number of the package
40    if there is one (None if there isn't) and `files` for a list of the files that import the package.
41
42    :param project_dir: Can be an absolute or relative path to a directory or a single file (.py).
43    If it is relative, it will be assumed to be relative to the current working directory.
44    If an argument isn't given, the current working directory will be scanned.
45    If the path doesn't exist, an empty dictionary is returned."""
46    if not project_dir:
47        project_dir = Pathier.cwd()
48    elif type(project_dir) is str or project_dir.is_file():
49        project_dir = Pathier(project_dir)
50    if not project_dir.is_absolute():
51        project_dir = project_dir.absolute()
52
53    # Raise error if project_dir doesn't exist
54    if not project_dir.exists():
55        raise FileNotFoundError(
56            f"Can't scan directory that doesn't exist: {project_dir}"
57        )
58    # You can scan a non python file one at a time if you reeeally want to.
59    if project_dir.is_file():
60        files = [project_dir]
61    else:
62        files = list(project_dir.rglob("*.py"))
63
64    bar = ProgBar(len(files), width_ratio=0.33)
65    used_packages = {}
66    for file in files:
67        bar.display(suffix=f"Scanning {file.name}")
68        source = file.read_text(encoding="utf-8")
69        packages = get_packages_from_source(source)
70        if not include_builtins:
71            packages = remove_builtins(packages)
72        for package in packages:
73            if file.with_stem(package) not in files:
74                if (
75                    package in used_packages
76                    and str(file) not in used_packages[package]["files"]
77                ):
78                    used_packages[package]["files"].append(str(file))
79                else:
80                    try:
81                        package_version = importlib.metadata.version(package)
82                    except ModuleNotFoundError:
83                        package_version = None
84                    except Exception as e:
85                        print(e)
86                        package_version = None
87                    used_packages[package] = {
88                        "files": [str(file)],
89                        "version": package_version,
90                    }
91    return used_packages

Recursively scans a directory for python files to determine what packages are in use, as well as the version number if applicable.

Returns a dictionary where the keys are package names and the values are dictionaries with the keys version for the version number of the package if there is one (None if there isn't) and files for a list of the files that import the package.

Parameters
  • project_dir: Can be an absolute or relative path to a directory or a single file (.py). If it is relative, it will be assumed to be relative to the current working directory. If an argument isn't given, the current working directory will be scanned. If the path doesn't exist, an empty dictionary is returned.