Repository: samuelcolvin/pytest-pretty (language: python)

Issue:
Add alternative for `pytester` fixture. 
This plugin is really nice and really improves the readability of the output. But it breaks the `pytester` fixture https://docs.pytest.org/en/7.2.x/reference/reference.html#std-fixture-pytester 

The `pytester` is based on parsing pytest output to determine number of failed/passed/etc. 

We use the older version (testdir) in napari to validate if the fixture works correctly. 
https://github.com/napari/napari/blob/f5fcb7d2e3bb32950de227df61f7bc69846a6aa4/napari/_tests/test_pytest_plugin.py

So we cannot simply add pytest-pretty to our workflow.


Discussion / hints:
Thanks for the issue, in concrete terms, what do you need from me here?
> Thanks for the issue, in concrete terms, what do you need from me here?

are you open for such a feature in this package (maybe implemented by someone else)?
I think so, but I'm a bit lost on what it really does.

If it doesn't add massive complexity, or get in the way when not using it, happy to review a PR.
> I think so, but I'm a bit lost on what it really does.

this fixture run test provides a string and pares output to check if it runs as expected. 



> If it doesn't add massive complexity, or get in the way when not using it, happy to review a PR.

I will try to do this. 
Sounds good. Happy to review.

Fix patch:
diff --git a/README.md b/README.md
index ae79b66..0d73478 100644
--- a/README.md
+++ b/README.md
@@ -45,3 +45,10 @@ env:
 ```
 
 This will mean the pytest output is wider and easier to use, more importantly, it'll make the error summary table printed by pytest-pretty much easier to read, see [this](https://github.com/Textualize/rich/issues/2769) discussion for more details.
+
+
+## `pytester_pretty` fixture
+
+The `pytest_pretty` provides `pytester_pretty` fixture that work with modified version of output. It is designed to drop in places replacement of `pytester` fixture and uses it internaly.
+
+So to use them it is required to set `pytest_plugins = "pytester"` as mentioned in pytest documentation https://docs.pytest.org/en/latest/reference/reference.html#pytester


Test patch:
diff --git a/pytest_pretty/__init__.py b/pytest_pretty/__init__.py
index 01b3a4a..8badf6c 100644
--- a/pytest_pretty/__init__.py
+++ b/pytest_pretty/__init__.py
@@ -1,8 +1,10 @@
 from __future__ import annotations as _annotations
 
+import re
 import sys
+from itertools import dropwhile
 from time import perf_counter_ns
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Callable
 
 import pytest
 from _pytest.terminal import TerminalReporter
@@ -98,3 +100,68 @@ def pytest_configure(config):
     custom_reporter = CustomTerminalReporter(config, sys.stdout)
     config.pluginmanager.unregister(standard_reporter)
     config.pluginmanager.register(custom_reporter, 'terminalreporter')
+
+
+ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
+stat_re = re.compile(r'(\d+) (\w+)')
+
+
+def create_new_parseoutcomes(runresult_instance) -> Callable[[], dict[str, int]]:
+    """
+    In this function there is a new implementation of `RunResult.parseoutcomes`
+    https://github.com/pytest-dev/pytest/blob/4a46ee8bc957b06265c016cc837862447dde79d2/src/_pytest/pytester.py#L557
+
+
+    Decision of reimplement this method is made based on implementation of
+    `RunResult.assert_outcomes`
+
+    https://github.com/pytest-dev/pytest/blob/4a46ee8bc957b06265c016cc837862447dde79d2/src/_pytest/pytester.py#L613
+    """
+
+    def parseoutcomes() -> dict[str, int]:
+        lines_with_stats = dropwhile(lambda x: 'Results' not in x, runresult_instance.outlines)
+        next(lines_with_stats)  # drop Results line
+        res = {}
+        for i, line in enumerate(lines_with_stats):
+            line = ansi_escape.sub('', line).strip()  # clean colors
+            match = stat_re.match(line)
+
+            if match is None:
+                break
+
+            res[match.group(2)] = int(match.group(1))
+
+        return res
+
+    return parseoutcomes
+
+
+class PytesterWrapper:
+    """
+    This is class for for make almost transparent wrapper
+    arround pytester output and allow substitute
+    `parseoutcomes` method of `RunResult` instance.
+    """
+
+    __slot__ = ('_pytester',)
+
+    def __init__(self, pytester):
+        object.__setattr__(self, '_pytester', pytester)
+
+    def runpytest(self):
+        """wraper to overwritte `parseoutcomes` method of `RunResult` instance"""
+        res = self._pytester.runpytest()
+        assert res is not None
+        res.parseoutcomes = create_new_parseoutcomes(res)
+        return res
+
+    def __getattr__(self, name):
+        return getattr(self._pytester, name)
+
+    def __setattr__(self, name, value):
+        setattr(self._pytester, name, value)
+
+
+@pytest.fixture()
+def pytester_pretty(pytester):
+    return PytesterWrapper(pytester)
diff --git a/tests/test_simple.py b/tests/test_simple.py
index 256cb47..7e9dec7 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -1,5 +1,11 @@
 import pytest
 
+pytest_plugins = "pytester"
+
+
+test_str = """
+import pytest
+
 def test_add():
     assert 1 + 2 == 3
 
@@ -12,3 +18,17 @@ def test_subtract():
 @pytest.mark.skipif(True, reason='This test is skipped')
 def test_multiply():
     assert 2 * 2 == 5
+"""
+
+def test_base(pytester):
+    pytester.makepyfile(test_str)
+    result = pytester.runpytest()
+    assert any("passed" in x and "1" in x for x in result.outlines)
+    assert any("skipped" in x and "1" in x for x in result.outlines)
+    assert any("xfailed" in x and "1" in x for x in result.outlines)
+
+
+def test_fixture(pytester_pretty):
+    pytester_pretty.makepyfile(test_str)
+    result = pytester_pretty.runpytest()
+    result.assert_outcomes(passed=1, skipped=1, xfailed=1)