Repository: canonical-ols/talisker (language: python)

Issue:
tests broken
e.g. for Python 3.8

```
❯ tox -r -e py38
py38: remove tox env folder /home/jugmac00/Canonical/sources/talisker/.tox/py38
py38: install_deps> python -I -m pip install . -r /home/jugmac00/Canonical/sources/talisker/requirements.tests.txt
py38: commands[0]> py.test -v --cov=talisker --no-success-flaky-report
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-4.6.9, py-1.11.0, pluggy-0.13.1 -- /home/jugmac00/Canonical/sources/talisker/.tox/py38/bin/python
cachedir: .tox/py38/.pytest_cache
rootdir: /home/jugmac00/Canonical/sources/talisker, inifile: setup.cfg, testpaths: tests, docs
plugins: postgresql-1.4.1, cov-2.8.1, timeout-1.3.4, flaky-3.6.1, xdist-1.31.0, forked-1.6.0
collected 225 items / 9 skipped / 216 selected                                 

tests/test_config.py::test_config_defaults PASSED                        [  0%]
tests/test_config.py::test_config_colour PASSED                          [  0%]
tests/test_config.py::test_logstatus_config PASSED                       [  1%]
tests/test_config.py::test_debuglog_config PASSED                        [  1%]
tests/test_config.py::test_query_threshold_config PASSED                 [  2%]
tests/test_config.py::test_explain_sql_config PASSED                     [  2%]
tests/test_config.py::test_request_timeout_config PASSED                 [  3%]
tests/test_config.py::test_sanitised_keys_config PASSED                  [  3%]
tests/test_config.py::test_id_header_config PASSED                       [  4%]
tests/test_config.py::test_load_env_config_filters PASSED                [  4%]
tests/test_config.py::test_load_env_config_file PASSED                   [  4%]
tests/test_config.py::test_git PASSED                                    [  5%]
tests/test_config.py::test_bzr PASSED                                    [  5%]
tests/test_config.py::test_bzr_version_info_py2 SKIPPED                  [  6%]
tests/test_config.py::test_version_info PASSED                           [  6%]
tests/test_config.py::test_setup_py PASSED                               [  7%]
tests/test_context.py::test_context_api PASSED                           [  7%]
tests/test_context.py::test_null_context PASSED                          [  8%]
tests/test_context.py::test_context_thread PASSED                        [  8%]
tests/test_context.py::test_context_gevent PASSED                        [  8%]
tests/test_context.py::test_context_eventlet SKIPPED                     [  9%]
tests/test_context.py::test_context_asyncio PASSED                       [  9%]
tests/test_context.py::test_stack_basic PASSED                           [ 10%]
tests/test_context.py::test_stack_context_manager PASSED                 [ 10%]
tests/test_context.py::test_stack_dict_arg PASSED                        [ 11%]
tests/test_context.py::test_stack_unwind PASSED                          [ 11%]
tests/test_context.py::test_null_context_stack PASSED                    [ 12%]
tests/test_context.py::test_does_not_use_or_modify_dict PASSED           [ 12%]
tests/test_context.py::test_tracking PASSED                              [ 12%]
tests/test_context.py::test_request_timeout PASSED                       [ 13%]
tests/test_functional.py::test_gunicorn_sync_worker SKIPPED              [ 13%]
tests/test_functional.py::test_gunicorn_gevent_worker SKIPPED            [ 14%]
tests/test_functional.py::test_gunicorn_eventlet_worker SKIPPED          [ 14%]
tests/test_functional.py::test_gunicorn_status_interface SKIPPED         [ 15%]
tests/test_functional.py::test_flask_app SKIPPED                         [ 15%]
tests/test_functional.py::test_django_app SKIPPED                        [ 16%]
tests/test_functional.py::test_gunicorn_timeout SKIPPED                  [ 16%]
tests/test_functional.py::test_celery_basic SKIPPED                      [ 16%]
tests/test_functional.py::test_multiprocess_metrics SKIPPED              [ 17%]
tests/test_functional.py::test_prometheus_lock_timeouts SKIPPED          [ 17%]
tests/test_init.py::test_run_entrypoint PASSED                           [ 18%]
tests/test_init.py::test_module_entrypoint PASSED                        [ 18%]
tests/test_init.py::test_run_sysexit PASSED                              [ 19%]
tests/test_init.py::test_gunicorn_entrypoint SKIPPED                     [ 19%]
tests/test_init.py::test_celery_entrypoint SKIPPED                       [ 20%]
tests/test_init.py::test_gunicorn_eventlet_entrypoint SKIPPED            [ 20%]
tests/test_init.py::test_gunicorn_gevent_entrypoint SKIPPED              [ 20%]
tests/test_logging.py::test_logging_context_ctx PASSED                   [ 21%]
tests/test_logging.py::test_logging_context_push PASSED                  [ 21%]
tests/test_logging.py::test_set_logging_context PASSED                   [ 22%]
tests/test_logging.py::test_extra_logging PASSED                         [ 22%]
tests/test_logging.py::test_make_record_no_extra PASSED                  [ 23%]
tests/test_logging.py::test_make_record_global_extra PASSED              [ 23%]
tests/test_logging.py::test_make_record_context_extra PASSED             [ 24%]
tests/test_logging.py::test_make_record_all_extra PASSED                 [ 24%]
tests/test_logging.py::test_make_record_extra_renamed PASSED             [ 24%]
tests/test_logging.py::test_make_record_context_renamed PASSED           [ 25%]
tests/test_logging.py::test_make_record_ordering PASSED                  [ 25%]
tests/test_logging.py::test_make_record_protected PASSED                 [ 26%]
tests/test_logging.py::test_logger_collects_raven_breadcrumbs SKIPPED    [ 26%]
tests/test_logging.py::test_formatter_no_args PASSED                     [ 27%]
tests/test_logging.py::test_formatter_escapes_quotes PASSED              [ 27%]
tests/test_logging.py::test_formatter_escapes_newlines PASSED            [ 28%]
tests/test_logging.py::test_formatter_with_extra PASSED                  [ 28%]
tests/test_logging.py::test_formatter_with_exception PASSED              [ 28%]
tests/test_logging.py::test_formatter_large_msg PASSED                   [ 29%]
tests/test_logging.py::test_formatter_protected PASSED                   [ 29%]
tests/test_logging.py::test_coloured_formatter PASSED                    [ 30%]
tests/test_logging.py::test_configure PASSED                             [ 30%]
tests/test_logging.py::test_configure_twice PASSED                       [ 31%]
tests/test_logging.py::test_configure_debug_log_bad_file PASSED          [ 31%]
tests/test_logging.py::test_configure_debug_log PASSED                   [ 32%]
tests/test_logging.py::test_configure_coloured PASSED                    [ 32%]
tests/test_logging.py::test_clean_message PASSED                         [ 32%]
tests/test_logging.py::test_logfmt_key[hi-hi0] PASSED                    [ 33%]
tests/test_logging.py::test_logfmt_key[hi-hi1] PASSED                    [ 33%]
tests/test_logging.py::test_logfmt_key[hi hi-hi_hi] PASSED               [ 34%]
tests/test_logging.py::test_logfmt_key[hi.hi-hi_hi] PASSED               [ 34%]
tests/test_logging.py::test_logfmt_key[hi=hi-hi_hi] PASSED               [ 35%]
tests/test_logging.py::test_logfmt_key[hi"hi-hi\\"hi] PASSED             [ 35%]
tests/test_logging.py::test_logfmt_key[1-1] PASSED                       [ 36%]
tests/test_logging.py::test_logfmt_key[input7-None] PASSED               [ 36%]
tests/test_logging.py::test_logfmt_key[True-None] PASSED                 [ 36%]
tests/test_logging.py::test_logfmt_key[False-None] PASSED                [ 37%]
tests/test_logging.py::test_logfmt_key[hi\nhi\nhi-hi___] PASSED          [ 37%]
tests/test_logging.py::test_logfmt_key_truncate PASSED                   [ 38%]
tests/test_logging.py::test_logfmt_value[hi-hi0] PASSED                  [ 38%]
tests/test_logging.py::test_logfmt_value[10-"10"] PASSED                 [ 39%]
tests/test_logging.py::test_logfmt_value[hi-hi1] PASSED                  [ 39%]
tests/test_logging.py::test_logfmt_value[ hi "hi" -"hi \\"hi\\""] PASSED [ 40%]
tests/test_logging.py::test_logfmt_value[True-true] PASSED               [ 40%]
tests/test_logging.py::test_logfmt_value[False-false] PASSED             [ 40%]
tests/test_logging.py::test_logfmt_value[1-1] PASSED                     [ 41%]
tests/test_logging.py::test_logfmt_value[input7-"<class 'dict'>"] PASSED [ 41%]
tests/test_logging.py::test_logfmt_value[input8-"<class 'list'>"] PASSED [ 42%]
tests/test_logging.py::test_logfmt_value[hi\nhi\nhi-hi...] PASSED        [ 42%]
tests/test_logging.py::test_logfmt_value_truncate PASSED                 [ 43%]
tests/test_logging.py::test_logfmt_atoms[input0-expected0] PASSED        [ 43%]
tests/test_logging.py::test_logfmt_atoms[input1-expected1] PASSED        [ 44%]
tests/test_logging.py::test_logfmt_atoms[input2-expected2] PASSED        [ 44%]
tests/test_logging.py::test_logfmt_atoms[input3-expected3] PASSED        [ 44%]
tests/test_logging.py::test_logfmt_atoms[input4-expected4] PASSED        [ 45%]
tests/test_logging.py::test_logfmt_atoms[input5-expected5] PASSED        [ 45%]
tests/test_logging.py::test_logfmt_atoms[input6-expected6] PASSED        [ 46%]
tests/test_logging.py::test_logfmt_atoms[input7-expected7] PASSED        [ 46%]
tests/test_logging.py::test_logfmt_atoms_subdict PASSED                  [ 47%]
tests/test_logging.py::test_string_needs_quoting[string-False] PASSED    [ 47%]
tests/test_logging.py::test_string_needs_quoting[space space-True] PASSED [ 48%]
tests/test_logging.py::test_string_needs_quoting[foo=bar-True] PASSED    [ 48%]
tests/test_logging.py::test_string_needs_quoting["hi"-True] PASSED       [ 48%]
tests/test_logging.py::test_string_needs_quoting[\\tescaped-True] PASSED [ 49%]
tests/test_logging.py::test_string_needs_quoting[1234567890-True] PASSED [ 49%]
tests/test_logging.py::test_string_needs_quoting[1234567890a-False] PASSED [ 50%]
tests/test_logging.py::test_string_needs_quoting[12.34-True] PASSED      [ 50%]
tests/test_logging.py::test_string_needs_quoting[12.34.56-False] PASSED  [ 51%]
tests/test_logging.py::test_safe_string[test-test] PASSED                [ 51%]
tests/test_logging.py::test_safe_string[newline\nnewline-newline...] PASSED [ 52%]
tests/test_logging.py::test_safe_string[longlonglong-longlon...] PASSED  [ 52%]
tests/test_logging.py::test_safe_string["quote"-\\"quote\\"] PASSED      [ 52%]
tests/test_render.py::test_content_simple_string PASSED                  [ 53%]
tests/test_render.py::test_content_escaped PASSED                        [ 53%]
tests/test_render.py::test_content_not_escaped PASSED                    [ 54%]
tests/test_render.py::test_content_disabled PASSED                       [ 54%]
tests/test_render.py::test_link PASSED                                   [ 55%]
tests/test_render.py::test_table PASSED                                  [ 55%]
tests/test_render.py::test_preformatted PASSED                           [ 56%]
tests/test_requests.py::test_collect_metadata PASSED                     [ 56%]
tests/test_requests.py::test_collect_metadata_hostname PASSED            [ 56%]
tests/test_requests.py::test_collect_metadata_request_body PASSED        [ 57%]
tests/test_requests.py::test_collect_metadata_querystring PASSED         [ 57%]
tests/test_requests.py::test_collect_metadata_with_response PASSED       [ 58%]
tests/test_requests.py::test_metric_hook PASSED                          [ 58%]
tests/test_requests.py::test_metric_hook_user_name PASSED                [ 59%]
tests/test_requests.py::test_metric_hook_registered_endpoint PASSED      [ 59%]
tests/test_requests.py::test_configured_session PASSED                   [ 60%]
tests/test_requests.py::test_configured_session_http_error PASSED        [ 60%]
tests/test_requests.py::test_configured_session_connection_error PASSED  [ 60%]
tests/test_requests.py::test_configured_session_with_user_name PASSED    [ 61%]
tests/test_requests.py::test_adapter_balances PASSED                     [ 61%]
tests/test_requests.py::test_adapter_requires_consistent_http_scheme PASSED [ 62%]
tests/test_requests.py::test_adapter_adds_default_timeout PASSED         [ 62%]
tests/test_requests.py::test_adapter_respects_context_timeout PASSED     [ 63%]
tests/test_requests.py::test_adapter_raises_when_context_deadline_exceeded PASSED [ 63%]
tests/test_requests.py::test_adapter_default_no_retries PASSED           [ 64%]
tests/test_requests.py::test_adapter_retry_on_errors[urllib3_error0-ConnectionError] PASSED [ 64%]
tests/test_requests.py::test_adapter_retry_on_errors[urllib3_error1-ConnectionError] PASSED [ 64%]
tests/test_requests.py::test_adapter_no_retry_on_read_timeout PASSED     [ 65%]
tests/test_requests.py::test_adapter_retry_on_status_raises FAILED       [ 65%]
tests/test_requests.py::test_adapter_retry_on_status_returns_when_no_raise FAILED [ 66%]
tests/test_requests.py::test_adapter_retry_on_status_header FAILED       [ 66%]
tests/test_requests.py::test_adapter_exceptions_match_default[None-response0] PASSED [ 67%]
tests/test_requests.py::test_adapter_exceptions_match_default[retry1-response1] PASSED [ 67%]
tests/test_requests.py::test_adapter_exceptions_match_default[retry2-response2] PASSED [ 68%]
tests/test_requests.py::test_adapter_timeout_formats PASSED              [ 68%]
tests/test_requests.py::test_adapter_bad_timeout_raises PASSED           [ 68%]
tests/test_requests.py::test_adapter_callsite_retries PASSED             [ 69%]
tests/test_requests.py::test_threadlocal_threading PASSED                [ 69%]
tests/test_requests.py::test_threadlocal_forking PASSED                  [ 70%]
tests/test_requests.py::test_talisker_adapter_proxy_config_is_used PASSED [ 70%]
tests/test_requests.py::test_talisker_adapter_no_proxy_works_normally PASSED [ 71%]
tests/test_requests.py::test_talisker_adapter_no_proxy_works_with_new_url PASSED [ 71%]
tests/test_statsd.py::test_parse_statsd_dsn_host PASSED                  [ 72%]
tests/test_statsd.py::test_parse_statsd_dsn_port PASSED                  [ 72%]
tests/test_statsd.py::test_parse_statsd_dsn_prefix PASSED                [ 72%]
tests/test_statsd.py::test_parse_statsd_dsn_prefix_with_slashes PASSED   [ 73%]
tests/test_statsd.py::test_parse_statsd_dsn_size PASSED                  [ 73%]
tests/test_statsd.py::test_parse_statsd_dsn_ipv6 PASSED                  [ 74%]
tests/test_statsd.py::test_dummyclient_basic PASSED                      [ 74%]
tests/test_statsd.py::test_dummyclient_pipeline PASSED                   [ 75%]
tests/test_statsd.py::test_dummyclient_nested_pipeline PASSED            [ 75%]
tests/test_statsd.py::test_dummyclient_collect PASSED                    [ 76%]
tests/test_statsd.py::test_no_network XFAIL                              [ 76%]
tests/test_statsd.py::test_dummyclient_memory PASSED                     [ 76%]
tests/test_statsd.py::test_metric_list PASSED                            [ 77%]
tests/test_testing.py::test_log_record_list PASSED                       [ 77%]
tests/test_testing.py::test_log_record_list_parse PASSED                 [ 78%]
tests/test_testing.py::test_test_context SKIPPED                         [ 78%]
tests/test_testing.py::test_serverprocess_success PASSED                 [ 79%]
tests/test_testing.py::test_serverprocess_failure PASSED                 [ 79%]
tests/test_testing.py::test_serverprocess_output PASSED                  [ 80%]
tests/test_testing.py::test_serverprocess_output_wait PASSED             [ 80%]
tests/test_testing.py::test_gunicornprocess_success SKIPPED              [ 80%]
tests/test_testing.py::test_gunicornprocess_bad_app PASSED               [ 81%]
tests/test_testing.py::test_log_record_list_no_extra PASSED              [ 81%]
tests/test_util.py::test_sanitize_url PASSED                             [ 82%]
tests/test_util.py::test_get_rounded_ms PASSED                           [ 82%]
tests/test_util.py::test_get_root_exception_implicit PASSED              [ 83%]
tests/test_util.py::test_get_root_exception_explicit PASSED              [ 83%]
tests/test_util.py::test_get_root_exception_mixed PASSED                 [ 84%]
tests/test_util.py::test_get_errno_fields_permissions PASSED             [ 84%]
tests/test_util.py::test_get_errno_fields_connection PASSED              [ 84%]
tests/test_util.py::test_get_errno_fields_dns PASSED                     [ 85%]
tests/test_util.py::test_local_forking PASSED                            [ 85%]
tests/test_util.py::test_local_threading PASSED                          [ 86%]
tests/test_wsgi.py::test_error_response_handler PASSED                   [ 86%]
tests/test_wsgi.py::test_error_response_handler_devel PASSED             [ 87%]
tests/test_wsgi.py::test_wsgi_request_start_response PASSED              [ 87%]
tests/test_wsgi.py::test_wsgi_request_soft_timeout_default SKIPPED       [ 88%]
tests/test_wsgi.py::test_wsgi_request_soft_explicit SKIPPED              [ 88%]
tests/test_wsgi.py::test_wsgi_request_soft_explicit_viewname SKIPPED     [ 88%]
tests/test_wsgi.py::test_wsgi_request_wrap_response PASSED               [ 89%]
tests/test_wsgi.py::test_wsgi_request_wrap_file PASSED                   [ 89%]
tests/test_wsgi.py::test_wsgi_request_wrap_error_in_iterator[Exception] PASSED [ 90%]
tests/test_wsgi.py::test_wsgi_request_wrap_error_in_iterator[RequestTimeout] PASSED [ 90%]
tests/test_wsgi.py::test_wsgi_request_wrap_error_headers_sent PASSED     [ 91%]
tests/test_wsgi.py::test_wsgi_request_wrap_no_body PASSED                [ 91%]
tests/test_wsgi.py::test_wsgi_request_log PASSED                         [ 92%]
tests/test_wsgi.py::test_wsgi_request_log_error PASSED                   [ 92%]
tests/test_wsgi.py::test_wsgi_request_log_timeout PASSED                 [ 92%]
tests/test_wsgi.py::test_wsgi_request_metrics PASSED                     [ 93%]
tests/test_wsgi.py::test_wsgi_request_metrics_timeout PASSED             [ 93%]
tests/test_wsgi.py::test_middleware_basic PASSED                         [ 94%]
tests/test_wsgi.py::test_middleware_sets_deadlines PASSED                [ 94%]
tests/test_wsgi.py::test_middleware_sets_header_deadline PASSED          [ 95%]
tests/test_wsgi.py::test_middleware_error_before_start_response[Exception] PASSED [ 95%]
tests/test_wsgi.py::test_middleware_error_before_start_response[RequestTimeout] PASSED [ 96%]
tests/test_wsgi.py::test_middleware_error_after_start_response[Exception] PASSED [ 96%]
tests/test_wsgi.py::test_middleware_error_after_start_response[RequestTimeout] PASSED [ 96%]
tests/test_wsgi.py::test_middleware_preserves_file_wrapper PASSED        [ 97%]
tests/test_wsgi.py::test_middleware_debug_middleware_error PASSED        [ 97%]
tests/test_wsgi.py::test_middleware_debug_middleware PASSED              [ 98%]
tests/test_wsgi.py::test_middleware_debug_middleware_no_content PASSED   [ 98%]
tests/test_wsgi.py::test_middleware_debug PASSED                         [ 99%]
tests/test_wsgi.py::test_middleware_debug_invalid_ip PASSED              [ 99%]
tests/test_wsgi.py::test_wrap PASSED                                     [100%]

=================================== FAILURES ===================================
_____________________ test_adapter_retry_on_status_raises ______________________

mock_urllib3 = <tests.test_requests.Urllib3Mock object at 0x7fe49ba40d90>
backends = <itertools.cycle object at 0x7fe49ba521c0>

    def test_adapter_retry_on_status_raises(mock_urllib3, backends):
        session = requests.Session()
        retry = urllib3.Retry(3, backoff_factor=1, status_forcelist=[503])
        adapter = talisker.requests.TaliskerAdapter(
            backend_iter=backends,
            max_retries=retry,
        )
        session.mount('http://name', adapter)
    
        mock_urllib3.set_response('OH NOES', '503 Service Unavailable')
    
        with pytest.raises(requests.exceptions.RetryError):
>           session.get('http://name/foo')

/home/jugmac00/Canonical/sources/talisker/tests/test_requests.py:610: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:602: in get
    return self.request("GET", url, **kwargs)
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py:468: in send
    return self._send(request, *args, **kwargs)
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py:522: in _send
    request._retry = request._retry.increment(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = Retry(total=3, connect=None, read=None, redirect=None, status=None)
method = 'GET', url = 'http://1.2.3.4:8000/foo'
response = <http.client.HTTPResponse object at 0x7fe49ba4cc70>, error = None
_pool = None, _stacktrace = None

    def increment(
        self,
        method: str | None = None,
        url: str | None = None,
        response: BaseHTTPResponse | None = None,
        error: Exception | None = None,
        _pool: ConnectionPool | None = None,
        _stacktrace: TracebackType | None = None,
    ) -> Retry:
        """Return a new Retry object with incremented retry counters.
    
        :param response: A response object, or None, if the server did not
            return a response.
        :type response: :class:`~urllib3.response.BaseHTTPResponse`
        :param Exception error: An error encountered during the request, or
            None if the response was received successfully.
    
        :return: A new ``Retry`` object.
        """
        if self.total is False and error:
            # Disabled, indicate to re-raise the error.
            raise reraise(type(error), error, _stacktrace)
    
        total = self.total
        if total is not None:
            total -= 1
    
        connect = self.connect
        read = self.read
        redirect = self.redirect
        status_count = self.status
        other = self.other
        cause = "unknown"
        status = None
        redirect_location = None
    
        if error and self._is_connection_error(error):
            # Connect retry?
            if connect is False:
                raise reraise(type(error), error, _stacktrace)
            elif connect is not None:
                connect -= 1
    
        elif error and self._is_read_error(error):
            # Read retry?
            if read is False or method is None or not self._is_method_retryable(method):
                raise reraise(type(error), error, _stacktrace)
            elif read is not None:
                read -= 1
    
        elif error:
            # Other retry?
            if other is not None:
                other -= 1
    
>       elif response and response.get_redirect_location():
E       AttributeError: 'HTTPResponse' object has no attribute 'get_redirect_location'

/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/urllib3/util/retry.py:479: AttributeError
------------------------------ Captured log call -------------------------------
DEBUG    urllib3.connectionpool:connectionpool.py:244 Starting new HTTP connection (1): 1.2.3.4:8000
______________ test_adapter_retry_on_status_returns_when_no_raise ______________

mock_urllib3 = <tests.test_requests.Urllib3Mock object at 0x7fe49b9e8b20>
backends = <itertools.cycle object at 0x7fe49b97f4c0>

    def test_adapter_retry_on_status_returns_when_no_raise(mock_urllib3, backends):
        session = requests.Session()
        retry = urllib3.Retry(
            3, backoff_factor=1, status_forcelist=[503], raise_on_status=False,
        )
        adapter = talisker.requests.TaliskerAdapter(
            backend_iter=backends,
            max_retries=retry,
        )
        session.mount('http://name', adapter)
    
        mock_urllib3.set_response('OH NOES', '503 Service Unavailable')
>       response = session.get('http://name/foo')

/home/jugmac00/Canonical/sources/talisker/tests/test_requests.py:632: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:602: in get
    return self.request("GET", url, **kwargs)
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py:468: in send
    return self._send(request, *args, **kwargs)
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py:522: in _send
    request._retry = request._retry.increment(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = Retry(total=3, connect=None, read=None, redirect=None, status=None)
method = 'GET', url = 'http://1.2.3.4:8000/foo'
response = <http.client.HTTPResponse object at 0x7fe49b97b6d0>, error = None
_pool = None, _stacktrace = None

    def increment(
        self,
        method: str | None = None,
        url: str | None = None,
        response: BaseHTTPResponse | None = None,
        error: Exception | None = None,
        _pool: ConnectionPool | None = None,
        _stacktrace: TracebackType | None = None,
    ) -> Retry:
        """Return a new Retry object with incremented retry counters.
    
        :param response: A response object, or None, if the server did not
            return a response.
        :type response: :class:`~urllib3.response.BaseHTTPResponse`
        :param Exception error: An error encountered during the request, or
            None if the response was received successfully.
    
        :return: A new ``Retry`` object.
        """
        if self.total is False and error:
            # Disabled, indicate to re-raise the error.
            raise reraise(type(error), error, _stacktrace)
    
        total = self.total
        if total is not None:
            total -= 1
    
        connect = self.connect
        read = self.read
        redirect = self.redirect
        status_count = self.status
        other = self.other
        cause = "unknown"
        status = None
        redirect_location = None
    
        if error and self._is_connection_error(error):
            # Connect retry?
            if connect is False:
                raise reraise(type(error), error, _stacktrace)
            elif connect is not None:
                connect -= 1
    
        elif error and self._is_read_error(error):
            # Read retry?
            if read is False or method is None or not self._is_method_retryable(method):
                raise reraise(type(error), error, _stacktrace)
            elif read is not None:
                read -= 1
    
        elif error:
            # Other retry?
            if other is not None:
                other -= 1
    
>       elif response and response.get_redirect_location():
E       AttributeError: 'HTTPResponse' object has no attribute 'get_redirect_location'

/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/urllib3/util/retry.py:479: AttributeError
------------------------------ Captured log call -------------------------------
DEBUG    urllib3.connectionpool:connectionpool.py:244 Starting new HTTP connection (1): 1.2.3.4:8000
_____________________ test_adapter_retry_on_status_header ______________________

mock_urllib3 = <tests.test_requests.Urllib3Mock object at 0x7fe49b80b610>
backends = <itertools.cycle object at 0x7fe49b8ccf40>

    def test_adapter_retry_on_status_header(mock_urllib3, backends):
        session = requests.Session()
        retry = urllib3.Retry(3, backoff_factor=1, status_forcelist=[503])
        adapter = talisker.requests.TaliskerAdapter(
            backend_iter=backends,
            max_retries=retry,
        )
        session.mount('http://name', adapter)
    
        headers = {'Retry-After': '1'}
        mock_urllib3.set_response(
            'OH NOES', '503 Service Unavailable', headers)
    
        with pytest.raises(requests.exceptions.RetryError):
>           session.get('http://name/foo')

/home/jugmac00/Canonical/sources/talisker/tests/test_requests.py:658: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:602: in get
    return self.request("GET", url, **kwargs)
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py:468: in send
    return self._send(request, *args, **kwargs)
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py:522: in _send
    request._retry = request._retry.increment(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = Retry(total=3, connect=None, read=None, redirect=None, status=None)
method = 'GET', url = 'http://1.2.3.4:8000/foo'
response = <http.client.HTTPResponse object at 0x7fe49b802550>, error = None
_pool = None, _stacktrace = None

    def increment(
        self,
        method: str | None = None,
        url: str | None = None,
        response: BaseHTTPResponse | None = None,
        error: Exception | None = None,
        _pool: ConnectionPool | None = None,
        _stacktrace: TracebackType | None = None,
    ) -> Retry:
        """Return a new Retry object with incremented retry counters.
    
        :param response: A response object, or None, if the server did not
            return a response.
        :type response: :class:`~urllib3.response.BaseHTTPResponse`
        :param Exception error: An error encountered during the request, or
            None if the response was received successfully.
    
        :return: A new ``Retry`` object.
        """
        if self.total is False and error:
            # Disabled, indicate to re-raise the error.
            raise reraise(type(error), error, _stacktrace)
    
        total = self.total
        if total is not None:
            total -= 1
    
        connect = self.connect
        read = self.read
        redirect = self.redirect
        status_count = self.status
        other = self.other
        cause = "unknown"
        status = None
        redirect_location = None
    
        if error and self._is_connection_error(error):
            # Connect retry?
            if connect is False:
                raise reraise(type(error), error, _stacktrace)
            elif connect is not None:
                connect -= 1
    
        elif error and self._is_read_error(error):
            # Read retry?
            if read is False or method is None or not self._is_method_retryable(method):
                raise reraise(type(error), error, _stacktrace)
            elif read is not None:
                read -= 1
    
        elif error:
            # Other retry?
            if other is not None:
                other -= 1
    
>       elif response and response.get_redirect_location():
E       AttributeError: 'HTTPResponse' object has no attribute 'get_redirect_location'

/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/urllib3/util/retry.py:479: AttributeError
------------------------------ Captured log call -------------------------------
DEBUG    urllib3.connectionpool:connectionpool.py:244 Starting new HTTP connection (1): 1.2.3.4:8000

---------- coverage: platform linux, python 3.8.10-final-0 -----------
Name                                                                                                     Stmts   Miss  Cover
----------------------------------------------------------------------------------------------------------------------------
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/__init__.py       144     85    41%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/__main__.py         3      0   100%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/celery.py         128    128     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/config.py         223     52    77%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/context.py        230    135    41%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/django.py          30     30     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/endpoints.py      197    197     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/flask.py           64     64     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/gunicorn.py        89     89     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/logs.py           252     82    67%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/metrics.py         82     40    51%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/postgresql.py     124    124     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/prometheus.py     111    111     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/render.py         175    175     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/requests.py       294    245    17%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/sentry.py         252    204    19%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/statsd.py          58     29    50%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/testing.py        327    327     0%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/util.py           155     59    62%
/home/jugmac00/Canonical/sources/talisker/.tox/py38/lib/python3.8/site-packages/talisker/wsgi.py           330    330     0%
/home/jugmac00/Canonical/sources/talisker/talisker/__init__.py                                             144    118    18%
/home/jugmac00/Canonical/sources/talisker/talisker/__main__.py                                               3      3     0%
/home/jugmac00/Canonical/sources/talisker/talisker/celery.py                                               128     86    33%
/home/jugmac00/Canonical/sources/talisker/talisker/config.py                                               223     18    92%
/home/jugmac00/Canonical/sources/talisker/talisker/context.py                                              230     27    88%
/home/jugmac00/Canonical/sources/talisker/talisker/django.py                                                30     30     0%
/home/jugmac00/Canonical/sources/talisker/talisker/endpoints.py                                            197    131    34%
/home/jugmac00/Canonical/sources/talisker/talisker/flask.py                                                 64     64     0%
/home/jugmac00/Canonical/sources/talisker/talisker/gunicorn.py                                              89     89     0%
/home/jugmac00/Canonical/sources/talisker/talisker/logs.py                                                 252     11    96%
/home/jugmac00/Canonical/sources/talisker/talisker/metrics.py                                               82     23    72%
/home/jugmac00/Canonical/sources/talisker/talisker/postgresql.py                                           124    124     0%
/home/jugmac00/Canonical/sources/talisker/talisker/prometheus.py                                           111     81    27%
/home/jugmac00/Canonical/sources/talisker/talisker/render.py                                               175     10    94%
/home/jugmac00/Canonical/sources/talisker/talisker/requests.py                                             294     20    93%
/home/jugmac00/Canonical/sources/talisker/talisker/sentry.py                                               252    196    22%
/home/jugmac00/Canonical/sources/talisker/talisker/statsd.py                                                58      3    95%
/home/jugmac00/Canonical/sources/talisker/talisker/testing.py                                              327     80    76%
/home/jugmac00/Canonical/sources/talisker/talisker/util.py                                                 155     27    83%
/home/jugmac00/Canonical/sources/talisker/talisker/wsgi.py                                                 330     47    86%
----------------------------------------------------------------------------------------------------------------------------
TOTAL                                                                                                     6536   3694    43%

========= 3 failed, 199 passed, 31 skipped, 1 xfailed in 6.02 seconds ==========
py38: exit 1 (6.75 seconds) /home/jugmac00/Canonical/sources/talisker> py.test -v --cov=talisker --no-success-flaky-report pid=3414916
  py38: FAIL code 1 (15.55=setup[8.81]+cmd[6.75] seconds)
  evaluation failed :( (15.61 seconds)
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml
index 280a7e5b..30f9f349 100644
--- a/.github/workflows/tox.yml
+++ b/.github/workflows/tox.yml
@@ -32,6 +32,8 @@ jobs:
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python }}
+        env:
+          PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org"
       - name: Install dependencies
         run: |
           sudo apt update
diff --git a/requirements.devel.txt b/requirements.devel.txt
index 5ea0a4f6..cea4f609 100644
--- a/requirements.devel.txt
+++ b/requirements.devel.txt
@@ -1,1 +1,1 @@
-tox==3.14.3
+tox==3.28.0
diff --git a/requirements.docs.txt b/requirements.docs.txt
index 8458faf2..7628b13e 100644
--- a/requirements.docs.txt
+++ b/requirements.docs.txt
@@ -1,4 +1,1 @@
-Sphinx==2.4.4
-docutils==0.16
-jinja2<3.1      # We need an older version due to compatilibility
-markupsafe<2.1  # Needed because of jinja2
+Sphinx==7.0.1
diff --git a/setup.cfg b/setup.cfg
index 32977d4b..5e74955c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -51,7 +51,9 @@ install_requires =
 	contextvars~=2.4;python_version>="3.5" and python_version<"3.7"
 
 [options.extras_require]
-gunicorn = gunicorn>=19.7.0
+gunicorn =
+	gunicorn>=19.7.0;python_version>"3.6"
+	gunicorn==19.7.0,<21.0;python_version>="3.5" and python_version<"3.8"
 raven = raven>=6.4.0
 celery =
 	celery~=4.4;python_version~="3.5.0"
diff --git a/setup.py b/setup.py
index 26576dbe..5668f57a 100644
--- a/setup.py
+++ b/setup.py
@@ -160,7 +160,8 @@
             'gevent>=20.9.0',
         ],
         gunicorn=[
-            'gunicorn>=19.7.0',
+            'gunicorn>=19.7.0;python_version>"3.6"',
+            'gunicorn>=19.7.0,<21.0;python_version>="3.5" and python_version<"3.8"',
         ],
         pg=[
             'sqlparse>=0.4.2',


Test patch:
diff --git a/requirements.tests.txt b/requirements.tests.txt
index d36d68c7..809d8914 100644
--- a/requirements.tests.txt
+++ b/requirements.tests.txt
@@ -16,7 +16,8 @@ setuptools==44.0.0;python_version<"3.10"
 setuptools>64;python_version>="3.10"
 coverage==5.0.3;python_version<"3.10"
 coverage>=6;python_version>="3.10"
-flaky==3.6.1
+flaky==3.7.0;python_version<"3.6"
+flaky==3.8.1;python_version>="3.6"
 
 # for integration tests
 # eventlet is pinned until https://github.com/benoitc/gunicorn/pull/2581
diff --git a/tests/test_requests.py b/tests/test_requests.py
index 11f3448e..156e67dc 100644
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -44,6 +44,15 @@
 import talisker.statsd
 import talisker.testing
 
+try:
+    # Compatible urllib3 HTTPResponses subclass their Base class.
+    URLLIB3_COMPATIBLE_VERSION = issubclass(
+        urllib3.response.HTTPResponse,
+        urllib3.response.BaseHTTPResponse
+    )
+except AttributeError:
+    URLLIB3_COMPATIBLE_VERSION = False
+
 
 def request(method='GET', host='http://example.com', url='/', **kwargs):
     req = requests.Request(method, url=host + url, **kwargs)
@@ -481,16 +490,36 @@ def set_responses(self, responses):
         self.response_iter = iter(responses)
 
     def make_response(self, content, status='200 OK', headers={}):
-        """Make a fake http.client.HTTPResponse based on a byte stream."""
+        """Make a fake HTTPResponse based on a byte stream.
+
+        For versions of urllib3 where urllib3.response.HTTPResponse is
+        not API-compatible with http.client.HTTPResponse, we return the
+        http.client version. For versions after, we return a
+        urllib3.response.HTTPResponse.
+        """
+        if not headers:
+            headers["Content-Type"] = "text/html"
+
         formatted_headers = '\r\n'.join(
             '{}: {}'.format(k, v) for k, v in headers.items()
         )
-        stream = 'HTTP/1.1 {}\r\n{}\r\n{}'.format(
+        stream = 'HTTP/1.1 {}\r\n{}\r\n\r\n{}'.format(
             status, formatted_headers, content,
         )
         sock = FakeSocket(stream.encode('utf8'))
-        response = http.client.HTTPResponse(sock)
-        response.begin()  # parse the stream
+        http_response = http.client.HTTPResponse(sock)
+        http_response.begin()
+
+        if not URLLIB3_COMPATIBLE_VERSION:
+            return http_response
+
+        response = urllib3.response.HTTPResponse(
+            body=http_response,
+            headers=headers,
+            status=http_response.status,
+            preload_content=False,
+            original_response=http_response,
+        )
         return response
 
     def make_request(self, pool, conn, method, url, **kwargs):
@@ -688,7 +717,8 @@ def test_adapter_exceptions_match_default(mock_urllib3, retry, response):
 
     exc = None
     try:
-        session.get('http://default/')
+        response = session.get('http://default/')
+        response.raise_for_status()
     except Exception as e:
         exc = e