diff --git a/CHANGES b/CHANGES
index fb31bdb..96ae89c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,30 @@
+0.13.3
+------
+
+* Switch from Travis to GHA for deployment.
+
+0.13.2
+------
+
+* Fixed incorrect type stubs for `add_callback`
+
+0.13.1
+------
+
+* Fixed packages not containing type stubs.
+
+0.13.0
+------
+
+* `responses.upsert()` was added. This method will `add()` a response if one
+  has not already been registered for a URL, or `replace()` an existing
+  response.
+* `responses.registered()` was added. The method allows you to get a list of
+  the currently registered responses. This formalizes the previously private
+  `responses.mock._matches` method.
+* A more useful `__repr__` has been added to `Response`.
+* Error messages have been improved.
+
 0.12.1
 ------
 
diff --git a/PKG-INFO b/PKG-INFO
index e9f944e..4508a4c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: responses
-Version: 0.12.1
+Version: 0.13.3
 Summary: A utility library for mocking out the `requests` Python library.
 Home-page: https://github.com/getsentry/responses
 Author: David Cramer
@@ -155,9 +155,9 @@ Description: Responses
                 responses.add(
                     responses.POST,
                     url='http://calc.com/sum',
-                    body=4,
+                    body="4",
                     match=[
-                        responses.urlencoded_params_matcher({"left": 1, "right": 3})
+                        responses.urlencoded_params_matcher({"left": "1", "right": "3"})
                     ]
                 )
                 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
@@ -338,6 +338,31 @@ Description: Responses
                 resp = requests.get('http://twitter.com/api/1/foobar')
                 assert resp.status_code == 200
         
+        Responses inside a unittest setUp()
+        -----------------------------------
+        
+        When run with unittest tests, this can be used to set up some
+        generic class-level responses, that may be complemented by each test
+        
+        .. code-block:: python
+        
+            def setUp():
+                self.responses = responses.RequestsMock()
+                self.responses.start()
+        
+                # self.responses.add(...)
+        
+                self.addCleanup(self.responses.stop)
+                self.addCleanup(self.responses.reset)
+        
+            def test_api(self):
+                self.responses.add(
+                    responses.GET, 'http://twitter.com/api/1/foobar',
+                    body='{}', status=200,
+                    content_type='application/json')
+                resp = requests.get('http://twitter.com/api/1/foobar')
+                assert resp.status_code == 200
+        
         Assertions on declared responses
         --------------------------------
         
@@ -457,9 +482,9 @@ Description: Responses
         Viewing/Modifying registered responses
         --------------------------------------
         
-        Registered responses are available as a private attribute of the RequestMock
+        Registered responses are available as a public method of the RequestMock
         instance. It is sometimes useful for debugging purposes to view the stack of
-        registered responses which can be accessed via ``responses.mock._matches``.
+        registered responses which can be accessed via ``responses.registered()``.
         
         The ``replace`` function allows a previously registered ``response`` to be
         changed. The method signature is identical to ``add``. ``response`` s are
@@ -482,6 +507,10 @@ Description: Responses
                 assert resp.json() == {'data': 2}
         
         
+        The ``upsert`` function allows a previously registered ``response`` to be
+        changed like ``replace``. If the response is registered, the ``upsert`` function
+        will registered it like ``add``.
+        
         ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
         matched responses from the registered list.
         
diff --git a/README.rst b/README.rst
index 8962fcc..20f5f27 100644
--- a/README.rst
+++ b/README.rst
@@ -148,9 +148,9 @@ other formats.
         responses.add(
             responses.POST,
             url='http://calc.com/sum',
-            body=4,
+            body="4",
             match=[
-                responses.urlencoded_params_matcher({"left": 1, "right": 3})
+                responses.urlencoded_params_matcher({"left": "1", "right": "3"})
             ]
         )
         requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
@@ -331,6 +331,31 @@ Responses as a pytest fixture
         resp = requests.get('http://twitter.com/api/1/foobar')
         assert resp.status_code == 200
 
