Skip to content

Commit 869bc92

Browse files
Pass parameters to the underlying htttp client (#520)
1 parent 240c5dd commit 869bc92

7 files changed

Lines changed: 345 additions & 6 deletions

File tree

tests/http_client_kwargs_test.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
"""Tests for HTTP client kwargs forwarding to httpx.
2+
3+
This test module verifies that all kwargs passed to Wikipedia/AsyncWikipedia
4+
constructors are properly forwarded to underlying httpx client constructors.
5+
"""
6+
7+
import httpx
8+
import pytest
9+
import respx
10+
11+
from tests.mock_data import create_mock_async_wikipedia
12+
from tests.mock_data import create_mock_wikipedia
13+
14+
15+
class TestSyncHTTPClientKwargs:
16+
"""Test kwargs forwarding in SyncHTTPClient."""
17+
18+
@respx.mock
19+
def test_timeout_parameter(self):
20+
"""Test that timeout parameter is forwarded to .Client."""
21+
wiki = create_mock_wikipedia(timeout=30.0)
22+
assert wiki._client.timeout == httpx.Timeout(30.0)
23+
24+
@respx.mock
25+
def test_proxies_parameter(self):
26+
"""Test that proxy parameter is forwarded to .Client."""
27+
proxy = "http://proxy.example.com:8080"
28+
wiki = create_mock_wikipedia(proxy=proxy)
29+
# httpx stores proxy info in the transport, not as a direct attribute
30+
# We can verify the client was created successfully
31+
assert wiki._client is not None
32+
assert hasattr(wiki._client, "_transport")
33+
34+
@respx.mock
35+
def test_verify_parameter_false(self):
36+
"""Test that verify=False is forwarded to .Client."""
37+
wiki = create_mock_wikipedia(verify=False)
38+
# httpx stores verify internally, but we can verify the client was created successfully
39+
assert wiki._client is not None
40+
# The fact that the client was created without error means verify was accepted
41+
42+
@respx.mock
43+
def test_verify_parameter_custom(self):
44+
"""Test that custom verify context is forwarded to .Client."""
45+
import ssl
46+
47+
custom_context = ssl.create_default_context()
48+
wiki = create_mock_wikipedia(verify=custom_context)
49+
# httpx stores verify internally, but we can verify the client was created successfully
50+
assert wiki._client is not None
51+
# The fact that the client was created without error means verify was accepted
52+
53+
@respx.mock
54+
def test_http2_parameter(self):
55+
"""Test that http2 parameter is forwarded to .Client."""
56+
try:
57+
wiki = create_mock_wikipedia(http2=True)
58+
assert wiki._client._http2 is True
59+
except ImportError as e:
60+
if "h2" in str(e):
61+
pytest.skip("http2 not available (h2 package not installed)")
62+
else:
63+
raise
64+
65+
@respx.mock
66+
def test_limits_parameter(self):
67+
"""Test that limits parameter is forwarded to .Client."""
68+
limits = httpx.Limits(max_connections=50, max_keepalive_connections=10)
69+
wiki = create_mock_wikipedia(limits=limits)
70+
# httpx stores limits internally, but we can verify client was created successfully
71+
assert wiki._client is not None
72+
# The fact that client was created without error means limits was accepted
73+
74+
@respx.mock
75+
def test_max_redirects_parameter(self):
76+
"""Test that max_redirects parameter is forwarded to .Client."""
77+
wiki = create_mock_wikipedia(max_redirects=5)
78+
assert wiki._client.max_redirects == 5
79+
80+
@respx.mock
81+
def test_follow_redirects_parameter(self):
82+
"""Test that follow_redirects parameter is forwarded to .Client."""
83+
wiki = create_mock_wikipedia(follow_redirects=True)
84+
assert wiki._client.follow_redirects is True
85+
86+
@respx.mock
87+
def test_auth_parameter(self):
88+
"""Test that auth parameter is forwarded to .Client."""
89+
auth = httpx.BasicAuth("username", "password")
90+
wiki = create_mock_wikipedia(auth=auth)
91+
assert wiki._client._auth == auth
92+
93+
@respx.mock
94+
def test_base_url_parameter(self):
95+
"""Test that base_url parameter is forwarded to .Client."""
96+
base_url = "https://custom-api.example.com"
97+
wiki = create_mock_wikipedia(base_url=base_url)
98+
assert str(wiki._client._base_url) == base_url
99+
100+
@respx.mock
101+
def test_default_encoding_parameter(self):
102+
"""Test that default_encoding parameter is forwarded to .Client."""
103+
wiki = create_mock_wikipedia(default_encoding="latin-1")
104+
assert wiki._client._default_encoding == "latin-1"
105+
106+
@respx.mock
107+
def test_trust_env_parameter(self):
108+
"""Test that trust_env parameter is forwarded to .Client."""
109+
wiki = create_mock_wikipedia(trust_env=False)
110+
assert wiki._client._trust_env is False
111+
112+
@respx.mock
113+
def test_combined_parameters(self):
114+
"""Test that multiple parameters work together."""
115+
proxy = "http://proxy.example.com:8080"
116+
limits = httpx.Limits(max_connections=50)
117+
118+
wiki = create_mock_wikipedia(
119+
timeout=20.0,
120+
proxy=proxy,
121+
verify=False,
122+
limits=limits,
123+
max_redirects=3,
124+
follow_redirects=True,
125+
)
126+
127+
# Test the parameters that are accessible
128+
assert wiki._client.timeout == httpx.Timeout(20.0)
129+
assert wiki._client.max_redirects == 3
130+
assert wiki._client.follow_redirects is True
131+
# Verify the client was created successfully (means all parameters were accepted)
132+
assert wiki._client is not None
133+
134+
@respx.mock
135+
def test_headers_are_preserved(self):
136+
"""Test that custom headers are preserved when using kwargs."""
137+
custom_headers = {"X-Custom-Header": "test-value"}
138+
wiki = create_mock_wikipedia(headers=custom_headers, timeout=30.0)
139+
140+
# Check that our custom header is present
141+
assert "X-Custom-Header" in wiki._client.headers
142+
assert wiki._client.headers["X-Custom-Header"] == "test-value"
143+
144+
# Check that User-Agent is still set correctly
145+
assert "User-Agent" in wiki._client.headers
146+
147+
@respx.mock
148+
def test_invalid_httpx_parameter_raises_error(self):
149+
"""Test that invalid httpx parameters raise appropriate errors."""
150+
# httpx should raise an error for invalid parameters
151+
with pytest.raises(TypeError):
152+
create_mock_wikipedia(invalid_param="should_fail")
153+
154+
155+
class TestAsyncHTTPClientKwargs:
156+
"""Test kwargs forwarding in AsyncHTTPClient."""
157+
158+
@respx.mock
159+
async def test_timeout_parameter(self):
160+
"""Test that timeout parameter is forwarded to .AsyncClient."""
161+
wiki = create_mock_async_wikipedia(timeout=30.0)
162+
assert wiki._client.timeout == httpx.Timeout(30.0)
163+
164+
@respx.mock
165+
async def test_proxies_parameter(self):
166+
"""Test that proxy parameter is forwarded to .AsyncClient."""
167+
proxy = "http://proxy.example.com:8080"
168+
wiki = create_mock_async_wikipedia(proxy=proxy)
169+
# httpx stores proxy info in the transport, not as a direct attribute
170+
# We can verify the client was created successfully
171+
assert wiki._client is not None
172+
assert hasattr(wiki._client, "_transport")
173+
174+
@respx.mock
175+
async def test_verify_parameter_false(self):
176+
"""Test that verify=False is forwarded to .AsyncClient."""
177+
wiki = create_mock_async_wikipedia(verify=False)
178+
# httpx stores verify internally, but we can verify the client was created successfully
179+
assert wiki._client is not None
180+
# The fact that the client was created without error means verify was accepted
181+
182+
@respx.mock
183+
async def test_http2_parameter(self):
184+
"""Test that http2 parameter is forwarded to .AsyncClient."""
185+
try:
186+
wiki = create_mock_async_wikipedia(http2=True)
187+
assert wiki._client._http2 is True
188+
except ImportError as e:
189+
if "h2" in str(e):
190+
pytest.skip("http2 not available (h2 package not installed)")
191+
else:
192+
raise
193+
194+
@respx.mock
195+
async def test_limits_parameter(self):
196+
"""Test that limits parameter is forwarded to .AsyncClient."""
197+
limits = httpx.Limits(max_connections=50, max_keepalive_connections=10)
198+
wiki = create_mock_async_wikipedia(limits=limits)
199+
# httpx stores limits internally, but we can verify client was created successfully
200+
assert wiki._client is not None
201+
# The fact that client was created without error means limits was accepted
202+
203+
@respx.mock
204+
async def test_max_redirects_parameter(self):
205+
"""Test that max_redirects parameter is forwarded to .AsyncClient."""
206+
wiki = create_mock_async_wikipedia(max_redirects=5)
207+
assert wiki._client.max_redirects == 5
208+
209+
@respx.mock
210+
async def test_follow_redirects_parameter(self):
211+
"""Test that follow_redirects parameter is forwarded to .AsyncClient."""
212+
wiki = create_mock_async_wikipedia(follow_redirects=True)
213+
assert wiki._client.follow_redirects is True
214+
215+
@respx.mock
216+
async def test_auth_parameter(self):
217+
"""Test that auth parameter is forwarded to .AsyncClient."""
218+
auth = httpx.BasicAuth("username", "password")
219+
wiki = create_mock_async_wikipedia(auth=auth)
220+
assert wiki._client._auth == auth
221+
222+
@respx.mock
223+
async def test_base_url_parameter(self):
224+
"""Test that base_url parameter is forwarded to .AsyncClient."""
225+
base_url = "https://custom-api.example.com"
226+
wiki = create_mock_async_wikipedia(base_url=base_url)
227+
assert str(wiki._client._base_url) == base_url
228+
229+
@respx.mock
230+
async def test_combined_parameters(self):
231+
"""Test that multiple parameters work together in async client."""
232+
proxy = "http://proxy.example.com:8080"
233+
limits = httpx.Limits(max_connections=50)
234+
235+
wiki = create_mock_async_wikipedia(
236+
timeout=20.0,
237+
proxy=proxy,
238+
verify=False,
239+
limits=limits,
240+
max_redirects=3,
241+
follow_redirects=True,
242+
)
243+
244+
# Test the parameters that are accessible
245+
assert wiki._client.timeout == httpx.Timeout(20.0)
246+
assert wiki._client.max_redirects == 3
247+
assert wiki._client.follow_redirects is True
248+
# Verify the client was created successfully (means all parameters were accepted)
249+
assert wiki._client is not None
250+
251+
@respx.mock
252+
async def test_headers_are_preserved(self):
253+
"""Test that custom headers are preserved when using kwargs in async client."""
254+
custom_headers = {"X-Custom-Header": "test-value"}
255+
wiki = create_mock_async_wikipedia(headers=custom_headers, timeout=30.0)
256+
257+
# Check that our custom header is present
258+
assert "X-Custom-Header" in wiki._client.headers
259+
assert wiki._client.headers["X-Custom-Header"] == "test-value"
260+
261+
# Check that User-Agent is still set correctly
262+
assert "User-Agent" in wiki._client.headers
263+
264+
@respx.mock
265+
async def test_invalid_httpx_parameter_raises_error(self):
266+
"""Test that invalid httpx parameters raise appropriate errors in async client."""
267+
# httpx should raise an error for invalid parameters
268+
with pytest.raises(TypeError):
269+
create_mock_async_wikipedia(invalid_param="should_fail")
270+
271+
272+
class TestKwargsBackwardCompatibility:
273+
"""Test that existing usage patterns still work."""
274+
275+
@respx.mock
276+
def test_backward_compatibility_timeout_only(self):
277+
"""Test that existing timeout-only usage still works."""
278+
# This is how users currently use timeout
279+
wiki = create_mock_wikipedia(timeout=25.0)
280+
assert wiki._client.timeout == httpx.Timeout(25.0)
281+
282+
@respx.mock
283+
async def test_backward_compatibility_timeout_only_async(self):
284+
"""Test that existing timeout-only usage still works in async client."""
285+
wiki = create_mock_async_wikipedia(timeout=25.0)
286+
assert wiki._client.timeout == httpx.Timeout(25.0)
287+
288+
@respx.mock
289+
def test_default_timeout_still_works(self):
290+
"""Test that default timeout is still applied when not specified."""
291+
wiki = create_mock_wikipedia() # No timeout specified
292+
assert wiki._client.timeout == httpx.Timeout(10.0) # Should be default
293+
294+
@respx.mock
295+
async def test_default_timeout_still_works_async(self):
296+
"""Test that default timeout is still applied when not specified in async client."""
297+
wiki = create_mock_async_wikipedia() # No timeout specified
298+
assert wiki._client.timeout == httpx.Timeout(10.0) # Should be default

tests/mock_data.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ async def api_request(language, params):
10061006
# ── CLI-specific mock data and helpers ───────────────────────────────────────────
10071007

10081008

1009-
def create_mock_wikipedia(language="en", variant=None, extract_format="wiki"):
1009+
def create_mock_wikipedia(language="en", variant=None, extract_format="wiki", **kwargs):
10101010
"""Create a mock Wikipedia instance for testing CLI functions."""
10111011
import wikipediaapi
10121012

@@ -1019,11 +1019,31 @@ def create_mock_wikipedia(language="en", variant=None, extract_format="wiki"):
10191019
if extract_format == "wiki"
10201020
else wikipediaapi.ExtractFormat.HTML
10211021
),
1022+
**kwargs,
10221023
)
10231024
wiki._get = wikipedia_api_request(wiki)
10241025
return wiki
10251026

