Skip to content

Commit

Permalink
fix: add support for no colors mode (#359)
Browse files Browse the repository at this point in the history
* fix: use env variable to disable terminal color

* docs: add no color to option list

* test: update cases to work with no colors

* fix: ensure term color is repected in assert repr

* refactor: support wider range of env values
  • Loading branch information
iamogbz committed Sep 20, 2020
1 parent 0b19b93 commit ec39b80
Show file tree
Hide file tree
Showing 14 changed files with 127 additions and 74 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ def __repr__(self) -> str:

These are the cli options exposed to `pytest` by the plugin.

| Option | Description | Default |
| ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------ |
| `--snapshot-update` | Snapshots will be updated to match assertions and unused snapshots will be deleted. | `False` |
| `--snapshot-warn-unused` | Prints a warning on unused snapshots rather than fail the test suite. | `False` |
| `--snapshot-default-extension` | Use to change the default snapshot extension class. | `syrupy.extensions.amber.AmberSnapshotExtension` |
| Option | Description | Default |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| `--snapshot-update` | Snapshots will be updated to match assertions and unused snapshots will be deleted. | `False` |
| `--snapshot-warn-unused` | Prints a warning on unused snapshots rather than fail the test suite. | `False` |
| `--snapshot-default-extension` | Use to change the default snapshot extension class. | `syrupy.extensions.amber.AmberSnapshotExtension` |
| `--snapshot-no-colors` | Disable test results output highlighting. Equivalent to setting the environment variables `ANSI_COLORS_DISABLED` or `NO_COLOR` | Disabled by default if not in terminal. |

### Assertion Options

Expand Down
9 changes: 9 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import typing

import pytest

from syrupy.utils import env_context

typing.TYPE_CHECKING = True
pytest_plugins = "pytester"


@pytest.fixture
def osenv():
return env_context
9 changes: 2 additions & 7 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ attrs==20.2.0 # via flake8-bugbear, pytest, syrupy
black==20.8b1 # via syrupy
bleach==3.2.1 # via readme-renderer
certifi==2020.6.20 # via requests
cffi==1.14.2 # via cryptography
chardet==3.0.4 # via requests
click==7.1.2 # via black, pip-tools
codecov==2.1.9 # via syrupy
colorama==0.4.3 # via twine
colored==1.4.2 # via syrupy
configparser==5.0.0 # via py-githooks
coverage[toml]==5.3 # via codecov, syrupy
cryptography==3.1 # via secretstorage
coverage[toml]==5.3 # via codecov, syrupy
deprecated==1.2.10 # via pygithub
docutils==0.16 # via readme-renderer
flake8-bugbear==20.1.4 # via syrupy
Expand All @@ -29,7 +27,6 @@ idna==2.10 # via requests
iniconfig==1.0.1 # via pytest
invoke==1.4.1 # via syrupy
isort==5.5.2 # via syrupy
jeepney==0.4.3 # via keyring, secretstorage
keyring==21.4.0 # via twine
mccabe==0.6.1 # via flake8
more-itertools==8.5.0 # via pytest
Expand All @@ -43,7 +40,6 @@ pluggy==0.13.1 # via pytest
py-githooks==1.1.0 # via syrupy
py==1.9.0 # via pytest
pycodestyle==2.6.0 # via flake8
pycparser==2.20 # via cffi
pyflakes==2.2.0 # via flake8
pygithub==1.53 # via syrupy
pygments==2.7.1 # via readme-renderer
Expand All @@ -56,9 +52,8 @@ regex==2020.7.14 # via black
requests-toolbelt==0.9.1 # via twine
requests==2.24.0 # via codecov, pygithub, requests-toolbelt, twine
rfc3986==1.4.0 # via twine
secretstorage==3.1.2 # via keyring
semver==2.10.2 # via syrupy
six==1.15.0 # via bleach, cryptography, packaging, pip-tools, readme-renderer
six==1.15.0 # via bleach, packaging, pip-tools, readme-renderer
toml==0.10.1 # via black, coverage, pytest
tqdm==4.49.0 # via twine
twine==3.2.0 # via syrupy
Expand Down
55 changes: 40 additions & 15 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import argparse
import glob
import sys
from gettext import gettext
from typing import (
Any,
ContextManager,
List,
Optional,
)

import pytest

from .assertion import SnapshotAssertion
from .constants import DISABLE_COLOR_ENV_VAR
from .exceptions import FailedToLoadModuleMember
from .extensions import DEFAULT_EXTENSION
from .location import TestLocation
Expand All @@ -19,7 +22,10 @@
reset,
snapshot_style,
)
from .utils import import_module_member
from .utils import (
env_context,
import_module_member,
)


