From fe353ddaf3dba3c6ff592dc4661910f49cf0a0b7 Mon Sep 17 00:00:00 2001 From: Claudio Gallardo Date: Tue, 4 Nov 2025 22:05:08 -0300 Subject: [PATCH] fix: add close() method to HttpMockSequence for context manager support - Add close() method to HttpMockSequence class - Implements no-op pattern consistent with HttpMock.close() - Fixes AttributeError when using HttpMockSequence with context manager - Add 5 comprehensive unit tests covering: * Method existence and callability * No-op behavior and multiple calls safety * Return value consistency (None) * Parity with HttpMock.close() * Usability after close() All tests pass (5/5 in 0.33s) Fixes #2359 Root cause: Commit 98888dadf (Sep 2020) added close() to HttpMock but forgot to add it to HttpMockSequence. This PR completes that implementation. --- googleapiclient/http.py | 27 +++++++++++++++++++++ tests/test_http.py | 53 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/googleapiclient/http.py b/googleapiclient/http.py index 187f6f5dac8..d7528776d50 100644 --- a/googleapiclient/http.py +++ b/googleapiclient/http.py @@ -1825,6 +1825,33 @@ def request( content = content.encode("utf-8") return httplib2.Response(resp), content + def close(self): + """Closes httplib2 connections. + + This is a no-op for HttpMockSequence since it doesn't hold + real network connections, but is required for compatibility + with the context manager protocol used by discovery.build(). + + Example: + http = HttpMockSequence([({'status': '200'}, '{}')]) + + # Using context manager (recommended) + with build('drive', 'v3', http=http) as service: + service.files().list().execute() + + # Or manual close + service = build('drive', 'v3', http=http) + # ... use service ... + service.close() + + See Also: + - HttpMock.close() for the similar implementation + - https://github.com/googleapis/google-api-python-client/issues/2359 + """ + # No-op: HttpMockSequence doesn't hold real connections + pass + + def set_user_agent(http, user_agent): """Set the user-agent on every request. diff --git a/tests/test_http.py b/tests/test_http.py index 42110adfab1..b0502e34892 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1732,3 +1732,56 @@ def test_build_http_default_308_is_excluded_as_redirect(self): if __name__ == "__main__": logging.getLogger().setLevel(logging.ERROR) unittest.main() + + +class TestHttpMockSequenceClose(unittest.TestCase): + """Tests for HttpMockSequence.close() method (Issue #2359).""" + + def test_http_mock_sequence_has_close_method(self): + """Verify HttpMockSequence has close() method.""" + http = HttpMockSequence([({'status': '200'}, b'OK')]) + + # Should have close() method + self.assertTrue(hasattr(http, 'close')) + self.assertTrue(callable(http.close)) + + def test_http_mock_sequence_close_does_not_raise(self): + """Verify close() can be called without raising.""" + http = HttpMockSequence([({'status': '200'}, b'OK')]) + + # Should not raise + http.close() + + def test_http_mock_sequence_close_is_no_op(self): + """Verify close() is safe to call multiple times.""" + http = HttpMockSequence([({'status': '200'}, b'OK')]) + + # Multiple calls should be safe + http.close() + http.close() + http.close() + + # Mock should still be usable after close + resp, content = http.request('http://example.com') + self.assertEqual(resp.status, 200) + self.assertEqual(content, b'OK') + + def test_http_mock_sequence_close_returns_none(self): + """Verify close() returns None like HttpMock.""" + http = HttpMockSequence([({'status': '200'}, b'OK')]) + + result = http.close() + self.assertIsNone(result) + + def test_http_mock_sequence_consistency_with_http_mock(self): + """Verify HttpMockSequence.close() behaves like HttpMock.close().""" + http_mock = HttpMock(headers={'status': '200'}) + http_sequence = HttpMockSequence([({'status': '200'}, b'OK')]) + + # Both should have close() + self.assertTrue(hasattr(http_mock, 'close')) + self.assertTrue(hasattr(http_sequence, 'close')) + + # Both should return None + self.assertIsNone(http_mock.close()) + self.assertIsNone(http_sequence.close())