10261027

1028+
def create_mock_async_wikipedia(language="en", variant=None, extract_format="wiki", **kwargs):
1029+
"""Create a mock AsyncWikipedia instance for testing."""
1030+
import wikipediaapi
1031+
1032+
wiki = wikipediaapi.AsyncWikipedia(
1033+
user_agent=user_agent,
1034+
language=language,
1035+
variant=variant,
1036+
extract_format=(
1037+
wikipediaapi.ExtractFormat.WIKI
1038+
if extract_format == "wiki"
1039+
else wikipediaapi.ExtractFormat.HTML
1040+
),
1041+
**kwargs,
1042+
)
1043+
wiki._get = async_wikipedia_api_request(wiki)
1044+
return wiki
1045+
1046+
10271047
def create_mock_page(title, pageid=1, exists=True, language="en", namespace=0):
10281048
"""Create a mock Wikipedia page for testing."""
10291049
import wikipediaapi

wikipediaapi/_http_client/async_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
5050
super().__init__(*args, **kwargs)
5151
self._client = httpx.AsyncClient(
5252
headers=self._default_headers,
53-
timeout=self._client_kwargs.get("timeout", 10.0),
53+
**self._client_kwargs,
5454
transport=httpx.AsyncHTTPTransport(),
5555
)
5656