+Responses inside a unittest setUp()
+-----------------------------------
+
+When run with unittest tests, this can be used to set up some
+generic class-level responses, that may be complemented by each test
+
+.. code-block:: python
+
+    def setUp():
+        self.responses = responses.RequestsMock()
+        self.responses.start()
+
+        # self.responses.add(...)
+
+        self.addCleanup(self.responses.stop)
+        self.addCleanup(self.responses.reset)
+
+    def test_api(self):
+        self.responses.add(
+            responses.GET, 'http://twitter.com/api/1/foobar',
+            body='{}', status=200,
+            content_type='application/json')
+        resp = requests.get('http://twitter.com/api/1/foobar')
+        assert resp.status_code == 200
+
 Assertions on declared responses
 --------------------------------
 
@@ -450,9 +475,9 @@ Regex can be used like:
 Viewing/Modifying registered responses
 --------------------------------------
 
-Registered responses are available as a private attribute of the RequestMock
+Registered responses are available as a public method of the RequestMock
 instance. It is sometimes useful for debugging purposes to view the stack of
-registered responses which can be accessed via ``responses.mock._matches``.
+registered responses which can be accessed via ``responses.registered()``.
 
 The ``replace`` function allows a previously registered ``response`` to be
 changed. The method signature is identical to ``add``. ``response`` s are
@@ -475,6 +500,10 @@ replaced.
         assert resp.json() == {'data': 2}
 
 
+The ``upsert`` function allows a previously registered ``response`` to be
+changed like ``replace``. If the response is registered, the ``upsert`` function
+will registered it like ``add``.
+
 ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
 matched responses from the registered list.
 
diff --git a/debian/changelog b/debian/changelog
index 80ccc49..36e65e1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+responses (0.13.3-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 05 Jun 2021 02:22:04 -0000
+
 responses (0.12.1-1) unstable; urgency=medium
 
   * Team upload.
diff --git a/responses.egg-info/PKG-INFO b/responses.egg-info/PKG-INFO
index e9f944e..4508a4c 100644
--- a/responses.egg-info/PKG-INFO
+++ b/responses.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: responses
-Version: 0.12.1
+Version: 0.13.3
 Summary: A utility library for mocking out the `requests` Python library.
 Home-page: https://github.com/getsentry/responses
 Author: David Cramer
@@ -155,9 +155,9 @@ Description: Responses
                 responses.add(
                     responses.POST,
                     url='http://calc.com/sum',
-                    body=4,
+                    body="4",
                     match=[
-                        responses.urlencoded_params_matcher({"left": 1, "right": 3})
+                        responses.urlencoded_params_matcher({"left": "1", "right": "3"})
                     ]
                 )
                 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
@@ -338,6 +338,31 @@ Description: Responses
                 resp = requests.get('http://twitter.com/api/1/foobar')
                 assert resp.status_code == 200
         
+        Responses inside a unittest setUp()
+        -----------------------------------
+        
+        When run with unittest tests, this can be used to set up some
+        generic class-level responses, that may be complemented by each test
+        
+        .. code-block:: python
+        
+            def setUp():
+                self.responses = responses.RequestsMock()
+                self.responses.start()
+        
+                # self.responses.add(...)
+        
+                self.addCleanup(self.responses.stop)
+                self.addCleanup(self.responses.reset)
+        
+            def test_api(self):
+                self.responses.add(
+                    responses.GET, 'http://twitter.com/api/1/foobar',
+                    body='{}', status=200,
+                    content_type='application/json')
+                resp = requests.get('http://twitter.com/api/1/foobar')
+                assert resp.status_code == 200
+        
         Assertions on declared responses
         --------------------------------
         
@@ -457,9 +482,9 @@ Description: Responses
         Viewing/Modifying registered responses
         --------------------------------------
         
-        Registered responses are available as a private attribute of the RequestMock
+        Registered responses are available as a public method of the RequestMock
         instance. It is sometimes useful for debugging purposes to view the stack of
-        registered responses which can be accessed via ``responses.mock._matches``.
+        registered responses which can be accessed via ``responses.registered()``.
         
         The ``replace`` function allows a previously registered ``response`` to be
         changed. The method signature is identical to ``add``. ``response`` s are
@@ -482,6 +507,10 @@ Description: Responses
                 assert resp.json() == {'data': 2}
         
         
+        The ``upsert`` function allows a previously registered ``response`` to be
+        changed like ``replace``. If the response is registered, the ``upsert`` function
+        will registered it like ``add``.
+        
         ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
         matched responses from the registered list.
         
diff --git a/responses.egg-info/SOURCES.txt b/responses.egg-info/SOURCES.txt
index 6ef4759..8dddf51 100644
--- a/responses.egg-info/SOURCES.txt
+++ b/responses.egg-info/SOURCES.txt
@@ -2,11 +2,11 @@ CHANGES
 LICENSE
 MANIFEST.in
 README.rst
-responses.py
 setup.cfg
 setup.py
-test_responses.py
 tox.ini
+responses/__init__.py
+responses/test_responses.py
 responses.egg-info/PKG-INFO
 responses.egg-info/SOURCES.txt
 responses.egg-info/dependency_links.txt
diff --git a/responses.egg-info/requires.txt b/responses.egg-info/requires.txt
index 4053be1..99221b2 100644
--- a/responses.egg-info/requires.txt
+++ b/responses.egg-info/requires.txt
@@ -19,3 +19,4 @@ pytest<5.0,>=4.6
 
 [tests:python_version >= "3.5"]
 pytest>=4.6
+mypy
diff --git a/responses.py b/responses/__init__.py
similarity index 94%
rename from responses.py
rename to responses/__init__.py
index eaf84f3..3eb9399 100644
--- a/responses.py
+++ b/responses/__init__.py
@@ -418,6 +418,17 @@ class Response(BaseResponse):
             preload_content=False,
         )
 
