Repository: ishepard/pydriller
Pull request #37 (2019-04-02 13:28:59)

Feature unique methods
This PR implements a list of unique methods changed in a commit.


Patch:
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 1921705..0000000
--- a/.pylintrc
+++ /dev/null
@@ -1,497 +0,0 @@
-[MASTER]
-
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code.
-extension-pkg-whitelist=
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=CVS
-
-# Add files or directories matching the regex patterns to the blacklist. The
-# regex matches against base names, not paths.
-ignore-patterns=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
-# number of processors available to use.
-jobs=1
-
-# Control the amount of potential inferred values when inferring a single
-# object. This can help the performance when dealing with large functions or
-# complex, nested conditions.
-limit-inference-results=100
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# Specify a configuration file.
-#rcfile=
-
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages.
-suggestion-mode=yes
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
-confidence=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once). You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use "--disable=all --enable=classes
-# --disable=W".
-disable=too-few-public-methods,
-        no-self-use
-
-# 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
-# multiple time (only on the command line, not in the configuration file where
-# it should appear only once). See also the "--disable" option for examples.
-enable=c-extension-no-member
-
-
-[REPORTS]
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details.
-# msg-template={abspath}:{line} [{msg_id}({symbol}), {obj}] {msg}
-
-# Set the output format. Available formats are text, parseable, colorized, json
-# and msvs (visual studio). You can also give a reporter class, e.g.
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Tells whether to display a full report or only the messages.
-reports=no
-
-# Activate the evaluation score.
-score=yes
-
-
-[REFACTORING]
-
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-# Complete name of functions that never returns. When checking for
-# inconsistent-return-statements if a never returning function is called then
-# it will be considered as an explicit return statement and no message will be
-# printed.
-never-returning-functions=sys.exit
-
-
-[LOGGING]
-
-# Format style used to check logging format string. `old` means using %
-# formatting, while `new` is for `{}` formatting.
-logging-format-style=old
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format.
-logging-modules=logging
-
-
-[SPELLING]
-
-# Limits count of emitted suggestions for spelling mistakes.
-max-spelling-suggestions=4
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package..
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,
-      XXX,
-      TODO
-
-
-[TYPECHECK]
-
-# List of decorators that produce context managers, such as
-# contextlib.contextmanager. Add to this list to register other decorators that
-# produce valid context managers.
-contextmanager-decorators=contextlib.contextmanager
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# Tells whether to warn about missing members when the owner of the attribute
-# is inferred to be None.
-ignore-none=yes
-
-# This flag controls whether pylint should warn about no-member and similar
-# checks whenever an opaque object is returned when inferring. The inference
-# can return multiple potential results while evaluating a Python object, but
-# some branches might not be evaluated, which results in partial inference. In
-# that case, it might be useful to still emit no-member and other checks for
-# the rest of the inferred objects.
-ignore-on-opaque-inference=yes
-
-# List of class names for which member attributes should not be checked (useful
-# for classes with dynamically set attributes). This supports the use of
-# qualified names.
-ignored-classes=optparse.Values,thread._local,_thread._local
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=
-
-# Show a hint with possible names when a member name was not found. The aspect
-# of finding the hint is based on edit distance.
-missing-member-hint=yes
-
-# The minimum edit distance a name should have in order to be considered a
-# similar match for a missing member name.
-missing-member-hint-distance=1
-
-# The total number of similar names that should be taken in consideration when
-# showing a hint for a missing member.
-missing-member-max-choices=1
-
-
-[VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid defining new builtins when possible.
-additional-builtins=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,
-          _cb
-
-# A regular expression matching the name of dummy variables (i.e. expected to
-# not be used).
-dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore.
-ignored-argument-names=_.*|^ignored_|^unused_
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
-
-
-[FORMAT]
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )?<?https?://\S+>?$
-
-# Number of spaces of indent required inside a hanging or continued line.
-indent-after-paren=4
-
-# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
-# tab).
-indent-string='    '
-
-# Maximum number of characters on a single line.
-max-line-length=79
-
-# Maximum number of lines in a module.
-max-module-lines=1000
-
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
-               dict-separator
-
-# Allow the body of a class to be on the same line as the declaration if body
-# contains single statement.
-single-line-class-stmt=no
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-
-[SIMILARITIES]
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-
-[BASIC]
-
-# Naming style matching correct argument names.
-argument-naming-style=snake_case
-
-# Regular expression matching correct argument names. Overrides argument-
-# naming-style.
-#argument-rgx=
-
-# Naming style matching correct attribute names.
-attr-naming-style=snake_case
-
-# Regular expression matching correct attribute names. Overrides attr-naming-
-# style.
-#attr-rgx=
-
-# Bad variable names which should always be refused, separated by a comma.
-bad-names=foo,
-          bar,
-          baz,
-          toto,
-          tutu,
-          tata
-
-# Naming style matching correct class attribute names.
-class-attribute-naming-style=any
-
-# Regular expression matching correct class attribute names. Overrides class-
-# attribute-naming-style.
-#class-attribute-rgx=
-
-# Naming style matching correct class names.
-class-naming-style=PascalCase
-
-# Regular expression matching correct class names. Overrides class-naming-
-# style.
-#class-rgx=
-
-# Naming style matching correct constant names.
-const-naming-style=UPPER_CASE
-
-# Regular expression matching correct constant names. Overrides const-naming-
-# style.
-#const-rgx=
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-# Naming style matching correct function names.
-function-naming-style=snake_case
-
-# Regular expression matching correct function names. Overrides function-
-# naming-style.
-#function-rgx=
-
-# Good variable names which should always be accepted, separated by a comma.
-good-names=i,
-           logger,
-           j,
-           l,
-           p,
-           dt,
-           k,
-           ex,
-           Run,
-           _
-
-# Include a hint for the correct naming format with invalid-name.
-include-naming-hint=no
-
-# Naming style matching correct inline iteration names.
-inlinevar-naming-style=any
-
-# Regular expression matching correct inline iteration names. Overrides
-# inlinevar-naming-style.
-#inlinevar-rgx=
-
-# Naming style matching correct method names.
-method-naming-style=snake_case
-
-# Regular expression matching correct method names. Overrides method-naming-
-# style.
-#method-rgx=
-
-# Naming style matching correct module names.
-module-naming-style=snake_case
-
-# Regular expression matching correct module names. Overrides module-naming-
-# style.
-#module-rgx=
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=^_
-
-# List of decorators that produce properties, such as abc.abstractproperty. Add
-# to this list to register other decorators that produce valid properties.
-# These decorators are taken in consideration only for invalid-name.
-property-classes=abc.abstractproperty
-
-# Naming style matching correct variable names.
-variable-naming-style=snake_case
-
-# Regular expression matching correct variable names. Overrides variable-
-# naming-style.
-#variable-rgx=
-
-
-[STRING]
-
-# This flag controls whether the implicit-str-concat-in-sequence should
-# generate a warning on implicit string concatenation in sequences defined over
-# several lines.
-check-str-concat-over-line-jumps=no
-
-
-[IMPORTS]
-
-# Allow wildcard imports from modules that define __all__.
-allow-wildcard-with-all=no
-
-# Analyse import fallback blocks. This can be used to support both Python 2 and
-# 3 compatible code, which means that the block might have code that exists
-# only in one or another interpreter, leading to false positives when analysed.
-analyse-fallback-blocks=no
-
-# Deprecated modules which should not be used, separated by a comma.
-deprecated-modules=optparse,tkinter.tix
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled).
-ext-import-graph=
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled).
-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled).
-int-import-graph=
-
-# Force import order to recognize a module as part of the standard
-# compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,
-                      __new__,
-                      setUp
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,
-                  _fields,
-                  _replace,
-                  _source,
-                  _make
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=cls
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method.
-max-args=5
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Maximum number of boolean expressions in an if statement.
-max-bool-expr=5
-
-# Maximum number of branch for function / method body.
-max-branches=12
-
-# Maximum number of locals for function / method body.
-max-locals=15
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-# Maximum number of return / yield for function / method body.
-max-returns=6
-
-# Maximum number of statements in function / method body.
-max-statements=50
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "BaseException, Exception".
-overgeneral-exceptions=BaseException,
-                       Exception
diff --git a/pydriller/__init__.py b/pydriller/__init__.py
index d2f536c..9e30a80 100644
--- a/pydriller/__init__.py
+++ b/pydriller/__init__.py
@@ -1,3 +1,2 @@
-# pylint: disable=C0111
 from .domain.commit import Commit
 from .repository_mining import RepositoryMining, GitRepository
