Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: mcous/decoy
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.0.0
Choose a base ref
...
head repository: mcous/decoy
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Loading
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -10,3 +10,4 @@ insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
quote_type = double
29 changes: 16 additions & 13 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -4,10 +4,10 @@ description: "Install development dependencies"
inputs:
python-version:
description: "Python version to install"
default: "3.11"
default: "3.12"
poetry-version:
description: "Poetry version to install"
default: "1.3.2"
default: "1.7.0"
cache:
description: "Cache directory"
default: "${{ runner.temp }}/cache"
@@ -20,11 +20,18 @@ runs:
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
update-environment: false

- name: "Set up Python 3.12 for Poetry"
id: setup-poetry-python
uses: actions/setup-python@v4
with:
python-version: 3.12

- name: "Set up dependency cache"
uses: actions/cache@v3
with:
key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ inputs.poetry-version }}-${{ hashFiles('poetry.lock') }}
key: ${{ runner.os }}-${{ steps.setup-poetry-python.outputs.python-version }}-${{ steps.setup-python.outputs.python-version }}-${{ inputs.poetry-version }}-${{ hashFiles('poetry.lock') }}
path: ${{ inputs.cache }}

- name: "Set up PATH on POSIX"
@@ -37,21 +44,17 @@ runs:
shell: bash
run: echo "${{ inputs.cache }}/tools/Scripts" >> $GITHUB_PATH

- name: "Check poetry installation"
id: check-poetry
shell: bash
continue-on-error: true
run: poetry --version

- name: "Install poetry"
- name: "Install Poetry"
shell: bash
if: ${{ steps.check-poetry.outcome == 'failure'}}
run: |
"${{ steps.setup-python.outputs.python-path }}" -m venv "${{ inputs.cache }}/tools"
pip install poetry==${{ inputs.poetry-version }}
if ! poetry --version; then
"${{ steps.setup-poetry-python.outputs.python-path }}" -m venv "${{ inputs.cache }}/tools"
pip install poetry==${{ inputs.poetry-version }}
fi
- name: "Install development dependencies"
shell: bash
run: |
poetry config cache-dir "${{ inputs.cache }}/poetry"
poetry env use "${{ steps.setup-python.outputs.python-path }}"
poetry install --sync
6 changes: 4 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -11,5 +11,7 @@ updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "sunday"
interval: "monthly"
versioning-strategy: "increase"
ignore:
- dependency-name: "python"
15 changes: 10 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: "Continuous integration"

on: [push, pull_request]
on:
pull_request:
branches: [main]
push:
branches: [main]
tags: [v*]

jobs:
test:
@@ -9,10 +14,10 @@ jobs:
strategy:
matrix:
os: [Ubuntu, Windows, macOS]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- name: "Check out repository"
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: "Set up Python and development dependencies"
uses: ./.github/actions/setup
@@ -30,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Check out repository"
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: "Set up Python and development dependencies"
uses: ./.github/actions/setup
@@ -44,7 +49,7 @@ jobs:
needs: [test, check]
steps:
- name: "Check out repository"
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: "Set up Python and development dependencies"
uses: ./.github/actions/setup
4 changes: 2 additions & 2 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ representative at an online or offline event.
## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to [Mike Cousins][].
reported to [Michael Cousins][].
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
@@ -129,4 +129,4 @@ For answers to common questions about this code of conduct, see the FAQ at
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
[mike cousins]: mailto:mike@cousins.io?subject=Decoy%20Code%20of%20Conduct
[michael cousins]: mailto:michael@cousins.io?subject=Decoy%20Code%20of%20Conduct
12 changes: 2 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -48,18 +48,11 @@ If you find yourself in a situation where Decoy's test suite has blown up, **con

### Checks

Decoy's source code is typechecked with [mypy][] and linted with [ruff][].
Decoy's source code is typechecked with [mypy][] and linted/formatted with [ruff][].

```bash
poetry run poe check
poetry run poe lint
```

### Formatting

Decoy's source code is formatted using [black][].

```bash
poetry run poe format
```

