Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-responses for
openSUSE:Factory checked in at 2022-02-17 23:40:01
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-responses (Old)
and /work/SRC/openSUSE:Factory/.python-responses.new.1958 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-responses"
Thu Feb 17 23:40:01 2022 rev:18 rq:955500 version:0.17.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-responses/python-responses.changes
2021-12-09 19:45:04.281117718 +0100
+++
/work/SRC/openSUSE:Factory/.python-responses.new.1958/python-responses.changes
2022-02-17 23:40:56.091700712 +0100
@@ -1,0 +2,18 @@
+Wed Feb 16 23:14:16 UTC 2022 - Dirk M??ller <[email protected]>
+
+- update to 0.17.0:
+ * This release is the last to support Python 2.7.
+ * Fixed issue when `response.iter_content` when `chunk_size=None` entered
infinite loop
+ * Fixed issue when `passthru_prefixes` persisted across tests.
+ Now `add_passthru` is valid only within a context manager or for a single
function and
+ cleared on exit
+ * Deprecate `match_querystring` argument in `Response` and
`CallbackResponse`.
+ Use `responses.matchers.query_param_matcher` or
`responses.matchers.query_string_matcher`
+ * Added support for non-UTF-8 bytes in `responses.matchers.multipart_matcher`
+ * Added `responses.registries`. Now user can create custom registries to
+ manipulate the order of responses in the match algorithm
+ `responses.activate(registry=CustomRegistry)`
+ * Fixed issue with response match when requests were performed between
adding responses with
+ same URL. See Issue #212
+
+-------------------------------------------------------------------
Old:
----
responses-0.16.0.tar.gz
New:
----
responses-0.17.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-responses.spec ++++++
--- /var/tmp/diff_new_pack.iRqrSR/_old 2022-02-17 23:40:56.623700707 +0100
+++ /var/tmp/diff_new_pack.iRqrSR/_new 2022-02-17 23:40:56.627700707 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-responses
#
-# Copyright (c) 2021 SUSE LLC
+# Copyright (c) 2022 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
Name: python-responses
-Version: 0.16.0
+Version: 0.17.0
Release: 0
Summary: A utility library for mocking out the `requests` Python library
License: Apache-2.0
++++++ responses-0.16.0.tar.gz -> responses-0.17.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/CHANGES new/responses-0.17.0/CHANGES
--- old/responses-0.16.0/CHANGES 2021-11-11 19:03:18.000000000 +0100
+++ new/responses-0.17.0/CHANGES 2022-01-07 17:18:13.000000000 +0100
@@ -1,3 +1,20 @@
+0.17.0
+------
+
+* This release is the last to support Python 2.7.
+* Fixed issue when `response.iter_content` when `chunk_size=None` entered
infinite loop
+* Fixed issue when `passthru_prefixes` persisted across tests.
+ Now `add_passthru` is valid only within a context manager or for a single
function and
+ cleared on exit
+* Deprecate `match_querystring` argument in `Response`` and `CallbackResponse`.
+ Use `responses.matchers.query_param_matcher` or
`responses.matchers.query_string_matcher`
+* Added support for non-UTF-8 bytes in `responses.matchers.multipart_matcher`
+* Added `responses.registries`. Now user can create custom registries to
+ manipulate the order of responses in the match algorithm
+ `responses.activate(registry=CustomRegistry)`
+* Fixed issue with response match when requests were performed between adding
responses with
+ same URL. See Issue #212
+
0.16.0
------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/MANIFEST.in
new/responses-0.17.0/MANIFEST.in
--- old/responses-0.16.0/MANIFEST.in 2021-11-11 19:03:18.000000000 +0100
+++ new/responses-0.17.0/MANIFEST.in 2022-01-07 17:18:13.000000000 +0100
@@ -1,4 +1,5 @@
include README.rst CHANGES LICENSE
-include test_responses.py tox.ini
+include test_responses.py test_matchers.py
include **/*.pyi
+include tox.ini
global-exclude *~
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/PKG-INFO
new/responses-0.17.0/PKG-INFO
--- old/responses-0.16.0/PKG-INFO 2021-11-11 19:03:24.265085000 +0100
+++ new/responses-0.17.0/PKG-INFO 2022-01-07 17:18:17.088163000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: responses
-Version: 0.16.0
+Version: 0.17.0
Summary: A utility library for mocking out the `requests` Python library.
Home-page: https://github.com/getsentry/responses
Author: David Cramer
@@ -129,6 +129,9 @@
The full resource URL.
match_querystring (``bool``)
+ DEPRECATED: Use `responses.matchers.query_param_matcher` or
+ `responses.matchers.query_string_matcher`
+
Include the query string when matching requests.
Enabled by default if the response URL contains a query string,
disabled if it doesn't or the URL is a regular expression.
@@ -294,7 +297,7 @@
req_files = {"file_name": b"Old World!"}
responses.add(
responses.POST, url="http://httpbin.org/post",
- match=[multipart_matcher(req_data, req_files)]
+ match=[multipart_matcher(req_files, data=req_data)]
)
resp = requests.post("http://httpbin.org/post", files={"file_name":
b"New World!"})
@@ -411,6 +414,49 @@
resp = session.send(prepped)
assert resp.text == "hello world"
+Response Registry
+---------------------------
+
+By default, ``responses`` will search all registered``Response`` objects and
+return a match. If only one ``Response`` is registered, the registry is kept
unchanged.
+However, if multiple matches are found for the same request, then first match
is returned and
+removed from registry.
+
+Such behavior is suitable for most of use cases, but to handle special
conditions, you can
+implement custom registry which must follow interface of
``registries.FirstMatchRegistry``.
+Redefining the ``find`` method will allow you to create custom search logic
and return
+appropriate ``Response``
+
+Example that shows how to set custom registry
+
+.. code-block:: python
+
+ import responses
+ from responses import registries
+
+
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+
+ """ Before tests: <responses.registries.FirstMatchRegistry object> """
+
+ # using function decorator
+ @responses.activate(registry=CustomRegistry)
+ def run():
+ """ Within test: <__main__.CustomRegistry object> """
+
+ run()
+ """ After test: <responses.registries.FirstMatchRegistry object> """
+
+ # using context manager
+ with responses.RequestsMock(registry=CustomRegistry) as rsps:
+ """ In context manager: <__main__.CustomRegistry object> """
+
+ """
+ After exit from context manager: <responses.registries.FirstMatchRegistry
object>
+ """
+
Dynamic Responses
-----------------
@@ -590,22 +636,26 @@
.. 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)
+ class TestMyApi(unittest.TestCase):
+ def setUp(self):
+ responses.add(responses.GET, 'https://example.com', body="within
setup")
+ # here go other self.responses.add(...)
+
+ @responses.activate
+ def test_my_func(self):
+ responses.add(
+ responses.GET,
+ "https://httpbin.org/get",
+ match=[matchers.query_param_matcher({"test": "1", "didi":
"pro"})],
+ body="within test"
+ )
+ resp = requests.get("https://example.com")
+ resp2 = requests.get("https://httpbin.org/get", params={"test":
"1", "didi": "pro"})
+ print(resp.text)
+ # >>> within setup
+ print(resp2.text)
+ # >>> within test
- 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
--------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/README.rst
new/responses-0.17.0/README.rst
--- old/responses-0.16.0/README.rst 2021-11-11 19:03:18.000000000 +0100
+++ new/responses-0.17.0/README.rst 2022-01-07 17:18:13.000000000 +0100
@@ -102,6 +102,9 @@
The full resource URL.
match_querystring (``bool``)
+ DEPRECATED: Use `responses.matchers.query_param_matcher` or
+ `responses.matchers.query_string_matcher`
+
Include the query string when matching requests.
Enabled by default if the response URL contains a query string,
disabled if it doesn't or the URL is a regular expression.
@@ -267,7 +270,7 @@
req_files = {"file_name": b"Old World!"}
responses.add(
responses.POST, url="http://httpbin.org/post",
- match=[multipart_matcher(req_data, req_files)]
+ match=[multipart_matcher(req_files, data=req_data)]
)
resp = requests.post("http://httpbin.org/post", files={"file_name":
b"New World!"})
@@ -384,6 +387,49 @@
resp = session.send(prepped)
assert resp.text == "hello world"
+Response Registry
+---------------------------
+
+By default, ``responses`` will search all registered``Response`` objects and
+return a match. If only one ``Response`` is registered, the registry is kept
unchanged.
+However, if multiple matches are found for the same request, then first match
is returned and
+removed from registry.
+
+Such behavior is suitable for most of use cases, but to handle special
conditions, you can
+implement custom registry which must follow interface of
``registries.FirstMatchRegistry``.
+Redefining the ``find`` method will allow you to create custom search logic
and return
+appropriate ``Response``
+
+Example that shows how to set custom registry
+
+.. code-block:: python
+
+ import responses
+ from responses import registries
+
+
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+
+ """ Before tests: <responses.registries.FirstMatchRegistry object> """
+
+ # using function decorator
+ @responses.activate(registry=CustomRegistry)
+ def run():
+ """ Within test: <__main__.CustomRegistry object> """
+
+ run()
+ """ After test: <responses.registries.FirstMatchRegistry object> """
+
+ # using context manager
+ with responses.RequestsMock(registry=CustomRegistry) as rsps:
+ """ In context manager: <__main__.CustomRegistry object> """
+
+ """
+ After exit from context manager: <responses.registries.FirstMatchRegistry
object>
+ """
+
Dynamic Responses
-----------------
@@ -563,22 +609,26 @@
.. 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)
+ class TestMyApi(unittest.TestCase):
+ def setUp(self):
+ responses.add(responses.GET, 'https://example.com', body="within
setup")
+ # here go other self.responses.add(...)
+
+ @responses.activate
+ def test_my_func(self):
+ responses.add(
+ responses.GET,
+ "https://httpbin.org/get",
+ match=[matchers.query_param_matcher({"test": "1", "didi":
"pro"})],
+ body="within test"
+ )
+ resp = requests.get("https://example.com")
+ resp2 = requests.get("https://httpbin.org/get", params={"test":
"1", "didi": "pro"})
+ print(resp.text)
+ # >>> within setup
+ print(resp2.text)
+ # >>> within test
- 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
--------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/__init__.py
new/responses-0.17.0/responses/__init__.py
--- old/responses-0.16.0/responses/__init__.py 2021-11-11 19:03:18.000000000
+0100
+++ new/responses-0.17.0/responses/__init__.py 2022-01-07 17:18:13.000000000
+0100
@@ -16,6 +16,8 @@
from requests.utils import cookiejar_from_dict
from responses.matchers import json_params_matcher as _json_params_matcher
from responses.matchers import urlencoded_params_matcher as
_urlencoded_params_matcher
+from responses.registries import FirstMatchRegistry
+from responses.matchers import query_string_matcher as _query_string_matcher
from warnings import warn
try:
@@ -76,6 +78,13 @@
logger = logging.getLogger("responses")
+if six.PY2:
+ warn(
+ "Support for Python 2.7 is being removed from the next release of
responses. "
+ "Pin your dependency to responses==0.17 or lower.",
+ DeprecationWarning,
+ )
+
def urlencoded_params_matcher(params):
warn(
@@ -156,7 +165,7 @@
"""
-def get_wrapped(func, responses):
+def get_wrapped(func, responses, registry=None):
if six.PY2:
args, a, kw, defaults = inspect.getargspec(func)
wrapper_args = inspect.formatargspec(args, a, kw, defaults)
@@ -193,6 +202,9 @@
signature = signature.replace(parameters=params_without_defaults)
func_args = str(signature)
+ if registry is not None:
+ responses._set_registry(registry)
+
evaldict = {"func": func, "responses": responses}
six.exec_(
_wrapper_template % {"wrapper_args": wrapper_args, "func_args":
func_args},
@@ -246,10 +258,35 @@
if isinstance(body, _io.BufferedReader):
return body
- return BufferIO(body)
+ data = BufferIO(body)
+
+ def is_closed():
+ """
+ Real Response uses HTTPResponse as body object.
+ Thus, when method is_closed is called first to check if there is any
more
+ content to consume and the file-like object is still opened
+
+ This method ensures stability to work for both:
+ https://github.com/getsentry/responses/issues/438
+ https://github.com/getsentry/responses/issues/394
+
+ where file should be intentionally be left opened to continue
consumption
+ """
+ if not data.closed and data.read(1):
+ # if there is more bytes to read then keep open, but return pointer
+ data.seek(-1, 1)
+ return False
+ else:
+ if not data.closed:
+ # close but return False to mock like is still opened
+ data.close()
+ return False
+ # only if file really closed (by us) return True
+ return True
-_unspecified = object()
+ data.isclosed = is_closed
+ return data
class BaseResponse(object):
@@ -259,11 +296,14 @@
stream = False
- def __init__(self, method, url, match_querystring=_unspecified, match=()):
+ def __init__(self, method, url, match_querystring=None, match=()):
self.method = method
# ensure the url has a default path set if the url is a string
self.url = _ensure_url_default_path(url)
- self.match_querystring =
self._should_match_querystring(match_querystring)
+
+ if self._should_match_querystring(match_querystring):
+ match = tuple(match) +
(_query_string_matcher(urlparse(self.url).query),)
+
self.match = match
self.call_count = 0
@@ -285,40 +325,32 @@
def __ne__(self, other):
return not self.__eq__(other)
- def _url_matches_strict(self, url, other):
- url_parsed = urlparse(url)
- other_parsed = urlparse(other)
-
- if url_parsed[:3] != other_parsed[:3]:
- return False
-
- url_qsl = sorted(parse_qsl(url_parsed.query))
- other_qsl = sorted(parse_qsl(other_parsed.query))
-
- return url_qsl == other_qsl
-
def _should_match_querystring(self, match_querystring_argument):
- if match_querystring_argument is not _unspecified:
- return match_querystring_argument
-
if isinstance(self.url, Pattern):
# the old default from <= 0.9.0
return False
+ if match_querystring_argument is not None:
+ warn(
+ (
+ "Argument 'match_querystring' is deprecated. "
+ "Use 'responses.matchers.query_param_matcher' or "
+ "'responses.matchers.query_string_matcher'"
+ ),
+ DeprecationWarning,
+ )
+ return match_querystring_argument
+
return bool(urlparse(self.url).query)
- def _url_matches(self, url, other, match_querystring=False):
+ def _url_matches(self, url, other):
if _is_string(url):
if _has_unicode(url):
url = _clean_unicode(url)
if not isinstance(other, six.text_type):
other = other.encode("ascii").decode("utf8")
- if match_querystring:
- normalize_url = parse_url(url).url
- return self._url_matches_strict(normalize_url, other)
- else:
- return _get_url_and_path(url) == _get_url_and_path(other)
+ return _get_url_and_path(url) == _get_url_and_path(other)
elif isinstance(url, Pattern) and url.match(other):
return True
@@ -350,7 +382,7 @@
if request.method != self.method:
return False, "Method does not match"
- if not self._url_matches(self.url, request.url,
self.match_querystring):
+ if not self._url_matches(self.url, request.url):
return False, "URL does not match"
valid, reason = self._req_attr_matches(self.match, request)
@@ -535,17 +567,35 @@
response_callback=None,
passthru_prefixes=(),
target="requests.adapters.HTTPAdapter.send",
+ registry=FirstMatchRegistry,
):
self._calls = CallList()
self.reset()
+ self._registry = registry() # call only after reset
self.assert_all_requests_are_fired = assert_all_requests_are_fired
self.response_callback = response_callback
self.passthru_prefixes = tuple(passthru_prefixes)
self.target = target
+ self._patcher = None
+ self._matches = []
+
+ def _get_registry(self):
+ return self._registry
+
+ def _set_registry(self, new_registry):
+ if self.registered():
+ err_msg = (
+ "Cannot replace Registry, current registry has responses.\n"
+ "Run 'responses.registry.reset()' first"
+ )
+ raise AttributeError(err_msg)
+
+ self._registry = new_registry()
def reset(self):
- self._matches = []
+ self._registry = FirstMatchRegistry()
self._calls.reset()
+ self.passthru_prefixes = ()
def add(
self,
@@ -557,8 +607,9 @@
**kwargs
):
"""
- A basic request:
+ >>> import responses
+ A basic request:
>>> responses.add(responses.GET, 'http://example.com')
You can also directly pass an object which implements the
@@ -582,23 +633,15 @@
>>> headers={'X-Header': 'foo'},
>>> )
-
- Strict query string matching:
-
- >>> responses.add(
- >>> method='GET',
- >>> url='http://example.com?foo=bar',
- >>> match_querystring=True
- >>> )
"""
if isinstance(method, BaseResponse):
- self._matches.append(method)
+ self._registry.add(method)
return
if adding_headers is not None:
kwargs.setdefault("headers", adding_headers)
- self._matches.append(Response(method=method, url=url, body=body,
**kwargs))
+ self._registry.add(Response(method=method, url=url, body=body,
**kwargs))
def add_passthru(self, prefix):
"""
@@ -607,6 +650,7 @@
For example, to allow any request to 'https://example.com', but require
mocks for the remainder, you would add the prefix as so:
+ >>> import responses
>>> responses.add_passthru('https://example.com')
Regex can be used like:
@@ -623,16 +667,16 @@
either by a response object inheriting ``BaseResponse`` or
``method`` and ``url``. Removes all matching responses.
- >>> response.add(responses.GET, 'http://example.org')
- >>> response.remove(responses.GET, 'http://example.org')
+ >>> import responses
+ >>> responses.add(responses.GET, 'http://example.org')
+ >>> responses.remove(responses.GET, 'http://example.org')
"""
if isinstance(method_or_response, BaseResponse):
response = method_or_response
else:
response = BaseResponse(method=method_or_response, url=url)
- while response in self._matches:
- self._matches.remove(response)
+ self._registry.remove(response)
def replace(self, method_or_response=None, url=None, body="", *args,
**kwargs):
"""
@@ -640,6 +684,7 @@
is identical to ``add()``. The response is identified using ``method``
and ``url``, and the first matching response is replaced.
+ >>> import responses
>>> responses.add(responses.GET, 'http://example.org', json={'data':
1})
>>> responses.replace(responses.GET, 'http://example.org',
json={'data': 2})
"""
@@ -649,11 +694,7 @@
else:
response = Response(method=method_or_response, url=url, body=body,
**kwargs)
- try:
- index = self._matches.index(response)
- except ValueError:
- raise ValueError("Response is not registered for URL %s" % url)
- self._matches[index] = response
+ self._registry.replace(response)
def upsert(self, method_or_response=None, url=None, body="", *args,
**kwargs):
"""
@@ -661,6 +702,7 @@
if no response exists. Responses are matched using ``method``and
``url``.
The first matching response is replaced.
+ >>> import responses
>>> responses.add(responses.GET, 'http://example.org', json={'data':
1})
>>> responses.upsert(responses.GET, 'http://example.org',
json={'data': 2})
"""
@@ -681,7 +723,7 @@
# ensure the url has a default path set if the url is a string
# url = _ensure_url_default_path(url, match_querystring)
- self._matches.append(
+ self._registry.add(
CallbackResponse(
url=url,
method=method,
@@ -693,7 +735,7 @@
)
def registered(self):
- return self._matches
+ return self._registry.registered
@property
def calls(self):
@@ -709,8 +751,14 @@
self.reset()
return success
- def activate(self, func):
- return get_wrapped(func, self)
+ def activate(self, func=None, registry=None):
+ if func is not None:
+ return get_wrapped(func, self)
+
+ def deco_activate(func):
+ return get_wrapped(func, self, registry)
+
+ return deco_activate
def _find_match(self, request):
"""
@@ -721,21 +769,7 @@
(Response) found match. If multiple found, then remove & return
the first match.
(list) list with reasons why other matches don't match
"""
- found = None
- found_match = None
- match_failed_reasons = []
- for i, match in enumerate(self._matches):
- match_result, reason = match.matches(request)
- if match_result:
- if found is None:
- found = i
- found_match = match
- else:
- # Multiple matches found. Remove & return the first match.
- return self._matches.pop(found), match_failed_reasons
- else:
- match_failed_reasons.append(reason)
- return found_match, match_failed_reasons
+ return self._registry.find(request)
def _parse_request_params(self, url):
params = {}
@@ -774,7 +808,7 @@
"- %s %s\n\n"
"Available matches:\n" % (request.method, request.url)
)
- for i, m in enumerate(self._matches):
+ for i, m in enumerate(self.registered()):
error_msg += "- {} {} {}\n".format(
m.method, m.url, match_failed_reasons[i]
)
@@ -798,11 +832,6 @@
response = resp_callback(response) if resp_callback else
response
raise
- stream = kwargs.get("stream")
- if not stream:
- response.content # NOQA required to ensure that response body is
read.
- response.close()
-
response = resp_callback(response) if resp_callback else response
match.call_count += 1
self._calls.add(request, response)
@@ -823,7 +852,7 @@
if not allow_assert:
return
- not_called = [m for m in self._matches if m.call_count == 0]
+ not_called = [m for m in self.registered() if m.call_count == 0]
if not_called:
raise AssertionError(
"Not all requests have been executed {0!r}".format(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/__init__.pyi
new/responses-0.17.0/responses/__init__.pyi
--- old/responses-0.16.0/responses/__init__.pyi 2021-11-11 19:03:18.000000000
+0100
+++ new/responses-0.17.0/responses/__init__.pyi 2022-01-07 17:18:13.000000000
+0100
@@ -23,6 +23,7 @@
from urllib.parse import quote as quote
from urllib3.response import HTTPHeaderDict
from .matchers import urlencoded_params_matcher, json_params_matcher
+from .registries import FirstMatchRegistry
def _clean_unicode(url: str) -> str: ...
@@ -38,7 +39,7 @@
def _has_unicode(s: str) -> bool: ...
def _is_string(s: Union[Pattern[str], str]) -> bool: ...
def get_wrapped(
- func: Callable[..., Any], responses: RequestsMock
+ func: Callable[..., Any], responses: RequestsMock, registry: Optional[Any]
) -> Callable[..., Any]: ...
@@ -51,7 +52,9 @@
MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]]
class CallList(Sequence[Call], Sized):
- def __init__(self) -> None: ...
+ def __init__(self) -> None:
+ self._calls = List[Call]
+ ...
def __iter__(self) -> Iterator[Call]: ...
def __len__(self) -> int: ...
def __getitem__(self, idx: int) -> Call: ... # type: ignore [override]
@@ -78,7 +81,7 @@
def __eq__(self, other: Any) -> bool: ...
def __ne__(self, other: Any) -> bool: ...
def _req_attr_matches(
- self, match: MatcherIterable, request: Optional[Union[bytes, str]]
+ self, match: MatcherIterable, request: PreparedRequest
) -> Tuple[bool, str]: ...
def _should_match_querystring(
self, match_querystring_argument: Union[bool, object]
@@ -144,6 +147,8 @@
) -> None: ...
def isclosed(self) -> bool: ...
+_F = TypeVar("_F", bound=Callable[..., Any])
+
class RequestsMock:
DELETE: Literal["DELETE"]
GET: Literal["GET"]
@@ -154,7 +159,7 @@
PUT: Literal["PUT"]
response_callback: Optional[Callable[[Any], Any]] = ...
assert_all_requests_are_fired: Any = ...
- passthru_prefixes: Tuple[str, ...] = ...
+ passthru_prefixes: Tuple[Union[str, Pattern[str]], ...] = ...
target: Any = ...
_matches: List[Any]
def __init__(
@@ -163,7 +168,11 @@
response_callback: Optional[Callable[[Any], Any]] = ...,
passthru_prefixes: Tuple[str, ...] = ...,
target: str = ...,
- ) -> None: ...
+ registry: Any = ...,
+ ) -> None:
+ self._patcher = Callable[[Any], Any]
+ self._calls = CallList
+ ...
def reset(self) -> None: ...
add: _Add
add_passthru: _AddPassthru
@@ -173,24 +182,23 @@
url: Optional[Union[Pattern[str], str]] = ...,
) -> None: ...
replace: _Replace
+ upsert: _Upsert
add_callback: _AddCallback
@property
def calls(self) -> CallList: ...
def __enter__(self) -> RequestsMock: ...
def __exit__(self, type: Any, value: Any, traceback: Any) -> bool: ...
- activate: _Activate
+ def activate(self, func: Optional[_F], registry: Optional[Any]) -> _F: ...
def start(self) -> None: ...
def stop(self, allow_assert: bool = ...) -> None: ...
def assert_call_count(self, url: str, count: int) -> bool: ...
def registered(self) -> List[Any]: ...
+ def _set_registry(self, registry: Any) -> None: ...
+ def _get_registry(self) -> Any: ...
-_F = TypeVar("_F", bound=Callable[..., Any])
HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]]
-class _Activate(Protocol):
- def __call__(self, func: _F) -> _F: ...
-
class _Add(Protocol):
def __call__(
self,
@@ -267,7 +275,7 @@
def __call__(self) -> List[Response]: ...
-activate: _Activate
+activate: Any
add: _Add
add_callback: _AddCallback
add_passthru: _AddPassthru
@@ -322,5 +330,5 @@
"start",
"stop",
"target",
- "upsert"
+ "upsert",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/matchers.py
new/responses-0.17.0/responses/matchers.py
--- old/responses-0.16.0/responses/matchers.py 2021-11-11 19:03:18.000000000
+0100
+++ new/responses-0.17.0/responses/matchers.py 2022-01-07 17:18:13.000000000
+0100
@@ -175,14 +175,16 @@
:param query: (str), same as constructed by request
:return: (func) matcher
"""
+ if six.PY2 and isinstance(query, unicode): # noqa: F821
+ query = query.encode("utf8")
def match(request):
reason = ""
data = parse_url(request.url)
request_query = data.query
- request_qsl = sorted(parse_qsl(request_query))
- matcher_qsl = sorted(parse_qsl(query))
+ request_qsl = sorted(parse_qsl(request_query)) if request_query else {}
+ matcher_qsl = sorted(parse_qsl(query)) if query else {}
valid = not query if request_query is None else request_qsl ==
matcher_qsl
@@ -275,12 +277,12 @@
)
request_body = request.body
- if isinstance(request_body, bytes):
- request_body = request_body.decode("utf-8")
-
prepared_body = prepared.body
+
if isinstance(prepared_body, bytes):
- prepared_body = prepared_body.decode("utf-8")
+ # since headers always come as str, need to convert to bytes
+ prepared_boundary = prepared_boundary.encode("utf-8")
+ request_boundary = request_boundary.encode("utf-8")
prepared_body = prepared_body.replace(prepared_boundary,
request_boundary)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/registries.py
new/responses-0.17.0/responses/registries.py
--- old/responses-0.16.0/responses/registries.py 1970-01-01
01:00:00.000000000 +0100
+++ new/responses-0.17.0/responses/registries.py 2022-01-07
17:18:13.000000000 +0100
@@ -0,0 +1,48 @@
+class FirstMatchRegistry(object):
+ def __init__(self):
+ self._responses = []
+
+ @property
+ def registered(self):
+ return self._responses
+
+ def reset(self):
+ self._responses = []
+
+ def find(self, request):
+ found = None
+ found_match = None
+ match_failed_reasons = []
+ for i, response in enumerate(self.registered):
+ match_result, reason = response.matches(request)
+ if match_result:
+ if found is None:
+ found = i
+ found_match = response
+ else:
+ if self.registered[found].call_count > 0:
+ # that assumes that some responses were added between
calls
+ self.registered.pop(found)
+ found_match = response
+ break
+ # Multiple matches found. Remove & return the first
response.
+ return self.registered.pop(found), match_failed_reasons
+ else:
+ match_failed_reasons.append(reason)
+ return found_match, match_failed_reasons
+
+ def add(self, response):
+ self.registered.append(response)
+
+ def remove(self, response):
+ while response in self.registered:
+ self.registered.remove(response)
+
+ def replace(self, response):
+ try:
+ index = self.registered.index(response)
+ except ValueError:
+ raise ValueError(
+ "Response is not registered for URL {}".format(response.url)
+ )
+ self.registered[index] = response
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/registries.pyi
new/responses-0.17.0/responses/registries.pyi
--- old/responses-0.16.0/responses/registries.pyi 1970-01-01
01:00:00.000000000 +0100
+++ new/responses-0.17.0/responses/registries.pyi 2022-01-07
17:18:13.000000000 +0100
@@ -0,0 +1,17 @@
+from typing import (
+ List,
+ Tuple,
+)
+from requests.adapters import PreparedRequest
+from responses import BaseResponse
+
+class FirstMatchRegistry:
+ _responses = List[BaseResponse]
+ def __init__(self) -> None: ...
+ @property
+ def registered(self) -> List[BaseResponse]: ...
+ def reset(self) -> None: ...
+ def find(self, request: PreparedRequest) -> Tuple[BaseResponse,
List[str]]: ...
+ def add(self, response: BaseResponse) -> None: ...
+ def remove(self, response: BaseResponse) -> None: ...
+ def replace(self, response: BaseResponse) -> None: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/test_matchers.py
new/responses-0.17.0/responses/test_matchers.py
--- old/responses-0.16.0/responses/test_matchers.py 2021-11-11
19:03:18.000000000 +0100
+++ new/responses-0.17.0/responses/test_matchers.py 2022-01-07
17:18:13.000000000 +0100
@@ -18,7 +18,7 @@
def assert_reset():
- assert len(responses._default_mock._matches) == 0
+ assert len(responses._default_mock.registered()) == 0
assert len(responses.calls) == 0
@@ -283,17 +283,32 @@
assert_reset()
-def test_multipart_matcher():
[email protected](
+ "req_file,match_file",
+ [
+ (b"Old World!", "Old World!"),
+ ("Old World!", b"Old World!"),
+ (b"Old World!", b"Old World!"),
+ ("Old World!", "Old World!"),
+ (b"\xacHello World!", b"\xacHello World!"),
+ ],
+)
+def test_multipart_matcher(req_file, match_file):
@responses.activate
def run():
req_data = {"some": "other", "data": "fields"}
- req_files = {"file_name": b"Old World!"}
responses.add(
responses.POST,
url="http://httpbin.org/post",
- match=[matchers.multipart_matcher(req_files, data=req_data)],
+ match=[
+ matchers.multipart_matcher(
+ files={"file_name": match_file}, data=req_data
+ )
+ ],
+ )
+ resp = requests.post(
+ "http://httpbin.org/post", data=req_data, files={"file_name":
req_file}
)
- resp = requests.post("http://httpbin.org/post", data=req_data,
files=req_files)
assert resp.status_code == 200
with pytest.raises(TypeError):
@@ -334,14 +349,24 @@
msg = str(excinfo.value)
assert "multipart/form-data doesn't match. Request body differs."
in msg
- assert (
- '\r\nContent-Disposition: form-data; name="file_name"; '
- 'filename="file_name"\r\n\r\nOld World!\r\n'
- ) in msg
- assert (
- '\r\nContent-Disposition: form-data; name="file_name"; '
- 'filename="file_name"\r\n\r\nNew World!\r\n'
- ) in msg
+ if six.PY2:
+ assert (
+ '\r\nContent-Disposition: form-data; name="file_name"; '
+ 'filename="file_name"\r\n\r\nOld World!\r\n'
+ ) in msg
+ assert (
+ '\r\nContent-Disposition: form-data; name="file_name"; '
+ 'filename="file_name"\r\n\r\nNew World!\r\n'
+ ) in msg
+ else:
+ assert (
+ r'\r\nContent-Disposition: form-data; name="file_name"; '
+ r'filename="file_name"\r\n\r\nOld World!\r\n'
+ ) in msg
+ assert (
+ r'\r\nContent-Disposition: form-data; name="file_name"; '
+ r'filename="file_name"\r\n\r\nNew World!\r\n'
+ ) in msg
# x-www-form-urlencoded request
with responses.RequestsMock(assert_all_requests_are_fired=False) as
rsps:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses/test_responses.py
new/responses-0.17.0/responses/test_responses.py
--- old/responses-0.16.0/responses/test_responses.py 2021-11-11
19:03:18.000000000 +0100
+++ new/responses-0.17.0/responses/test_responses.py 2022-01-07
17:18:13.000000000 +0100
@@ -18,6 +18,7 @@
PassthroughResponse,
matchers,
CallbackResponse,
+ registries,
)
@@ -28,7 +29,7 @@
def assert_reset():
- assert len(responses._default_mock._matches) == 0
+ assert len(responses._default_mock.registered()) == 0
assert len(responses.calls) == 0
@@ -1070,24 +1071,24 @@
# check that assert_all_requests_are_fired=True doesn't remove urls
with responses.RequestsMock(assert_all_requests_are_fired=True) as m:
m.add(responses.GET, "http://example.com", body=b"test")
- assert len(m._matches) == 1
+ assert len(m.registered()) == 1
requests.get("http://example.com")
- assert len(m._matches) == 1
+ assert len(m.registered()) == 1
# check that assert_all_requests_are_fired=True counts mocked errors
with responses.RequestsMock(assert_all_requests_are_fired=True) as m:
m.add(responses.GET, "http://example.com", body=Exception())
- assert len(m._matches) == 1
+ assert len(m.registered()) == 1
with pytest.raises(Exception):
requests.get("http://example.com")
- assert len(m._matches) == 1
+ assert len(m.registered()) == 1
with responses.RequestsMock(assert_all_requests_are_fired=True) as m:
m.add_callback(responses.GET, "http://example.com",
request_callback)
- assert len(m._matches) == 1
+ assert len(m.registered()) == 1
with pytest.raises(BaseException):
requests.get("http://example.com")
- assert len(m._matches) == 1
+ assert len(m.registered()) == 1
run()
assert_reset()
@@ -1219,6 +1220,9 @@
https://github.com/psf/requests/pull/3563
Now user can manually patch URL3 lib to achieve the same
+
+ See discussion in
+ https://github.com/getsentry/responses/issues/394
"""
@responses.activate
@@ -1248,6 +1252,30 @@
assert_reset()
+def test_stream_with_none_chunk_size():
+ """
+ See discussion in
+ https://github.com/getsentry/responses/issues/438
+ """
+
+ @responses.activate
+ def run():
+ responses.add(
+ responses.GET,
+ "https://example.com",
+ status=200,
+ content_type="application/octet-stream",
+ body=b"This is test",
+ auto_calculate_content_length=True,
+ )
+ res = requests.get("https://example.com", stream=True)
+ for chunk in res.iter_content(chunk_size=None):
+ assert chunk == b"This is test"
+
+ run()
+ assert_reset()
+
+
def test_legacy_adding_headers():
@responses.activate
def run():
@@ -1377,15 +1405,48 @@
def run():
responses.add(responses.GET, "http://example.com", body="test")
responses.add(responses.GET, "http://example.com", body="rest")
+ responses.add(responses.GET, "http://example.com", body="fest")
+ responses.add(responses.GET, "http://example.com", body="best")
resp = requests.get("http://example.com")
assert_response(resp, "test")
+
resp = requests.get("http://example.com")
assert_response(resp, "rest")
+
+ resp = requests.get("http://example.com")
+ assert_response(resp, "fest")
+
+ resp = requests.get("http://example.com")
+ assert_response(resp, "best")
+
# After all responses are used, last response should be repeated
resp = requests.get("http://example.com")
+ assert_response(resp, "best")
+
+ run()
+ assert_reset()
+
+
+def test_multiple_responses_intermixed():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com", body="test")
+ resp = requests.get("http://example.com")
+ assert_response(resp, "test")
+
+ responses.add(responses.GET, "http://example.com", body="rest")
+ resp = requests.get("http://example.com")
assert_response(resp, "rest")
+ responses.add(responses.GET, "http://example.com", body="best")
+ resp = requests.get("http://example.com")
+ assert_response(resp, "best")
+
+ # After all responses are used, last response should be repeated
+ resp = requests.get("http://example.com")
+ assert_response(resp, "best")
+
run()
assert_reset()
@@ -1546,6 +1607,37 @@
assert_reset()
+def test_passthru_does_not_persist_across_tests(httpserver):
+ """
+ passthru should be erased on exit from context manager
+ see:
+ https://github.com/getsentry/responses/issues/322
+ """
+ httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
+
+ @responses.activate
+ def with_a_passthru():
+ assert not responses._default_mock.passthru_prefixes
+ responses.add_passthru(re.compile(".*"))
+ try:
+ response = requests.get("https://example.com")
+ except ConnectionError as err:
+ if "Failed to establish" in str(err):
+ pytest.skip("Cannot resolve DNS for example.com")
+ raise err
+
+ assert response.status_code == 200
+
+ @responses.activate
+ def without_a_passthru():
+ assert not responses._default_mock.passthru_prefixes
+ with pytest.raises(requests.exceptions.ConnectionError):
+ requests.get("https://example.com")
+
+ with_a_passthru()
+ without_a_passthru()
+
+
def test_method_named_param():
@responses.activate
def run():
@@ -1767,7 +1859,7 @@
mocks_list = responses.registered()
- assert mocks_list == responses.mock._matches
+ assert mocks_list == responses.mock.registered()
assert mocks_list == [first_response, second_response, third_response]
run()
@@ -1792,3 +1884,86 @@
run()
assert_reset()
+
+
+def test_set_registry_not_empty():
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+ @responses.activate
+ def run():
+ url = "http://fizzbuzz/foo"
+ responses.add(method=responses.GET, url=url)
+ with pytest.raises(AttributeError) as excinfo:
+ responses.mock._set_registry(CustomRegistry)
+ msg = str(excinfo.value)
+ assert "Cannot replace Registry, current registry has responses" in msg
+
+ run()
+ assert_reset()
+
+
+def test_set_registry():
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+ @responses.activate(registry=CustomRegistry)
+ def run_with_registry():
+ assert type(responses.mock._get_registry()) == CustomRegistry
+
+ @responses.activate
+ def run():
+ # test that registry does not leak to another test
+ assert type(responses.mock._get_registry()) ==
registries.FirstMatchRegistry
+
+ run_with_registry()
+ run()
+ assert_reset()
+
+
+def test_set_registry_context_manager():
+ def run():
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+ with responses.RequestsMock(
+ assert_all_requests_are_fired=False, registry=CustomRegistry
+ ) as rsps:
+ assert type(rsps._get_registry()) == CustomRegistry
+ assert type(responses.mock._get_registry()) ==
registries.FirstMatchRegistry
+
+ run()
+ assert_reset()
+
+
+def test_registry_reset():
+ def run():
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+ with responses.RequestsMock(
+ assert_all_requests_are_fired=False, registry=CustomRegistry
+ ) as rsps:
+ rsps._get_registry().reset()
+ assert not rsps.registered()
+
+ run()
+ assert_reset()
+
+
+def test_requests_between_add():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "https://example.com/", json={"response":
"old"})
+ assert requests.get("https://example.com/").content == b'{"response":
"old"}'
+ assert requests.get("https://example.com/").content == b'{"response":
"old"}'
+ assert requests.get("https://example.com/").content == b'{"response":
"old"}'
+
+ responses.add(responses.GET, "https://example.com/", json={"response":
"new"})
+
+ assert requests.get("https://example.com/").content == b'{"response":
"new"}'
+ assert requests.get("https://example.com/").content == b'{"response":
"new"}'
+ assert requests.get("https://example.com/").content == b'{"response":
"new"}'
+
+ run()
+ assert_reset()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses.egg-info/PKG-INFO
new/responses-0.17.0/responses.egg-info/PKG-INFO
--- old/responses-0.16.0/responses.egg-info/PKG-INFO 2021-11-11
19:03:24.000000000 +0100
+++ new/responses-0.17.0/responses.egg-info/PKG-INFO 2022-01-07
17:18:16.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: responses
-Version: 0.16.0
+Version: 0.17.0
Summary: A utility library for mocking out the `requests` Python library.
Home-page: https://github.com/getsentry/responses
Author: David Cramer
@@ -129,6 +129,9 @@
The full resource URL.
match_querystring (``bool``)
+ DEPRECATED: Use `responses.matchers.query_param_matcher` or
+ `responses.matchers.query_string_matcher`
+
Include the query string when matching requests.
Enabled by default if the response URL contains a query string,
disabled if it doesn't or the URL is a regular expression.
@@ -294,7 +297,7 @@
req_files = {"file_name": b"Old World!"}
responses.add(
responses.POST, url="http://httpbin.org/post",
- match=[multipart_matcher(req_data, req_files)]
+ match=[multipart_matcher(req_files, data=req_data)]
)
resp = requests.post("http://httpbin.org/post", files={"file_name":
b"New World!"})
@@ -411,6 +414,49 @@
resp = session.send(prepped)
assert resp.text == "hello world"
+Response Registry
+---------------------------
+
+By default, ``responses`` will search all registered``Response`` objects and
+return a match. If only one ``Response`` is registered, the registry is kept
unchanged.
+However, if multiple matches are found for the same request, then first match
is returned and
+removed from registry.
+
+Such behavior is suitable for most of use cases, but to handle special
conditions, you can
+implement custom registry which must follow interface of
``registries.FirstMatchRegistry``.
+Redefining the ``find`` method will allow you to create custom search logic
and return
+appropriate ``Response``
+
+Example that shows how to set custom registry
+
+.. code-block:: python
+
+ import responses
+ from responses import registries
+
+
+ class CustomRegistry(registries.FirstMatchRegistry):
+ pass
+
+
+ """ Before tests: <responses.registries.FirstMatchRegistry object> """
+
+ # using function decorator
+ @responses.activate(registry=CustomRegistry)
+ def run():
+ """ Within test: <__main__.CustomRegistry object> """
+
+ run()
+ """ After test: <responses.registries.FirstMatchRegistry object> """
+
+ # using context manager
+ with responses.RequestsMock(registry=CustomRegistry) as rsps:
+ """ In context manager: <__main__.CustomRegistry object> """
+
+ """
+ After exit from context manager: <responses.registries.FirstMatchRegistry
object>
+ """
+
Dynamic Responses
-----------------
@@ -590,22 +636,26 @@
.. 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)
+ class TestMyApi(unittest.TestCase):
+ def setUp(self):
+ responses.add(responses.GET, 'https://example.com', body="within
setup")
+ # here go other self.responses.add(...)
+
+ @responses.activate
+ def test_my_func(self):
+ responses.add(
+ responses.GET,
+ "https://httpbin.org/get",
+ match=[matchers.query_param_matcher({"test": "1", "didi":
"pro"})],
+ body="within test"
+ )
+ resp = requests.get("https://example.com")
+ resp2 = requests.get("https://httpbin.org/get", params={"test":
"1", "didi": "pro"})
+ print(resp.text)
+ # >>> within setup
+ print(resp2.text)
+ # >>> within test
- 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
--------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/responses.egg-info/SOURCES.txt
new/responses-0.17.0/responses.egg-info/SOURCES.txt
--- old/responses-0.16.0/responses.egg-info/SOURCES.txt 2021-11-11
19:03:24.000000000 +0100
+++ new/responses-0.17.0/responses.egg-info/SOURCES.txt 2022-01-07
17:18:16.000000000 +0100
@@ -9,6 +9,8 @@
responses/__init__.pyi
responses/matchers.py
responses/matchers.pyi
+responses/registries.py
+responses/registries.pyi
responses/test_matchers.py
responses/test_responses.py
responses.egg-info/PKG-INFO
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/setup.py
new/responses-0.17.0/setup.py
--- old/responses-0.16.0/setup.py 2021-11-11 19:03:18.000000000 +0100
+++ new/responses-0.17.0/setup.py 2022-01-07 17:18:13.000000000 +0100
@@ -59,7 +59,7 @@
setup(
name="responses",
- version="0.16.0",
+ version="0.17.0",
author="David Cramer",
description=("A utility library for mocking out the `requests` Python
library."),
url="https://github.com/getsentry/responses",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/responses-0.16.0/tox.ini new/responses-0.17.0/tox.ini
--- old/responses-0.16.0/tox.ini 2021-11-11 19:03:18.000000000 +0100
+++ new/responses-0.17.0/tox.ini 2022-01-07 17:18:13.000000000 +0100
@@ -1,5 +1,5 @@
[tox]
-envlist = py27,py35,py36,py37,py38,py39,py310,mypy
+envlist = py27,py35,py36,py37,py38,py39,py310,mypy,precom
[testenv]
extras = tests
@@ -12,3 +12,10 @@
basepython = python3.7
commands =
python -m mypy --config-file=mypy.ini -p responses
+
+[testenv:precom]
+description = Run pre-commit hooks (black, flake, etc)
+basepython = python3.7
+deps = pre-commit
+commands =
+ pre-commit run --all-files