diff --git a/pydriller/domain/commit.py b/pydriller/domain/commit.py
index 83af8e7..7e928c2 100644
--- a/pydriller/domain/commit.py
+++ b/pydriller/domain/commit.py
@@ -11,13 +11,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
-"""
-This module contains all the classes regarding a specific commit, such as
-Commit, Modification,
-ModificationType and Method.
-"""
-
 import logging
 from _datetime import datetime
 from enum import Enum
@@ -27,38 +20,28 @@ from typing import List, Set, Dict
 import lizard
 from git import Repo, Diff, Git, Commit as GitCommit
 
-from pydriller.domain.developer import Developer
-
 logger = logging.getLogger(__name__)
+from pydriller.domain.developer import Developer
 
 NULL_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
 
 
 class ModificationType(Enum):
-    """
-    Type of Modification. Can be ADD, COPY, RENAME, DELETE, MODIFY or UNKNOWN.
-    """
-
-    ADD = 1
-    COPY = 2
-    RENAME = 3
-    DELETE = 4
-    MODIFY = 5
+    ADD = 1,
+    COPY = 2,
+    RENAME = 3,
+    DELETE = 4,
+    MODIFY = 5,
     UNKNOWN = 6
 
 
-class Method:  # pylint: disable=R0902
-    """
-    This class represents a method in a class. Contains various information
-    extracted through Lizard.
-    """
-
+class Method:
     def __init__(self, func):
         """
         Initialize a method object. This is calculated using Lizard: it parses
-        the source code of all the modifications in a commit, extracting
-        information of the methods contained in the file (if the file is a
-        source code written in one of the supported programming languages).
+        the source code of all the modifications in a commit, extracting information
+        of the methods contained in the file (if the file is a source code written
+        in one of the supported programming languages).
         """
 
         self.name = func.name
