Repository: getsentry/responses
Pull request #745 (2024-12-26 15:00:26)

Recorder without decorator
I would like to use the recorded in the repl and record my requests.

This adds the code, tests and documentation for this.


Patch:
diff --git a/README.rst b/README.rst
index da473bf..001a45c 100644
--- a/README.rst
+++ b/README.rst
@@ -130,6 +130,22 @@ will produce next output:
         status: 202
         url: https://httpstat.us/202
 
+If you are in the REPL, you can also activete the recorder for all following responses:
+
+.. code-block:: python
+
+    import requests
+    from responses import _recorder
+
+    _recorder.recorder.start()
+
+    requests.get("https://httpstat.us/500")
+
+    _recorder.recorder.dump_to_file("out.yaml")
+
+    # you can stop or reset the recorder
+    _recorder.recorder.stop()
+    _recorder.recorder.reset()
 
 Replay responses (populate registry) from files
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..f676d01
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+[build-system]
+# Suggest a reasonably modern floor for setuptools to ensure
+# the source dist package is assembled with all the expected resources.
+requires = ["setuptools >= 60", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/responses/_recorder.py b/responses/_recorder.py
index 64533e2..151e47a 100644
--- a/responses/_recorder.py
+++ b/responses/_recorder.py
@@ -9,6 +9,7 @@ if TYPE_CHECKING:  # pragma: no cover
     from typing import Callable
     from typing import Dict
     from typing import List
+    from typing import Optional
     from typing import Type
     from typing import Union
     from responses import FirstMatchRegistry
@@ -122,10 +123,13 @@ class Recorder(RequestsMock):
 
     def dump_to_file(
         self,
-        *,
         file_path: "Union[str, bytes, os.PathLike[Any]]",
-        registered: "List[BaseResponse]",
+        *,
+        registered: "Optional[List[BaseResponse]]" = None,
     ) -> None:
+        """Dump the recorded responses to a file."""
+        if registered is None:
+            registered = self.get_registry().registered
         with open(file_path, "w") as file:
             _dump(registered, file, yaml.dump)
 
diff --git a/setup.py b/setup.py
index 26d6b39..916c75b 100644
--- a/setup.py
+++ b/setup.py
@@ -9,12 +9,7 @@ A utility library for mocking out the `requests` Python library.
 :license: Apache 2.0
 """
 
-import sys
-
 from setuptools import setup
-from setuptools.command.test import test as TestCommand
-
-setup_requires = []
 
 install_requires = [
     "requests>=2.30.0,<3.0",
@@ -37,31 +32,11 @@ tests_require = [
     "tomli-w",
 ]
 
-if "test" in sys.argv:
-    setup_requires.extend(tests_require)
-
 extras_require = {"tests": tests_require}
 
-
-class PyTest(TestCommand):
-    """Designed to be run via `python setup.py test`"""
-
-    def finalize_options(self):
-        TestCommand.finalize_options(self)
-        self.test_args = []
-        self.test_suite = True
-
-    def run_tests(self):
-        # import here, cause outside the eggs aren't loaded
-        import pytest
-
-        errno = pytest.main(self.test_args)
-        sys.exit(errno)
-
-
 setup(
     name="responses",
-    version="0.25.3",
+    version="0.25.4",
     author="David Cramer",
     description="A utility library for mocking out the `requests` Python library.",
     url="https://github.com/getsentry/responses",
@@ -72,17 +47,13 @@ setup(
         "Source Code": "https://github.com/getsentry/responses",
     },
     license="Apache 2.0",
-    long_description=open("README.rst").read(),
+    long_description=open("README.rst", encoding="utf-8").read(),
     long_description_content_type="text/x-rst",
     packages=["responses"],
     zip_safe=False,
     python_requires=">=3.8",
     install_requires=install_requires,
     extras_require=extras_require,
-    tests_require=tests_require,
-    setup_requires=setup_requires,
-    cmdclass={"test": PyTest},
-    include_package_data=True,
     classifiers=[
         "Intended Audience :: Developers",
         "Intended Audience :: System Administrators",
diff --git a/tox.ini b/tox.ini
index d7dd486..f0a8d96 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ filterwarnings =
 [testenv]
 extras = tests
 commands =
-    pytest . --asyncio-mode=auto --cov responses --cov-report term-missing
+    pytest . --asyncio-mode=auto --cov responses --cov-report term-missing {posargs}
 
 
 [testenv:mypy]


Test patch:
diff --git a/responses/tests/test_recorder.py b/responses/tests/test_recorder.py
index 9adbca7..abe03bb 100644
--- a/responses/tests/test_recorder.py
+++ b/responses/tests/test_recorder.py
@@ -152,6 +152,35 @@ class TestRecord:
         url400 = httpserver.url_for("/status/wrong")
         return url202, url400, url404, url500
 
+    def test_use_recorder_without_decorator(self, httpserver):
+        """I want to be able to record in the REPL."""
+        url202, url400, url404, url500 = self.prepare_server(httpserver)
+
+        _recorder.recorder.start()
+
+        def another():
+            requests.get(url500)
+            requests.put(url202)
+
+        def run():
+            requests.get(url404)
+            requests.get(url400)
+            another()
+
+        run()
+
+        _recorder.recorder.stop()
+        _recorder.recorder.dump_to_file(self.out_file)
+
+        with open(self.out_file) as file:
+            data = yaml.safe_load(file)
+        assert data == get_data(httpserver.host, httpserver.port)
+
+        # Now, we test that the recorder is properly reset
+        assert _recorder.recorder.get_registry().registered
+        _recorder.recorder.reset()
+        assert not _recorder.recorder.get_registry().registered
+
 
 class TestReplay:
     def setup_method(self):