Skip to content

Commit

Permalink
Merge pull request #864 from UltimateLobster/feature/pycharm-diff-pat…
Browse files Browse the repository at this point in the history
…cher

feat: Added a new CLI flag: --snapshot-patch-pycharm-diff
  • Loading branch information
noahnu authored Aug 23, 2024
2 parents 3c2b962 + b867d38 commit 6585004
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 3 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,13 @@ Both options will generate equivalent snapshots but the latter is only viable wh
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-details` | Includes details of unused snapshots (test name and snapshot location) in the final report. | `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. | [AmberSnapshotExtension](https://github.com/syrupy-project/syrupy/blob/main/src/syrupy/extensions/amber/__init__.py) |
| `--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. |
| `--snapshot-patch-pycharm-diff`| Override PyCharm's default diffs viewer when looking at snapshot diffs. See [IDE Integrations](#ide-integrations) | `False` |

### Assertion Options

Expand Down Expand Up @@ -470,6 +471,21 @@ The generated snapshot:
- [JPEG image extension](https://github.com/syrupy-project/syrupy/tree/main/tests/examples/test_custom_image_extension.py)
- [Built-in image extensions](https://github.com/syrupy-project/syrupy/blob/main/tests/syrupy/extensions/image/test_image_svg.py)

## IDE Integrations

### PyCharm

The [PyCharm](https://www.jetbrains.com/pycharm/) IDE comes with a built-in tool for visualizing differences between expected and actual results in a test. To properly render Syrupy snapshots in the PyCharm diff viewer, we need to apply a patch to the diff viewer library. To do this, use the `--snapshot-patch-pycharm-diff` flag, e.g.:

In your `pytest.ini`:

```ini
[pytest]
addopts = --snapshot-patch-pycharm-diff
```

See [#675](https://github.com/syrupy-project/syrupy/issues/675) for the original issue.

## Uninstalling

```python
Expand Down
22 changes: 20 additions & 2 deletions src/syrupy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import (
Any,
ContextManager,
Iterator,
List,
Optional,
)
Expand All @@ -16,6 +17,7 @@
from .exceptions import FailedToLoadModuleMember
from .extensions import DEFAULT_EXTENSION
from .location import PyTestLocation
from .patches.pycharm_diff import patch_pycharm_diff
from .session import SnapshotSession
from .terminal import (
received_style,
Expand Down Expand Up @@ -85,6 +87,13 @@ def pytest_addoption(parser: Any) -> None:
dest="no_colors",
help="Disable test results output highlighting",
)
group.addoption(
"--snapshot-patch-pycharm-diff",
action="store_true",
default=False,
dest="patch_pycharm_diff",
help="Patch PyCharm diff",
)


def __terminal_color(config: Any) -> "ContextManager[None]":
Expand Down Expand Up @@ -185,10 +194,19 @@ def pytest_terminal_summary(


@pytest.fixture
def snapshot(request: Any) -> "SnapshotAssertion":
def snapshot(request: "pytest.FixtureRequest") -> "SnapshotAssertion":
return SnapshotAssertion(
update_snapshots=request.config.option.update_snapshots,
extension_class=__import_extension(request.config.option.default_extension),
test_location=PyTestLocation(request.node),
session=request.session.config._syrupy,
session=request.session.config._syrupy, # type: ignore
)


@pytest.fixture(scope="session", autouse=True)
def _syrupy_apply_ide_patches(request: "pytest.FixtureRequest") -> Iterator[None]:
if request.config.option.patch_pycharm_diff:
with patch_pycharm_diff():
yield
else:
yield
Empty file added src/syrupy/patches/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions src/syrupy/patches/pycharm_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import warnings
from contextlib import contextmanager
from functools import wraps
from inspect import signature
from typing import (
Any,
Iterator,
)

from syrupy.assertion import SnapshotAssertion


@contextmanager
def patch_pycharm_diff() -> Iterator[None]:
"""
Applies PyCharm diff patch to add Syrupy snapshot support.
See: https://github.com/syrupy-project/syrupy/issues/675
"""

try:
from teamcity.diff_tools import EqualsAssertionError # type: ignore
except ImportError:
warnings.warn(
"Failed to patch PyCharm's diff tools. Skipping patch.",
stacklevel=2,
)
yield
return

old_init = EqualsAssertionError.__init__
old_init_signature = signature(old_init)

@wraps(old_init)
def new_init(self: "EqualsAssertionError", *args: Any, **kwargs: Any) -> None:

# Extract the __init__ arguments as originally passed in order to
# process them later
parameters = old_init_signature.bind(self, *args, **kwargs)
parameters.apply_defaults()

expected = parameters.arguments["expected"]
actual = parameters.arguments["actual"]
real_exception = parameters.arguments["real_exception"]

if isinstance(expected, SnapshotAssertion):
snapshot = expected
elif isinstance(actual, SnapshotAssertion):
snapshot = actual
else:
snapshot = None

old_init(self, *args, **kwargs)

# No snapshot was involved in the assertion. Let the old logic do its
# thing.
if snapshot is None:
return

# Although a snapshot was involved in the assertion, it seems the error
# was a result of a non-assertion exception (Ex. `assert 1/0`).
# Therefore, We will not do anything here either.
if real_exception is not None:
return

assertion_result = snapshot.executions[snapshot.num_executions - 1]
if assertion_result.exception is not None:
return

self.expected = str(assertion_result.recalled_data)
self.actual = str(assertion_result.asserted_data)

try:
EqualsAssertionError.__init__ = new_init
yield
finally:
EqualsAssertionError.__init__ = old_init
Loading

0 comments on commit 6585004

Please sign in to comment.