Repository: ross/requests-futures (language: python)

Issue:
Use pytest-httpbin?
Is there a specific need to use a public httpbin service during tests? E.g. [pytest-httpbin](https://pypi.org/project/pytest-httpbin/) can easily provide the equivalent service locally and would make the test suite work reliably independently of the current state of Internet connection and the remote service.


Discussion / hints:
Nothing prevents using pytest-httpbin. It didn't exist when things were first written and I didn't find anything similar at the time.
Ah, cool. Does that mean you're likely to do it or more like you're open to pull requests if somebody else does it?
> Ah, cool. Does that mean you're likely to do it or more like you're open to pull requests if somebody else does it?

It'll happen faster if someone else opens the PR. Otherwise it'll get throw onto my TODO list with fairly low priority since it's just a testing improvement and not a super meaningful change for clients.
It should be trivial but right now httpbin doesn't work out of the box without patching or restricting werkzeug version, and I think it's better to solve that at pytest-httpbin level than add workarounds here. I've asked for a workaround on https://github.com/kevin1024/pytest-httpbin/issues/72.
Ok, `httpbin` is fixed now. However, FWICS the test suite is currently using the unittest-based class approach which is incompatible with pytest fixtures. Would you mind me rewriting the test suite to use pure pytest?
> Would you mind me rewriting the test suite to use pure pytest?

Should be fine. Don't imagine it changes much.
Actually, I've found out how to combine fixtures with unittest-style tests, so I managed without ;-). I'll submit a "minimal change" PR in a minute.

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index dfbce85..3f4a557 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,4 @@ known_first_party="requests_futures"
 line_length=80
 
 [tool.pytest.ini_options]
-markers = [
-    "network: tests that require network connectivity to pass"
-]
 pythonpath = "."
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 2e6943c..330d5b6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,34 +1,60 @@
-Pygments==2.15.1
-black==23.3.0
+Flask==2.3.2
+Jinja2==3.1.2
+MarkupSafe==2.1.3
+PyYAML==6.0.1
+Pygments==2.16.1
+SecretStorage==3.3.3
+Werkzeug==2.3.6
+attrs==23.1.0
+black==23.7.0
 bleach==6.0.0
+blinker==1.6.2
+brotlicffi==1.0.9.2
 build==0.10.0
-click==8.1.3
+cffi==1.15.1
+click==8.1.6
 coverage==7.2.7
+cryptography==41.0.3
+decorator==5.1.1
 docutils==0.20.1
-importlib-metadata==6.7.0
+flasgger==0.9.7.1
+gevent==23.7.0
+greenlet==2.0.2
+httpbin==0.10.0
+importlib-metadata==6.8.0
 iniconfig==2.0.0
 isort==5.12.0
-jaraco.classes==3.2.3
-keyring==23.13.1
+itsdangerous==2.1.2
+jaraco.classes==3.3.0
+jeepney==0.8.0
+jsonschema-specifications==2023.7.1
+jsonschema==4.19.0
+keyring==24.2.0
 markdown-it-py==3.0.0
 mdurl==0.1.2
-more-itertools==9.1.0
+mistune==3.0.1
+more-itertools==10.1.0
 mypy-extensions==1.0.0
 packaging==23.1
-pathspec==0.11.1
+pathspec==0.11.2
 pkginfo==1.9.6
-platformdirs==3.6.0
-pluggy==1.1.0
-pyflakes==3.0.1
+platformdirs==3.10.0
+pluggy==1.2.0
+pycparser==2.21
+pyflakes==3.1.0
 pyproject_hooks==1.0.0
 pytest-cov==4.1.0
-pytest-network==0.0.1
-pytest==7.3.2
+pytest-httpbin==2.0.0
+pytest==7.4.0
 readme-renderer==40.0
+referencing==0.30.2
 requests-toolbelt==1.0.0
 rfc3986==2.0.0
-rich==13.4.2
+rich==13.5.2
+rpds-py==0.9.2
 six==1.16.0
 twine==4.0.2
 webencodings==0.5.1
-zipp==3.15.0
+zipp==3.16.2
+zope.event==5.0
+zope.interface==6.0
diff --git a/requirements.txt b/requirements.txt
index fd41302..8ef629c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 certifi==2023.7.22
-charset-normalizer==3.1.0
+charset-normalizer==3.2.0
 idna==3.4
 requests==2.31.0
-urllib3==2.0.3
+urllib3==2.0.4
diff --git a/script/cibuild-setup-py b/script/cibuild-setup-py
index 009013c..2f47280 100755
--- a/script/cibuild-setup-py
+++ b/script/cibuild-setup-py
@@ -15,7 +15,6 @@ python setup.py build
 echo "## validate setup.py install ###################################################"
 python setup.py install
 echo "## validate tests can run against installed code ###############################"
-pip install pytest pytest-network
-# TODO: --disable-network
+pip install pytest pytest-httpbin
 pytest 
 echo "## complete ####################################################################"
diff --git a/script/coverage b/script/coverage
index efbaf9a..4b121dd 100755
--- a/script/coverage
+++ b/script/coverage
@@ -41,7 +41,6 @@ grep -r -I --line-number "# pragma: +no.*cover" $SOURCE_DIR && {
 export PYTHONPATH=.:$PYTHONPATH
 
 # TODO: 
-# --disable-network \
 # --cov-fail-under=100 \
 pytest \
   --cov-reset \
diff --git a/setup.py b/setup.py
index 8d3589c..c2344b7 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@
 
 requires = ['requests>=1.2.0']
 
-tests_require = ('pytest>=6.2.5', 'pytest-cov>=3.0.0', 'pytest-network>=0.0.1')
+tests_require = ('pytest>=6.2.5', 'pytest-cov>=3.0.0', 'pytest-httpbin')
 
 setup(
     name='requests-futures',


Test patch:
diff --git a/script/test b/script/test
index 9207cfe..036a736 100755
--- a/script/test
+++ b/script/test
@@ -32,5 +32,4 @@ export ARM_SUBSCRIPTION_ID=
 
 export PYTHONPATH=.:$PYTHONPATH
 
-# TODO: --disable-network \
 pytest "$@"
diff --git a/tests/test_requests_futures.py b/tests/test_requests_futures.py
index ddea3fa..626ebc5 100644
--- a/tests/test_requests_futures.py
+++ b/tests/test_requests_futures.py
@@ -26,24 +26,23 @@
 logging.getLogger('FuturesSession').level = logging.ERROR
 
 
-def httpbin(*suffix):
-    """Returns url for HTTPBIN resource."""
-    return HTTPBIN + '/'.join(suffix)
+@pytest.fixture(scope="class", autouse=True)
+def httpbin_on_class(request, httpbin):
+    request.cls.httpbin = httpbin
 
 
 class RequestsTestCase(TestCase):
-    @pytest.mark.network
     def test_futures_session(self):
         # basic futures get
         sess = FuturesSession()
-        future = sess.get(httpbin('get'))
+        future = sess.get(self.httpbin.join('get'))
         self.assertIsInstance(future, Future)
         resp = future.result()
         self.assertIsInstance(resp, Response)
         self.assertEqual(200, resp.status_code)
 
         # non-200, 404
-        future = sess.get(httpbin('status/404'))
+        future = sess.get(self.httpbin.join('status/404'))
         resp = future.result()
         self.assertEqual(404, resp.status_code)
 
@@ -53,7 +52,7 @@ def cb(s, r):
             # add the parsed json data to the response
             r.data = r.json()
 
-        future = sess.get(httpbin('get'), background_callback=cb)
+        future = sess.get(self.httpbin.join('get'), background_callback=cb)
         # this should block until complete
         resp = future.result()
         self.assertEqual(200, resp.status_code)
@@ -63,18 +62,19 @@ def cb(s, r):
         def rasing_cb(s, r):
             raise Exception('boom')
 
-        future = sess.get(httpbin('get'), background_callback=rasing_cb)
+        future = sess.get(
+            self.httpbin.join('get'), background_callback=rasing_cb
+        )
         with self.assertRaises(Exception) as cm:
             resp = future.result()
         self.assertEqual('boom', cm.exception.args[0])
 
-    @pytest.mark.network
     def test_supplied_session(self):
         """Tests the `session` keyword argument."""
         requests_session = session()
         requests_session.headers['Foo'] = 'bar'
         sess = FuturesSession(session=requests_session)
-        future = sess.get(httpbin('headers'))
+        future = sess.get(self.httpbin.join('headers'))
         self.assertIsInstance(future, Future)
         resp = future.result()
         self.assertIsInstance(resp, Response)
@@ -116,21 +116,19 @@ def test_adapter_kwargs(self):
         )
         self.assertEqual(session.get_adapter('http://')._pool_connections, 20)
 
-    @pytest.mark.network
     def test_redirect(self):
         """Tests for the ability to cleanly handle redirects."""
         sess = FuturesSession()
-        future = sess.get(httpbin('redirect-to?url=get'))
+        future = sess.get(self.httpbin.join('redirect-to?url=get'))
         self.assertIsInstance(future, Future)
         resp = future.result()
         self.assertIsInstance(resp, Response)
         self.assertEqual(200, resp.status_code)
 
-        future = sess.get(httpbin('redirect-to?url=status/404'))
+        future = sess.get(self.httpbin.join('redirect-to?url=status/404'))
         resp = future.result()
         self.assertEqual(404, resp.status_code)
 
-    @pytest.mark.network
     def test_context(self):
         class FuturesSessionTestHelper(FuturesSession):
             def __init__(self, *args, **kwargs):
@@ -146,7 +144,7 @@ def __exit__(self, *args, **kwargs):
         passout = None
         with FuturesSessionTestHelper() as sess:
             passout = sess
-            future = sess.get(httpbin('get'))
+            future = sess.get(self.httpbin.join('get'))
             self.assertIsInstance(future, Future)
             resp = future.result()
             self.assertIsInstance(resp, Response)
@@ -187,18 +185,15 @@ def setUp(self):
         self.proc_executor = ProcessPoolExecutor(max_workers=2)
         self.session = session()
 
-    @pytest.mark.network
     @skipIf(session_required, 'not supported in python < 3.5')
     def test_futures_session(self):
         self._assert_futures_session()
 
-    @pytest.mark.network
     @skipIf(not session_required, 'fully supported on python >= 3.5')
     def test_exception_raised(self):
         with self.assertRaises(RuntimeError):
             self._assert_futures_session()
 
-    @pytest.mark.network
     def test_futures_existing_session(self):
         self.session.headers['Foo'] = 'bar'
         self._assert_futures_session(session=self.session)
@@ -210,19 +205,20 @@ def _assert_futures_session(self, session=None):
         else:
             sess = FuturesSession(executor=self.proc_executor)
 
-        future = sess.get(httpbin('get'))
+        future = sess.get(self.httpbin.join('get'))
         self.assertIsInstance(future, Future)
         resp = future.result()
         self.assertIsInstance(resp, Response)
         self.assertEqual(200, resp.status_code)
 
         # non-200, 404
-        future = sess.get(httpbin('status/404'))
+        future = sess.get(self.httpbin.join('status/404'))
         resp = future.result()
         self.assertEqual(404, resp.status_code)
 
         future = sess.get(
-            httpbin('get'), background_callback=global_cb_modify_response
+            self.httpbin.join('get'),
+            background_callback=global_cb_modify_response,
         )
         # this should block until complete
         resp = future.result()
@@ -233,35 +229,36 @@ def _assert_futures_session(self, session=None):
         self.assertTrue(hasattr(resp, 'data'))
 
         future = sess.get(
-            httpbin('get'), background_callback=global_cb_return_result
+            self.httpbin.join('get'),
+            background_callback=global_cb_return_result,
         )
         # this should block until complete
         resp = future.result()
         # make sure the callback was invoked
         self.assertIsInstance(resp, dict)
 
-        future = sess.get(httpbin('get'), background_callback=global_rasing_cb)
+        future = sess.get(
+            self.httpbin.join('get'), background_callback=global_rasing_cb
+        )
         with self.assertRaises(Exception) as cm:
             resp = future.result()
         self.assertEqual('boom', cm.exception.args[0])
 
         # Tests for the ability to cleanly handle redirects
-        future = sess.get(httpbin('redirect-to?url=get'))
+        future = sess.get(self.httpbin.join('redirect-to?url=get'))
         self.assertIsInstance(future, Future)
         resp = future.result()
         self.assertIsInstance(resp, Response)
         self.assertEqual(200, resp.status_code)
 
-        future = sess.get(httpbin('redirect-to?url=status/404'))
+        future = sess.get(self.httpbin.join('redirect-to?url=status/404'))
         resp = future.result()
         self.assertEqual(404, resp.status_code)
 
-    @pytest.mark.network
     @skipIf(session_required, 'not supported in python < 3.5')
     def test_context(self):
         self._assert_context()
 
-    @pytest.mark.network
     def test_context_with_session(self):
         self._assert_context(session=self.session)
 
@@ -275,7 +272,7 @@ def _assert_context(self, session=None):
         passout = None
         with helper_instance as sess:
             passout = sess
-            future = sess.get(httpbin('get'))
+            future = sess.get(self.httpbin.join('get'))
             self.assertIsInstance(future, Future)
             resp = future.result()
             self.assertIsInstance(resp, Response)
@@ -296,12 +293,11 @@ def __exit__(self, *args, **kwargs):
 
 @skipIf(not unsupported_platform, 'Exception raised when unsupported')
 class ProcessPoolExceptionRaisedTestCase(TestCase):
-    @pytest.mark.network
     def test_exception_raised(self):
         executor = ProcessPoolExecutor(max_workers=2)
         sess = FuturesSession(executor=executor, session=session())
         with self.assertRaises(RuntimeError):
-            sess.get(httpbin('get'))
+            sess.get(self.httpbin.join('get'))
 
 
 if __name__ == '__main__':