def __default_extension_option(value: str) -> Any:
Expand Down Expand Up @@ -56,23 +62,41 @@ def pytest_addoption(parser: Any) -> None:
dest="default_extension",
help="Specify the default snapshot extension",
)
group.addoption(
"--snapshot-no-colors",
action="store_true",
default=not sys.stdout.isatty(),
dest="no_colors",
help="Disable test results output highlighting",
)


def __terminal_color(config: Any) -> "ContextManager[None]":
env = {}
if config.option.no_colors:
env[DISABLE_COLOR_ENV_VAR] = "true"

return env_context(**env)


def pytest_assertrepr_compare(op: str, left: Any, right: Any) -> Optional[List[str]]:
def pytest_assertrepr_compare(
config: Any, op: str, left: Any, right: Any
) -> Optional[List[str]]:
"""
Return explanation for comparisons in failing assert expressions.
https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_assertrepr_compare
"""
if isinstance(left, SnapshotAssertion):
assert_msg = reset(
f"{snapshot_style(left.name)} {op} {received_style('received')}"
)
return [assert_msg] + left.get_assert_diff()
elif isinstance(right, SnapshotAssertion):
assert_msg = reset(
f"{received_style('received')} {op} {snapshot_style(right.name)}"
)
return [assert_msg] + right.get_assert_diff()
with __terminal_color(config):
if isinstance(left, SnapshotAssertion):
assert_msg = reset(
f"{snapshot_style(left.name)} {op} {received_style('received')}"
)
return [assert_msg] + left.get_assert_diff()
elif isinstance(right, SnapshotAssertion):
assert_msg = reset(
f"{received_style('received')} {op} {snapshot_style(right.name)}"
)
return [assert_msg] + right.get_assert_diff()
return None


