Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions .github/workflows/continuous_integration.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
name: Continuous Integration

on:
workflow_dispatch:
pull_request:
branches:
- main
paths-ignore:
- '**/*.md'
- '**/*.yaml'
- '**/*.yml'
- '**/*.toml'

env:
UV_VERSION: 0.7.7
UV_VERSION: 0.7.12

jobs:
lint_and_type_check:
Expand All @@ -21,17 +19,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.9"

- name: Install UV
run: |
curl -LsSf "https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh" | sh

- name: Install dependencies
run: uv sync
run: uv sync --all-extras

- name: Run ruff
run: uv run ruff check snappylapy --output-format=github
Expand All @@ -44,24 +37,32 @@ jobs:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: [3.9, 3.13]
include-extras: [true]
include:
- python-version: 3.9
include-extras: false

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install UV
run: |
curl -LsSf "https://astral.sh/uv/${{ env.UV_VERSION }}/install.sh" | sh

- name: Pin python version
run: uv python pin ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync
run: |
if [ "${{ matrix.include-extras }}" = "true" ]; then
uv sync --all-extras
else
uv sync
fi

- name: Run tests
run: uv run pytest tests/ --junit-xml=test-results.xml
Expand Down
9 changes: 1 addition & 8 deletions .github/workflows/mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
- "[0-9]+\\.[0-9]+\\.[0-9]+rc[0-9]+"

env:
UV_VERSION: 0.7.7
UV_VERSION: 0.7.12

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
Expand Down Expand Up @@ -39,13 +39,6 @@ jobs:
with:
submodules: true

# Setup Python 3.9
- name: Setup Python
id: python
uses: actions/setup-python@v5
with:
python-version: '3.9'

# Install dependencies using UV
- name: Install UV
id: uv
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

env:
PACKAGE_NAME: "snappylapy"
UV_VERSION: 0.7.7
UV_VERSION: 0.7.12

jobs:
details:
Expand Down
16 changes: 12 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies = [
"pytest>=7.0",
"jsonpickle>=1.0",
"typer",
"pandas",
"typing-extensions ; python_full_version == '3.9.*'",
]
name = "snappylapy"
version = "0.3.2"
Expand All @@ -25,6 +25,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
Expand All @@ -40,9 +41,11 @@ keywords = [
]

[project.urls]
"Bug Tracker" = "https://github.com/martinmoldrup/snappylapy/issues"
repository = "https://github.com/martinmoldrup/snappylapy"
documentation = "https://martinmoldrup.github.io/snappylapy"
Homepage = "https://martinmoldrup.github.io/snappylapy"
Documentation = "https://martinmoldrup.github.io/snappylapy"
Repository = "https://github.com/martinmoldrup/snappylapy"
Issues = "https://github.com/martinmoldrup/snappylapy/issues"
Changelog = "https://github.com/martinmoldrup/snappylapy/blob/master/CHANGELOG.md"


[tool.setuptools]
Expand All @@ -58,6 +61,11 @@ snappylapy = "snappylapy._plugin"
[project.scripts]
snappylapy = "snappylapy._cli:app"

[project.optional-dependencies]
pandas = [
"pandas",
]

[tool.setuptools.package-data]
snappylapy = ["py.typed"]

