From 08e04717b259f787bc1d22597a2519fa61a226e5 Mon Sep 17 00:00:00 2001 From: Petr Muller Date: Tue, 17 Jul 2018 11:53:09 +0200 Subject: [PATCH] feat(git): Add `pyff-git` to compare git repo revisions --- pyff/modules.py | 2 +- pyff/repositories.py | 51 +++++++++++++++++++++++++++ pyff/run.py | 34 ++++++++++++++---- requirements-tests.txt | 1 + requirements.txt | 1 + setup.py | 1 + tests/pyff-git/pyff.clitest | 8 ++--- tests/unit/test_classes.py | 7 ++++ tests/unit/test_repositories.py | 62 +++++++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 pyff/repositories.py create mode 100644 tests/unit/test_repositories.py diff --git a/pyff/modules.py b/pyff/modules.py index 6717909..eaecab9 100644 --- a/pyff/modules.py +++ b/pyff/modules.py @@ -71,7 +71,7 @@ def __str__(self): return "\n".join( [ f"Module {hl(module)} changed:\n " + str(change).replace("\n", "\n ") - for module, change in self.changed.items() + for module, change in sorted(self.changed.items()) ] ) diff --git a/pyff/repositories.py b/pyff/repositories.py new file mode 100644 index 0000000..d50e428 --- /dev/null +++ b/pyff/repositories.py @@ -0,0 +1,51 @@ +"""This module contains code that handles comparing revisions in Git repository""" + +import tempfile +import shutil +import pathlib +from typing import Optional +import git + + +import pyff.directories as pd +import pyff.packages as pp + + +class RevisionsPyfference: # pylint: disable=too-few-public-methods + """Represents a difference between two revisions""" + + # RevisionsPyfference is basically the same as DirectoryPyfference, so we + # embed DirectoryPyfference and delegate everything to it + def __init__(self, change: pd.DirectoryPyfference) -> None: + self._change: pd.DirectoryPyfference = change + + def __str__(self): + return str(self._change) + + @property + def packages(self) -> Optional[pp.PackagesPyfference]: + """Return what Python packages differ between revisions""" + return self._change.packages + + +def pyff_git_revision(repository: str, old: str, new: str) -> Optional[RevisionsPyfference]: + """Compare two revisions in a Git repository""" + with tempfile.TemporaryDirectory() as temporary_wd: + working_directory = pathlib.Path(temporary_wd) + source_dir = working_directory / "source" + old_dir = working_directory / "old" + new_dir = working_directory / "new" + + repo = git.Repo.clone_from(repository, source_dir) + + repo.git.checkout(old) + shutil.copytree(source_dir, old_dir) + + repo.git.checkout(new) + shutil.copytree(source_dir, new_dir) + + change = pd.pyff_directory(old_dir, new_dir) + if change: + return RevisionsPyfference(change) + + return None diff --git a/pyff/run.py b/pyff/run.py index 73aa77b..7e3e3e0 100644 --- a/pyff/run.py +++ b/pyff/run.py @@ -9,13 +9,13 @@ from pyff.modules import pyff_module_code from pyff.packages import pyff_package_path from pyff.directories import pyff_directory +from pyff.repositories import pyff_git_revision from pyff.kitchensink import highlight, HIGHLIGHTS LOGGER = logging.getLogger(__name__) -def _pyff_that(function: Callable, what: str) -> None: - parser = ArgumentParser() +def _pyff_that(function: Callable, what: str, parser: ArgumentParser = ArgumentParser()) -> None: parser.add_argument("old") parser.add_argument("new") @@ -30,7 +30,7 @@ def _pyff_that(function: Callable, what: str) -> None: ) LOGGER.debug(f"Python Diff: old {what} {args.old} | new {what} {args.new}") - changes = function(pathlib.Path(args.old), pathlib.Path(args.new)) + changes = function(pathlib.Path(args.old), pathlib.Path(args.new), args) if changes is None: print( @@ -45,7 +45,7 @@ def _pyff_that(function: Callable, what: str) -> None: def pyffmod() -> None: """Entry point for the `pyff` command""" - def compare(old, new): + def compare(old, new, _): """Open two arguments as files and compare them""" with open(old, "r") as old_module, open(new, "r") as new_module: old_version = old_module.read() @@ -58,12 +58,34 @@ def compare(old, new): def pyffpkg() -> None: """Entry point for the `pyff-package` command""" - _pyff_that(pyff_package_path, "package") + + def compare(old, new, _): + """Compare two packages""" + return pyff_package_path(old, new) + + _pyff_that(compare, "package") def pyffdir() -> None: """Entry point for the `pyff-dir` command""" - _pyff_that(pyff_directory, "directory") + + def compare(old, new, _): + """Compare two directories""" + return pyff_directory(old, new) + + _pyff_that(compare, "directory") + + +def pyffgit() -> None: + """Entry point for the `pyff-git` command""" + parser = ArgumentParser() + parser.add_argument("repository") + + def compare(old, new, args): + """Compare two revisions in a given Git repo""" + return pyff_git_revision(args.repository, old, new) + + _pyff_that(compare, "revision", parser) if __name__ == "__main__": diff --git a/requirements-tests.txt b/requirements-tests.txt index 7a54f71..6fa1fe6 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,6 +2,7 @@ astroid==2.0.0.dev0 black==18.6b2 colorama==0.3.9 coverage==4.5.1 +GitPython==2.1.10 mypy==0.610 pre-commit==1.10.2 pyfakefs==3.4.3 diff --git a/requirements.txt b/requirements.txt index fdfb25f..562b846 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ colorama==0.3.9 +GitPython==2.1.10 diff --git a/setup.py b/setup.py index 6cb32f0..720dce8 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ "pyff=pyff.run:pyffmod", "pyff-package=pyff.run:pyffpkg", "pyff-dir=pyff.run:pyffdir", + "pyff-git=pyff.run:pyffgit", ] }, ) diff --git a/tests/pyff-git/pyff.clitest b/tests/pyff-git/pyff.clitest index 588e73f..56d709f 100644 --- a/tests/pyff-git/pyff.clitest +++ b/tests/pyff-git/pyff.clitest @@ -4,10 +4,6 @@ # # $ pyff-git . 6b1c70980e491235bfc938b6109b9280477e560a f1c3e475ef5dd0d2315d954deef6f815473f5811 --highlight-names quotes # Package 'pyff' changed: -# Module 'pyfference.py' changed: -# New imported 'Dict' from 'typing' -# New class 'FromImportPyfference' with 0 public methods -# New class 'ModulePyfference' with 0 public methods # Module 'pyff.py' changed: # New imported 'Module', 'NodeVisitor' from 'ast' # New imported 'defaultdict' from new 'collections' @@ -16,4 +12,8 @@ # New function '_pyff_from_imports' # New function '_pyff_modules' # New function 'pyff_module' +# Module 'pyfference.py' changed: +# New imported 'Dict' from 'typing' +# New class 'FromImportPyfference' with 0 public methods +# New class 'ModulePyfference' with 0 public methods # $ diff --git a/tests/unit/test_classes.py b/tests/unit/test_classes.py index 8e9a440..59cbb4c 100644 --- a/tests/unit/test_classes.py +++ b/tests/unit/test_classes.py @@ -71,6 +71,13 @@ def test_class_summary(self, classdef): assert cls.public_methods == {"a", "b", "c"} assert str(cls) == "class ``Klass'' with 3 public methods" + another_def = self.classdef() + another_def.name = "Llass" + another = pc.ClassSummary(methods=set(), attributes={}, definition=another_def) + assert cls < another + another_def.name = "Jlass" + assert cls > another + def test_attributes(self, classdef): cls = pc.ClassSummary( methods={"__init__"}, definition=classdef, attributes={"attrib", "field"} diff --git a/tests/unit/test_repositories.py b/tests/unit/test_repositories.py new file mode 100644 index 0000000..91b791a --- /dev/null +++ b/tests/unit/test_repositories.py @@ -0,0 +1,62 @@ +# pylint: disable=missing-docstring, no-self-use, too-few-public-methods + +import os +import pathlib +from unittest.mock import MagicMock, patch + +import pyff.directories as pd +import pyff.repositories as pr + + +class TestRevisionsPyfference: + def test_sanity(self): + directory_change = MagicMock(spec=pd.DirectoryPyfference) + directory_change.__str__.return_value = "le change" + change = pr.RevisionsPyfference(change=directory_change) + + assert str(change) == "le change" + + +class TestPyffGitRevision: + @staticmethod + def _make_fake_clone(fs, revisions): # pylint: disable=invalid-name + def _fake_clone_method(_, directory): + fs.create_dir(directory) + fake_repo = MagicMock() + + def _fake_checkout(revision): + if revision in revisions: + oldcwd = os.getcwd() + os.chdir(directory) + revisions[revision]() + os.chdir(oldcwd) + + fake_repo.git.checkout = _fake_checkout + return fake_repo + + return _fake_clone_method + + def test_difference(self, fs): # pylint: disable=invalid-name + def checkout_old(): + fs.create_file("old_package/__init__.py") + + def checkout_new(): + fs.create_file("package/__init__.py") + + with patch("git.Repo.clone_from") as fake_clone: + + fake_clone.side_effect = self._make_fake_clone( + fs, {"old": checkout_old, "new": checkout_new} + ) + change = pr.pyff_git_revision("repo", "old", "new") + assert change is not None + assert pathlib.Path("package") in change.packages.new + + def test_identical(self, fs): # pylint: disable=invalid-name + def checkout_old(): + fs.create_file("package/__init__.py") + + with patch("git.Repo.clone_from") as fake_clone: + fake_clone.side_effect = self._make_fake_clone(fs, {"old": checkout_old}) + change = pr.pyff_git_revision("repo", "old", "new") + assert change is None