pytest-cov icon indicating copy to clipboard operation
pytest-cov copied to clipboard

Do not report abstract methods as missing coverage

Open ayang-waelder opened this issue 5 years ago • 7 comments

Summary

This feature requests especially concerns the term-missing report. By their nature, methods decorated with abc.abstractmethod cannot be called directly.

In the example below, pytest-cov would consider pass to be an uncovered statement.

@abc.abstractmethod
def some_abstract_method():
    pass

In this situation, it's impossible to reach 100% coverage. I acknowledge that it's possible to use the raise NotImplementedError approach instead of @abc.abstractmethod. However, I personally prefer to make use of abstract methods.

I would therefore greatly appreciate a feature that allows pytest-cov to omit abstract methods.

ayang-waelder avatar Aug 26 '20 09:08 ayang-waelder

You might want to take a look at https://coverage.readthedocs.io/en/coverage-5.2.1/excluding.html

ionelmc avatar Aug 26 '20 10:08 ionelmc

@ionelmc Thank you for your hint! Adding #pragma: no cover comment to the method definition causes the desired behavior. Still, it feels somewhat redundant to annotate abstract methods like this.

ayang-waelder avatar Aug 26 '20 11:08 ayang-waelder

Indeed I wonder if it's really right to report pass lines as no coverage at all. Certainly reporting an abstract method with a single pass line is overkill. It would be nice if this were fixed, rather than having to put excessive comments throughout.

couling avatar Apr 15 '21 22:04 couling

For future readers, I've just realised this can easily by done by adding pass to exclude_lines in .coveragerc (described here and here):

[report]
exclude_lines =
    # Skip any pass lines such as may be used for @abstractmethod
    pass

    # Have to re-enable the standard pragma
    pragma: no cover

    # Don't complain about missing debug-only code:
    def __repr__
    if self\.debug

    # Don't complain if tests don't hit defensive assertion code:
    raise AssertionError
    raise NotImplementedError

    # Don't complain if non-runnable code isn't run:
    if 0:
    if __name__ == .__main__.:

couling avatar Apr 16 '21 09:04 couling

I was just looking for a way to do this. I think it is worth mentioning that the statement in the original report

By their nature, methods decorated with abc.abstractmethod cannot be called directly.

is not entirely correct; abstract methods can be called from a child class. For example, if you run the following script it will output 2:

from abc import ABC, abstractmethod


class Abstract(ABC):
    @abstractmethod
    def mymethod(self):
        return 1


class Concrete(Abstract):
    def mymethod(self):
        return super().mymethod() + 1


print(Concrete().mymethod())

Any option to ignore abstract methods would at least need a warning that it would ignore any with code in them. Ideally it would only ignore methods with no actual code.

Also, if you choose to add it to the list of excluded lines I would suggest using a full regular expression to make sure pass is the only code on the line. If you just add pass to the list then any line with the word pass anywhere in it (for example, num_passes += 1) will also be ignored. This includes any lines which have an inline comment with pass in it. The regular expression ^\s*pass\s*$ works for me: it requires the line to have only the word pass plus optional leading or trailing whitespace.

bcbnz avatar Jul 22 '21 12:07 bcbnz

@bcbnz The note about adding "pass" being too broad is a good one, I will add that to the coverage.py docs.

There's something people are missing in this discussion: since coverage.py v4.1, if you exclude a decorator, it excludes the entire decorated function. So the best exclude line here would be:

[report]
exclude_lines =
    @abstractmethod
    @abc.abstractmethod

Once you do that, the entire abstract method is excluded from coverage, no matter what statements it contains.

BTW, this is also being discuss in coverage.py: https://github.com/nedbat/coveragepy/issues/1131

nedbat avatar Jul 22 '21 14:07 nedbat

Another approach, which I now prefer, is to remove the pass altogether. It's not required if the method has a doc string.

Future readers may prefer to simply document their abstract methods rather than leaving them empty. So this can be changed:

class Foo(ABC):
    @abstractmethod
    def bar(self):
        pass

To this:

class Foo(ABC):
    @abstractmethod
    def bar(self):
       """
       Do xyz to this object
       """

Some may prefer to leave pass showing red in the coverage report as a gentle reminder to document.

couling avatar Aug 13 '21 16:08 couling