Expand Down Expand Up @@ -144,9 +168,10 @@ def pytest_terminal_summary(
Add syrupy report to pytest.
https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_terminal_summary
"""
terminalreporter.write_sep("-", gettext("snapshot report summary"))
for line in terminalreporter.config._syrupy.report.lines:
terminalreporter.write_line(line)
with __terminal_color(config):
terminalreporter.write_sep("-", gettext("snapshot report summary"))
for line in terminalreporter.config._syrupy.report.lines:
terminalreporter.write_line(line)


@pytest.fixture
Expand Down
3 changes: 3 additions & 0 deletions src/syrupy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
SYMBOL_ELLIPSIS = "..." # U+2026
SYMBOL_NEW_LINE = "␤" # U+2424
SYMBOL_CARRIAGE = "␍" # U+240D

DISABLE_COLOR_ENV_VAR = "ANSI_COLORS_DISABLED"
DISABLE_COLOR_ENV_VARS = {DISABLE_COLOR_ENV_VAR, "NO_COLOR"}
38 changes: 27 additions & 11 deletions src/syrupy/terminal.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
from typing import Union
from typing import (
Any,
Union,
)

import colored

from .constants import DISABLE_COLOR_ENV_VARS
from .utils import get_env_value


def _is_color_disabled() -> bool:
return any(map(get_env_value, DISABLE_COLOR_ENV_VARS))


def _stylize(text: Union[str, int], *args: Any) -> str:
if _is_color_disabled():
return str(text)
return colored.stylize(text, *args)


def reset(text: Union[str, int]) -> str:
return colored.stylize(text, colored.attr("reset"))
return _stylize(text, colored.attr("reset"))


def red(text: Union[str, int]) -> str:
return colored.stylize(text, colored.fg("red"))
return _stylize(text, colored.fg("red"))


def yellow(text: Union[str, int]) -> str:
return colored.stylize(text, colored.fg("yellow"))
return _stylize(text, colored.fg("yellow"))


def green(text: Union[str, int]) -> str:
return colored.stylize(text, colored.fg("green"))
return _stylize(text, colored.fg("green"))


def bold(text: Union[str, int]) -> str:
return colored.stylize(text, colored.attr("bold"))
return _stylize(text, colored.attr("bold"))


def error_style(text: Union[str, int]) -> str:
Expand All @@ -36,20 +52,20 @@ def success_style(text: Union[str, int]) -> str:


def snapshot_style(text: Union[str, int]) -> str:
return colored.stylize(text, colored.bg(225) + colored.fg(90))
return _stylize(text, colored.bg(225) + colored.fg(90))


def snapshot_diff_style(text: Union[str, int]) -> str:
return colored.stylize(text, colored.bg(90) + colored.fg(225))
return _stylize(text, colored.bg(90) + colored.fg(225))


def received_style(text: Union[str, int]) -> str:
return colored.stylize(text, colored.bg(195) + colored.fg(23))
return _stylize(text, colored.bg(195) + colored.fg(23))


def received_diff_style(text: Union[str, int]) -> str:
return colored.stylize(text, colored.bg(23) + colored.fg(195))
return _stylize(text, colored.bg(23) + colored.fg(195))


def context_style(text: Union[str, int]) -> str:
return colored.stylize(text, colored.attr("dim"))
return _stylize(text, colored.attr("dim"))
20 changes: 20 additions & 0 deletions src/syrupy/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json
import os
from contextlib import contextmanager
from gettext import gettext
from importlib import import_module
from pathlib import Path
Expand Down Expand Up @@ -46,3 +49,20 @@ def import_module_member(path: str) -> Any:
module_name,
)
)


def get_env_value(env_var_name: str) -> object:
try:
return json.loads(os.environ[env_var_name])
except (KeyError, TypeError, json.decoder.JSONDecodeError):
return os.environ.get(env_var_name)


@contextmanager
def env_context(**kwargs: str) -> Iterator[None]:
prev_env = {**os.environ}
try:
yield os.environ.update(kwargs)
finally:
os.environ.clear()
os.environ.update(prev_env)
2 changes: 1 addition & 1 deletion tests/__snapshots__/test_integration_default.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
you do not want to see this line
or this line
this line should show up because it changes\n
this line should show up because it changes color
this line should show up because it changes color
and this line does not exist in the first one
'''
E AssertionError: assert snapshot == received
Expand Down
7 changes: 3 additions & 4 deletions tests/test_extension_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from syrupy.extensions.base import SnapshotReporter

from .utils import clean_output


class SnapshotReporterNoContext(SnapshotReporter):
@property
Expand All @@ -27,5 +25,6 @@ class TestSnapshotReporter:
],
ids=lambda _: "",
)
def test_diff_lines(self, a, b, Reporter, snapshot):
assert "\n".join(map(clean_output, Reporter().diff_lines(a, b))) == snapshot
def test_diff_lines(self, a, b, Reporter, snapshot, osenv):
with osenv(NO_COLOR="true"):
assert "\n".join(Reporter().diff_lines(a, b)) == snapshot
6 changes: 2 additions & 4 deletions tests/test_integration_custom.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import pytest

from .utils import clean_output


@pytest.fixture
def testcases(testdir):
Expand Down Expand Up @@ -56,8 +54,8 @@ def test_passed_custom(snapshot_custom):

def test_warns_on_snapshot_name(testdir, testcases):
testdir.makepyfile(test_file=testcases["passed"])
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
result = testdir.runpytest("-v", "--snapshot-update", "--snapshot-no-colors")
result_stdout = result.stdout.str()
assert "2 snapshots generated" in result_stdout
assert "Warning:" in result_stdout
assert "Can not relate snapshot name" in result_stdout
Expand Down
18 changes: 8 additions & 10 deletions tests/test_integration_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

import pytest

from .utils import clean_output


def get_result_snapshot_summary(result: object) -> str:
result_stdout = clean_output(result.stdout.str())
result_stdout = result.stdout.str()
start_idx = result_stdout.find("\n", result_stdout.find("---- snapshot report"))
end_idx = result_stdout.find("====", start_idx)
return result_stdout[start_idx:end_idx].strip()
Expand Down Expand Up @@ -120,7 +118,7 @@ def test_case_2(self, snapshot):

test_filepath = Path(testdir.tmpdir, "test_content.py")
result = testdir.runpytest(str(test_filepath), "-v", "-k test_case_2")
result_stdout = clean_output(result.stdout.str())
result_stdout = result.stdout.str()
assert "1 snapshot passed" in result_stdout
assert "snapshot unused" not in result_stdout

Expand All @@ -144,7 +142,7 @@ def test_case_2(snapshot):

test_filepath = Path(testdir.tmpdir, "test_content.py")
result = testdir.runpytest(str(test_filepath), "-v", "-k test_case_2")
result_stdout = clean_output(result.stdout.str())
result_stdout = result.stdout.str()
assert "1 snapshot passed" in result_stdout
assert "snapshot unused" not in result_stdout

Expand All @@ -161,7 +159,7 @@ def test_case_1(snapshot):
testdir.makepyfile(test_content=test_content)
result = testdir.runpytest("-v", "--snapshot-update")

result_stdout = clean_output(result.stdout.str())
result_stdout = result.stdout.str()
assert "Can not relate snapshot name" not in result_stdout


Expand Down Expand Up @@ -305,8 +303,8 @@ def test_generated_snapshots(stubs, snapshot):
def test_failing_snapshots_diff(stubs, testcases_updated, snapshot):
testdir = stubs[1]
testdir.makepyfile(test_file="\n\n".join(testcases_updated.values()))
result = testdir.runpytest("-vv")
result_stdout = clean_output(result.stdout.str())
result = testdir.runpytest("-vv", "--snapshot-no-colors")
result_stdout = result.stdout.str()
start_index = result_stdout.find("\n", result_stdout.find("==== FAILURES"))
end_index = result_stdout.find("\n", result_stdout.find("---- snapshot report"))
assert result_stdout[start_index:end_index] == snapshot
Expand Down Expand Up @@ -426,7 +424,7 @@ def test_removed_hanging_snapshot_fossil(stubs, snapshot):
hanging_filepath = Path(testdir.tmpdir, "__snapshots__/test_hanging.abc")
assert hanging_filepath.exists()
result = testdir.runpytest("-v", "--snapshot-update")
result_stdout = clean_output(result.stdout.str())
result_stdout = result.stdout.str()
assert str(Path(filepath).relative_to(Path.cwd())) not in result_stdout
assert "1 unused snapshot deleted" in result_stdout
assert "unknown snapshot" in result_stdout
Expand Down Expand Up @@ -464,7 +462,7 @@ def test_snapshot_default_extension_option_failure(testdir, testcases, snapshot)
"--snapshot-default-extension",
"syrupy.extensions.amber.DoesNotExistExtension",
)
result_stderr = clean_output(result.stderr.str())
result_stderr = result.stderr.str()
assert "error: argument --snapshot-default-extension" in result_stderr
assert "Member 'DoesNotExistExtension' not found" in result_stderr
assert result.ret
Loading

0 comments on commit ec39b80

Please sign in to comment.