From 7bf14ca72ed9e085f9f5de803e60f017f34ea29f Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Sun, 8 Sep 2024 22:36:47 -0700 Subject: [PATCH] Improve documentation of MockAirtable.passthrough --- pyairtable/testing.py | 63 ++++++++++++++++++++++++++-- tests/test_testing__mock_airtable.py | 21 ++++++++-- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/pyairtable/testing.py b/pyairtable/testing.py index 3f00eed8..41ee6912 100644 --- a/pyairtable/testing.py +++ b/pyairtable/testing.py @@ -7,11 +7,13 @@ import random import string from collections import defaultdict -from contextlib import ExitStack +from contextlib import ExitStack, contextmanager +from functools import partialmethod from typing import ( Any, Dict, Iterable, + Iterator, List, Optional, Sequence, @@ -162,10 +164,12 @@ class MockAirtable: from pyairtable import Api from pyairtable.testing import MockAirtable + table = Api.base("baseId").table("tableName") + with MockAirtable() as m: m.add_record("baseId", "tableName", {"Name": "Alice"}) - records = t.all() - assert len(t.all()) == 1 + records = table.all() + assert len(table.all()) == 1 If you use pytest, you might want to include this as a fixture. @@ -191,10 +195,27 @@ def test_your_function(): Traceback (most recent call last): ... RuntimeError: unhandled call to Api.request - This behavior can be overridden by setting the ``passthrough`` argument to True, + You can allow unhandled requests by setting the ``passthrough`` argument to True, either on the constructor or temporarily on the MockAirtable instance. This is useful when using another library, like `requests-mock `_, to prepare responses for complex cases (like code that retrieves the schema). + + .. code-block:: python + + def test_your_function(requests_mock, mock_airtable, monkeypatch): + base = Api.base("baseId") + + # load and cache our mock schema + requests_mock.get( + base.meta_url("tables"), + json={"tables": [...]} + ) + with mock_airtable.enable_passthrough(): + base.schema() + + # code below will fail if any more unhandled requests are made + ... + """ # The list of APIs that are mocked by this class. @@ -255,6 +276,40 @@ def __exit__(self, *exc_info: Any) -> None: if self._stack: self._stack.__exit__(*exc_info) + @contextmanager + def set_passthrough(self, allowed: bool) -> Iterator[Self]: + """ + Context manager that temporarily changes whether unmocked methods + are allowed to perform real network requests. For convenience, there are + also shortcuts ``enable_passthrough()`` and ``disable_passthrough()``. + + Usage: + + .. code-block:: python + + with MockAirtable() as m: + with m.enable_passthrough(): + schema = base.schema() + hooks = table.webhooks() + + # no more network requests allowed + ... + + Args: + allowed: If ``True``, unmocked methods will be allowed to perform real + network requests within this context manager. If ``False``, + they will not be allowed. + """ + original = self.passthrough + self.passthrough = allowed + try: + yield self + finally: + self.passthrough = original + + enable_passthrough = partialmethod(set_passthrough, True) + disable_passthrough = partialmethod(set_passthrough, False) + @overload def add_records( self, diff --git a/tests/test_testing__mock_airtable.py b/tests/test_testing__mock_airtable.py index 0ee0b132..767a4081 100644 --- a/tests/test_testing__mock_airtable.py +++ b/tests/test_testing__mock_airtable.py @@ -190,7 +190,7 @@ def test_table_batch_upsert(mock_airtable, table): "table.schema()", ], ) -def test_unhandled_methods(mock_airtable, expr, api, base, table): +def test_unhandled_methods(mock_airtable, monkeypatch, expr, api, base, table): """ Test that unhandled methods raise an error. """ @@ -203,5 +203,20 @@ def test_passthrough(mock_airtable, requests_mock, base, monkeypatch): Test that we can temporarily pass through unhandled methods to the requests library. """ requests_mock.get(base.meta_url("tables"), json={"tables": []}) - monkeypatch.setattr(mock_airtable, "passthrough", True) - assert base.schema().tables == [] # no RuntimeError + + with monkeypatch.context() as mctx: + mctx.setattr(mock_airtable, "passthrough", True) + assert base.schema(force=True).tables == [] # no RuntimeError + + with mock_airtable.enable_passthrough(): + assert base.schema(force=True).tables == [] # no RuntimeError + with mock_airtable.disable_passthrough(): + with pytest.raises(RuntimeError): + base.schema(force=True) + + with mock_airtable.set_passthrough(True): + assert base.schema(force=True).tables == [] # no RuntimeError + + with mock_airtable.set_passthrough(False): + with pytest.raises(RuntimeError): + base.schema(force=True)