@@ -101,7 +94,6 @@ git push --follow-tags
[pytest]: https://docs.pytest.org/
[pytest-xdist]: https://github.com/pytest-dev/pytest-xdist
[mypy]: https://mypy.readthedocs.io
[ruff]: https://github.com/charliermarsh/ruff
[black]: https://black.readthedocs.io
[ruff]: https://github.com/astral-sh/ruff
[mkdocs]: https://www.mkdocs.org/
[semantic versioning]: https://semver.org/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020-2023, Mike Cousins
Copyright (c) 2020-2023, Michael Cousins

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div align="center">
<img alt="Decoy logo" src="https://mike.cousins.io/decoy/img/decoy.png" width="256px">
<img alt="Decoy logo" src="https://michael.cousins.io/decoy/img/decoy.png" width="256px">
<h1 class="decoy-title">Decoy</h1>
<p>Opinionated mocking library for Python</p>
<p>
@@ -11,7 +11,7 @@
<a title="Supported Python Versions" href="https://pypi.org/project/decoy/"><img src="https://img.shields.io/pypi/pyversions/decoy?style=flat-square"></a>
</p>
<p>
<a href="https://mike.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
<a href="https://michael.cousins.io/decoy/" class="decoy-hidden">Usage guide and documentation</a>
</p>
</div>

@@ -59,28 +59,33 @@ plugins = decoy.mypy

Decoy works well with [pytest][], but if you use another testing library or framework, you can still use Decoy! You just need to do two things:

