petr-muller/pyff

View on GitHub
pyff/modules.py

Summary

Maintainability
A
0 mins
Test Coverage
"""This module contains code that handles comparing modules"""

import ast
import logging
import pathlib
from typing import List, Optional, Dict

import pyff.classes as pc
import pyff.functions as pf
import pyff.imports as pi
from pyff.kitchensink import hl, pluralize, hlistify


LOGGER = logging.getLogger(__name__)


class ModuleSummary:  # pylint: disable=too-few-public-methods
    """Holds summary information about a module"""

    def __init__(self, name: str, node: ast.Module) -> None:
        self.name: str = name
        self.node: ast.Module = node


class ModulePyfference:  # pylint: disable=too-few-public-methods
    """Holds differences between two Python modules"""

    def __init__(
        self,
        imports: Optional[pi.ImportsPyfference] = None,
        classes: Optional[pc.ClassesPyfference] = None,
        functions: Optional[pf.FunctionsPyfference] = None,
    ) -> None:

        self.other: List = []
        self.imports: Optional[pi.ImportsPyfference] = imports
        self.classes: Optional[pc.ClassesPyfference] = classes
        self.functions: Optional[pf.FunctionsPyfference] = functions

    def __str__(self):
        changes = [self.imports, self.classes, self.functions] + self.other
        return "\n".join([str(change) for change in changes if change is not None])

    def simplify(self) -> Optional["ModulePyfference"]:
        """Cleans empty differences, empty sets etc. after manipulation"""
        if self.imports is not None:
            self.imports = self.imports.simplify()

        if self.classes is not None:
            self.classes = self.classes.simplify()

        if self.functions is not None:
            self.functions = self.functions.simplify()

        return self if (self.functions or self.classes or self.imports or self.other) else None


class ModulesPyfference:  # pylint: disable=too-few-public-methods
    """Holds difference between modules in a package"""

    def __init__(
        self,
        removed: Dict[pathlib.Path, ModuleSummary],
        changed: Dict[pathlib.Path, ModulePyfference],
        new: Dict[pathlib.Path, ModuleSummary],
    ) -> None:
        self.removed: Dict[pathlib.Path, ModuleSummary] = removed
        self.changed: Dict[pathlib.Path, ModulePyfference] = changed
        self.new: Dict[pathlib.Path, ModuleSummary] = new

    def __str__(self):
        lines = []

        if self.removed:
            lines.append(
                f"Removed {pluralize('module', self.removed)} {hlistify(sorted(self.removed))}"
            )

        if self.changed:
            lines.append(
                "\n".join(
                    [
                        f"Module {hl(module)} changed:\n  " + str(change).replace("\n", "\n  ")
                        for module, change in sorted(self.changed.items())
                    ]
                )
            )

        if self.new:
            lines.append(f"New {pluralize('module', self.new)} {hlistify(sorted(self.new))}")

        return "\n".join(lines)

    def __repr__(self):
        return (
            f"ModulesPyfference(removed={repr(self.removed)}, "
            f"changed={repr(self.changed)}, new={repr(self.new)})"
        )

    def __bool__(self):
        return bool(self.removed or self.changed or self.new)


def summarize_module(module: pathlib.Path) -> ModuleSummary:
    """Return a ModuleSummary of a given module"""
    return ModuleSummary(name=module.name, node=ast.parse(module.read_text()))


def pyff_module(old: ModuleSummary, new: ModuleSummary) -> Optional[ModulePyfference]:
    """Return difference between two Python modules, or None if they are identical"""
    old_imports = pi.ImportedNames.extract(old.node)
    new_imports = pi.ImportedNames.extract(new.node)
    imports = pi.pyff_imports(old.node, new.node)
    classes = pc.pyff_classes(old.node, new.node, old_imports, new_imports)
    functions = pf.pyff_functions(old.node, new.node, old_imports, new_imports)

    if imports or classes or functions:
        LOGGER.debug("Modules differ")
        pyfference = ModulePyfference(imports, classes, functions)
        return pyfference

    LOGGER.debug("Modules are identical")
    return None


def pyff_module_path(old: pathlib.Path, new: pathlib.Path) -> Optional[ModulePyfference]:
    """Return difference between two Python modules, or None if they are identical"""
    return pyff_module(summarize_module(old), summarize_module(new))