diff --git a/README.md b/README.md index 6d5f8b0..c011cff 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A library and a script to plot Python code coverage results. Run the tests for your project with the test coverage, and convert the coverage results to a JSON or XML format. As a result, you should find a coverage.json or coverage.xml file in your current working directory. ``` -coverage run pytest +coverage run -m pytest coverage xml # or coverage json ``` diff --git a/coverage_plot/fake_implementations.py b/coverage_plot/fake_implementations.py new file mode 100644 index 0000000..3810083 --- /dev/null +++ b/coverage_plot/fake_implementations.py @@ -0,0 +1,53 @@ +import datetime +from typing import List + +import faker +from attr import field, frozen + +fake = faker.Faker() + + +@frozen +class FakeDeveloper: + """ + Fake object implementing the subset of the interface of pydriller's Developer. + + Ref: https://pydriller.readthedocs.io/en/latest/commit.html + """ + + name: str = field(factory=fake.name) # type: ignore + email: str = field(factory=fake.email) # type: ignore + + +@frozen +class FakeModification: + """ + Fake object implementing the subset of the interface of pydriller's Modification. + + Ref: https://pydriller.readthedocs.io/en/latest/modifications.html + """ + + path: str = field(factory=fake.file_path) # type: ignore + + @property + def old_path(self): + return self.path + + @property + def new_path(self): + return self.path + + +@frozen +class FakeCommit: + """ + Fake object implementing the subset of the interface of pydriller's Commit. + """ + + author: FakeDeveloper = field(factory=FakeDeveloper) # type: ignore + author_date: datetime.datetime = field( + factory=fake.date_time_this_year, # type: ignore + ) + msg: str = field(factory=fake.sentence) # type: ignore + hash: str = field(factory=fake.sha1) # type: ignore + modifications: List[FakeModification] = field(factory=list) # type: ignore diff --git a/coverage_plot/git_changes.py b/coverage_plot/git_changes.py index 879c7a7..7270179 100644 --- a/coverage_plot/git_changes.py +++ b/coverage_plot/git_changes.py @@ -2,11 +2,22 @@ import enum import fnmatch from datetime import datetime, timezone -from typing import Generator, Iterator, List, Optional +from typing import Generator, Iterator, List, Optional, Union -from attrs import frozen import pydriller +from attrs import frozen from pydriller import Commit, Modification +from pydriller.domain.developer import Developer + +from coverage_plot.fake_implementations import ( + FakeCommit, + FakeDeveloper, + FakeModification, +) + +DeveloperT = Union[Developer, FakeDeveloper] +CommitT = Union[Commit, FakeCommit] +ModificationT = Union[Modification, FakeModification] class FilterResult(enum.Enum): @@ -25,7 +36,7 @@ class NormalizedModification: path: str @classmethod - def from_commit_modification(cls, commit: Commit, modification: Modification): + def from_commit_modification(cls, commit: CommitT, modification: ModificationT): return NormalizedModification( hash=commit.hash, msg=commit.msg, @@ -39,12 +50,14 @@ def from_commit_modification(cls, commit: Commit, modification: Modification): class CommitFilter(abc.ABC): - def filter_commit(self, commit: Commit) -> FilterResult: + @abc.abstractmethod + def filter_commit(self, commit: CommitT) -> FilterResult: ... class ModificationFilter(abc.ABC): - def filter_modification(self, modification: Modification) -> FilterResult: + @abc.abstractmethod + def filter_modification(self, modification: ModificationT) -> FilterResult: ... @@ -52,7 +65,7 @@ def filter_modification(self, modification: Modification) -> FilterResult: class ExcludeAuthor(CommitFilter): author_name: str - def filter_commit(self, commit: Commit) -> FilterResult: + def filter_commit(self, commit: CommitT) -> FilterResult: if self.author_name in commit.author.name: return FilterResult.EXCLUDE if self.author_name in commit.author.email: @@ -64,7 +77,7 @@ def filter_commit(self, commit: Commit) -> FilterResult: class ExcludeMessage(CommitFilter): message: str - def filter_commit(self, commit: Commit) -> FilterResult: + def filter_commit(self, commit: CommitT) -> FilterResult: if self.message in commit.msg: return FilterResult.EXCLUDE return FilterResult.DONT_KNOW @@ -72,22 +85,21 @@ def filter_commit(self, commit: Commit) -> FilterResult: @frozen class ExcludeAllCommits(CommitFilter): - def filter_commit(self, commit: Commit) -> FilterResult: + def filter_commit(self, commit: CommitT) -> FilterResult: return FilterResult.EXCLUDE @frozen class IncludeAllCommits(CommitFilter): - def filter_commit(self, commit: Commit) -> FilterResult: + def filter_commit(self, commit: CommitT) -> FilterResult: return FilterResult.INCLUDE @frozen class IncludeFile(ModificationFilter): - file_pattern: str - def filter_modification(self, modification: Modification) -> FilterResult: + def filter_modification(self, modification: ModificationT) -> FilterResult: path = modification.old_path or modification.new_path if fnmatch.fnmatch(path, self.file_pattern): return FilterResult.INCLUDE @@ -103,7 +115,7 @@ class ExcludeAllModifications(ModificationFilter): "exclude the modification." """ - def filter_modification(self, modification: Modification) -> FilterResult: + def filter_modification(self, modification: ModificationT) -> FilterResult: return FilterResult.EXCLUDE @@ -116,7 +128,7 @@ class IncludeAllModifications(ModificationFilter): "include the modification." """ - def filter_modification(self, modification: Modification) -> FilterResult: + def filter_modification(self, modification: ModificationT) -> FilterResult: return FilterResult.INCLUDE @@ -152,7 +164,7 @@ def filter_modifications( yield NormalizedModification.from_commit_modification(commit, mod) -def apply_commit_filters(commit: Commit, commit_filters: List[CommitFilter]): +def apply_commit_filters(commit: CommitT, commit_filters: List[CommitFilter]): for filt in commit_filters: result = filt.filter_commit(commit) if result in (FilterResult.INCLUDE, FilterResult.EXCLUDE): @@ -161,7 +173,7 @@ def apply_commit_filters(commit: Commit, commit_filters: List[CommitFilter]): def apply_modification_filters( - modification: Modification, modification_filters: List[ModificationFilter] + modification: ModificationT, modification_filters: List[ModificationFilter] ): for filt in modification_filters: result = filt.filter_modification(modification) diff --git a/coverage_plot/importance_filesize.py b/coverage_plot/importance_filesize.py index 72fbae2..a040a55 100644 --- a/coverage_plot/importance_filesize.py +++ b/coverage_plot/importance_filesize.py @@ -1,6 +1,7 @@ +from attrs import define + from coverage_plot.importance_interface import Importance from coverage_plot.plot import Report -from attrs import define @define diff --git a/coverage_plot/importance_interface.py b/coverage_plot/importance_interface.py index 0256ac6..30094ca 100644 --- a/coverage_plot/importance_interface.py +++ b/coverage_plot/importance_interface.py @@ -9,8 +9,8 @@ class Importance(abc.ABC): an importance score for it. """ + @abc.abstractmethod def get_importance(self, filename: str) -> int: """ Return an importance score for a file. """ - ... diff --git a/coverage_plot/importance_recency.py b/coverage_plot/importance_recency.py index 9e5097f..27f7dbb 100644 --- a/coverage_plot/importance_recency.py +++ b/coverage_plot/importance_recency.py @@ -91,6 +91,7 @@ def convert_to_last_modified( last_modified_dict: Dict[str, datetime] = {} for mod in modifications: last_modified_dict[mod.path] = cast( - datetime, max(mod.author_date, last_modified_dict.get(mod.path, sentinel)), + datetime, + max(mod.author_date, last_modified_dict.get(mod.path, sentinel)), ) return last_modified_dict diff --git a/coverage_plot/plot.py b/coverage_plot/plot.py index 4421f6f..cf23224 100644 --- a/coverage_plot/plot.py +++ b/coverage_plot/plot.py @@ -5,8 +5,8 @@ import pandas as pd import plotly.express as px -from plotly.graph_objs import Figure from attrs import frozen +from plotly.graph_objs import Figure from coverage_plot.importance_interface import Importance @@ -79,7 +79,10 @@ def export_df(report: Report, importance: Importance) -> pd.DataFrame: } records.append(record) - records = sorted(records, key=lambda k: k["path"]) + def _sorter(record_: Dict) -> str: + return record_["path"] + + records = sorted(records, key=_sorter) return pd.DataFrame(records) diff --git a/tests/test_git_changes.py b/tests/test_git_changes.py index 230bbd7..17d7ffe 100644 --- a/tests/test_git_changes.py +++ b/tests/test_git_changes.py @@ -1,9 +1,10 @@ -import datetime -from typing import List - -import attr import faker +from coverage_plot.fake_implementations import ( + FakeCommit, + FakeDeveloper, + FakeModification, +) from coverage_plot.git_changes import ( ExcludeAllModifications, ExcludeAuthor, @@ -16,52 +17,6 @@ filter_modifications, ) -fake = faker.Faker() - - -@attr.s(auto_attribs=True, frozen=True) -class FakeDeveloper: - """ - Fake object implementing the subset of the interface of pydriller's Developer. - - Ref: https://pydriller.readthedocs.io/en/latest/commit.html - """ - - name: str = attr.ib(factory=fake.name) - email: str = attr.ib(factory=fake.email) - - -@attr.s(auto_attribs=True, frozen=True) -class FakeModification: - """ - Fake object implementing the subset of the interface of pydriller's Modification. - - Ref: https://pydriller.readthedocs.io/en/latest/modifications.html - """ - - path: str = attr.ib(factory=fake.file_path) - - @property - def old_path(self): - return self.path - - @property - def new_path(self): - return self.path - - -@attr.s(auto_attribs=True, frozen=True) -class FakeCommit: - """ - Fake object implementing the subset of the interface of pydriller's Commit. - """ - - author: FakeDeveloper = attr.ib(factory=FakeDeveloper) - author_date: datetime.datetime = attr.ib(factory=fake.date_time_this_year) - msg: str = attr.ib(factory=fake.sentence) - hash: str = attr.ib(factory=fake.sha1) - modifications: List[FakeModification] = attr.ib(factory=list) - def test_include_file_include(): m1 = FakeModification(path="foo.py")