1. Create a new instance of [`Decoy()`](https://mike.cousins.io/decoy/api/#decoy.Decoy) before each test
2. Call [`decoy.reset()`](https://mike.cousins.io/decoy/api/#decoy.Decoy.reset) after each test
1. Create a new instance of [`Decoy()`](https://michael.cousins.io/decoy/api/#decoy.Decoy) before each test
2. Call [`decoy.reset()`](https://michael.cousins.io/decoy/api/#decoy.Decoy.reset) after each test

For example, using the built-in [unittest][] framework, you would use the `setUp` fixture method to do `self.decoy = Decoy()` and the `tearDown` method to call `self.decoy.reset()`. For a working example, see [`tests/test_unittest.py`](https://github.com/mcous/decoy/blob/main/tests/test_unittest.py).

## Basic Usage

This basic example assumes you are using [pytest][]. For more detailed documentation, see Decoy's [usage guide][] and [API reference][].

### Define your test

Decoy will add a `decoy` fixture that provides its mock creation API.
Decoy will add a `decoy` fixture to pytest that provides its mock creation API.

```python
from decoy import Decoy
from todo import TodoAPI, TodoItem
from todo.store TodoStore

def test_add_todo(decoy: Decoy) -> None:
def test_something(decoy: Decoy) -> None:
...
```

!!! note

Importing the `Decoy` interface for type annotations is recommended, but optional. If your project does not use type annotations, you can simply write:

```python
def test_something(decoy):
...
```

### Create a mock

Use `decoy.mock` to create a mock based on some specification. From there, inject the mock into your test subject.
@@ -139,8 +144,8 @@ See [spying with verify][] for more details.
[unittest]: https://docs.python.org/3/library/unittest.html
[typing]: https://docs.python.org/3/library/typing.html
[mypy]: https://mypy.readthedocs.io/
[api reference]: https://mike.cousins.io/decoy/api/
[usage guide]: https://mike.cousins.io/decoy/usage/create/
[creating mocks]: https://mike.cousins.io/decoy/usage/create/
[stubbing with when]: https://mike.cousins.io/decoy/usage/when/
[spying with verify]: https://mike.cousins.io/decoy/usage/verify/
[api reference]: https://michael.cousins.io/decoy/api/
[usage guide]: https://michael.cousins.io/decoy/usage/create/
[creating mocks]: https://michael.cousins.io/decoy/usage/create/
[stubbing with when]: https://michael.cousins.io/decoy/usage/when/
[spying with verify]: https://michael.cousins.io/decoy/usage/verify/
18 changes: 10 additions & 8 deletions decoy/__init__.py
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ class Decoy:
You should create a new Decoy instance before each test and call
[`reset`][decoy.Decoy.reset] after each test. If you use the
[`decoy` pytest fixture][decoy.pytest_plugin.decoy], this is done
automatically. See the [setup guide](../#setup) for more details.
automatically. See the [setup guide][] for more details.
!!! example
```python
@@ -32,6 +32,8 @@ class Decoy:
decoy.reset()
```
[setup guide]: index.md#setup
"""

def __init__(self) -> None:
@@ -59,7 +61,7 @@ def mock(
) -> Any:
"""Create a mock. See the [mock creation guide] for more details.
[mock creation guide]: ../usage/create/
[mock creation guide]: usage/create.md
Arguments:
cls: A class definition that the mock should imitate.
@@ -94,7 +96,7 @@ def when(
) -> "Stub[ReturnT]":
"""Create a [`Stub`][decoy.Stub] configuration using a rehearsal call.
See [stubbing usage guide](../usage/when/) for more details.
See [stubbing usage guide](usage/when.md) for more details.
Arguments:
_rehearsal_result: The return value of a rehearsal, used for typechecking.
@@ -133,7 +135,7 @@ def verify(
) -> None:
"""Verify a mock was called using one or more rehearsals.
See [verification usage guide](../usage/verify/) for more details.
See [verification usage guide](usage/verify.md) for more details.
Arguments:
_rehearsal_results: The return value of rehearsals, unused except
@@ -174,7 +176,7 @@ def test_create_something(decoy: Decoy):
def prop(self, _rehearsal_result: ReturnT) -> "Prop[ReturnT]":
"""Create property setter and deleter rehearsals.
See [property mocking guide](../advanced/properties/) for more details.
See [property mocking guide](advanced/properties.md) for more details.
Arguments:
_rehearsal_result: The property to mock, for typechecking.
@@ -202,7 +204,7 @@ def reset(self) -> None:
class Stub(Generic[ReturnT]):
"""A rehearsed Stub that can be used to configure mock behaviors.
See [stubbing usage guide](../usage/when/) for more details.
See [stubbing usage guide](usage/when.md) for more details.
"""

def __init__(self, core: StubCore) -> None:
@@ -291,7 +293,7 @@ def then_enter_with(
The wrapping context manager is compatible with both the synchronous and
asynchronous context manager interfaces.
See the [context manager usage guide](../advanced/context-managers/)
See the [context manager usage guide](advanced/context-managers.md)
for more details.
Arguments:
@@ -303,7 +305,7 @@ def then_enter_with(
class Prop(Generic[ReturnT]):
"""Rehearsal creator for mocking property setters and deleters.
See [property mocking guide](../advanced/properties/) for more details.
See [property mocking guide](advanced/properties.md) for more details.
"""

def __init__(self, core: PropCore) -> None:
4 changes: 2 additions & 2 deletions decoy/core.py
Original file line number Diff line number Diff line change
@@ -33,11 +33,11 @@ def __init__(
self._warning_checker = warning_checker or WarningChecker()
self._stub_store = stub_store or StubStore()
self._spy_log = spy_log or SpyLog()
self._call_hander = call_handler or CallHandler(
self._call_handler = call_handler or CallHandler(
spy_log=self._spy_log,
stub_store=self._stub_store,
)
self._spy_creator = spy_creator or SpyCreator(call_handler=self._call_hander)
self._spy_creator = spy_creator or SpyCreator(call_handler=self._call_handler)

def mock(
self,
12 changes: 6 additions & 6 deletions decoy/errors.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
See the [errors guide][] for more details.
[errors guide]: ../usage/errors-and-warnings/#errors
[errors guide]: usage/errors-and-warnings.md#errors
"""
from typing import Optional, Sequence

@@ -11,11 +11,11 @@


class MockNameRequiredError(ValueError):
"""An error reaised if a name is not provided for a mock.
"""An error raised if a name is not provided for a mock.
See the [MockNameRequiredError guide][] for more details.
[MockNameRequiredError guide]: ../usage/errors-and-warnings/#mocknamerequirederror
[MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
"""

def __init__(self) -> None:
@@ -32,7 +32,7 @@ class MissingRehearsalError(ValueError):
See the [MissingRehearsalError guide][] for more details.
[MissingRehearsalError guide]: ../usage/errors-and-warnings/#missingrehearsalerror
[MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
"""

def __init__(self) -> None:
@@ -46,7 +46,7 @@ class MockNotAsyncError(TypeError):
to a synchronous stub's `then_do` method.
See the [MockNotAsyncError guide][] for more details.
[MockNotAsyncError guide]: ../usage/errors-and-warnings/#mocknotasyncerror
[MockNotAsyncError guide]: usage/errors-and-warnings.md#mocknotasyncerror
"""


@@ -55,7 +55,7 @@ class VerifyError(AssertionError):
See [spying with verify][] for more details.
[spying with verify]: ../usage/verify/
[spying with verify]: usage/verify.md
Attributes:
rehearsals: Rehearsals that were being verified.
Loading