Skip to content

Commit

Permalink
Merge d604e21 into c376619
Browse files Browse the repository at this point in the history
  • Loading branch information
petr-muller committed Jun 29, 2018
2 parents c376619 + d604e21 commit c498193
Show file tree
Hide file tree
Showing 18 changed files with 623 additions and 147 deletions.
4 changes: 4 additions & 0 deletions helpers/helpers.sh
Expand Up @@ -6,6 +6,10 @@ example() {
pyff tests/examples/$1*.old tests/examples/$1*.new
}

extest() {
helpers/clitest --prefix '# ' --diff-options '-u --color=always' tests/examples/$1*.new
}

exdebug() {
pyff tests/examples/$1*.old tests/examples/$1*.new --debug
}
Expand Down
4 changes: 3 additions & 1 deletion pyff/__init__.py
@@ -1 +1,3 @@
__version__ = '0.1.1'
"""pyff: Python Diff"""

__version__ = "0.1.1"
114 changes: 91 additions & 23 deletions pyff/classes.py
@@ -1,9 +1,15 @@
"""This module contains code that handles comparing function implementations"""

import ast
from typing import Union, List, Optional, Set, FrozenSet
import logging
from types import MappingProxyType
from typing import Union, List, Optional, Set, FrozenSet, Dict, Mapping
from pyff.kitchensink import hl, pluralize
import pyff.imports as pi
import pyff.functions as pf


LOGGER = logging.getLogger(__name__)


class LocalBaseClass:
Expand Down Expand Up @@ -36,16 +42,21 @@ class ClassSummary: # pylint: disable=too-few-public-methods

def __init__(
self,
name: str,
methods: int,
private: int,
definition: ast.ClassDef,
baseclasses: Optional[List[BaseClassType]] = None,
) -> None:
self.name: str = name
self.methods: int = methods
self.private_methods: int = private
self.public_methods: int = methods - private
self.baseclasses: Optional[List[BaseClassType]] = baseclasses
self.definition = definition

@property
def name(self):
"""Returns class name"""
return self.definition.name

def __str__(self) -> str:
class_part: str = f"class {hl(self.name)}"
Expand All @@ -64,20 +75,20 @@ class ClassesExtractor(ast.NodeVisitor):
"""Extracts information about classes in a module"""

def __init__(self, names: Optional[pi.ImportedNames] = None) -> None:
self._classes: Set[ClassSummary] = set()
self._classes: Dict[str, ClassSummary] = {}
self._private_methods: int = 0
self._methods: int = 0
self._names: Optional[pi.ImportedNames] = names

@property
def classes(self) -> FrozenSet[ClassSummary]:
def classes(self) -> Mapping[str, ClassSummary]:
"""Return a set of extracted class summaries"""
return frozenset(self._classes)
return MappingProxyType(self._classes)

@property
def classnames(self) -> Set[str]:
def classnames(self) -> FrozenSet[str]:
"""Return a set of class names in the module"""
return {cls.name for cls in self._classes}
return frozenset(self._classes.keys())

def visit_ClassDef(self, node): # pylint: disable=invalid-name
"""Save information about classes that appeared in a module"""
Expand All @@ -92,8 +103,10 @@ def visit_ClassDef(self, node): # pylint: disable=invalid-name
else:
bases.append(LocalBaseClass(base.id))

summary = ClassSummary(node.name, self._methods, self._private_methods, baseclasses=bases)
self._classes.add(summary)
summary = ClassSummary(
self._methods, self._private_methods, baseclasses=bases, definition=node
)
self._classes[node.name] = summary

def visit_FunctionDef(self, node): # pylint: disable=invalid-name
"""Save counts of encountered private/public methods"""
Expand All @@ -102,34 +115,89 @@ def visit_FunctionDef(self, node): # pylint: disable=invalid-name
self._methods += 1


class ClassPyfference: # pylint: disable=too-few-public-methods
"""Represents differences between two classes"""

def __init__(self, methods: Optional[pf.FunctionsPyfference]) -> None:
self.methods: Optional[pf.FunctionsPyfference] = methods
self.name = "Game"

def __str__(self):
lines = [f"Class {hl(self.name)} changed:"]
if self.methods:
self.methods.set_method()
methods = str(self.methods)
lines.append(methods)

return "\n".join(lines).replace("\n", "\n ")


class ClassesPyfference: # pylint: disable=too-few-public-methods
"""Holds differences between classes defined in a module"""

def __init__(self, new: Set[ClassSummary]) -> None:
def __init__(self, new: Set[ClassSummary], changed: Dict[str, ClassPyfference]) -> None:
self.new: Set[ClassSummary] = new
self.changed: Dict[str, ClassPyfference] = changed

def __str__(self):
return "\n".join([f"New {cls}" for cls in sorted(self.new)])
new = [f"New {cls}" for cls in sorted(self.new)]
changed = [str(self.changed[name]) for name in sorted(self.changed)]
return "\n".join(changed + new)

def simplify(self) -> Optional["ClassesPyfference"]:
"""Cleans empty differences, empty sets etc. after manipulation"""
return self if self.new else None


def pyff_classes(old: ast.Module, new: ast.Module) -> Optional[ClassesPyfference]:
"""Return differences in classes defined in two modules"""
old_import_walker = pi.ImportExtractor()
new_import_walker = pi.ImportExtractor()
def pyff_class(
old: ClassSummary,
new: ClassSummary,
old_imports: pi.ImportedNames,
new_imports: pi.ImportedNames,
) -> Optional[ClassPyfference]:
"""Return differences in two classes"""
methods = pf.pyff_functions(old.definition, new.definition, old_imports, new_imports)
if methods:
LOGGER.debug(f"Class '{new.name}' differs")
return ClassPyfference(methods=methods)

old_import_walker.visit(old)
new_import_walker.visit(new)
LOGGER.debug(f"Class '{old.name}' is identical")
return None

first_walker = ClassesExtractor(names=old_import_walker.names)
second_walker = ClassesExtractor(names=new_import_walker.names)

def pyff_classes(
old: ast.Module, new: ast.Module, old_imports: pi.ImportedNames, new_imports: pi.ImportedNames
) -> Optional[ClassesPyfference]:
"""Return differences in classes defined in two modules"""
first_walker = ClassesExtractor(names=old_imports)
second_walker = ClassesExtractor(names=new_imports)

first_walker.visit(old)
second_walker.visit(new)

appeared = {cls for cls in second_walker.classes if cls.name not in first_walker.classnames}

return ClassesPyfference(appeared) if appeared else None
differences: Dict[str, ClassPyfference] = {}
both = first_walker.classnames.intersection(second_walker.classnames)
LOGGER.debug(f"Classes present in both module versions: {both}")
for klass in both:
LOGGER.debug(f"Comparing class '{klass}'")
difference = pyff_class(
first_walker.classes[klass], second_walker.classes[klass], old_imports, new_imports
)
LOGGER.debug(f"Difference: {difference}")
if difference:
LOGGER.debug(f"Class {klass} differs")
differences[klass] = difference
else:
LOGGER.debug(f"Class {klass} is identical")

new_classes = {
cls for cls in second_walker.classes.values() if cls.name not in first_walker.classnames
}
LOGGER.debug(f"New classes: {new_classes}")

if differences or new_classes:
LOGGER.debug("Classes differ")
return ClassesPyfference(new=new_classes, changed=differences)

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

0 comments on commit c498193

Please sign in to comment.