Skip to content

Commit 9962338

Browse files
committed
libtmux(textframe): Add pytest plugin with hooks and fixtures
why: Auto-register TextFrame assertion hooks and snapshot fixture for downstream users who install libtmux[textframe]. what: - Move tests/textframe/plugin.py → src/libtmux/textframe/plugin.py - Add pytest_assertrepr_compare hook for rich diff output - Add textframe_snapshot fixture for downstream users - Export TextFrameExtension from __init__.py - Simplify tests/textframe/conftest.py (hooks now in plugin)
1 parent e9a6ac4 commit 9962338

File tree

4 files changed

+155
-121
lines changed

4 files changed

+155
-121
lines changed

src/libtmux/textframe/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from __future__ import annotations
44

55
from libtmux.textframe.core import ContentOverflowError, TextFrame
6+
from libtmux.textframe.plugin import TextFrameExtension
67

7-
__all__ = ["ContentOverflowError", "TextFrame"]
8+
__all__ = ["ContentOverflowError", "TextFrame", "TextFrameExtension"]

src/libtmux/textframe/plugin.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"""Syrupy snapshot extension and pytest hooks for TextFrame.
2+
3+
This module provides:
4+
- TextFrameExtension: A syrupy extension for .frame snapshot files
5+
- pytest_assertrepr_compare: Rich assertion output for TextFrame comparisons
6+
- textframe_snapshot: Pre-configured snapshot fixture
7+
8+
When installed via `pip install libtmux[textframe]`, this plugin is
9+
auto-discovered by pytest through the pytest11 entry point.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import typing as t
15+
from difflib import ndiff
16+
17+
import pytest
18+
from syrupy.assertion import SnapshotAssertion
19+
from syrupy.extensions.single_file import SingleFileSnapshotExtension, WriteMode
20+
21+
from libtmux.textframe.core import ContentOverflowError, TextFrame
22+
23+
24+
class TextFrameExtension(SingleFileSnapshotExtension):
25+
"""Single-file extension for TextFrame snapshots (.frame files).
26+
27+
Each test snapshot is stored in its own .frame file, providing cleaner
28+
git diffs compared to the multi-snapshot .ambr format.
29+
30+
Notes
31+
-----
32+
This extension serializes:
33+
- TextFrame objects → their render() output
34+
- ContentOverflowError → their overflow_visual attribute
35+
- Other types → str() representation
36+
"""
37+
38+
_write_mode = WriteMode.TEXT
39+
file_extension = "frame"
40+
41+
def serialize(
42+
self,
43+
data: t.Any,
44+
*,
45+
exclude: t.Any = None,
46+
include: t.Any = None,
47+
matcher: t.Any = None,
48+
) -> str:
49+
"""Serialize data to ASCII frame representation.
50+
51+
Parameters
52+
----------
53+
data : Any
54+
The data to serialize.
55+
exclude : Any
56+
Properties to exclude (unused for TextFrame).
57+
include : Any
58+
Properties to include (unused for TextFrame).
59+
matcher : Any
60+
Custom matcher (unused for TextFrame).
61+
62+
Returns
63+
-------
64+
str
65+
ASCII representation of the data.
66+
"""
67+
if isinstance(data, TextFrame):
68+
return data.render()
69+
if isinstance(data, ContentOverflowError):
70+
return data.overflow_visual
71+
return str(data)
72+
73+
74+
# pytest hooks (auto-discovered via pytest11 entry point)
75+
76+
77+
def pytest_assertrepr_compare(
78+
config: pytest.Config,
79+
op: str,
80+
left: t.Any,
81+
right: t.Any,
82+
) -> list[str] | None:
83+
"""Provide rich assertion output for TextFrame comparisons.
84+
85+
This hook provides detailed diff output when two TextFrame objects
86+
are compared with ==, showing dimension mismatches and content diffs.
87+
88+
Parameters
89+
----------
90+
config : pytest.Config
91+
The pytest configuration object.
92+
op : str
93+
The comparison operator (e.g., "==", "!=").
94+
left : Any
95+
The left operand of the comparison.
96+
right : Any
97+
The right operand of the comparison.
98+
99+
Returns
100+
-------
101+
list[str] | None
102+
List of explanation lines, or None to use default behavior.
103+
"""
104+
if not isinstance(left, TextFrame) or not isinstance(right, TextFrame):
105+
return None
106+
if op != "==":
107+
return None
108+
109+
lines = ["TextFrame comparison failed:"]
110+
111+
# Dimension mismatch
112+
if left.content_width != right.content_width:
113+
lines.append(f" width: {left.content_width} != {right.content_width}")
114+
if left.content_height != right.content_height:
115+
lines.append(f" height: {left.content_height} != {right.content_height}")
116+
117+
# Content diff
118+
left_render = left.render().splitlines()
119+
right_render = right.render().splitlines()
120+
if left_render != right_render:
121+
lines.append("")
122+
lines.append("Content diff:")
123+
lines.extend(ndiff(right_render, left_render))
124+
125+
return lines
126+
127+
128+
@pytest.fixture
129+
def textframe_snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion:
130+
"""Snapshot fixture configured with TextFrameExtension.
131+
132+
This fixture is auto-discovered when libtmux[textframe] is installed.
133+
It provides a pre-configured snapshot for TextFrame objects.
134+
135+
Parameters
136+
----------
137+
snapshot : SnapshotAssertion
138+
The default syrupy snapshot fixture.
139+
140+
Returns
141+
-------
142+
SnapshotAssertion
143+
Snapshot configured with TextFrame serialization.
144+
145+
Examples
146+
--------
147+
>>> def test_my_frame(textframe_snapshot):
148+
... frame = TextFrame(content_width=10, content_height=2)
149+
... frame.set_content(["hello", "world"])
150+
... assert frame == textframe_snapshot
151+
"""
152+
return snapshot.use_extension(TextFrameExtension)

tests/textframe/conftest.py

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,10 @@
22

33
from __future__ import annotations
44

5-
import typing as t
6-
from difflib import ndiff
7-
85
import pytest
96
from syrupy.assertion import SnapshotAssertion
107

11-
from libtmux.textframe import TextFrame
12-
13-
from .plugin import TextFrameExtension
14-
15-
16-
def pytest_assertrepr_compare(
17-
config: pytest.Config,
18-
op: str,
19-
left: t.Any,
20-
right: t.Any,
21-
) -> list[str] | None:
22-
"""Provide rich assertion output for TextFrame comparisons.
23-
24-
This hook provides detailed diff output when two TextFrame objects
25-
are compared with ==, showing dimension mismatches and content diffs.
26-
27-
Parameters
28-
----------
29-
config : pytest.Config
30-
The pytest configuration object.
31-
op : str
32-
The comparison operator (e.g., "==", "!=").
33-
left : Any
34-
The left operand of the comparison.
35-
right : Any
36-
The right operand of the comparison.
37-
38-
Returns
39-
-------
40-
list[str] | None
41-
List of explanation lines, or None to use default behavior.
42-
"""
43-
if not isinstance(left, TextFrame) or not isinstance(right, TextFrame):
44-
return None
45-
if op != "==":
46-
return None
47-
48-
lines = ["TextFrame comparison failed:"]
49-
50-
# Dimension mismatch
51-
if left.content_width != right.content_width:
52-
lines.append(f" width: {left.content_width} != {right.content_width}")
53-
if left.content_height != right.content_height:
54-
lines.append(f" height: {left.content_height} != {right.content_height}")
55-
56-
# Content diff
57-
left_render = left.render().splitlines()
58-
right_render = right.render().splitlines()
59-
if left_render != right_render:
60-
lines.append("")
61-
lines.append("Content diff:")
62-
lines.extend(ndiff(right_render, left_render))
63-
64-
return lines
8+
from libtmux.textframe import TextFrameExtension
659

6610

6711
@pytest.fixture

tests/textframe/plugin.py

Lines changed: 0 additions & 63 deletions
This file was deleted.

0 commit comments

Comments
 (0)