@@ -77,18 +60,13 @@ class Method:  # pylint: disable=R0902
         self.top_nesting_level = func.top_nesting_level
 
 
-class Modification:  # pylint: disable=R0902
-    """
-    This class contains information regarding a modified file in a commit.
-    """
-
+class Modification:
     def __init__(self, old_path: str, new_path: str,
                  change_type: ModificationType,
                  diff_and_sc: Dict[str, str]):
         """
-        Initialize a modification. A modification carries on information
-        regarding the changed file. Normally, you shouldn't initialize a new
-        one.
+        Initialize a modification. A modification carries on information regarding
+        the changed file. Normally, you shouldn't initialize a new one.
         """
         self._old_path = Path(old_path) if old_path is not None else None
         self._new_path = Path(new_path) if new_path is not None else None
@@ -100,6 +78,7 @@ class Modification:  # pylint: disable=R0902
         self._complexity = None
         self._token_count = None
         self._function_list = []
+        self._changed_functions_list = []
 
     @property
     def added(self) -> int:
@@ -207,10 +186,23 @@ class Modification:  # pylint: disable=R0902
         self._calculate_metrics()
         return self._function_list
 
+    @property
+    def changed_methods(self) -> List[Method]:
+        """
+        Returns the list of methods changed in this modification.
+        Every method contains various information like complexicy,
+        loc, name, etc. You can find more information in the lizard
+        documentation: https://github.com/terryyin/lizard
+
+        :return: list of methods
+        """
+        self._filter_methods()
+        return self._changed_functions_list
+
     def _calculate_metrics(self):
         if self.source_code and self._nloc is None:
-            l = lizard.analyze_file.analyze_source_code(self.filename,
-                                                        self.source_code)
+            l = lizard.analyze_file.analyze_source_code(
+                self.filename, self.source_code)
 
             self._nloc = l.nloc
             self._complexity = l.CCN
@@ -219,12 +211,33 @@ class Modification:  # pylint: disable=R0902
             for func in l.function_list:
                 self._function_list.append(Method(func))
 
+    def _filter_methods(self) -> List[Method]:
+        if len(self._changed_functions_list) > 0:
+            return
+
+        def check_range(string, change):
+            num = string.split(",")
+            start = int(num[0].replace("-", "").replace("+", ""))
+            end = start + int(num[1])
+
+            if (start < change.start_line < end) or (start < change.end_line < end):
+                return True
+            return False
+
+        line_numbers = self.diff.split("\n")[0]
+        if line_numbers:
+            token = line_numbers.split(" ")
+            for m in self.methods:
+                if check_range(token[1], m) or check_range(token[2], m):
+                    self._changed_functions_list.append(m)
+
     def __eq__(self, other):
         if not isinstance(other, Modification):
             return NotImplemented
-        if self is other:
+        elif self is other:
             return True