wikipediaapi/_http_client/base_http_client.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ class BaseHTTPClient(ABC):
4343
Subclasses must implement ``_get(language, params)`` (sync or async)
4444
that issues the actual HTTP request.
4545
46+
**Note on HTTP Library**: This implementation uses ``httpx`` as the underlying
47+
HTTP client library. Advanced HTTP configuration (timeouts, proxies,
48+
SSL settings, connection limits, etc.) is exposed through the
49+
``SyncHTTPClient`` and ``AsyncHTTPClient`` classes. For most use
50+
cases, use the standard Wikipedia API parameters. Direct httpx
51+
configuration should only be needed for advanced use cases.
52+
4653
Instance attributes set by :meth:`__init__`:
4754
4855
:attr language: normalised Wikipedia language code (e.g. ``"en"``)
@@ -87,7 +94,13 @@ def __init__(
8794
:param retry_wait: base wait time in seconds between retries
8895
(exponential backoff); overridden by ``Retry-After`` for 429
8996
:param kwargs: forwarded to ``httpx`` client constructor
90-
(e.g. ``timeout=30.0``); ``timeout`` defaults to ``10.0``
97+
(e.g. ``timeout=30.0``, ``proxy={'https://': 'http://proxy.example.com:8080'}``,
98+
``verify=False``, ``http2=True``); ``timeout`` defaults to ``10.0``.
99+
**Advanced Usage**: These parameters provide direct access to httpx
100+
capabilities. For standard Wikipedia API usage, prefer the
101+
documented parameters above. Use httpx parameters only for
102+
specific requirements like custom proxies, SSL configuration, or
103+
connection pooling.
91104
:raises AssertionError: if *user_agent* is too short or
92105
*language* is empty
93106
"""

wikipediaapi/_http_client/sync_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
4848
super().__init__(*args, **kwargs)
4949
self._client = httpx.Client(
5050
headers=self._default_headers,
51-
timeout=self._client_kwargs.get("timeout", 10.0),
51+
**self._client_kwargs,
5252
transport=httpx.HTTPTransport(),
5353
)
5454

wikipediaapi/async_wikipedia.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ async def main():
6565
``Retry-After`` header value is used instead. Defaults to
6666
``1.0``.
6767
:param kwargs: additional keyword arguments forwarded to
68-
``httpx.AsyncClient`` (e.g. ``timeout=30.0``).
68+
``httpx.AsyncClient`` (e.g. ``timeout=30.0``, ``proxies={…}``,
69+
``verify=False``). **Advanced Usage**: These provide direct
70+
access to httpx capabilities. For most use cases, prefer the
71+
standard parameters above. Use httpx parameters only for specific
72+
requirements like custom proxies, SSL configuration, or connection pooling.
6973
"""
7074

7175
pass

0 commit comments

Comments
 (0)