Skip to content

Commit

Permalink
Big code reorganization
Browse files Browse the repository at this point in the history
Moved a lot of code around and gave it some logical structure.
Unfortunately the examples ceased to work as they did before (it's not
just refactoring unfortunately) but I am working on this for too long
already.
  • Loading branch information
petr-muller committed Jun 14, 2018
1 parent 41241a4 commit eff8c37
Show file tree
Hide file tree
Showing 30 changed files with 1,946 additions and 842 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -102,3 +102,7 @@ ENV/

# vim
*.swp

# Eclipse
.project
.pydevproject
3 changes: 2 additions & 1 deletion .pylintrc
Expand Up @@ -126,7 +126,8 @@ disable=print-statement,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating
dict-values-not-iterating,
bad-continuation

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
5 changes: 3 additions & 2 deletions .travis.yml
Expand Up @@ -5,15 +5,16 @@ python:
cache: pip

install:
- pip install codacy-coverage codecov python-coveralls mypy
- pip install codacy-coverage codecov python-coveralls
- pip install -r requirements-tests.txt
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter

before_script:
- ./cc-test-reporter before-build && python setup.py develop

script:
- python setup.py test && helpers/clitest --prefix '# ' --diff-options '-u' tests/examples/*.new && mypy pyff
- python setup.py test && mypy pyff

after_success:
- python-codacy-coverage -r coverage.xml
Expand Down
6 changes: 4 additions & 2 deletions helpers/helpers.sh
@@ -1,5 +1,7 @@
#!/bin/bash

. v/bin/activate

example() {
pyff tests/examples/$1*.old tests/examples/$1*.new
}
Expand All @@ -20,8 +22,8 @@ ft() {
st() {
cat helpers/strict-setup.cfg > setup.cfg
python setup.py test &&
helpers/clitest --prefix '# ' --diff-options '-u --color=always' tests/examples/*.new &&
mypy pyff
mypy pyff &&
helpers/clitest --prefix '# ' --diff-options '-u --color=always' tests/examples/*.new
}

cov() {
Expand Down
135 changes: 135 additions & 0 deletions pyff/classes.py
@@ -0,0 +1,135 @@
"""This module contains code that handles comparing function implementations"""

import ast
from typing import Union, List, Optional, Set, FrozenSet
from pyff.kitchensink import hl, pluralize
import pyff.imports as pi


class LocalBaseClass:
# pylint: disable=too-few-public-methods
"""Represents part of a class summary for case when base class is local"""

def __init__(self, name):
self.name = name

def __str__(self):
return f"local {hl(self.name)}"


class ImportedBaseClass:
# pylint: disable=too-few-public-methods
"""Represents part of a class summary for case when base class is imported"""

def __init__(self, name):
self.name = name

def __str__(self):
return f"imported {hl(self.name)}"


BaseClassType = Union[LocalBaseClass, ImportedBaseClass] # pylint: disable=invalid-name


class ClassSummary: # pylint: disable=too-few-public-methods
"""Contains summary information about a class"""

def __init__(
self,
name: str,
methods: int,
private: int,
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

def __str__(self) -> str:
class_part: str = f"class {hl(self.name)}"
methods = pluralize("method", self.public_methods)
method_part: str = f"with {self.public_methods} public {methods}"

if not self.baseclasses:
return f"{class_part} {method_part}"
elif len(self.baseclasses) == 1:
return f"{class_part} derived from {str(self.baseclasses[0])} {method_part}"

raise Exception("Multiple inheritance not yet implemented")


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._private_methods: int = 0
self._methods: int = 0
self._names: Optional[pi.ImportedNames] = names

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

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

def visit_ClassDef(self, node): # pylint: disable=invalid-name
"""Save information about classes that appeared in a module"""
self._private_methods: int = 0
self._methods: int = 0
self.generic_visit(node)

bases: List[str] = []
for base in node.bases:
if base.id in self._names:
bases.append(ImportedBaseClass(base.id))
else:
bases.append(LocalBaseClass(base.id))

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

def visit_FunctionDef(self, node): # pylint: disable=invalid-name
"""Save counts of encountered private/public methods"""
if node.name.startswith("_"):
self._private_methods += 1
self._methods += 1


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

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

def __str__(self):
return "\n".join([f"New {cls}" for cls in sorted(self.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()

old_import_walker.visit(old)
new_import_walker.visit(new)

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

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

0 comments on commit eff8c37

Please sign in to comment.