-        return self.__dict__ == other.__dict__
+        else:
+            return self.__dict__ == other.__dict__
 
     def __str__(self):
         return (
@@ -238,19 +251,12 @@ class Modification:  # pylint: disable=R0902
 
 
 class Commit:
-    """
-    Class representing a Commit. Contains all the important information such
-    as hash, author, dates, and modified files.
-    """
-
-    def __init__(self, commit: GitCommit, project_path: Path,
-                 main_branch: str) -> None:
+    def __init__(self, commit: GitCommit, project_path: Path, main_branch: str) -> None:
         """
         Create a commit object.
 
         :param commit: GitPython Commit object
-        :param project_path: path to the project (temporary folder in case
-        of a remote repository)
+        :param project_path: path to the project(temporary folder in case of a remote repository)
         :param main_branch: main branch of the repo
         """
         self._c_object = commit
@@ -276,8 +282,7 @@ class Commit:
 
         :return: author
         """
-        return Developer(self._c_object.author.name,
-                         self._c_object.author.email)
+        return Developer(self._c_object.author.name, self._c_object.author.email)
 
     @property
     def committer(self) -> Developer:
@@ -286,8 +291,7 @@ class Commit:
 
         :return: committer
         """
-        return Developer(self._c_object.committer.name,
-                         self._c_object.committer.email)
+        return Developer(self._c_object.committer.name, self._c_object.committer.email)
 
     @property
     def project_name(self) -> str:
@@ -380,13 +384,12 @@ class Commit:
         repo = Repo(str(self.project_path))
         commit = self._c_object
 
-        if self.parents:
+        if len(self.parents) > 0:
             # the commit has a parent
-            diff_index = self._c_object.parents[0].diff(commit,
-                                                        create_patch=True)
+            diff_index = self._c_object.parents[0].diff(
+                commit, create_patch=True)
         else:
-            # this is the first commit of the repo. Comparing it with git
-            # NULL TREE
+            # this is the first commit of the repo. Comparing it with git NULL TREE
             parent = repo.tree(NULL_TREE)
             diff_index = parent.diff(commit.tree, create_patch=True)
 
@@ -394,10 +397,10 @@ class Commit:
 
     def _parse_diff(self, diff_index) -> List[Modification]:
         modifications_list = []
-        for diff in diff_index:
-            old_path = diff.a_path
-            new_path = diff.b_path
-            change_type = self._from_change_to_modification_type(diff)
+        for d in diff_index:
+            old_path = d.a_path
+            new_path = d.b_path
+            change_type = self._from_change_to_modification_type(d)
 
             diff_and_sc = {
                 'diff': '',
@@ -405,15 +408,15 @@ class Commit:
             }
 
             try:
-                diff_and_sc['diff'] = diff.diff.decode('utf-8')
-                diff_and_sc['source_code'] = diff.b_blob.data_stream.read()\
-                    .decode('utf-8')
+                diff_and_sc['diff'] = d.diff.decode('utf-8')
+                diff_and_sc['source_code'] = d.b_blob.data_stream.read().decode(
+                    'utf-8')
             except (UnicodeDecodeError, AttributeError, ValueError):
-                logger.debug('Could not load source code or the diff of a '
-                             'file in commit %s', self._c_object.hexsha)
+                logger.debug(
+                    'Could not load source code or the diff of a file in commit {}'.format(self._c_object.hexsha))
 
-            modifications_list.append(Modification(old_path, new_path,
-                                                   change_type, diff_and_sc))
+            modifications_list.append(Modification(
+                old_path, new_path, change_type, diff_and_sc))
 
         return modifications_list
 
@@ -445,43 +448,38 @@ class Commit:
             branches.add(branch.strip().replace('* ', ''))
         return branches
 
-    # pylint disable=R0902
-    def _from_change_to_modification_type(self, diff: Diff):
-        if diff.new_file:
+    def _from_change_to_modification_type(self, d: Diff):
+        if d.new_file:
             return ModificationType.ADD
-        if diff.deleted_file:
+        elif d.deleted_file:
             return ModificationType.DELETE
-        if diff.renamed_file:
+        elif d.renamed_file:
             return ModificationType.RENAME
-        if diff.a_blob and diff.b_blob and diff.a_blob != diff.b_blob:
+        elif d.a_blob and d.b_blob and d.a_blob != d.b_blob:
             return ModificationType.MODIFY
-
-        return ModificationType.UNKNOWN
+        else:
+            return ModificationType.UNKNOWN
 
     def __eq__(self, other):
         if not isinstance(other, Commit):
             return NotImplemented
-        if self is other:
+        elif self is other:
             return True
-
-        return self.__dict__ == other.__dict__
+        else:
+            return self.__dict__ == other.__dict__
 
     def __str__(self):
-        return (
-            'Hash: {}\n'.format(self.hash) +
-            'Author: {}\n'.format(self.author.name) +
-            'Author email: {}\n'.format(self.author.email) +
-            'Committer: {}\n'.format(self.committer.name) +
-            'Committer email: {}\n'.format(self.committer.email) +
-            'Author date: {}\n'.format(
-                self.author_date.strftime("%Y-%m-%d %H:%M:%S")) +
-            'Committer date: {}\n'.format(
-                self.committer_date.strftime("%Y-%m-%d %H:%M:%S")) +
-            'Message: {}\n'.format(self.msg) +
-            'Parent: {}\n'.format("\n".join(map(str, self.parents))) +
-            'Merge: {}\n'.format(self.merge) +
-            'Modifications: \n{}'.format(
-                "\n".join(map(str, self.modifications))) +
-            'Branches: \n{}'.format("\n".join(map(str, self.branches))) +
-            'In main branch: {}\n'.format(self.in_main_branch)
-        )
+        return ('Hash: {}\n'.format(self.hash) +
+                'Author: {}\n'.format(self.author.name) +
+                'Author email: {}\n'.format(self.author.email) +
+                'Committer: {}\n'.format(self.committer.name) +
+                'Committer email: {}\n'.format(self.committer.email) +
+                'Author date: {}\n'.format(self.author_date.strftime("%Y-%m-%d %H:%M:%S")) +
+                'Committer date: {}\n'.format(self.committer_date.strftime("%Y-%m-%d %H:%M:%S")) +
+                'Message: {}\n'.format(self.msg) +
+                'Parent: {}\n'.format("\n".join(map(str, self.parents))) +
+                'Merge: {}\n'.format(self.merge) +
+                'Modifications: \n{}'.format("\n".join(map(str, self.modifications))) +
+                'Branches: \n{}'.format("\n".join(map(str, self.branches))) +
+                'In main branch: {}\n'.format(self.in_main_branch)
+                )
diff --git a/pydriller/domain/developer.py b/pydriller/domain/developer.py
index 7b62ce2..dc537d4 100644
--- a/pydriller/domain/developer.py
+++ b/pydriller/domain/developer.py
@@ -11,16 +11,8 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-"""
-This module includes only 1 class, Developer, representing a developer.
-"""
-
 
 class Developer:
-    """
-    This class represents a developer. We save the email and the name.
-    """
-
     def __init__(self, name: str, email: str):
         """
         Class to identify a developer.
@@ -34,7 +26,7 @@ class Developer:
     def __eq__(self, other):
         if not isinstance(other, Developer):
             return NotImplemented
-        if self is other:
+        elif self is other:
             return True
-
-        return self.__dict__ == other.__dict__
+        else:
+            return self.__dict__ == other.__dict__
diff --git a/pydriller/git_repository.py b/pydriller/git_repository.py
index a0910d1..02550c1 100644
--- a/pydriller/git_repository.py
+++ b/pydriller/git_repository.py
@@ -12,10 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""
-This module includes 1 class, GitRepository, representing a repository in Git.
-"""
-
 import logging
 import os
 from pathlib import Path
@@ -32,11 +28,6 @@ NULL_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
 
 
 class GitRepository:
-    """
-    Class representing a repository in Git. It contains most of the logic of
-    PyDriller: obtaining the list of commits, checkout, reset, etc.
-    """
-
     def __init__(self, path: str):
         """
         Init the Git Repository.
@@ -87,14 +78,11 @@ class GitRepository:
         head_commit = self.repo.head.commit
         return Commit(head_commit, self.path, self.main_branch)
 
-    def get_list_commits(self, branch: str = None,
-                         reverse_order: bool = True) \
-            -> Generator[Commit, None, None]:
+    def get_list_commits(self, branch: str = None, reverse_order: bool = True) -> Generator[Commit, None, None]:
         """
         Return a generator of commits of all the commits in the repo.
 
-        :return: Generator[Commit], the generator of all the commits in the
-        repo
+        :return: Generator[Commit], the generator of all the commits in the repo
         """
         for commit in self.repo.iter_commits(branch, reverse=reverse_order):
             yield self.get_commit_from_gitpython(commit)
@@ -122,8 +110,8 @@ class GitRepository:
     def checkout(self, _hash: str) -> None:
         """
         Checkout the repo at the speficied commit.
-        BE CAREFUL: this will change the state of the repo, hence it should
-        *not* be used with more than 1 thread.
+        BE CAREFUL: this will change the state of the repo, hence it should *not*
+        be used with more than 1 thread.
 
         :param _hash: commit hash to checkout
         """
@@ -133,8 +121,7 @@ class GitRepository:
 
     def _delete_tmp_branch(self) -> None:
         try:
-            # we are already in _PD, so checkout the master branch before
-            # deleting it
+            # we are already in _PD, so checkout the master branch before deleting it
             if self.repo.active_branch.name == '_PD':
                 self.git.checkout('-f', self.main_branch)
             self.repo.delete_head('_PD', force=True)
@@ -148,7 +135,7 @@ class GitRepository:
         :return: List[str], the list of the files
         """
         _all = []
-        for path, _, files in os.walk(str(self.path)):
+        for path, subdirs, files in os.walk(str(self.path)):
             if '.git' in path:
                 continue
             for name in files:
@@ -157,8 +144,7 @@ class GitRepository:
 
     def reset(self) -> None:
         """
-        Reset the state of the repo, checking out the main branch and
-        discarding
+        Reset the state of the repo, checking out the main branch and discarding
         local changes (-f option).
 
         """
@@ -185,16 +171,15 @@ class GitRepository:
             selected_tag = self.repo.tags[tag]
             return self.get_commit(selected_tag.commit.hexsha)
         except (IndexError, AttributeError):
-            logger.debug('Tag %s not found', tag)
+            logger.debug('Tag {} not found'.format(tag))
             raise
 
     def parse_diff(self, diff: str) -> Dict[str, List[Tuple[int, str]]]:
         """
         Given a diff, returns a dictionary with the added and deleted lines.
         The dictionary has 2 keys: "added" and "deleted", each containing the
-        corresponding added or deleted lines. For both keys, the value is a
-        list of Tuple (int, str), corresponding to (number of line in the file,
-        actual line).
+        corresponding added or deleted lines. For both keys, the value is a list
+        of Tuple (int, str), corresponding to (number of line in the file, actual line).
 
 
         :param str diff: diff of the commit
@@ -232,18 +217,14 @@ class GitRepository:
         token = line.split(" ")
         numbers_old_file = token[1]
         numbers_new_file = token[2]
-        delete_line_number = int(numbers_old_file.split(",")[0]
-                                 .replace("-", "")) - 1
+        delete_line_number = int(numbers_old_file.split(",")[0].replace("-", "")) - 1
         additions_line_number = int(numbers_new_file.split(",")[0]) - 1
         return delete_line_number, additions_line_number
 
-    def get_commits_last_modified_lines(self, commit: Commit,
-                                        modification: Modification = None) \
-            -> Set[str]:
+    def get_commits_last_modified_lines(self, commit: Commit, modification: Modification = None) -> Set[str]:
         """
-        Given the Commit object, returns the set of commits that last
-        "touched" the lines that are modified in the files included in the
-        commit. It applies SZZ.
+        Given the Commit object, returns the set of commits that last "touched" the lines
+        that are modified in the files included in the commit. It applies SZZ.
         The algorithm works as follow: (for every file in the commit)
 
         1- obtain the diff
@@ -252,8 +233,7 @@ class GitRepository:
 
         3- blame the file and obtain the commits were those lines were added
 
-        Can also be passed as parameter a single Modification, in this case
-        only this file
+        Can also be passed as parameter a single Modification, in this case only this file
         will be analyzed.
 
         :param Commit commit: the commit to analyze
@@ -269,44 +249,28 @@ class GitRepository:
 
         for mod in modifications:
             path = mod.new_path
-            if mod.change_type == ModificationType.RENAME or \
-                    mod.change_type == ModificationType.DELETE:
+            if mod.change_type == ModificationType.RENAME or mod.change_type == ModificationType.DELETE:
                 path = mod.old_path
 
             deleted_lines = self.parse_diff(mod.diff)['deleted']
             try:
-                blame = self.git.blame(commit.hash + '^',
-                                       '--', path).split('\n')
+                blame = self.git.blame(commit.hash + '^', '--', path).split('\n')
                 for num_line, line in deleted_lines:
                     if not self._useless_line(line.strip()):
-                        buggy_commit = blame[num_line - 1].split(' ')[
-                            0].replace('^', '')
+                        buggy_commit = blame[num_line - 1].split(' ')[0].replace('^', '')
                         buggy_commits.add(self.get_commit(buggy_commit).hash)
             except GitCommandError:
-                logger.debug(
-                    "Could not found file %s in commit %s. Probably a double "
-                    "rename!", mod.filename, commit.hash)
+                logger.debug("Could not found file %s in commit %s. Probably a double rename!", mod.filename,
+                             commit.hash)
+
         return buggy_commits
 
     def _useless_line(self, line: str):
-        # this covers comments in Java and Python, as well as empty lines.
-        # More have to be added!
-        return not line or \
-               line.startswith('//') or \
-               line.startswith('#') or \
-               line.startswith("/*") or \
-               line.startswith("'''") or \
-               line.startswith('"""') or \
-               line.startswith("*")
+        # this covers comments in Java and Python, as well as empty lines. More have to be added!
+        return not line or line.startswith('//') or line.startswith('#') or line.startswith("/*") or \
+               line.startswith("'''") or line.startswith('"""') or line.startswith("*")
 
     def get_commits_modified_file(self, filepath: str) -> List[str]:
-        """
-        Given a filepath, returns all the commits that modified this file
-        (following renames).
-
-        :param str filepath: path to the file
-        :return: the list of commits' hash
-        """
         path = str(Path(filepath))
 
         commits = []
diff --git a/pydriller/repository_mining.py b/pydriller/repository_mining.py
index 7c5210d..e040bf4 100644
--- a/pydriller/repository_mining.py
+++ b/pydriller/repository_mining.py
@@ -12,17 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""
-This module includes 1 class, RepositoryMining, main class of PyDriller.
-"""
-
 import logging
 import os
 import tempfile
 from datetime import datetime
 from typing import List, Generator, Union
 
-import pytz
+import pytz as pytz
 from git import Repo
 
 from pydriller.domain.commit import Commit
@@ -32,11 +28,6 @@ logger = logging.getLogger(__name__)
 
 
 class RepositoryMining:
-    """
-    This is the main class of PyDriller, responsible for running the study.
-    """
-
-    # pylint: disable=R0902,R0913,R0914
     def __init__(self, path_to_repo: Union[str, List[str]],
                  single: str = None,
                  since: datetime = None, to: datetime = None,
@@ -50,46 +41,28 @@ class RepositoryMining:
                  only_commits: List[str] = None,
                  filepath: str = None):
         """
-        Init a repository mining. The only required parameter is
-        "path_to_repo": to analyze a
-        single repo, pass the absolute path to the repo; if you need to
-        analyze more
-        repos, pass a list of absolute paths.
-
-        Furthermore, PyDriller supports local and remote repositories: if
-        you pass a path to a
-        repo, PyDriller will run the study on that repo; if you pass an URL,
-        PyDriller will clone
-        the repo in a temporary folder, run the study, and delete the
-        temporary folder.
-
-        :param Union[str,List[str]] path_to_repo: absolute path (or list of
-        absolute paths) to
-        the repository(ies) to analyze
+        Init a repository mining. The only required parameter is "path_to_repo": to analyze a single repo, pass the absolute
+        path to the repo; if you need to analyze more repos, pass a list of absolute paths.
+
+        Furthermore, PyDriller supports local and remote repositories: if you pass a path to a repo, PyDriller will run the study
+        on that repo; if you pass an URL, PyDriller will clone the repo in a temporary folder, run the study, and
+        delete the temporary folder.
+
+        :param Union[str,List[str]] path_to_repo: absolute path (or list of absolute paths) to the repository(ies) to analyze
         :param str single: hash of a single commit to analyze
         :param datetime since: starting date
         :param datetime to: ending date
         :param str from_commit: starting commit (only if `since` is None)
         :param str to_commit: ending commit (only if `to` is None)
-        :param str from_tag: starting the analysis from specified tag (only
-        if `since` and
-        `from_commit` are None)
-        :param str to_tag: ending the analysis from specified tag (only if
-        `to` and `to_commit`
-        are None)
-        :param bool reversed_order: whether the commits should be analyzed
-        in reversed order
+        :param str from_tag: starting the analysis from specified tag (only if `since` and `from_commit` are None)
+        :param str to_tag: ending the analysis from specified tag (only if `to` and `to_commit` are None)
+        :param bool reversed_order: whether the commits should be analyzed in reversed order
         :param str only_in_branch: only commits in this branch will be analyzed
-        :param List[str] only_modifications_with_file_types: only
-        modifications with that file
-        types will be analyzed
+        :param List[str] only_modifications_with_file_types: only modifications with that file types will be analyzed
         :param bool only_no_merge: if True, merges will not be analyzed
-        :param List[str] only_authors: only commits of these authors will be
-        analyzed (the check
-        is done on the username, NOT the email)
+        :param List[str] only_authors: only commits of these authors will be analyzed (the check is done on the username, NOT the email)
         :param List[str] only_commits: only these commits will be analyzed
-        :param str filepath: only commits that modified this file will be
-        analyzed
+        :param str filepath: only commits that modified this file will be analyzed
         """
 
         self._sanity_check_repos(path_to_repo)
@@ -104,8 +77,7 @@ class RepositoryMining:
         self._to = to
         self._reversed_order = reversed_order
         self._only_in_branch = only_in_branch
-        self._only_modifications_with_file_types = \
-            only_modifications_with_file_types
+        self._only_modifications_with_file_types = only_modifications_with_file_types
         self._only_no_merge = only_no_merge
         self._only_authors = only_authors
         self._only_commits = only_commits
@@ -113,60 +85,47 @@ class RepositoryMining:
         self._filepath_commits = None
 
     def _sanity_check_repos(self, path_to_repo):
-        if not isinstance(path_to_repo, str) and \
-                not isinstance(path_to_repo, list):
-            raise Exception("The path to the repo has to be of type "
-                            "'string' or 'list of strings'!")
+        if not isinstance(path_to_repo, str) and not isinstance(path_to_repo, list):
+            raise Exception("The path to the repo has to be of type 'string' or 'list of strings'!")
 
     def _sanity_check_filters(self, git_repo: GitRepository):
-        # If single is defined, no other filters should be
+        # If single is defined, not other filters should be
         if self._single is not None:
-            # pylint: disable=R0916
-            if self._since is not None or self._to is not None or \
-                    self._from_commit is not None or \
-                    self._to_commit is not None or self._from_tag is not \
-                    None or self._to_tag is not None:
-                raise Exception('You can not specify a single commit with '
-                                'other filters')
+            if self._since is not None or self._to is not None or self._from_commit is not None or \
+                    self._to_commit is not None or self._from_tag is not None or self._to_tag is not None:
+                raise Exception('You can not specify a single commit with other filters')
 
         # If from_commit is defined, since should not be
         if self._from_commit is not None:
             if self._since is not None:
-                raise Exception('You can not specify both <since date> '
-                                'and <from commit>')
+                raise Exception('You can not specify both <since date> and <from commit>')
             self._since = git_repo.get_commit(self._from_commit).committer_date
 
         # If to_commit is defined, to should not be
         if self._to_commit is not None:
             if self._to is not None:
-                raise Exception('You can not specify both <to date> '
-                                'and <to commit>')
+                raise Exception('You can not specify both <to date> and <to commit>')
             self._to = git_repo.get_commit(self._to_commit).committer_date
 
         # If from_tag is defined, since and from_commit should not be
         if self._from_tag is not None:
             if self._since is not None or self._from_commit is not None:
-                raise Exception('You can not specify <since date> or '
-                                '<from commit> when using <from tag>')
-            self._since = git_repo.get_commit_from_tag(
-                self._from_tag).committer_date
+                raise Exception('You can not specify <since date> or <from commit> when using <from tag>')
+            self._since = git_repo.get_commit_from_tag(self._from_tag).committer_date
 
         # If to_tag is defined, to and to_commit should not be
         if self._to_tag is not None:
             if self._to is not None or self._to_commit is not None:
-                raise Exception('You can not specify <to date> or <to commit> '
-                                'when using <to tag>')
-            self._to = git_repo.get_commit_from_tag(
-                self._to_tag).committer_date
+                raise Exception('You can not specify <to date> or <to commit> when using <to tag>')
+            self._to = git_repo.get_commit_from_tag(self._to_tag).committer_date
 
     def _isremote(self, repo: str) -> bool:
         return repo.startswith("git@") or repo.startswith("https://")
 
     def _clone_remote_repos(self, tmp_folder: str, repo: str) -> str:
 
-        repo_folder = os.path.join(tmp_folder,
-                                   self._get_repo_name_from_url(repo))
-        logger.info("Cloning %s in temporary folder %s", repo, repo_folder)
+        repo_folder = os.path.join(tmp_folder, self._get_repo_name_from_url(repo))
+        logger.info("Cloning {} in temporary folder {}".format(repo, repo_folder))
         Repo.clone_from(url=repo, to_path=repo_folder)
 
         return repo_folder
@@ -184,40 +143,34 @@ class RepositoryMining:
             # if it is a remote repo, clone it first in a temporary folder!
             if self._isremote(path_repo):
                 tmp_folder = tempfile.TemporaryDirectory()
-                path_repo = self._clone_remote_repos(tmp_folder.name,
-                                                     path_repo)
+                path_repo = self._clone_remote_repos(tmp_folder.name, path_repo)
 
             git_repo = GitRepository(path_repo)
 
             self._sanity_check_filters(git_repo)
             self._check_timezones()
 
-            logger.info('Analyzing git repository in %s', git_repo.path)
+            logger.info('Analyzing git repository in {}'.format(git_repo.path))
 
             if self._filepath is not None:
-                self._filepath_commits = git_repo.get_commits_modified_file(
-                    self._filepath)
+                self._filepath_commits = git_repo.get_commits_modified_file(self._filepath)
 
-            for commit in git_repo.get_list_commits(self._only_in_branch,
-                                                    not self._reversed_order):
-                logger.info('Commit #%s in %s from %s', commit.hash,
-                            commit.committer_date,
-                            commit.author.name)
+            for commit in git_repo.get_list_commits(self._only_in_branch, not self._reversed_order):
+                logger.info('Commit #{} in {} from {}'.format(commit.hash, commit.committer_date, commit.author.name))
 
                 if self._is_commit_filtered(commit):
-                    logger.info('Commit #%s filtered', commit.hash)
+                    logger.info('Commit #{} filtered'.format(commit.hash))
                     continue
 
                 yield commit
 
-    def _is_commit_filtered(self, commit: Commit):  # pylint: disable=R0911
+    def _is_commit_filtered(self, commit: Commit):
         if self._single is not None:
             if commit.hash != self._single:
-                logger.debug(
-                    'Commit filtered because is not the defined in single')
+                logger.debug('Commit filtered because is not the defined in single')
                 return True
-        if (self._since is not None and commit.committer_date < self._since) \
-                or (self._to is not None and commit.committer_date > self._to):
+        if (self._since is not None and commit.committer_date < self._since) or \
+                (self._to is not None and commit.committer_date > self._to):
             return True
         if self._only_modifications_with_file_types is not None:
             if not self._has_modification_with_file_type(commit):
@@ -226,27 +179,21 @@ class RepositoryMining:
         if self._only_no_merge is True and commit.merge is True:
             logger.debug('Commit filtered for no merge')
             return True
-        if self._only_authors is not None and commit.author.name not in \
-                self._only_authors:
+        if self._only_authors is not None and commit.author.name not in self._only_authors:
             logger.debug("Commit filtered for author")
             return True
-        if self._only_commits is not None and commit.hash not in \
-                self._only_commits:
-            logger.debug("Commit filtered because it is not one of the "
-                         "specified commits")
+        if self._only_commits is not None and commit.hash not in self._only_commits:
+            logger.debug("Commit filtered because it is not one of the specified commits")
             return True
-        if self._filepath_commits is not None and commit.hash not in \
-                self._filepath_commits:
-            logger.debug("Commit filtered because it did not modify the "
-                         "specified file")
+        if self._filepath_commits is not None and commit.hash not in self._filepath_commits:
+            logger.debug("Commit filtered because it did not modify the specified file")
             return True
 
         return False
 
     def _has_modification_with_file_type(self, commit):
         for mod in commit.modifications:
-            if mod.filename.endswith(
-                    tuple(self._only_modifications_with_file_types)):
+            if mod.filename.endswith(tuple(self._only_modifications_with_file_types)):
                 return True
         return False
 


Test patch:
diff --git a/tests/test_commit.py b/tests/test_commit.py
index a1f65aa..b8e4717 100644
--- a/tests/test_commit.py
+++ b/tests/test_commit.py
@@ -16,7 +16,8 @@ import logging
 
 from pydriller.domain.commit import Modification, ModificationType
 
-logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
+logging.basicConfig(
+    format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
 
 import pytest
 from pathlib import Path
@@ -56,6 +57,32 @@ def test_filename():
     assert m3 == m1
 
 
+def test_changed_methods_nomethod():
+    gr = GitRepository('test-repos/test1')
+    # select a change with no methods (adds an empty class)
+    commits = [c for c in gr.get_list_commits()]
+    ch = [ch for c in commits for ch in c.modifications]
+
+    assert len(ch[-2].changed_methods) == 0
+
+
+def test_changed_methods_multiple_methods():
+    gr = GitRepository('test-repos/test1')
+    commits = [c for c in gr.get_list_commits()]
+    # use only the first commit and the first
+    # change for testing because this contains new methods
+    ch = commits[0].modifications[0]
+
+    assert len(ch.changed_methods) == 7
+
+
+def test_changed_methods_different_repo():
+    gr = GitRepository('test-repos/test3')
+    commits = [c for c in gr.get_list_commits()]
+    ch = commits[0].modifications[0]
+    assert len(ch.changed_methods) == 19
+
+
 def test_metrics_python():
     with open('test-repos/test6/git_repository.py') as f:
         sc = f.read()