packagelister.packagelister API documentation

# packagelister.packagelister

View Source

     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]: View Source

    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]: View Source

    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: View Source

    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.