Expand Down
6 changes: 2 additions & 4 deletions snappylapy/expectation_classes/expect_bytes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Snapshot testing and expectations for bytes."""

from __future__ import annotations

from .base_snapshot import BaseSnapshot
Expand All @@ -10,10 +11,7 @@ class BytesExpect(BaseSnapshot[bytes]):

serializer_class = BytesSerializer

def __call__(self,
data_to_snapshot: bytes,
name: str | None = None,
filetype: str = "bytes.txt") -> BytesExpect:
def __call__(self, data_to_snapshot: bytes, name: str | None = None, filetype: str = "bytes.txt") -> BytesExpect:
"""Prepare bytes for snapshot testing."""
self._prepare_test(data_to_snapshot, name, filetype)
return self
45 changes: 40 additions & 5 deletions snappylapy/expectation_classes/expect_dataframe.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
"""Snapshot testing and expectations for dicts."""
"""Snapshot testing and expectations for dataframes."""

from __future__ import annotations

import pandas as pd
import sys
from .base_snapshot import BaseSnapshot
from functools import wraps
from snappylapy.serialization import JsonPickleSerializer
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast

if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias

class DataframeExpect(BaseSnapshot[pd.DataFrame]):
if TYPE_CHECKING:
import pandas as pd

F = TypeVar("F", bound=Callable[..., Any])


def require_pandas(func: F) -> F:
"""Decorate to require pandas for the function."""

@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
try:
import pandas as pd # noqa: F401, PLC0415
except ImportError as exc:
error_message = "pandas is required for this function."
raise ImportError(error_message) from exc
return func(*args, **kwargs)

return cast("F", wrapper)


class DataframeExpect(BaseSnapshot["pd.DataFrame"]):
"""Snapshot testing for dataframes."""

serializer_class = JsonPickleSerializer[pd.DataFrame]
serializer_class = JsonPickleSerializer["pd.DataFrame"]
DataFrame: TypeAlias = "pd.DataFrame"

@require_pandas
def __call__(
self, data_to_snapshot: pd.DataFrame, name: str | None = None, filetype: str = "dataframe.json",
self,
data_to_snapshot: "pd.DataFrame", # noqa: UP037
name: str | None = None,
filetype: str = "dataframe.json",
) -> DataframeExpect:
"""Prepare a dataframe for snapshot testing."""
self._prepare_test(data_to_snapshot, name, filetype)
return self

@require_pandas
def column_not_to_contain_nulls(
self,
column_name: str,
Expand All @@ -43,6 +76,7 @@ def column_not_to_contain_nulls(
)
return self

@require_pandas
def columns_not_to_contain_nulls(
self,
column_names: list[str] | None = None,
Expand All @@ -65,6 +99,7 @@ def columns_not_to_contain_nulls(
raise ValueError("\n".join(error_texts))
return self

@require_pandas
def columns_to_match_regex(
self,
column_to_regex: dict[str, str],
Expand Down
73 changes: 71 additions & 2 deletions snappylapy/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
The fixtures module provides classes returned by fixtures registred by pytest in snappylapy.
The fixtures module provides classes returned by fixtures registered by pytest in snappylapy.

Snappylapy provides the following fixtures.

Expand All @@ -26,7 +26,20 @@
)
from snappylapy.constants import directory_names
from snappylapy.session import SnapshotSession
from typing import Any
from typing import Any, Protocol, overload


class CallableExpectation(Protocol):
"""Protocol for callable expectations to use internally in this module."""

def __call__(
self,
data_to_snapshot: Any, # noqa: ANN401
name: str | None = None,
filetype: str = "snapshot.txt",
) -> DictExpect | ListExpect | StringExpect | BytesExpect | DataframeExpect:
"""Call the expectation with the given parameters."""
...


class Expect:
Expand Down Expand Up @@ -194,6 +207,62 @@ def read_test_results(self) -> bytes:
"""Read the test results file."""
return (self.settings.test_results_dir / self.settings.filename).read_bytes()

@overload
def __call__(self, data_to_snapshot: dict, name: str | None = None, filetype: str | None = None) -> DictExpect: ...
@overload
def __call__(
self,
data_to_snapshot: list[Any],
name: str | None = None,
filetype: str | None = None,
) -> ListExpect: ...

@overload
def __call__(self, data_to_snapshot: str, name: str | None = None, filetype: str | None = None) -> StringExpect: ...

@overload
def __call__(
self,
data_to_snapshot: bytes,
name: str | None = None,
filetype: str | None = None,
) -> BytesExpect: ...

@overload
def __call__(
self, data_to_snapshot: DataframeExpect.DataFrame, name: str | None = None, filetype: str | None = None,
) -> DataframeExpect: ...

def __call__(
self,
data_to_snapshot: dict | list[Any] | str | bytes | DataframeExpect.DataFrame,
name: str | None = None,
filetype: str | None = None,
) -> DictExpect | ListExpect | StringExpect | BytesExpect | DataframeExpect:
"""Call the fixture with the given parameters."""
kwargs: dict[str, str] = {}
if name is not None:
kwargs["name"] = name
if filetype is not None:
kwargs["filetype"] = filetype

type_map: dict[type, CallableExpectation] = {
dict: self.dict,
list: self.list,
str: self.string,
bytes: self.bytes,
DataframeExpect.DataFrame: self.dataframe,
}

for typ, func in type_map.items():
if isinstance(data_to_snapshot, typ):
return func(data_to_snapshot, **kwargs)

error_message = f"Unsupported type {type(data_to_snapshot)}. Expected one of: dict, list, str, bytes."
raise TypeError(
error_message,
)


class LoadSnapshot:
"""Snapshot loading class."""
Expand Down
Loading