Skip to content

Commit cac249b

Browse files
Prepare for release of version 0.5.0 and update documentation (#4)
Release version 0.5.0 includes a new CLI command for updating snapshots and adds support for pandas DataFrame assertions. The documentation has been updated to reflect these changes and the changelog has been corrected for accuracy.
2 parents 2734741 + af3d0f4 commit cac249b

File tree

9 files changed

+1001
-911
lines changed

9 files changed

+1001
-911
lines changed

CHANGELOG.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,51 @@ All notable changes to this project will be documented in this file.
33

44
*NOTE:* Version 0.X.X might have breaking changes in bumps of the minor version number. This is because the project is still in early development and the API is not yet stable. It will still be marked clearly in the release notes.
55

6-
## [unreleased]
7-
- 🆕 Add new cli command for easy update of snapshots without the need to rerun tests
6+
## [0.5.0] - 2025-07-30
7+
- 🆕 Add new cli command for easy update of snapshots without the need to rerun tests. Added: `snappylapy update` CLI command that will update the snapshots that have changed or create new snapshots.
88

9-
## [0.4.0]
9+
## [0.4.0] - 2025-06-08
1010
- 🆕 Add support for pandas DataFrame assertions (pandas is an optional dependency, and snappylapy can be used without it)
1111
- 🆕 Add new simpler and more intuitive Expect API. We can use expect() calls and an overload will select the appropriate Expect class based on the type of the input. This is in contrast with expect.dict(), expect.list(), etc. This is not a breaking change, since the old API is still supported.
1212

13-
## [0.3.2]
13+
## [0.3.2] - 2025-04-26
1414
- 🐞 Bug fix of test_directory fixture, not loading correctly depending fixture
1515

16-
## [0.3.1]
16+
## [0.3.1] - 2025-01-29
1717
- 🐞 Fix load_snapshot fixture not loading correctly
1818
- 🐞 Fix wrong naming for cases where custom name is falsy
1919
- Change the ident property of json to make it more human readable
2020

2121

22-
## [0.3.0]
22+
## [0.3.0] - 2025-01-26
2323
- Add snappylapy parametization functionality to snappylapy and allow it to be loaded in the load_snapshot fixture
2424
- Do reporting showing count of updated, created and deleted snapshots
2525
- **Breaking Changes**
2626
- Change setting of path to only have a single path configurable and update such it is configured through the marker
2727
- Make the folder names of __snapshots__ and __test_results__ enforced to be fixed (needed for cleanup and diffing)
2828
- Rename output file names of snapshots to match [filename][testname][name].extention conversion
29-
30-
## [0.2.1] - 13-01-2025
29+
## [0.2.1] - 2025-01-13
3130
- Added missing dependency for typer to make the CLI work
3231

33-
## [0.2.0] - 13-01-2025
32+
## [0.2.0] - 2025-01-13
3433
- Better error messages by using pytest assertion rewriting
3534
- Allow users to set the snapshot directory when using the load_snapshot fixture
3635
- Add CLI for for init and clear commands
3736
- Added automated generation of documentation using mkdocs
3837

39-
## [0.1.1] - 10-01-2025
38+
## [0.1.1] - 2025-01-10
4039
- Update dependencies with the lower bounds of package compatibility
4140
- Refactor to make code easier for users of package to modify and extend
4241

43-
## [0.1.0] - 08-01-2025
42+
## [0.1.0] - 2025-01-08
4443
- Added fixture for loading snapshots from previous tests (load_snapshot fixture)
4544
- Added the snappylapy marker for tests that depend on previous tests (pytest.mark.snappylapy). This will be used for more advanced features in the future.
4645

47-
## [0.0.2] - 07-01-2025
46+
## [0.0.2] - 2025-01-07
4847
- 🐞 Added fix for python 3.9, by refactoring incompatible type annotation
4948
- Loosened the version requirements for pytest (until the lower bound have been discovered, with automated testing)
5049
- Improved metadata for pypi
5150

52-
## [0.0.1] - 06-01-2025
51+
## [0.0.1] - 2025-01-06
5352
- Initial release of Snappylapy
5453
- Implemented basic snapshot testing functionality for dict, list, bytes and str data types

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ from mypackage import my_function
4646
def test_snapshot_dict(expect: Expect):
4747
"""Test snapshot with dictionary data."""
4848
data: dict = my_function()
49-
expect.dict(data).to_match_snapshot()
50-
# or expect(data).to_match_snapshot()
49+
expect(data).to_match_snapshot()
50+
# or expect.dict(data).to_match_snapshot()
5151
```
5252

5353
In this example, `snappylapy` captures the output of `my_function` and compares it against a stored snapshot. If the output changes unexpectedly, pytest will flag the test, allowing you to review the differences and ensure your code behaves as expected.
@@ -59,7 +59,7 @@ from snappylapy import Expect, LoadSnapshot
5959

6060
def test_snapshot_dict(expect: Expect):
6161
"""Test snapshot with dictionary data."""
62-
expect.dict({
62+
expect({
6363
"name": "John Doe",
6464
"age": 31
6565
}).to_match_snapshot()
@@ -68,6 +68,8 @@ def test_snapshot_dict(expect: Expect):
6868
def test_load_snapshot_from_file(load_snapshot: LoadSnapshot):
6969
"""Test loading snapshot data created in test_snapshot_dict from a file using the deserializer."""
7070
data = load_snapshot.dict()
71+
# Normally you would use the data as an input for some other function
72+
# For demonstration, we will just assert the data matches the expected snapshot
7173
assert data == {"name": "John Doe", "age": 31}
7274
```
7375

@@ -81,12 +83,18 @@ The results is split into two folders, for ease of comparison, and for handling
8183
- __snapshots__: Updated only when --snapshot-update flag is used when running the test suite. Commit this to your version control system.
8284

8385
## Usage
84-
Update snapshots with:
86+
Snapshots can be updated when running pytest:
8587

8688
```bash
8789
pytest --snapshot-update
8890
```
8991

92+
Alternatively, you can use the CLI command to update snapshots:
93+
94+
```bash
95+
snappylapy update
96+
```
97+
9098
## Fixtures and roadmap
9199
Registers fixtures:
92100
- expect ✅

devtools/create_new_release.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Create a new release for the project, by reading the version from the pyproject.toml file, and adding and pushing a new tag to the repository."""
2+
import sys
3+
import toml
4+
import typer
5+
import pathlib
6+
import requests
7+
import subprocess
8+
from packaging.version import Version
9+
from toolit import tool
10+
11+
PYPROJECT_TOML = pathlib.Path(__file__).parent.parent / "pyproject.toml"
12+
CHANGELOG_MD = pathlib.Path(__file__).parent.parent / "CHANGELOG.md"
13+
PYPI_ENDPOINT = "https://pypi.org/pypi/snappylapy/json"
14+
15+
def read_pyproject_version() -> Version:
16+
"""Read the version from the pyproject.toml file."""
17+
with open(PYPROJECT_TOML, "r", encoding="utf-8") as file:
18+
data = toml.load(file)
19+
version_str = data["project"]["version"]
20+
return Version(version_str)
21+
22+
def read_pypi_version() -> Version:
23+
"""Check if the version is newer than the one in pypi."""
24+
response = requests.get(PYPI_ENDPOINT)
25+
version_str = response.json()["info"]["version"]
26+
return Version(version_str)
27+
28+
def check_change_log(version: Version) -> bool:
29+
"""Check if the version has a corresponding entry in the change log. It will be a line starting with ## [0.0.2]"""
30+
with open(CHANGELOG_MD, "r", encoding="utf-8") as file:
31+
lines = file.readlines()
32+
version_str = f"## [{version}]"
33+
return any(line.startswith(version_str) for line in lines)
34+
35+
36+
@tool
37+
def create_new_release() -> None:
38+
"""Create a new release for the project by reading the version from the pyproject.toml file, checking the change log, and pushing a new tag to the repository."""
39+
if not PYPROJECT_TOML.exists():
40+
raise FileNotFoundError(f"{PYPROJECT_TOML} does not exist. Please run this script from the project root directory, and ensure the {PYPROJECT_TOML} file exists.")
41+
if not CHANGELOG_MD.exists():
42+
raise FileNotFoundError(f"{CHANGELOG_MD} does not exist. Please run this script from the project root directory, and ensure the {CHANGELOG_MD} file exists.")
43+
version_pyproject: Version = validate_and_get_version()
44+
validate_change_log_entry(version_pyproject)
45+
response: str = typer.prompt(
46+
f"Do you want to create a new release for version {typer.style(str(version_pyproject), fg=typer.colors.GREEN, bold=True)}? "
47+
"This will create a new tag and push it to the repository. (yes/no)",
48+
default="no",
49+
)
50+
if response.lower() not in ("yes", "y"):
51+
typer.secho("Release creation aborted.", fg=typer.colors.RED, bold=True)
52+
53+
sys.exit(1)
54+
55+
# Create a new tag
56+
res: subprocess.CompletedProcess = subprocess.run(["git", "tag", str(version_pyproject)])
57+
if res.returncode != 0:
58+
raise ValueError(f"Error creating tag {version_pyproject}")
59+
60+
# Push the new tag
61+
res = subprocess.run(["git", "push", "origin", str(version_pyproject)])
62+
if res.returncode != 0:
63+
raise ValueError(f"Error pushing tag {version_pyproject}")
64+
65+
typer.secho("Release created successfully.", fg=typer.colors.GREEN, bold=True)
66+
67+
68+
def validate_and_get_version() -> Version:
69+
"""Validate the version from the pyproject.toml file and check if it is newer than the one in pypi."""
70+
version_pyproject: Version = read_pyproject_version()
71+
typer.echo(f"Creating a new release for version {typer.style(str(version_pyproject), fg=typer.colors.GREEN, bold=True)} (read from {PYPROJECT_TOML}).")
72+
version_pypi: Version = read_pypi_version()
73+
typer.echo(
74+
f"Current version in pypi is {typer.style(str(version_pypi), fg=typer.colors.YELLOW, bold=True)} (read from {PYPI_ENDPOINT})."
75+
)
76+
typer.echo(
77+
f"Validating the "
78+
f"{typer.style(str(version_pypi), fg=typer.colors.YELLOW, bold=True)}"
79+
" -> "
80+
f"{typer.style(str(version_pyproject), fg=typer.colors.GREEN, bold=True)} deployment."
81+
)
82+
if version_pyproject <= version_pypi:
83+
error_message = (
84+
f"Version {version_pyproject} is not newer than the one in pypi. "
85+
"Please update the version in pyproject.toml."
86+
)
87+
raise ValueError(error_message)
88+
return version_pyproject
89+
90+
def validate_change_log_entry(version_pyproject: Version):
91+
"""Validate that the version has a corresponding entry in the change log."""
92+
if not check_change_log(version_pyproject):
93+
error_message = (
94+
f"Version {version_pyproject} does not have a corresponding entry in the change log."
95+
)
96+
raise ValueError(error_message)
97+
typer.secho(
98+
f"Version {version_pyproject} has a corresponding entry in the change log.",
99+
fg=typer.colors.GREEN,
100+
bold=True,
101+
)
102+
103+
if __name__ == "__main__":
104+
create_new_release()

devtools/readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In this project, toolit is used as a command runner used for easy automation with scripts, such they can be run with the `toolit` command line interface (CLI), MCP server, or as VSCode tasks.
2+
3+
Add the automation scripts in this folder with @tool decorator from `toolit` package.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies = [
1010
"typing-extensions ; python_full_version == '3.9.*'",
1111
]
1212
name = "snappylapy"
13-
version = "0.4.0"
13+
version = "0.5.0"
1414
description = "A snapshot library for python optimized for easy of use, human readable snapshots and enabling decoupling of chained integration tests."
1515
readme = "README.md"
1616
classifiers = [
@@ -83,4 +83,5 @@ dev = [
8383
"mkdocs-literate-nav<1.0.0,>=0.6.1",
8484
"mkdocs-section-index<1.0.0,>=0.3.9",
8585
"pandas-stubs>=2.2.2.240807",
86+
"toolit>=0.2.0",
8687
]

scripts/create_new_release.py

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

snappylapy/fixtures.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from typing import Any, Protocol, overload
3030

3131

32-
class CallableExpectation(Protocol):
32+
class _CallableExpectation(Protocol):
3333
"""Protocol for callable expectations to use internally in this module."""
3434

3535
def __call__(
@@ -215,6 +215,7 @@ def read_test_results(self) -> bytes:
215215

216216
@overload
217217
def __call__(self, data_to_snapshot: dict, name: str | None = None, filetype: str | None = None) -> DictExpect: ...
218+
218219
@overload
219220
def __call__(
220221
self,
@@ -236,7 +237,10 @@ def __call__(
236237

237238
@overload
238239
def __call__(
239-
self, data_to_snapshot: DataframeExpect.DataFrame, name: str | None = None, filetype: str | None = None,
240+
self,
241+
data_to_snapshot: DataframeExpect.DataFrame,
242+
name: str | None = None,
243+
filetype: str | None = None,
240244
) -> DataframeExpect: ...
241245

242246
def __call__(
@@ -252,7 +256,7 @@ def __call__(
252256
if filetype is not None:
253257
kwargs["filetype"] = filetype
254258

255-
type_map: dict[type, CallableExpectation] = {
259+
type_map: dict[type, _CallableExpectation] = {
256260
dict: self.dict,
257261
list: self.list,
258262
str: self.string,
@@ -261,13 +265,14 @@ def __call__(
261265
}
262266

263267
for typ, func in type_map.items():
264-
if isinstance(data_to_snapshot, typ):
268+
if isinstance(typ, type) and isinstance(data_to_snapshot, typ):
265269
return func(data_to_snapshot, **kwargs)
266270

267-
error_message = f"Unsupported type {type(data_to_snapshot)}. Expected one of: dict, list, str, bytes."
268-
raise TypeError(
269-
error_message,
270-
)
271+
supported_types: list[str] = [
272+
getattr(typ, "__name__", str(typ)) if isinstance(typ, type) else typ for typ in type_map.keys() # noqa: SIM118
273+
]
274+
error_message = f"Unsupported type: {type(data_to_snapshot)}. Supported types: {', '.join(supported_types)}."
275+
raise TypeError(error_message)
271276

272277

273278
class LoadSnapshot:
@@ -307,3 +312,10 @@ def bytes(self) -> bytes:
307312
"""Load bytes snapshot."""
308313
self.settings.depending_filename_extension = "bytes.txt"
309314
return BytesSerializer().deserialize(self._read_snapshot())
315+
316+
def dataframe(self) -> DataframeExpect.DataFrame:
317+
"""Load dataframe snapshot."""
318+
self.settings.depending_filename_extension = "dataframe.json"
319+
return DataframeExpect.DataFrame(
320+
JsonPickleSerializer[DataframeExpect.DataFrame]().deserialize(self._read_snapshot()),
321+
)

tests/test_snappylappy_new.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ def test_snapshot_string(expect: Expect):
77
"""Test snapshot with string data."""
88
expect.string("Hello World").to_match_snapshot()
99

10+
def test_unsupported_type(expect: Expect):
11+
"""Test snapshot with unsupported type."""
12+
class Unsupported:
13+
"""An unsupported type for snapshot."""
14+
pass
15+
16+
with pytest.raises(TypeError):
17+
expect(Unsupported())
18+
1019
def test_snapshot_bytes(expect: Expect):
1120
"""Test snapshot with bytes data."""
1221
expect.bytes(b"Hello World", name="bytes_snapshot").to_match_snapshot()

0 commit comments

Comments
 (0)