coveo-ref


Namecoveo-ref JSON
Version 1.0.4 PyPI version JSON
download
home_pagehttps://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref
SummaryAllows using unittest.patch() without hardcoding strings.
upload_time2024-03-24 12:46:31
maintainerNone
docs_urlNone
authorJonathan Piché
requires_python>=3.8
licenseApache-2.0
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            Make mocking simple, free of hardcoded trings and therefore... refactorable!

<!-- TOC -->
* [Tutorial](#tutorial)
* [Common Mock Recipes](#common-mock-recipes)
  * [Mock something globally without context](#mock-something-globally-without-context)
    * [Option 1: by leveraging the import mechanism](#option-1-by-leveraging-the-import-mechanism)
    * [Option 2: By wrapping a hidden function](#option-2-by-wrapping-a-hidden-function)
  * [Mock something for a given context](#mock-something-for-a-given-context)
    * [Brief Example:](#brief-example)
    * [Detailed Example:](#detailed-example)
  * [Mock something for the current context](#mock-something-for-the-current-context)
  * [Mock a method on a class](#mock-a-method-on-a-class)
  * [Mock a method on one instance of a class](#mock-a-method-on-one-instance-of-a-class)
  * [Mock an attribute on a class/instance/module/function/object/etc](#mock-an-attribute-on-a-classinstancemodulefunctionobjectetc)
  * [Mock a property](#mock-a-property)
  * [Mock a classmethod or staticmethod on a specific instance](#mock-a-classmethod-or-staticmethod-on-a-specific-instance)
<!-- TOC -->


# Tutorial

Consider this common piece of code:

```python
from unittest.mock import patch, MagicMock

@patch("mymodule.clients.APIClient._do_request")
def test(api_client_mock: MagicMock) -> None:
    ...
```

Because the mock target is a string, it makes it difficult to move things around without breaking the tests. You need a
tool that can extract the string representation of a python objet. This is what `ref` was built for:

```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import APIClient

@patch(*ref(APIClient._do_request))
def test(api_client_mock: MagicMock) -> None:
    ...
```

🚀 This way, you can rename or move `mymodule`, `clients`, `APIClient` or even `_do_request`, and your IDE should find
these and adjust them just like any other reference in your project.

Let's examine a more complex example:

```python
from unittest.mock import patch, MagicMock
from mymodule.tasks import process

@patch("mymodule.tasks.get_api_client")
def test(get_api_client_mock: MagicMock) -> None:
    assert process() is None  # pretend this tests the process function
```

The interesting thing in this example is that we're mocking `get_api_client` in the `tasks` module. 
Let's take a look at the `tasks` module:

```python
from typing import Optional
from mymodule.clients import get_api_client

def process() -> Optional[bool]:
    client = get_api_client()
    return ...
```

As we can see, `get_api_client` is defined in another module.
The test needs to patch the function _in the tasks module_ since that's the context it will be called from. 
Unfortunately, inspecting `get_api_client` from the `tasks` module at runtime leads us back to `mymodule.clients`.

This single complexity means that hardcoding the context `mymodule.tasks` and symbol `get_api_client` into a string
for the patch is the straightforward solution.

But with `ref`, you specify the context separately:

```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import get_api_client
from mymodule.tasks import process


@patch(*ref(get_api_client, context=process))
def test(get_api_client_mock: MagicMock) -> None:
    assert process() is None  # pretend this tests the process function
```

🚀 By giving a context to `ref`, the symbol `get_api_client` will be resolved from the context of `process`, which is the
`mymodule.tasks` module. The result is `mymodule.tasks.get_api_client`.

If either objects (`get_api_client` or `process`) are moved or renamed using a refactoring tool, the mock will still
point to the correct name and context.

🚀 And a nice bonus is that your IDE can jump to `get_api_client`'s definition from the test file now!

It should be noted that this isn't just some string manipulation. `ref` will import and inspect modules and objects
to make sure that they're correct. Here's a more complex case with a renamed symbol:

The module:

```python
from typing import Optional
from mymodule.clients import get_api_client as client_factory  # it got renamed! 😱

def process() -> Optional[bool]:
    client = client_factory()
    return ...
```

The test:

```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref
from mymodule.clients import get_api_client
from mymodule.tasks import process


@patch(*ref(get_api_client, context=process))
def test(get_api_client_mock: MagicMock) -> None:
    assert process() is None  # pretend this tests the process function
```

Notice how the test and patch did not change despite the renamed symbol?

🚀 This is because `ref` will find `get_api_client` as `client_factory` when inspecting `mymodule.tasks` module,
and return `mymodule.tasks.client_factory`.

We can also use ref with `patch.object()` in order to patch a single instance. Consider the following code:

```python
from unittest.mock import patch
from mymodule.clients import APIClient

def test() -> None:
    client = APIClient()
    with patch.object(client, "_do_request"):
        ...
```

🚀 By specifying `obj=True` to `ref`, you will obtain a `Tuple[instance, attribute_to_patch_as_a_string]` that you
can unpack to `patch.object()`:

```python
from unittest.mock import patch
from coveo_ref import ref
from mymodule.clients import APIClient

def test() -> None:
    client = APIClient()
    with patch.object(*ref(client._do_request, obj=True)):
        ...
```

Please refer to the docstring of `ref` for argument usage information.

# Common Mock Recipes
## Mock something globally without context
### Option 1: by leveraging the import mechanism

To mock something globally without regards for the context, it has to be accessed through a dot `.` by the context.

For instance, consider this test:

```python
from http.client import HTTPResponse
from unittest.mock import patch, MagicMock
from coveo_ref import ref

from mymodule.tasks import process


@patch(*ref(HTTPResponse.close))
def test(http_response_close_mock: MagicMock) -> None:
    assert process()
```

The target is `HTTPResponse.close`, which lives in the `http.client` module.
The context of the test is the `process` function, which lives in the `mymodule.tasks` module.
Let's take a look at `mymodule.tasks`'s source code:


```python
from http import client

def process() -> bool:
    _ = client.HTTPResponse(...)  # of course this is fake, but serves the example
    return ...
```

Since `mymodule.tasks` reaches `HTTPResponse` through a dot (i.e.: `client.HTTPResponse`), we can patch `HTTPResponse`
without using `mymodule.tasks` as the context.

However, if `mymodule.tasks` was written like this:

```python
from http.client import HTTPResponse

def process() -> bool:
    _ = HTTPResponse(...)
    return ...
```

Then the patch would not affect the object used by the `process` function anymore. However, it would affect any other 
module that uses the dot to reach `HTTPResponse` since the patch was _still_ applied globally.
 

### Option 2: By wrapping a hidden function

Another approach to mocking things globally is to hide a function behind another, and mock the hidden function.
This allows modules to use whatever import style they want, and the mocks become straightforward to setup.

Pretend this is `mymodule.clients`:

```python
class APIClient:
    ...

def get_api_client() -> APIClient:
    return _get_api_client()

def _get_api_client() -> APIClient:
    return APIClient()
```

And this is `mymodule.tasks`:

```python
from mymodule.clients import get_api_client

def process() -> bool:
    return get_api_client() is not None
```

So you _know_ this works globally, because no one will (should?) import the private one except the test:

```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref

from mymodule.tasks import process
from mymodule.clients import _get_api_client


@patch(*ref(_get_api_client))
def test(api_client_mock: MagicMock) -> None:
    assert process()
```


## Mock something for a given context

If you don't use a global mock, then you _must_ specify the context of the mock.

The context is a reference point for `ref`.
Most of the time, the class or function you're testing should be the context.
Generally speaking, pick a context as close to your implementation as possible to allow seamless refactoring.

### Brief Example:
  
```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref

from ... import thing_to_mock
from ... import thing_to_test

@patch(*ref(thing_to_mock, context=thing_to_test))
def test(mocked_thing: MagicMock) -> None:
    assert thing_to_test()
    mocked_thing.assert_called()
```

### Detailed Example:

`mymodule.tasks`:

```python
from mymodule.clients import get_api_client

def process() -> bool:
    client = get_api_client()
    return ...
```

The test, showing 3 different methods that work:

```python
from unittest.mock import patch, MagicMock
from coveo_ref import ref

from mymodule.clients import get_api_client
from mymodule.tasks import process

# you can pass the module as the context
import mymodule

@patch(*ref(get_api_client, context=mymodule.tasks))
def test(get_api_client_mock: MagicMock) -> None:
    assert process()

# you can pass the module as the context, version 2
from mymodule import tasks
    
@patch(*ref(get_api_client, context=tasks))
def test(get_api_client_mock: MagicMock) -> None:
    assert process()

# you can also pass a function or a class defined in the `tasks` module
from mymodule.tasks import process
@patch(*ref(get_api_client, context=process))
def test(get_api_client_mock: MagicMock) -> None:
    assert process()
```

The 3rd method is encouraged: provide the function or class that is actually using the `get_api_client` import.
In our example, that's the `process` function.
If `process` was ever moved to a different module, it would carry the `get_api_client` import, and the mock would
be automatically adjusted to target `process`'s new module without changes. 🚀

## Mock something for the current context

Sometimes, the test file _is_ the context. When that happens, just pass `__name__` as the context:

```python
from unittest.mock import patch
from coveo_ref import ref
from mymodule.clients import get_api_client, APIClient

def _prepare_test() -> APIClient:
    client = get_api_client()
    ...
    return client
    
@patch(*ref(get_api_client, context=__name__))
def test() -> None:
    client = _prepare_test()
    ...
```


## Mock a method on a class

Since a method cannot be imported and can only be accessed through the use of a dot `.` on a class or instance, 
you can always patch methods globally:

```python
with patch(*ref(MyClass.fn)): ...
```

This is because no module can import `fn`; it has to go through an import of `MyClass`.

## Mock a method on one instance of a class

Simply add `obj=True` and use `patch.object()`:

```python
with patch.object(*ref(instance.fn, obj=True)): ...
```


## Mock an attribute on a class/instance/module/function/object/etc

`ref` cannot help with this task:
- You cannot refer an attribute that exists (you would pass the value, not a reference)
- You cannot refer an attribute that doesn't exist (because it doesn't exist!)

For this, there's no going around hardcoding the attribute name in a string:

```python
class MyClass:
    def __init__(self) -> None:
        self.a = 1


def test_attr() -> None:
    instance = MyClass()
    with patch.object(instance, "a", new=2):
        assert instance.a == 2
        assert MyClass().a == 1
```

This sometimes work when patching **instances**. 
The example works because `a` is a simple attribute that lives in `instance.__dict__` and `patch.object` knows
about that.

But if you tried to patch `MyClass` instead of `instance`, `mock.patch` would complain that there's no 
such thing as `a` over there.
Thus, patching an attribute globally will most likely result in a lot of wasted time, and should be avoided.

There's no way to make the example work with `ref` because there's no way to refer `instance.a` without actually
getting the value of `a`, unless we hardcode a string, which defeats the purpose of `ref` completely.


## Mock a property

You can only patch a property globally, through its class:

```python
class MyClass:
    @property
    def get(self) -> bool:
        return False
```

```python
from unittest.mock import PropertyMock, patch, MagicMock
from coveo_ref import ref

from mymodule import MyClass

@patch(*ref(MyClass.get), new_callable=PropertyMock, return_value=True)
def test(my_class_get_mock: MagicMock) -> None:
    assert MyClass().get == True
    my_class_get_mock.assert_called_once()
```

You **cannot** patch a property on an instance, this is a limitation of `unittest.mock` because of the way
properties work.
If you try, `mock.patch.object()` will complain that the property is read only.


## Mock a classmethod or staticmethod on a specific instance

When inspecting these special methods on an instance, `ref` ends up finding the class instead of the instance.

Therefore, `ref` is unable to return a `Tuple[instance, function_name]`.
It would return `Tuple[class, function_name]`, resulting in a global patch. 😱

But `ref` will detect this mistake, and will raise a helpful exception if it cannot return an instance when you
specified `obj=True`.

For this particular scenario, the workaround is to provide the instance as the context:

```python
from unittest.mock import patch
from coveo_ref import ref


class MyClass:
    @staticmethod
    def get() -> bool:
        return False

    
def test() -> None:
    instance = MyClass()
    with patch.object(*ref(instance.get, context=instance, obj=True)) as fn_mock:
        assert instance.get == True
        assert MyClass().get == False  # new instances are not affected by the object mock
        fn_mock.assert_called_once()
```

Some may prefer a more semantically-correct version by specifying the target through the class instead of the 
instance. In the end, these are all equivalent:

```python
with patch.object(instance, "get"): 
    ...

with patch.object(*ref(instance.get, context=instance, obj=True)): 
    ...

with patch.object(*ref(MockClass.get, context=instance, obj=True)): 
    ...
```

In this case, the version without ref is much shorter and arguably more pleasant for the eye, but `get` can no longer
be renamed without altering the tests.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref",
    "name": "coveo-ref",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": "Jonathan Pich\u00e9",
    "author_email": "tools@coveo.com",
    "download_url": "https://files.pythonhosted.org/packages/49/e5/df697d2fed1a7a20275ffbf153437e8697b85c44bcea456532c63ecf4dea/coveo_ref-1.0.4.tar.gz",
    "platform": null,
    "description": "Make mocking simple, free of hardcoded trings and therefore... refactorable!\n\n<!-- TOC -->\n* [Tutorial](#tutorial)\n* [Common Mock Recipes](#common-mock-recipes)\n  * [Mock something globally without context](#mock-something-globally-without-context)\n    * [Option 1: by leveraging the import mechanism](#option-1-by-leveraging-the-import-mechanism)\n    * [Option 2: By wrapping a hidden function](#option-2-by-wrapping-a-hidden-function)\n  * [Mock something for a given context](#mock-something-for-a-given-context)\n    * [Brief Example:](#brief-example)\n    * [Detailed Example:](#detailed-example)\n  * [Mock something for the current context](#mock-something-for-the-current-context)\n  * [Mock a method on a class](#mock-a-method-on-a-class)\n  * [Mock a method on one instance of a class](#mock-a-method-on-one-instance-of-a-class)\n  * [Mock an attribute on a class/instance/module/function/object/etc](#mock-an-attribute-on-a-classinstancemodulefunctionobjectetc)\n  * [Mock a property](#mock-a-property)\n  * [Mock a classmethod or staticmethod on a specific instance](#mock-a-classmethod-or-staticmethod-on-a-specific-instance)\n<!-- TOC -->\n\n\n# Tutorial\n\nConsider this common piece of code:\n\n```python\nfrom unittest.mock import patch, MagicMock\n\n@patch(\"mymodule.clients.APIClient._do_request\")\ndef test(api_client_mock: MagicMock) -> None:\n    ...\n```\n\nBecause the mock target is a string, it makes it difficult to move things around without breaking the tests. You need a\ntool that can extract the string representation of a python objet. This is what `ref` was built for:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\nfrom mymodule.clients import APIClient\n\n@patch(*ref(APIClient._do_request))\ndef test(api_client_mock: MagicMock) -> None:\n    ...\n```\n\n\ud83d\ude80 This way, you can rename or move `mymodule`, `clients`, `APIClient` or even `_do_request`, and your IDE should find\nthese and adjust them just like any other reference in your project.\n\nLet's examine a more complex example:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom mymodule.tasks import process\n\n@patch(\"mymodule.tasks.get_api_client\")\ndef test(get_api_client_mock: MagicMock) -> None:\n    assert process() is None  # pretend this tests the process function\n```\n\nThe interesting thing in this example is that we're mocking `get_api_client` in the `tasks` module. \nLet's take a look at the `tasks` module:\n\n```python\nfrom typing import Optional\nfrom mymodule.clients import get_api_client\n\ndef process() -> Optional[bool]:\n    client = get_api_client()\n    return ...\n```\n\nAs we can see, `get_api_client` is defined in another module.\nThe test needs to patch the function _in the tasks module_ since that's the context it will be called from. \nUnfortunately, inspecting `get_api_client` from the `tasks` module at runtime leads us back to `mymodule.clients`.\n\nThis single complexity means that hardcoding the context `mymodule.tasks` and symbol `get_api_client` into a string\nfor the patch is the straightforward solution.\n\nBut with `ref`, you specify the context separately:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\nfrom mymodule.clients import get_api_client\nfrom mymodule.tasks import process\n\n\n@patch(*ref(get_api_client, context=process))\ndef test(get_api_client_mock: MagicMock) -> None:\n    assert process() is None  # pretend this tests the process function\n```\n\n\ud83d\ude80 By giving a context to `ref`, the symbol `get_api_client` will be resolved from the context of `process`, which is the\n`mymodule.tasks` module. The result is `mymodule.tasks.get_api_client`.\n\nIf either objects (`get_api_client` or `process`) are moved or renamed using a refactoring tool, the mock will still\npoint to the correct name and context.\n\n\ud83d\ude80 And a nice bonus is that your IDE can jump to `get_api_client`'s definition from the test file now!\n\nIt should be noted that this isn't just some string manipulation. `ref` will import and inspect modules and objects\nto make sure that they're correct. Here's a more complex case with a renamed symbol:\n\nThe module:\n\n```python\nfrom typing import Optional\nfrom mymodule.clients import get_api_client as client_factory  # it got renamed! \ud83d\ude31\n\ndef process() -> Optional[bool]:\n    client = client_factory()\n    return ...\n```\n\nThe test:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\nfrom mymodule.clients import get_api_client\nfrom mymodule.tasks import process\n\n\n@patch(*ref(get_api_client, context=process))\ndef test(get_api_client_mock: MagicMock) -> None:\n    assert process() is None  # pretend this tests the process function\n```\n\nNotice how the test and patch did not change despite the renamed symbol?\n\n\ud83d\ude80 This is because `ref` will find `get_api_client` as `client_factory` when inspecting `mymodule.tasks` module,\nand return `mymodule.tasks.client_factory`.\n\nWe can also use ref with `patch.object()` in order to patch a single instance. Consider the following code:\n\n```python\nfrom unittest.mock import patch\nfrom mymodule.clients import APIClient\n\ndef test() -> None:\n    client = APIClient()\n    with patch.object(client, \"_do_request\"):\n        ...\n```\n\n\ud83d\ude80 By specifying `obj=True` to `ref`, you will obtain a `Tuple[instance, attribute_to_patch_as_a_string]` that you\ncan unpack to `patch.object()`:\n\n```python\nfrom unittest.mock import patch\nfrom coveo_ref import ref\nfrom mymodule.clients import APIClient\n\ndef test() -> None:\n    client = APIClient()\n    with patch.object(*ref(client._do_request, obj=True)):\n        ...\n```\n\nPlease refer to the docstring of `ref` for argument usage information.\n\n# Common Mock Recipes\n## Mock something globally without context\n### Option 1: by leveraging the import mechanism\n\nTo mock something globally without regards for the context, it has to be accessed through a dot `.` by the context.\n\nFor instance, consider this test:\n\n```python\nfrom http.client import HTTPResponse\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule.tasks import process\n\n\n@patch(*ref(HTTPResponse.close))\ndef test(http_response_close_mock: MagicMock) -> None:\n    assert process()\n```\n\nThe target is `HTTPResponse.close`, which lives in the `http.client` module.\nThe context of the test is the `process` function, which lives in the `mymodule.tasks` module.\nLet's take a look at `mymodule.tasks`'s source code:\n\n\n```python\nfrom http import client\n\ndef process() -> bool:\n    _ = client.HTTPResponse(...)  # of course this is fake, but serves the example\n    return ...\n```\n\nSince `mymodule.tasks` reaches `HTTPResponse` through a dot (i.e.: `client.HTTPResponse`), we can patch `HTTPResponse`\nwithout using `mymodule.tasks` as the context.\n\nHowever, if `mymodule.tasks` was written like this:\n\n```python\nfrom http.client import HTTPResponse\n\ndef process() -> bool:\n    _ = HTTPResponse(...)\n    return ...\n```\n\nThen the patch would not affect the object used by the `process` function anymore. However, it would affect any other \nmodule that uses the dot to reach `HTTPResponse` since the patch was _still_ applied globally.\n \n\n### Option 2: By wrapping a hidden function\n\nAnother approach to mocking things globally is to hide a function behind another, and mock the hidden function.\nThis allows modules to use whatever import style they want, and the mocks become straightforward to setup.\n\nPretend this is `mymodule.clients`:\n\n```python\nclass APIClient:\n    ...\n\ndef get_api_client() -> APIClient:\n    return _get_api_client()\n\ndef _get_api_client() -> APIClient:\n    return APIClient()\n```\n\nAnd this is `mymodule.tasks`:\n\n```python\nfrom mymodule.clients import get_api_client\n\ndef process() -> bool:\n    return get_api_client() is not None\n```\n\nSo you _know_ this works globally, because no one will (should?) import the private one except the test:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule.tasks import process\nfrom mymodule.clients import _get_api_client\n\n\n@patch(*ref(_get_api_client))\ndef test(api_client_mock: MagicMock) -> None:\n    assert process()\n```\n\n\n## Mock something for a given context\n\nIf you don't use a global mock, then you _must_ specify the context of the mock.\n\nThe context is a reference point for `ref`.\nMost of the time, the class or function you're testing should be the context.\nGenerally speaking, pick a context as close to your implementation as possible to allow seamless refactoring.\n\n### Brief Example:\n  \n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom ... import thing_to_mock\nfrom ... import thing_to_test\n\n@patch(*ref(thing_to_mock, context=thing_to_test))\ndef test(mocked_thing: MagicMock) -> None:\n    assert thing_to_test()\n    mocked_thing.assert_called()\n```\n\n### Detailed Example:\n\n`mymodule.tasks`:\n\n```python\nfrom mymodule.clients import get_api_client\n\ndef process() -> bool:\n    client = get_api_client()\n    return ...\n```\n\nThe test, showing 3 different methods that work:\n\n```python\nfrom unittest.mock import patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule.clients import get_api_client\nfrom mymodule.tasks import process\n\n# you can pass the module as the context\nimport mymodule\n\n@patch(*ref(get_api_client, context=mymodule.tasks))\ndef test(get_api_client_mock: MagicMock) -> None:\n    assert process()\n\n# you can pass the module as the context, version 2\nfrom mymodule import tasks\n    \n@patch(*ref(get_api_client, context=tasks))\ndef test(get_api_client_mock: MagicMock) -> None:\n    assert process()\n\n# you can also pass a function or a class defined in the `tasks` module\nfrom mymodule.tasks import process\n@patch(*ref(get_api_client, context=process))\ndef test(get_api_client_mock: MagicMock) -> None:\n    assert process()\n```\n\nThe 3rd method is encouraged: provide the function or class that is actually using the `get_api_client` import.\nIn our example, that's the `process` function.\nIf `process` was ever moved to a different module, it would carry the `get_api_client` import, and the mock would\nbe automatically adjusted to target `process`'s new module without changes. \ud83d\ude80\n\n## Mock something for the current context\n\nSometimes, the test file _is_ the context. When that happens, just pass `__name__` as the context:\n\n```python\nfrom unittest.mock import patch\nfrom coveo_ref import ref\nfrom mymodule.clients import get_api_client, APIClient\n\ndef _prepare_test() -> APIClient:\n    client = get_api_client()\n    ...\n    return client\n    \n@patch(*ref(get_api_client, context=__name__))\ndef test() -> None:\n    client = _prepare_test()\n    ...\n```\n\n\n## Mock a method on a class\n\nSince a method cannot be imported and can only be accessed through the use of a dot `.` on a class or instance, \nyou can always patch methods globally:\n\n```python\nwith patch(*ref(MyClass.fn)): ...\n```\n\nThis is because no module can import `fn`; it has to go through an import of `MyClass`.\n\n## Mock a method on one instance of a class\n\nSimply add `obj=True` and use `patch.object()`:\n\n```python\nwith patch.object(*ref(instance.fn, obj=True)): ...\n```\n\n\n## Mock an attribute on a class/instance/module/function/object/etc\n\n`ref` cannot help with this task:\n- You cannot refer an attribute that exists (you would pass the value, not a reference)\n- You cannot refer an attribute that doesn't exist (because it doesn't exist!)\n\nFor this, there's no going around hardcoding the attribute name in a string:\n\n```python\nclass MyClass:\n    def __init__(self) -> None:\n        self.a = 1\n\n\ndef test_attr() -> None:\n    instance = MyClass()\n    with patch.object(instance, \"a\", new=2):\n        assert instance.a == 2\n        assert MyClass().a == 1\n```\n\nThis sometimes work when patching **instances**. \nThe example works because `a` is a simple attribute that lives in `instance.__dict__` and `patch.object` knows\nabout that.\n\nBut if you tried to patch `MyClass` instead of `instance`, `mock.patch` would complain that there's no \nsuch thing as `a` over there.\nThus, patching an attribute globally will most likely result in a lot of wasted time, and should be avoided.\n\nThere's no way to make the example work with `ref` because there's no way to refer `instance.a` without actually\ngetting the value of `a`, unless we hardcode a string, which defeats the purpose of `ref` completely.\n\n\n## Mock a property\n\nYou can only patch a property globally, through its class:\n\n```python\nclass MyClass:\n    @property\n    def get(self) -> bool:\n        return False\n```\n\n```python\nfrom unittest.mock import PropertyMock, patch, MagicMock\nfrom coveo_ref import ref\n\nfrom mymodule import MyClass\n\n@patch(*ref(MyClass.get), new_callable=PropertyMock, return_value=True)\ndef test(my_class_get_mock: MagicMock) -> None:\n    assert MyClass().get == True\n    my_class_get_mock.assert_called_once()\n```\n\nYou **cannot** patch a property on an instance, this is a limitation of `unittest.mock` because of the way\nproperties work.\nIf you try, `mock.patch.object()` will complain that the property is read only.\n\n\n## Mock a classmethod or staticmethod on a specific instance\n\nWhen inspecting these special methods on an instance, `ref` ends up finding the class instead of the instance.\n\nTherefore, `ref` is unable to return a `Tuple[instance, function_name]`.\nIt would return `Tuple[class, function_name]`, resulting in a global patch. \ud83d\ude31\n\nBut `ref` will detect this mistake, and will raise a helpful exception if it cannot return an instance when you\nspecified `obj=True`.\n\nFor this particular scenario, the workaround is to provide the instance as the context:\n\n```python\nfrom unittest.mock import patch\nfrom coveo_ref import ref\n\n\nclass MyClass:\n    @staticmethod\n    def get() -> bool:\n        return False\n\n    \ndef test() -> None:\n    instance = MyClass()\n    with patch.object(*ref(instance.get, context=instance, obj=True)) as fn_mock:\n        assert instance.get == True\n        assert MyClass().get == False  # new instances are not affected by the object mock\n        fn_mock.assert_called_once()\n```\n\nSome may prefer a more semantically-correct version by specifying the target through the class instead of the \ninstance. In the end, these are all equivalent:\n\n```python\nwith patch.object(instance, \"get\"): \n    ...\n\nwith patch.object(*ref(instance.get, context=instance, obj=True)): \n    ...\n\nwith patch.object(*ref(MockClass.get, context=instance, obj=True)): \n    ...\n```\n\nIn this case, the version without ref is much shorter and arguably more pleasant for the eye, but `get` can no longer\nbe renamed without altering the tests.\n",
    "bugtrack_url": null,
    "license": "Apache-2.0",
    "summary": "Allows using unittest.patch() without hardcoding strings.",
    "version": "1.0.4",
    "project_urls": {
        "Homepage": "https://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref",
        "Repository": "https://github.com/coveooss/coveo-python-oss/tree/main/coveo-ref"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e315d0d2abc361f90bcb7c50131f15a2f6f05423b90e70e1853974d7f580930c",
                "md5": "1ef746b2ba442d553042f2076c7d0be1",
                "sha256": "b5d5dd7e47ebfcf702c79cf2647e9b32695057f1745d49dae32aa96f1adf5115"
            },
            "downloads": -1,
            "filename": "coveo_ref-1.0.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "1ef746b2ba442d553042f2076c7d0be1",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 10216,
            "upload_time": "2024-03-24T12:46:29",
            "upload_time_iso_8601": "2024-03-24T12:46:29.808347Z",
            "url": "https://files.pythonhosted.org/packages/e3/15/d0d2abc361f90bcb7c50131f15a2f6f05423b90e70e1853974d7f580930c/coveo_ref-1.0.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "49e5df697d2fed1a7a20275ffbf153437e8697b85c44bcea456532c63ecf4dea",
                "md5": "06b99b20efbee9689079ff9d46bc2e7f",
                "sha256": "b822bfb0c309b0f6b9e3b54e6303b90a01f50312a860ddf64322569cf6274bef"
            },
            "downloads": -1,
            "filename": "coveo_ref-1.0.4.tar.gz",
            "has_sig": false,
            "md5_digest": "06b99b20efbee9689079ff9d46bc2e7f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 12776,
            "upload_time": "2024-03-24T12:46:31",
            "upload_time_iso_8601": "2024-03-24T12:46:31.473433Z",
            "url": "https://files.pythonhosted.org/packages/49/e5/df697d2fed1a7a20275ffbf153437e8697b85c44bcea456532c63ecf4dea/coveo_ref-1.0.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-24 12:46:31",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "coveooss",
    "github_project": "coveo-python-oss",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "coveo-ref"
}
        
Elapsed time: 0.26043s