Metadata-Version: 2.4
Name: varsnap
Version: 1.6.0
Summary: Python client for Varsnap, a snapshot testing service that records and compares function inputs and outputs
Author-email: Varsnap <admin@varsnap.com>
License-Expression: MIT
Project-URL: Homepage, https://www.varsnap.com/
Keywords: testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Topic :: Software Development
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: dill<0.5.0,>=0.4.0
Requires-Dist: requests<3.0,>=2.0
Provides-Extra: test
Requires-Dist: coverage==7.15.0; extra == "test"
Requires-Dist: ruff==0.15.20; extra == "test"
Requires-Dist: mypy==2.1.0; extra == "test"
Requires-Dist: types-requests==2.33.0.20260518; extra == "test"

Varsnap Python
==============

[![Build Status](https://drone.albertyw.com/api/badges/albertyw/varsnap-python/status.svg)](https://drone.albertyw.com/albertyw/varsnap-python)
[![Maintainability](https://qlty.sh/badges/cdd092ce-fea4-49fb-a2a5-bef489c21681/maintainability.svg)](https://qlty.sh/gh/albertyw/projects/varsnap-python)
[![Code Coverage](https://qlty.sh/badges/cdd092ce-fea4-49fb-a2a5-bef489c21681/coverage.svg)](https://qlty.sh/gh/albertyw/projects/varsnap-python)

[Python Varsnap Client](https://www.varsnap.com/)

Installation
------------

Install from PyPI - `pip install varsnap`

Requirements
------------

The client depends on four environment variables to be set:

 - `VARSNAP` - Should be either `true` or `false`.  Varsnap will be disabled if the variable is anything other than `true`.
 - `ENV` - If set to `development`, the client will receive events from production.  If set to `production`, the client will emit events.
 - `VARSNAP_PRODUCER_TOKEN` - Only clients with this token may emit production snapshots.  Copied from https://www.varsnap.com/user/
 - `VARSNAP_CONSUMER_TOKEN` - Only clients with this token may consume production snapshots in development.  Copied from https://www.varsnap.com/user/

Usage
-----

Add the varsnap decorator in front of any function you'd like to make better:

```python
from varsnap import varsnap


@varsnap
def example(args, **kwargs):
    return 'output'
```

Custom serialization
--------------------

Varsnap serializes a function's inputs and outputs as JSON when possible and
falls back to pickle otherwise.  To control how a value of a particular type is
serialized, give that type a pair of `varsnap_serialize` / `varsnap_deserialize`
classmethods.  Varsnap reads the function's type annotations and uses these
classmethods for any annotated parameter or return value:

```python
from varsnap import varsnap


class Money:
    def __init__(self, cents):
        self.cents = cents

    @classmethod
    def varsnap_serialize(cls, value):
        return str(value.cents)

    @classmethod
    def varsnap_deserialize(cls, data):
        return cls(int(data))


@varsnap
def add_tax(price: Money) -> Money:
    return Money(round(price.cents * 1.1))
```

If a function isn't annotated (or you want to override its annotations), pass
the types explicitly to the decorator:

```python
@varsnap(types={'price': Money}, returns=Money)
def add_tax(price):
    return Money(round(price.cents * 1.1))
```

The type is always taken from the decorated function, never from the serialized
data, so stored snapshots can't redirect deserialization to a different type.
Values whose type doesn't provide these classmethods continue to use the
default JSON/pickle serialization.

Security of deserialization
---------------------------

Snapshots are fetched from the varsnap server and deserialized on your machine,
so the wire format is treated as untrusted input.  Varsnap serializes values in
one of three formats:

 - `json:` — plain JSON.  Always safe to deserialize.
 - `type:` — produced by a type's `varsnap_serialize`.  Safe: the class is taken
   from the decorated function's annotations, never from the payload.
 - `pickle:` — a `dill`/`pickle` fallback for values JSON can't represent.
   **Unpickling executes arbitrary code embedded in the payload**, so anyone who
   can influence stored snapshot data could run code on the machine that reads
   it.  Varsnap emits a warning whenever it falls back to this path.

Prefer the `type:` path for any value that would otherwise be pickled: give its
type `varsnap_serialize` / `varsnap_deserialize` classmethods, which is both
safe and version-stable.

Testing
-------

With the proper environment variables set, in a test file, add:

```python
import unittest
from varsnap import test

class TestIntegration(unittest.TestCase):
    def test_varsnap(self):
        matches, logs = test()
        if matches is None:
            raise unittest.case.SkipTest('No Snaps found')
        self.assertTrue(matches, logs)
```

If you're testing a Flask application, set up a
[test request context](https://flask.palletsprojects.com/en/stable/api/#flask.Flask.test_request_context) when testing:

```python
# app = Flask()
with app.test_request_context():
    matches, logs = test()
```

Troubleshooting
---------------

**Decorators changing function names**

Using decorators may change the name of functions.  In order to not confuse
varsnap, set the decorated function's `__qualname__` and `__signature__` to
match the original function:

```python
import inspect


def decorator(func):
    def decorated(*args, **kwargs):
        return func(*args, **kwargs)
    decorated.__qualname__ = func.__qualname__
    decorated.__signature__ = inspect.signature(func)
    return decorated
```

Publishing
----------

```bash
pip install build twine
python -m build
twine upload dist/*
```