+    def __repr__(self):
+        return (
+            "<Response(url='{url}' status={status} "
+            "content_type='{content_type}' headers='{headers}')>".format(
+                url=self.url,
+                status=self.status,
+                content_type=self.content_type,
+                headers=json_module.dumps(self.headers),
+            )
+        )
+
 
 class CallbackResponse(BaseResponse):
     def __init__(
@@ -610,13 +621,31 @@ class RequestsMock(object):
         >>> responses.replace(responses.GET, 'http://example.org', json={'data': 2})
         """
         if isinstance(method_or_response, BaseResponse):
+            url = method_or_response.url
             response = method_or_response
         else:
             response = Response(method=method_or_response, url=url, body=body, **kwargs)
 
-        index = self._matches.index(response)
+        try:
+            index = self._matches.index(response)
+        except ValueError:
+            raise ValueError("Response is not registered for URL %s" % url)
         self._matches[index] = response
 
+    def upsert(self, method_or_response=None, url=None, body="", *args, **kwargs):
+        """
+        Replaces a response previously added using ``add()``, or adds the response
+        if no response exists.  Responses are matched using ``method``and ``url``.
+        The first matching response is replaced.
+
+        >>> responses.add(responses.GET, 'http://example.org', json={'data': 1})
+        >>> responses.upsert(responses.GET, 'http://example.org', json={'data': 2})
+        """
+        try:
+            self.replace(method_or_response, url, body, *args, **kwargs)
+        except ValueError:
+            self.add(method_or_response, url, body, *args, **kwargs)
+
     def add_callback(
         self, method, url, callback, match_querystring=False, content_type="text/plain"
     ):
@@ -633,6 +662,9 @@ class RequestsMock(object):
             )
         )
 
+    def registered(self):
+        return self._matches
+
     @property
     def calls(self):
         return self._calls
@@ -791,6 +823,7 @@ __all__ = [
     "PATCH",
     "POST",
     "PUT",
+    "registered",
     "remove",
     "replace",
     "reset",
@@ -798,6 +831,7 @@ __all__ = [
     "start",
     "stop",
     "target",
+    "upsert",
 ]
 
 activate = _default_mock.activate
@@ -815,6 +849,7 @@ passthru_prefixes = _default_mock.passthru_prefixes
 PATCH = _default_mock.PATCH
 POST = _default_mock.POST
 PUT = _default_mock.PUT
+registered = _default_mock.registered
 remove = _default_mock.remove
 replace = _default_mock.replace
 reset = _default_mock.reset
@@ -822,3 +857,4 @@ response_callback = _default_mock.response_callback
 start = _default_mock.start
 stop = _default_mock.stop
 target = _default_mock.target
+upsert = _default_mock.upsert
diff --git a/test_responses.py b/responses/test_responses.py
similarity index 87%
rename from test_responses.py
rename to responses/test_responses.py
index 02089b9..6ab8606 100644
--- a/test_responses.py
+++ b/responses/test_responses.py
@@ -16,7 +16,7 @@ from responses import BaseResponse, Response
 try:
     from mock import patch, Mock
 except ImportError:
-    from unittest.mock import patch, Mock
+    from unittest.mock import patch, Mock  # type: ignore
 
 
 def assert_reset():
@@ -34,6 +34,14 @@ def assert_response(resp, body=None, content_type="text/plain"):
     assert resp.text == body
 
 
+def assert_params(resp, expected):
+    assert hasattr(resp, "request"), "Missing request"
+    assert hasattr(
+        resp.request, "params"
+    ), "Missing params on request that responses should add"
+    assert getattr(resp.request, "params") == expected, "Incorrect parameters"
+
+
 def test_response():
     @responses.activate
     def run():
@@ -138,8 +146,98 @@ def test_replace_error(original, replacement):
     @responses.activate
     def run():
         responses.add(responses.GET, original)
-        with pytest.raises(ValueError):
+        with pytest.raises(ValueError) as excinfo:
             responses.replace(responses.GET, replacement)
+        assert "Response is not registered for URL %s" % replacement in str(
+            excinfo.value
+        )
+
+    run()
+    assert_reset()
+
+
+def test_replace_response_object_error():
+    @responses.activate
+    def run():
+        responses.add(Response(method=responses.GET, url="http://example.com/one"))
+        with pytest.raises(ValueError) as excinfo:
+            responses.replace(
+                Response(method=responses.GET, url="http://example.com/two")
+            )
+        assert "Response is not registered for URL http://example.com/two" in str(
+            excinfo.value
+        )
+
+    run()
+    assert_reset()
+
+
+@pytest.mark.parametrize(
+    "original,replacement",
+    [
+        ("http://example.com/two", "http://example.com/two"),
+        (
+            Response(method=responses.GET, url="http://example.com/two"),
+            Response(
+                method=responses.GET, url="http://example.com/two", body="testtwo"
+            ),
+        ),
+        (
+            re.compile(r"http://example\.com/two"),
+            re.compile(r"http://example\.com/two"),
+        ),
+    ],
+)
+def test_upsert_replace(original, replacement):
+    @responses.activate
+    def run():
+        responses.add(responses.GET, "http://example.com/one", body="test1")
+
+        if isinstance(original, BaseResponse):
+            responses.add(original)
+        else:
+            responses.add(responses.GET, original, body="test2")
+
+        if isinstance(replacement, BaseResponse):
+            responses.upsert(replacement)
+        else:
+            responses.upsert(responses.GET, replacement, body="testtwo")
+
+        resp = requests.get("http://example.com/two")
+        assert_response(resp, "testtwo")
+
+    run()
+    assert_reset()
+
+
+@pytest.mark.parametrize(
+    "original,replacement",
+    [
+        ("http://example.com/two", "http://example.com/two"),
+        (
+            Response(method=responses.GET, url="http://example.com/two"),
+            Response(
+                method=responses.GET, url="http://example.com/two", body="testtwo"
+            ),
+        ),
+        (
+            re.compile(r"http://example\.com/two"),
+            re.compile(r"http://example\.com/two"),
+        ),
+    ],
+)
+def test_upsert_add(original, replacement):
+    @responses.activate
+    def run():
+        responses.add(responses.GET, "http://example.com/one", body="test1")
+
+        if isinstance(replacement, BaseResponse):
+            responses.upsert(replacement)
+        else:
+            responses.upsert(responses.GET, replacement, body="testtwo")
+
+        resp = requests.get("http://example.com/two")
+        assert_response(resp, "testtwo")
 
     run()
     assert_reset()
@@ -783,7 +881,7 @@ def test_response_callback():
             resp = requests.get("http://example.com")
             assert resp.text == "test"
             assert hasattr(resp, "_is_mocked")
-            assert resp._is_mocked is True
+            assert getattr(resp, "_is_mocked") is True
 
     run()
     assert_reset()
@@ -794,8 +892,8 @@ def test_response_filebody():
 
     def run():
         with responses.RequestsMock() as m:
-            with open("README.rst", "rb") as out:
-                m.add(responses.GET, "http://example.com", body=out, stream=True)
+            with open("README.rst", "r") as out:
+                m.add(responses.GET, "http://example.com", body=out.read(), stream=True)
                 resp = requests.get("http://example.com")
             with open("README.rst", "r") as out:
                 assert resp.text == out.read()
@@ -944,7 +1042,7 @@ def test_handles_buffered_reader_body():
 
     @responses.activate
     def run():
-        responses.add(responses.GET, url, body=BufferedReader(BytesIO(b"test")))
+        responses.add(responses.GET, url, body=BufferedReader(BytesIO(b"test")))  # type: ignore
 
         resp = requests.get(url)
 
@@ -1142,11 +1240,11 @@ def test_request_param(url):
         )
         resp = requests.get(url, params=params)
         assert_response(resp, "test")
-        assert resp.request.params == params
+        assert_params(resp, params)
 
         resp = requests.get(url)
         assert_response(resp, "test")
-        assert resp.request.params == {}
+        assert_params(resp, {})
 
     run()
     assert_reset()
@@ -1164,7 +1262,7 @@ def test_request_param_with_multiple_values_for_the_same_key():
         )
         resp = requests.get(url, params=params)
         assert_response(resp, "test")
-        assert resp.request.params == params
+        assert_params(resp, params)
 
     run()
     assert_reset()
@@ -1299,3 +1397,78 @@ def test_fail_request_error():
 
     run()
     assert_reset()
+
+
+@pytest.mark.parametrize(
+    "response_params, expected_representation",
+    [
+        (
+            {"method": responses.GET, "url": "http://example.com/"},
+            (
+                "<Response(url='http://example.com/' status=200 "
+                "content_type='text/plain' headers='null')>"
+            ),
+        ),
+        (
+            {
+                "method": responses.POST,
+                "url": "http://another-domain.com/",
+                "content_type": "application/json",
+                "status": 404,
+            },
+            (
+                "<Response(url='http://another-domain.com/' status=404 "
+                "content_type='application/json' headers='null')>"
+            ),
+        ),
+        (
+            {
+                "method": responses.PUT,
+                "url": "http://abcd.com/",
+                "content_type": "text/html",
+                "status": 500,
+                "headers": {"X-Test": "foo"},
+                "body": {"it_wont_be": "considered"},
+            },
+            (
+                "<Response(url='http://abcd.com/' status=500 "
+                "content_type='text/html' headers='{\"X-Test\": \"foo\"}')>"
+            ),
+        ),
+    ],
+)
+def test_response_representations(response_params, expected_representation):
+    response = Response(**response_params)
+
+    assert str(response) == expected_representation
+    assert repr(response) == expected_representation
+
+
+def test_mocked_responses_list_registered():
+    @responses.activate
+    def run():
+        first_response = Response(
+            responses.GET,
+            "http://example.com/",
+            body="",
+            headers={"X-Test": "foo"},
+            status=404,
+        )
+        second_response = Response(
+            responses.GET, "http://example.com/", body="", headers={"X-Test": "foo"}
+        )
+        third_response = Response(
+            responses.POST,
+            "http://anotherdomain.com/",
+        )
+        responses.add(first_response)
+        responses.add(second_response)
+        responses.add(third_response)
+
+        mocks_list = responses.registered()
+
+        assert mocks_list == responses.mock._matches
+        assert mocks_list == [first_response, second_response, third_response]
+
+    run()
+    assert_reset()
diff --git a/setup.py b/setup.py
index 1fc0449..d06001f 100644
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,7 @@ tests_require = [
     "pytest-cov",
     "pytest-localserver",
     "flake8",
+    "mypy; python_version >= '3.5'",
 ]
 
 extras_require = {"tests": tests_require}
@@ -55,14 +56,14 @@ class PyTest(TestCommand):
 
 setup(
     name="responses",
-    version="0.12.1",
+    version="0.13.3",
     author="David Cramer",
     description=("A utility library for mocking out the `requests` Python library."),
     url="https://github.com/getsentry/responses",
     license="Apache 2.0",
     long_description=open("README.rst").read(),
     long_description_content_type="text/x-rst",
-    py_modules=["responses"],
+    packages=["responses"],
     zip_safe=False,
     python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
     install_requires=install_requires,
@@ -70,6 +71,7 @@ setup(
     tests_require=tests_require,
     setup_requires=setup_requires,
     cmdclass={"test": PyTest},
+    package_data={"responses": ["py.typed", "__init__.pyi"]},
     include_package_data=True,
     classifiers=[
         "Intended Audience :: Developers",