protestr


Nameprotestr JSON
Version 4.1.0 PyPI version JSON
download
home_pageNone
SummaryPro Test Fixture Provider
upload_time2024-12-02 06:35:40
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords dependency fixture inject spec specification teardown test
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Protestr: Pro Test Fixture Provider

[![PyPI - Version](https://img.shields.io/pypi/v/protestr.svg)](https://pypi.org/project/protestr)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protestr.svg)](https://pypi.org/project/protestr)

-----

Protestr is a simple, powerful [fixture](#specs-and-fixtures) provider for Python tests.
Whether writing unit tests, integration tests, or anything in between, Protestr's
intuitive [API](#documentation) lets you generate versatile fixtures for your test cases
and inject them as dependencies on demand. It's designed to maximize focus on acts and
assertions by simplifying the complexities of fixture management. Its declarative syntax
allows you to:

- **Re-run tests**  
  Provide dynamic test dependencies, inject on demand, and re-run a test for different
  scenarios instead of duplicating it with little change.

- **Ensure teardown**  
  Have your defined cleanup logic run consistently after every test run.

- **Use anywhere**  
  Integrate seamlessly with all popular Python testing frameworks, such as `unittest`,
  `pytest`, and `nose2`, facing zero disruption to your existing testing practices.

The examples in this doc have been carefully crafted to help you master its concepts and
get the most out of it.

> [!NOTE]
> Protestr was tested with Protestr.

## Next Up

- [Quick Examples](#quick-examples)
- [Getting Started](#getting-started)
  - [Installation](#installation)
  - [Specs and Fixtures](#specs-and-fixtures)
  - [Creating Specs](#creating-specs)
  - [Using Specs](#using-specs)
  - [Ensuring Teardown](#ensuring-teardown)
- [Documentation](#documentation)
  - [`protestr`](#protestr)
  - [`protestr.specs`](#protestrspecs)
- [License](#license)

## Quick Examples

This test expects a MongoDB container and some test users, which the test framework
can't provide out of the box.

```python
import unittest
from unittest.mock import patch as mock


class TestWithMongo(unittest.TestCase):
    @mock("examples.lib.os")
    def test_add_to_users_db_should_add_all_users(
        self,   #  ✅  Provided by `unittest`
        os,     #  ✅  Provided by `mock()`
        users,  #  ❌  Unexpected param `users`
        mongo,  #  ❌  Unexpected param `mongo`
    ):
        os.environ.__getitem__.return_value = "localhost"

        add_to_users_db(users)

        added = mongo.client.users_db.users.count_documents({})
        self.assertEqual(added, len(users))
```

With Protestr, you can define a *fixture* to generate and inject these dependencies
elegantly. You can also provide multiple fixtures to *repeat* the test for different
scenarios.

```python
...
from protestr import provide
from examples.specs import User, MongoDB


class TestWithMongo(unittest.TestCase):
    @provide(              #  ▶️  Fixture Ⅰ
        users=[User] * 3,  #  ✨  Generate 3 test users. Spin up a MongoDB container.
        mongo=MongoDB,     #  🔌  After each test, disconnect and remove the container.
    )
    @provide(users=[])     #  ▶️  Fixture Ⅱ: Patch the first fixture.
    @mock("examples.lib.os")
    def test_add_to_users_db_should_add_all_users(self, os, users, mongo):
        os.environ.__getitem__.return_value = "localhost"

        add_to_users_db(users)

        added = mongo.client.users_db.users.count_documents({})
        self.assertEqual(added, len(users))
```

Here, `User` and `MongoDB` are *specs* for generating test data/infrastructure.

> [!NOTE]
> When multiple `provide()` decorators are chained, their order of execution is **top to
> bottom.** The first one must specify all specs in the fixture, whereas others only
> need to provide patches of the *first* fixture.

Protestr uses specs supplied in `provide()` to generate test data/infrastructure. When
specs are specified as *keyword* args in `provide()`, they are also injected into the
target (method/class/spec) through matching parameters, if any. Keyword specs can also
be patched in chained `provide()` calls, as shown above and overridden altogether
(explained in "[Using Specs](#using-specs)"). On the other hand, non-keyword specs are
useful for generating indirect test dependencies, such as containers running in the
background.

```python
class TestWithRedis(unittest.TestCase):
    @provide(
        Redis,                #  ✨  Spin up a Redis container in the background.
        response={str: str},
    )
    @provide(response=None)   #  ✨  Recreate the container in another scenario
    def test_cached_should_cache_fn(self, response):
        costly_computation = MagicMock()

        @cached
        def fn():
            costly_computation()
            return response

        self.assertEqual(response, fn())
        self.assertEqual(response, fn())

        costly_computation.assert_called_once()
```

Protestr offers some great specs in [`protestr.specs`](#protestrspecs) and makes it
*incredibly* easy to define new ones (detailed in "[Creating Specs](#creating-specs)").
Following are the definitions of the specs used above.

```python
from protestr.specs import between


@provide(id=between(1, 99), name=str, password=str)
class User:
    def __init__(self, id, name, password):
        self.id = id
        self.name = name
        self.password = password


class MongoDB:
    def __init__(self):
        self.container = docker.from_env().containers.run(
            "mongo", detach=True, ports={27017: 27017}
        )
        self.client = pymongo.MongoClient("localhost", 27017)

    def __teardown__(self):      #  ♻️  Ensure teardown after each test.
        self.client.close()
        self.container.stop()
        self.container.remove()


class Redis:
    def __init__(self):
        self.container = docker.from_env().containers.run(
            "redis:8.0-M02", detach=True, ports={6379: 6379}
        )

        # wait for the port
        time.sleep(0.1)

    def __teardown__(self):      #  ♻️  Ensure teardown after each test.
        self.container.stop()
        self.container.remove()
```

See also: [examples/](examples/).

## Getting Started

### Installation

Install [`protestr`](https://pypi.org/project/protestr/) from PyPI:

```shell
pip install protestr
```

### Specs and Fixtures

Specs are blueprints for generating test data/infrastructure. A fixture is a combination
of specs provided to a class/function—usually a test method—using `provide()`.

Specs are *resolved* by Protestr to generate usable values and entities. There are three
types of specs:

1. **Python primitives:** `int`, `float`, `complex`, `bool`, or `str`.

1. **Classes and functions that are callable without args.**  
   If a constructor or a function contains required parameters, it can be transformed
   into a spec by auto-providing those parameters using `provide()` (explained in
   "[Creating Specs](#creating-specs)").

1. **Tuples, lists, sets, or dictionaries of specs** in any configuration, such as a
   list of lists of specs.

Specs are resolved in two ways:

1. **By resolving**

   ```pycon
   >>> from protestr import resolve
   >>> from protestr.specs import choice
   >>> bits = [choice(0, 1)] * 8
   >>> resolve(bits)
   [1, 0, 0, 1, 1, 0, 1, 0]
   ```

1. **By calling/resolving a *spec-provided* class/function**
   ```pycon
   >>> @provide(where=choice("home", "work", "vacation"))
   ... def test(where):
   ...     return where
   ...
   >>> test()
   'vacation'
   >>> resolve(test)
   'home'
   ```

The resolution of specs is *recursive*. If a spec produces another spec, Protestr will
resolve that spec, and so on.

```python
@provide(x=int, y=int)
def point(x, y):
    return x, y


def triangle():
    return [point] * 3


print(resolve(triangle))
# [(971, 704), (268, 581), (484, 548)]
```

> [!TIP]
> A spec-provided class/function itself becomes a spec and can be resolved recursively.
>
> ```python
> >>> @provide(n=int)
> ... def f(n):
> ...     def g():
> ...         return n
> ...     return g
> ...
> >>> resolve(f)
> 784
> ``````

Protestr simplifies spec creation so that you can create custom specs effortlessly for
*your* testing requirements.

### Creating Specs

Creating a spec usually takes two steps:

1. **Write a class/function**

   ```python
   class GeoCoordinate:
       def __init__(self, latitude, longitude, altitude):
           self.latitude = latitude
           self.longitude = longitude
           self.altitude = altitude


   # def geo_coordinate(latitude, longitude, altitude):
   #     return latitude, longitude, altitude
   ```
 
1. **Provide specs for required parameters,** *if any*

   ```python
   @provide(
       latitude=between(-90.0, 90.0),
       longitude=between(-180.0, 180.0),
       altitude=float,
   )
   class GeoCoordinate:
       def __init__(self, latitude, longitude, altitude):
           self.latitude = latitude
           self.longitude = longitude
           self.altitude = altitude


   # @provide(
   #     latitude=between(-90.0, 90.0),
   #     longitude=between(-180.0, 180.0),
   #     altitude=float,
   # )
   # def geo_coordinate(latitude, longitude, altitude):
   #     return latitude, longitude, altitude
   ```

Thus, our new spec is ready for use like any other spec.

### Using Specs

Specs can be used in the following ways.

- **Resolve**

  ```pycon
  >>> resolve(GeoCoordinate).altitude
  247.70713408051304
  >>> GeoCoordinate().altitude
  826.6117116092906
  ```

- **Override**

  ```pycon
  >>> coord = GeoCoordinate(altitude=int)  #  Override the `altitude` spec.
  >>> coord.altitude
  299
  ```

- **Provide**

  ```python
  import unittest
  from protestr import provide
  
  
  class TestLocations(unittest.TestCase):
  
      @provide(locs=[GeoCoordinate] * 100)  #  Provide 💯 of them.
      def test_locations(self, locs):
  
          self.assertEqual(100, len(locs))
  
          for loc in locs:
              self.assertTrue(hasattr(loc, "latitude"))
              self.assertTrue(hasattr(loc, "longitude"))
              self.assertTrue(hasattr(loc, "altitude"))
  
  
  if __name__ == "__main__":
      unittest.main()
  ```

Find more sophisticated usages in the [Documentation](#documentation).

### Ensuring Teardown

Good fixture design demands remembering to dispose of resources at the end of tests.
Protestr takes care of it out of the box with the `__teardown__` function. Whenever a
`provide()`-applied function returns or terminates abnormally, it looks for
`__teardown__` in each (resolved) object it provided and invokes it on the object if
found. So, all you need to do is define `__teardown__` once in a class, and it will be
called every time you provide one.

```python
class MongoDB:
    def __init__(self):
        ...

    def __teardown__(self):
        self.client.close()
        self.container.stop()
        self.container.remove()
```

## Documentation

### `protestr`

$\large\textcolor{gray}{@protestr.}\textbf{provide(\*specs, \*\*kwspecs)}$

Transform a class/function to automatically generate, inject, and teardown test
data/infrastructure.

```python
@provide(
    keyword1=spec1,
    keyword2=spec2,
    keyword3=spec3,
    ...
)
@provide(...)
@provide(...)
...
def fn(foo, bar, whatever, keyword1, keyword2, keyword3, ...):
    ...
```

Keywords are optional. When specs are provided as keyword arguments, the generated
objects are injected into the target through matching parameters, if any. They can also
be patched in chained `provide()` calls and overridden altogether.

When multiple `provide()` decorators are chained, they are executed from **top to
bottom.** The first one must specify all specs in the fixture, and others only need to
patch the *first* one. Teardowns are performed consistently after every test (see
"[Ensuring Teardown](#ensuring-teardown)").

```python
class TestFactorial(unittest.TestCase):
    @provide(
        n=0,
        expected=1,
    )
    @provide(
        n=1,
        # expected=1 implicitly provided from the first fixture
    )
    @provide(
        n=5,
        expected=120,
    )
    def test_factorial_should_return_for_valid_numbers(self, n, expected):
        self.assertEqual(expected, factorial(n))

    @provide(
        n=float,
        expected="n must be a whole number",
    )
    @provide(
        n=between(-1000, -1),
        expected="n must be >= 0",
    )
    def test_factorial_should_raise_for_invalid_number(self, n, expected):
        try:
            factorial(n)
        except Exception as e:
            (message,) = e.args

        self.assertEqual(expected, message)
```

##

$\large\textcolor{gray}{protestr.}\textbf{resolve(spec)}$

Resolve a spec.

Specs can be any of the following types:

1. **Python primitives:** `int`, `float`, `complex`, `bool`, or `str`.

1. **Classes and functions that are callable without args.**  
   If a constructor or a function contains required parameters, it can be transformed
   into a spec by auto-providing those parameters using `provide()`.

1. **Tuples, lists, sets, or dictionaries of specs** in any configuration, such as a
   list of lists of specs.

```pycon
>>> resolve(str)
'jKKbbyNgzj'
```
```pycon
>>> resolve([bool] * 3)
[False, False, True]
```
```pycon
>>> resolve({"name": str})
{'name': 'raaqSzSdfCIYxbIhuTGdxi'}
```
```pycon
>>> class Foo:
...     def __init__(self):
...         self.who = "I'm Foo"
...
>>> resolve(Foo).who
"I'm Foo"
```

##

### `protestr.specs`

$\large\textcolor{gray}{protestr.specs.}\textbf{between(x, y)}$

Return a spec representing a number between `x` and `y`.

`x` and `y` must be specs that evaluate to numbers. If both `x` and `y` evaluate to
integers, the resulting number is also an integer.

```pycon
>>> resolve(between(10, -10))
3
```
```pycon
>>> resolve(between(-10, 10.0))
-4.475185425413375
```
```pycon
>>> resolve(between(int, int))
452
```

##

$\large\textcolor{gray}{protestr.specs.}\textbf{choice(*elems)}$

Return a spec representing a member of `elems`.

```pycon
>>> colors = ["red", "green", "blue"]
>>> resolve(choice(colors))
'green'
```
```pycon
>>> resolve(choice(str)) # a char from a generated str
'T'
```
```pycon
>>> resolve(choice(str, str, str)) # an str from three generated str objects
'NOBuybxrf'
```

##

$\large\textcolor{gray}{protestr.specs.}\textbf{choices(*elems, k)}$

Return a spec representing `k` members chosen from `elems` with replacement.

`k` must be a spec that evaluates to some natural number.

```pycon
>>> resolve(choices(["red", "green", "blue"], k=5))
['blue', 'red', 'green', 'blue', 'green']
```
```pycon
>>> resolve(choices("red", "green", "blue", k=5))
('red', 'blue', 'red', 'blue', 'green')
```
```pycon
>>> resolve(choices(ascii_letters, k=10))
'OLDpaXOGGj'
```

##

$\large\textcolor{gray}{protestr.specs.}\textbf{sample(*elems, k)}$

Return a spec representing `k` members chosen from `elems` without replacement.

`k` must be a spec that evaluates to some natural number.

```pycon
>>> colors = ["red", "green", "blue"]
>>> resolve(sample(colors, k=2))
['blue', 'green']
```
```pycon
>>> resolve(sample("red", "green", "blue", k=3))
('red', 'blue', 'green')
```
```pycon
>>> resolve(sample(ascii_letters, k=10))
'tkExshCbTi'
```
```pycon
>>> resolve(sample([int] * 3, k=between(2, 3))) # generate 3, pick 2, for demo only
[497, 246]
```

##

$\large\textcolor{gray}{protestr.specs.}\textbf{recipe(*specs, then)}$

Return a spec representing the result of calling a given function with some given specs
resolved.

`then` must be callable with a collection containing the resolved specs.

```pycon
>>> from string import ascii_letters, digits
>>> resolve(
...     recipe(
...         sample(ascii_letters, k=5),
...         sample(digits, k=5),
...         then="-".join,
...     )
... )
'JzRYQ-51428'
```

## License

Protestr is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html)
license.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "protestr",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "dependency, fixture, inject, spec, specification, teardown, test",
    "author": null,
    "author_email": "Ikram Khan <ikramkhanfahim@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/2e/20/a7d7e7cfa55ab4d1ddc73839820d964f3838b71d7f6a2d79a9cac3a9e18e/protestr-4.1.0.tar.gz",
    "platform": null,
    "description": "# Protestr: Pro Test Fixture Provider\n\n[![PyPI - Version](https://img.shields.io/pypi/v/protestr.svg)](https://pypi.org/project/protestr)\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protestr.svg)](https://pypi.org/project/protestr)\n\n-----\n\nProtestr is a simple, powerful [fixture](#specs-and-fixtures) provider\u00a0for Python tests.\nWhether writing unit tests, integration tests, or anything in between, Protestr's\nintuitive [API](#documentation)\u00a0lets you generate versatile fixtures for your test cases\nand inject them as dependencies on demand. It's designed to maximize focus on acts and\nassertions by simplifying the complexities of fixture management. Its declarative syntax\nallows you to:\n\n- **Re-run tests**  \n  Provide dynamic test dependencies, inject on demand, and re-run a test for different\n  scenarios instead of duplicating it with little change.\n\n- **Ensure teardown**  \n  Have your defined cleanup logic run consistently after every test run.\n\n- **Use anywhere**  \n  Integrate seamlessly with all popular Python testing frameworks, such as\u00a0`unittest`,\n  `pytest`, and\u00a0`nose2`, facing zero disruption to your existing testing practices.\n\nThe examples in this doc have been carefully crafted to help you master its concepts and\nget the most out of it.\n\n> [!NOTE]\n> Protestr was tested with Protestr.\n\n## Next Up\n\n- [Quick Examples](#quick-examples)\n- [Getting Started](#getting-started)\n  - [Installation](#installation)\n  - [Specs and Fixtures](#specs-and-fixtures)\n  - [Creating Specs](#creating-specs)\n  - [Using Specs](#using-specs)\n  - [Ensuring Teardown](#ensuring-teardown)\n- [Documentation](#documentation)\n  - [`protestr`](#protestr)\n  - [`protestr.specs`](#protestrspecs)\n- [License](#license)\n\n## Quick Examples\n\nThis test expects a MongoDB container and some test users, which the test framework\ncan't provide out of the box.\n\n```python\nimport unittest\nfrom unittest.mock import patch as mock\n\n\nclass TestWithMongo(unittest.TestCase):\n    @mock(\"examples.lib.os\")\n    def test_add_to_users_db_should_add_all_users(\n        self,   #  \u2705  Provided by `unittest`\n        os,     #  \u2705  Provided by `mock()`\n        users,  #  \u274c  Unexpected param `users`\n        mongo,  #  \u274c  Unexpected param `mongo`\n    ):\n        os.environ.__getitem__.return_value = \"localhost\"\n\n        add_to_users_db(users)\n\n        added = mongo.client.users_db.users.count_documents({})\n        self.assertEqual(added, len(users))\n```\n\nWith Protestr, you can define a *fixture*\u00a0to generate and inject these dependencies\nelegantly. You can also provide multiple fixtures to *repeat* the test for different\nscenarios.\n\n```python\n...\nfrom protestr import provide\nfrom examples.specs import User, MongoDB\n\n\nclass TestWithMongo(unittest.TestCase):\n    @provide(              #  \u25b6\ufe0f  Fixture \u2160\n        users=[User] * 3,  #  \u2728  Generate 3 test users. Spin up a MongoDB container.\n        mongo=MongoDB,     #  \ud83d\udd0c  After each test, disconnect and remove the container.\n    )\n    @provide(users=[])     #  \u25b6\ufe0f  Fixture \u2161: Patch the first fixture.\n    @mock(\"examples.lib.os\")\n    def test_add_to_users_db_should_add_all_users(self, os, users, mongo):\n        os.environ.__getitem__.return_value = \"localhost\"\n\n        add_to_users_db(users)\n\n        added = mongo.client.users_db.users.count_documents({})\n        self.assertEqual(added, len(users))\n```\n\nHere, `User` and `MongoDB` are *specs* for generating test data/infrastructure.\n\n> [!NOTE]\n> When multiple `provide()` decorators are chained, their order of execution is **top to\n> bottom.** The first one must specify all specs in the fixture, whereas others only\n> need to provide patches of the *first* fixture.\n\nProtestr uses specs supplied in `provide()` to generate test data/infrastructure. When\nspecs are specified as *keyword* args in `provide()`, they are also injected into the\ntarget (method/class/spec) through matching parameters, if any. Keyword specs can also\nbe patched in chained `provide()` calls, as shown above and overridden altogether\n(explained in \"[Using Specs](#using-specs)\"). On the other hand, non-keyword specs are\nuseful for generating indirect test dependencies, such as containers running in the\nbackground.\n\n```python\nclass TestWithRedis(unittest.TestCase):\n    @provide(\n        Redis,                #  \u2728  Spin up a Redis container in the background.\n        response={str: str},\n    )\n    @provide(response=None)   #  \u2728  Recreate the container in another scenario\n    def test_cached_should_cache_fn(self, response):\n        costly_computation = MagicMock()\n\n        @cached\n        def fn():\n            costly_computation()\n            return response\n\n        self.assertEqual(response, fn())\n        self.assertEqual(response, fn())\n\n        costly_computation.assert_called_once()\n```\n\nProtestr offers some great specs in [`protestr.specs`](#protestrspecs) and makes it\n*incredibly* easy to define new ones (detailed in \"[Creating Specs](#creating-specs)\").\nFollowing are the definitions of the specs used above.\n\n```python\nfrom protestr.specs import between\n\n\n@provide(id=between(1, 99), name=str, password=str)\nclass User:\n    def __init__(self, id, name, password):\n        self.id = id\n        self.name = name\n        self.password = password\n\n\nclass MongoDB:\n    def __init__(self):\n        self.container = docker.from_env().containers.run(\n            \"mongo\", detach=True, ports={27017: 27017}\n        )\n        self.client = pymongo.MongoClient(\"localhost\", 27017)\n\n    def __teardown__(self):      #  \u267b\ufe0f  Ensure teardown after each test.\n        self.client.close()\n        self.container.stop()\n        self.container.remove()\n\n\nclass Redis:\n    def __init__(self):\n        self.container = docker.from_env().containers.run(\n            \"redis:8.0-M02\", detach=True, ports={6379: 6379}\n        )\n\n        # wait for the port\n        time.sleep(0.1)\n\n    def __teardown__(self):      #  \u267b\ufe0f  Ensure teardown after each test.\n        self.container.stop()\n        self.container.remove()\n```\n\nSee also: [examples/](examples/).\n\n## Getting Started\n\n### Installation\n\nInstall [`protestr`](https://pypi.org/project/protestr/) from PyPI:\n\n```shell\npip install protestr\n```\n\n### Specs and Fixtures\n\nSpecs are blueprints for generating test data/infrastructure. A fixture is a combination\nof specs provided to a class/function\u2014usually a test method\u2014using `provide()`.\n\nSpecs are *resolved* by Protestr to generate usable values and entities. There are three\ntypes of specs:\n\n1. **Python primitives:** `int`, `float`, `complex`, `bool`, or `str`.\n\n1. **Classes and functions that are callable without args.**  \n   If a constructor or a function contains required parameters, it can be transformed\n   into a spec by auto-providing those parameters using `provide()` (explained in\n   \"[Creating Specs](#creating-specs)\").\n\n1. **Tuples, lists, sets, or dictionaries of specs** in any configuration, such as a\n   list of lists of specs.\n\nSpecs are resolved in two ways:\n\n1. **By resolving**\n\n   ```pycon\n   >>> from protestr import resolve\n   >>> from protestr.specs import choice\n   >>> bits = [choice(0, 1)] * 8\n   >>> resolve(bits)\n   [1, 0, 0, 1, 1, 0, 1, 0]\n   ```\n\n1. **By calling/resolving a *spec-provided* class/function**\n   ```pycon\n   >>> @provide(where=choice(\"home\", \"work\", \"vacation\"))\n   ... def test(where):\n   ...     return where\n   ...\n   >>> test()\n   'vacation'\n   >>> resolve(test)\n   'home'\n   ```\n\nThe resolution of specs is *recursive*. If a spec produces another spec, Protestr will\nresolve that spec, and so on.\n\n```python\n@provide(x=int, y=int)\ndef point(x, y):\n    return x, y\n\n\ndef triangle():\n    return [point] * 3\n\n\nprint(resolve(triangle))\n# [(971, 704), (268, 581), (484, 548)]\n```\n\n> [!TIP]\n> A spec-provided class/function itself becomes a spec and can be resolved recursively.\n>\n> ```python\n> >>> @provide(n=int)\n> ... def f(n):\n> ...     def g():\n> ...         return n\n> ...     return g\n> ...\n> >>> resolve(f)\n> 784\n> ``````\n\nProtestr simplifies spec creation so that you can create custom specs effortlessly for\n*your* testing requirements.\n\n### Creating Specs\n\nCreating a spec usually takes two steps:\n\n1. **Write a class/function**\n\n   ```python\n   class GeoCoordinate:\n       def __init__(self, latitude, longitude, altitude):\n           self.latitude = latitude\n           self.longitude = longitude\n           self.altitude = altitude\n\n\n   # def geo_coordinate(latitude, longitude, altitude):\n   #     return latitude, longitude, altitude\n   ```\n \n1. **Provide specs for required parameters,** *if any*\n\n   ```python\n   @provide(\n       latitude=between(-90.0, 90.0),\n       longitude=between(-180.0, 180.0),\n       altitude=float,\n   )\n   class GeoCoordinate:\n       def __init__(self, latitude, longitude, altitude):\n           self.latitude = latitude\n           self.longitude = longitude\n           self.altitude = altitude\n\n\n   # @provide(\n   #     latitude=between(-90.0, 90.0),\n   #     longitude=between(-180.0, 180.0),\n   #     altitude=float,\n   # )\n   # def geo_coordinate(latitude, longitude, altitude):\n   #     return latitude, longitude, altitude\n   ```\n\nThus, our new spec is ready for use like any other spec.\n\n### Using Specs\n\nSpecs can be used in the following ways.\n\n- **Resolve**\n\n  ```pycon\n  >>> resolve(GeoCoordinate).altitude\n  247.70713408051304\n  >>> GeoCoordinate().altitude\n  826.6117116092906\n  ```\n\n- **Override**\n\n  ```pycon\n  >>> coord = GeoCoordinate(altitude=int)  #  Override the `altitude` spec.\n  >>> coord.altitude\n  299\n  ```\n\n- **Provide**\n\n  ```python\n  import unittest\n  from protestr import provide\n  \n  \n  class TestLocations(unittest.TestCase):\n  \n      @provide(locs=[GeoCoordinate] * 100)  #  Provide \ud83d\udcaf of them.\n      def test_locations(self, locs):\n  \n          self.assertEqual(100, len(locs))\n  \n          for loc in locs:\n              self.assertTrue(hasattr(loc, \"latitude\"))\n              self.assertTrue(hasattr(loc, \"longitude\"))\n              self.assertTrue(hasattr(loc, \"altitude\"))\n  \n  \n  if __name__ == \"__main__\":\n      unittest.main()\n  ```\n\nFind more sophisticated usages in the [Documentation](#documentation).\n\n### Ensuring Teardown\n\nGood fixture design demands remembering to dispose of resources at the end of tests.\nProtestr takes care of it out of the box with the `__teardown__` function. Whenever a\n`provide()`-applied function returns or terminates abnormally, it looks for\n`__teardown__` in each (resolved) object it provided and invokes it on the object if\nfound. So, all you need to do is define `__teardown__` once in a class, and it will be\ncalled every time you provide one.\n\n```python\nclass MongoDB:\n    def __init__(self):\n        ...\n\n    def __teardown__(self):\n        self.client.close()\n        self.container.stop()\n        self.container.remove()\n```\n\n## Documentation\n\n### `protestr`\n\n$\\large\\textcolor{gray}{@protestr.}\\textbf{provide(\\*specs, \\*\\*kwspecs)}$\n\nTransform a class/function to automatically generate, inject, and teardown test\ndata/infrastructure.\n\n```python\n@provide(\n    keyword1=spec1,\n    keyword2=spec2,\n    keyword3=spec3,\n    ...\n)\n@provide(...)\n@provide(...)\n...\ndef fn(foo, bar, whatever, keyword1, keyword2, keyword3, ...):\n    ...\n```\n\nKeywords are optional. When specs are provided as keyword arguments, the generated\nobjects are injected into the target through matching parameters, if any. They can also\nbe patched in chained `provide()` calls and overridden altogether.\n\nWhen multiple `provide()` decorators are chained, they are executed from **top to\nbottom.** The first one must specify all specs in the fixture, and others only need to\npatch the *first* one. Teardowns are performed consistently after every test (see\n\"[Ensuring Teardown](#ensuring-teardown)\").\n\n```python\nclass TestFactorial(unittest.TestCase):\n    @provide(\n        n=0,\n        expected=1,\n    )\n    @provide(\n        n=1,\n        # expected=1 implicitly provided from the first fixture\n    )\n    @provide(\n        n=5,\n        expected=120,\n    )\n    def test_factorial_should_return_for_valid_numbers(self, n, expected):\n        self.assertEqual(expected, factorial(n))\n\n    @provide(\n        n=float,\n        expected=\"n must be a whole number\",\n    )\n    @provide(\n        n=between(-1000, -1),\n        expected=\"n must be >= 0\",\n    )\n    def test_factorial_should_raise_for_invalid_number(self, n, expected):\n        try:\n            factorial(n)\n        except Exception as e:\n            (message,) = e.args\n\n        self.assertEqual(expected, message)\n```\n\n##\n\n$\\large\\textcolor{gray}{protestr.}\\textbf{resolve(spec)}$\n\nResolve a spec.\n\nSpecs can be any of the following types:\n\n1. **Python primitives:** `int`, `float`, `complex`, `bool`, or `str`.\n\n1. **Classes and functions that are callable without args.**  \n   If a constructor or a function contains required parameters, it can be transformed\n   into a spec by auto-providing those parameters using `provide()`.\n\n1. **Tuples, lists, sets, or dictionaries of specs** in any configuration, such as a\n   list of lists of specs.\n\n```pycon\n>>> resolve(str)\n'jKKbbyNgzj'\n```\n```pycon\n>>> resolve([bool] * 3)\n[False, False, True]\n```\n```pycon\n>>> resolve({\"name\": str})\n{'name': 'raaqSzSdfCIYxbIhuTGdxi'}\n```\n```pycon\n>>> class Foo:\n...     def __init__(self):\n...         self.who = \"I'm Foo\"\n...\n>>> resolve(Foo).who\n\"I'm Foo\"\n```\n\n##\n\n### `protestr.specs`\n\n$\\large\\textcolor{gray}{protestr.specs.}\\textbf{between(x, y)}$\n\nReturn a spec representing a number between `x` and `y`.\n\n`x` and `y` must be specs that evaluate to numbers. If both `x` and `y` evaluate to\nintegers, the resulting number is also an integer.\n\n```pycon\n>>> resolve(between(10, -10))\n3\n```\n```pycon\n>>> resolve(between(-10, 10.0))\n-4.475185425413375\n```\n```pycon\n>>> resolve(between(int, int))\n452\n```\n\n##\n\n$\\large\\textcolor{gray}{protestr.specs.}\\textbf{choice(*elems)}$\n\nReturn a spec representing a member of `elems`.\n\n```pycon\n>>> colors = [\"red\", \"green\", \"blue\"]\n>>> resolve(choice(colors))\n'green'\n```\n```pycon\n>>> resolve(choice(str)) # a char from a generated str\n'T'\n```\n```pycon\n>>> resolve(choice(str, str, str)) # an str from three generated str objects\n'NOBuybxrf'\n```\n\n##\n\n$\\large\\textcolor{gray}{protestr.specs.}\\textbf{choices(*elems, k)}$\n\nReturn a spec representing `k` members chosen from `elems` with replacement.\n\n`k` must be a spec that evaluates to some natural number.\n\n```pycon\n>>> resolve(choices([\"red\", \"green\", \"blue\"], k=5))\n['blue', 'red', 'green', 'blue', 'green']\n```\n```pycon\n>>> resolve(choices(\"red\", \"green\", \"blue\", k=5))\n('red', 'blue', 'red', 'blue', 'green')\n```\n```pycon\n>>> resolve(choices(ascii_letters, k=10))\n'OLDpaXOGGj'\n```\n\n##\n\n$\\large\\textcolor{gray}{protestr.specs.}\\textbf{sample(*elems, k)}$\n\nReturn a spec representing `k` members chosen from `elems` without replacement.\n\n`k` must be a spec that evaluates to some natural number.\n\n```pycon\n>>> colors = [\"red\", \"green\", \"blue\"]\n>>> resolve(sample(colors, k=2))\n['blue', 'green']\n```\n```pycon\n>>> resolve(sample(\"red\", \"green\", \"blue\", k=3))\n('red', 'blue', 'green')\n```\n```pycon\n>>> resolve(sample(ascii_letters, k=10))\n'tkExshCbTi'\n```\n```pycon\n>>> resolve(sample([int] * 3, k=between(2, 3))) # generate 3, pick 2, for demo only\n[497, 246]\n```\n\n##\n\n$\\large\\textcolor{gray}{protestr.specs.}\\textbf{recipe(*specs, then)}$\n\nReturn a spec representing the result of calling a given function with some given specs\nresolved.\n\n`then` must be callable with a collection containing the resolved specs.\n\n```pycon\n>>> from string import ascii_letters, digits\n>>> resolve(\n...     recipe(\n...         sample(ascii_letters, k=5),\n...         sample(digits, k=5),\n...         then=\"-\".join,\n...     )\n... )\n'JzRYQ-51428'\n```\n\n## License\n\nProtestr is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html)\nlicense.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Pro Test Fixture Provider",
    "version": "4.1.0",
    "project_urls": {
        "Documentation": "https://github.com/Grimmscorpp/protestr?tab=readme-ov-file#documentation",
        "Homepage": "https://github.com/Grimmscorpp/protestr",
        "Issues": "https://github.com/Grimmscorpp/protestr/issues",
        "Source": "https://github.com/Grimmscorpp/protestr"
    },
    "split_keywords": [
        "dependency",
        " fixture",
        " inject",
        " spec",
        " specification",
        " teardown",
        " test"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5fb2fb49df3da0666ed42beb49cee47b7f411b5b3b7e54d9d039b96da6e261e7",
                "md5": "3b8486a7fb802a8520390082a1a6deb4",
                "sha256": "ba97276974348e56a93b29c08594b387e8cd78ae11617bdbd8818c79d0189299"
            },
            "downloads": -1,
            "filename": "protestr-4.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "3b8486a7fb802a8520390082a1a6deb4",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 9551,
            "upload_time": "2024-12-02T06:35:38",
            "upload_time_iso_8601": "2024-12-02T06:35:38.641459Z",
            "url": "https://files.pythonhosted.org/packages/5f/b2/fb49df3da0666ed42beb49cee47b7f411b5b3b7e54d9d039b96da6e261e7/protestr-4.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "2e20a7d7e7cfa55ab4d1ddc73839820d964f3838b71d7f6a2d79a9cac3a9e18e",
                "md5": "e1b143fb7fbd074d75ffa5ee7214f32f",
                "sha256": "93ad28caf459bb9fb0e77c9410af246d69dc201c05170e984ccca1a777bb7d73"
            },
            "downloads": -1,
            "filename": "protestr-4.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "e1b143fb7fbd074d75ffa5ee7214f32f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 13914,
            "upload_time": "2024-12-02T06:35:40",
            "upload_time_iso_8601": "2024-12-02T06:35:40.086751Z",
            "url": "https://files.pythonhosted.org/packages/2e/20/a7d7e7cfa55ab4d1ddc73839820d964f3838b71d7f6a2d79a9cac3a9e18e/protestr-4.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-12-02 06:35:40",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Grimmscorpp",
    "github_project": "protestr?tab=readme-ov-file#documentation",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "requirements": [],
    "lcname": "protestr"
}
        
Elapsed time: 0.40106s