diff --git a/mlbstatsapi/mlb_api.py b/mlbstatsapi/mlb_api.py index 1e8a6d5..ce55eaf 100644 --- a/mlbstatsapi/mlb_api.py +++ b/mlbstatsapi/mlb_api.py @@ -52,12 +52,14 @@ def __init__( timeout: TimeoutType = DEFAULT_TIMEOUT, session: requests.Session | None = None, *, - strict_http: bool = False, + strict_http: bool = True, ): # One session is shared by the v1 and v1.1 adapters. The library closes # only sessions it creates; caller-injected sessions remain caller-owned. # The versioned User-Agent and retry adapters are applied only to # library-created Sessions. + # strict_http defaults to True in version 1.0; pass False for the + # historical empty-result compatibility path on final non-404 4xx. self._owns_session = session is None if session is None: self._session = requests.Session() diff --git a/mlbstatsapi/mlb_dataadapter.py b/mlbstatsapi/mlb_dataadapter.py index 4bc66d1..1395cbb 100644 --- a/mlbstatsapi/mlb_dataadapter.py +++ b/mlbstatsapi/mlb_dataadapter.py @@ -247,8 +247,10 @@ def __init__( timeout: TimeoutType = DEFAULT_TIMEOUT, session: requests.Session | None = None, *, - strict_http: bool = False, + strict_http: bool = True, ): + # strict_http defaults to True in version 1.0; pass False for the + # historical empty-result compatibility path on final non-404 4xx. self.url = f'https://{hostname}/api/{ver}/' self._logger = logger or logging.getLogger(__name__) self._timeout = timeout diff --git a/tests/http_contract_support.py b/tests/http_contract_support.py index 5b17318..27822ae 100644 --- a/tests/http_contract_support.py +++ b/tests/http_contract_support.py @@ -12,9 +12,9 @@ from urllib3.util.retry import Retry -# Final non-404 client errors that currently return an empty MlbResult by -# default rather than raising MlbHttpError. Later strict-mode work should -# reuse this matrix when asserting the opposite behavior. +# Final non-404 client errors that raise MlbHttpError under the version 1.0 +# strict default, and return an empty MlbResult with a compatibility warning +# when strict_http=False. COMPATIBILITY_CLIENT_ERRORS = ( 400, 401, @@ -93,11 +93,8 @@ def assert_library_retry_policy(retry: Retry) -> None: API_VERSIONS = ("v1", "v1.1") -# Pending version 1.0 default strict HTTP behavior (#284). -XFAIL_PENDING_STRICT_DEFAULT = pytest.mark.xfail( - strict=True, - reason="Pending #284: strict HTTP behavior becomes the 1.0 default", -) +# Sentinel so helpers can omit strict_http and exercise the real constructor default. +_UNSET = object() # Pending compatibility warning caller location via public Mlb endpoints (#285). XFAIL_PENDING_WARNING_CALL_SITE = pytest.mark.xfail( @@ -119,13 +116,19 @@ def standalone_adapter_for_version( session, api_version: str, *, - strict_http: bool = False, + strict_http=_UNSET, ) -> "MlbDataAdapter": - """Build a versioned MlbDataAdapter sharing a mocked session.""" + """Build a versioned MlbDataAdapter sharing a mocked session. + + Omitting ``strict_http`` leaves the constructor argument unset so tests + exercise the real production default rather than an explicit False. + """ from mlbstatsapi import MlbDataAdapter - return MlbDataAdapter( - session=session, - ver=api_version, - strict_http=strict_http, - ) + kwargs = { + "session": session, + "ver": api_version, + } + if strict_http is not _UNSET: + kwargs["strict_http"] = strict_http + return MlbDataAdapter(**kwargs) diff --git a/tests/test_http_contract.py b/tests/test_http_contract.py index 447dddb..2de72fe 100644 --- a/tests/test_http_contract.py +++ b/tests/test_http_contract.py @@ -1,7 +1,6 @@ """Offline HTTP contract tests for version 1.0 strict defaults and compatibility mode. -Documents the version 1.0 HTTP behavior in deterministic tests. Unimplemented -1.0 default wiring is marked xfail pending issue #284. +Documents the version 1.0 HTTP behavior in deterministic tests. """ from __future__ import annotations @@ -30,7 +29,6 @@ HTTP_REASON_BY_STATUS, NOT_FOUND_STATUS, SERVER_ERRORS, - XFAIL_PENDING_STRICT_DEFAULT, adapter_for_api_version, assert_library_retry_policy, standalone_adapter_for_version, @@ -114,10 +112,9 @@ def test_status_matrices_document_compatibility_baseline(): assert set(SERVER_ERRORS).isdisjoint(COMPATIBILITY_CLIENT_ERRORS) -# --- Version 1.0 default strict wiring (pending implementation #284) --- +# --- Version 1.0 default strict wiring --- -@XFAIL_PENDING_STRICT_DEFAULT def test_mlb_default_matches_explicit_strict_mode_wiring(): """Mlb() must default to strict mode on the client and both adapters.""" mlb = Mlb() @@ -129,7 +126,6 @@ def test_mlb_default_matches_explicit_strict_mode_wiring(): mlb.close() -@XFAIL_PENDING_STRICT_DEFAULT def test_mlb_data_adapter_default_is_strict(): """MlbDataAdapter() must default to strict HTTP in version 1.0.""" adapter = MlbDataAdapter() @@ -236,12 +232,11 @@ def test_compatibility_client_errors_do_not_raise_mlb_http_error( assert result.data == {} -# --- Version 1.0 default: final non-404 4xx raises (pending #284) --- +# --- Version 1.0 default: final non-404 4xx raises --- @pytest.mark.parametrize("api_version", API_VERSIONS) @pytest.mark.parametrize("status_code", COMPATIBILITY_CLIENT_ERRORS) -@XFAIL_PENDING_STRICT_DEFAULT def test_default_adapter_raises_on_final_non_404_client_error( api_version, status_code, @@ -275,7 +270,6 @@ def test_default_adapter_raises_on_final_non_404_client_error( @pytest.mark.parametrize("status_code", COMPATIBILITY_CLIENT_ERRORS) -@XFAIL_PENDING_STRICT_DEFAULT def test_default_mlb_raises_on_final_non_404_client_error(status_code): """Default Mlb() raises MlbHttpError for final non-404 4xx.""" reason = HTTP_REASON_BY_STATUS[status_code] @@ -665,7 +659,7 @@ def test_mlb_constructors_remain_compatible(): try: assert isinstance(mlb_default._session, requests.Session) assert mlb_default._timeout == DEFAULT_TIMEOUT - assert mlb_default._strict_http is False + assert mlb_default._strict_http is True finally: mlb_default.close() @@ -697,7 +691,7 @@ def test_mlb_positional_timeout_remains_third_argument(): assert session.calls[0]["timeout"] == 10 assert session.calls[1]["timeout"] == 10 assert mlb._session is session - assert mlb._strict_http is False + assert mlb._strict_http is True def test_mlb_strict_http_is_keyword_only(): @@ -709,7 +703,7 @@ def test_mlb_strict_http_is_keyword_only(): mlb = Mlb("statsapi.mlb.com", logger, 10, session) assert mlb._session is session assert mlb._timeout == 10 - assert mlb._strict_http is False + assert mlb._strict_http is True mlb_strict = Mlb( "statsapi.mlb.com", @@ -724,6 +718,17 @@ def test_mlb_strict_http_is_keyword_only(): assert mlb_strict._mlb_adapter_v1._strict_http is True assert mlb_strict._mlb_adapter_v1_1._strict_http is True + mlb_compat = Mlb( + "statsapi.mlb.com", + logger, + 10, + session, + strict_http=False, + ) + assert mlb_compat._strict_http is False + assert mlb_compat._mlb_adapter_v1._strict_http is False + assert mlb_compat._mlb_adapter_v1_1._strict_http is False + with pytest.raises(TypeError): Mlb("statsapi.mlb.com", logger, 10, session, True) @@ -738,7 +743,7 @@ def test_adapter_positional_construction_remains_compatible(): assert adapter._logger is logger assert adapter._timeout == (5.0, 60.0) assert adapter._session is session - assert adapter._strict_http is False + assert adapter._strict_http is True adapter.get(endpoint="game") assert session.calls[0]["timeout"] == (5.0, 60.0) diff --git a/tests/test_mlb_retries.py b/tests/test_mlb_retries.py index a66eccf..0366808 100644 --- a/tests/test_mlb_retries.py +++ b/tests/test_mlb_retries.py @@ -27,10 +27,11 @@ NON_RETRYABLE_CLIENT_ERRORS, RETRYABLE_STATUS_CODES, SERVER_ERRORS, - XFAIL_PENDING_STRICT_DEFAULT, assert_library_retry_policy, ) +_UNSET = object() + def test_create_retry_policy_is_publicly_importable(): """create_retry_policy is available through the package public API.""" @@ -196,9 +197,12 @@ def no_retry_sleep(monkeypatch): def _adapter_against_local_server( port: int, *, - strict_http: bool = False, + strict_http=_UNSET, ) -> MlbDataAdapter: - adapter = MlbDataAdapter(strict_http=strict_http) + kwargs = {} + if strict_http is not _UNSET: + kwargs["strict_http"] = strict_http + adapter = MlbDataAdapter(**kwargs) adapter.url = f"http://127.0.0.1:{port}/api/v1/" return adapter @@ -250,18 +254,23 @@ def test_non_retryable_client_errors_are_not_retried( scripted_http_server, no_retry_sleep, ): - """Ordinary client errors are returned immediately without retries.""" + """Ordinary client errors complete after one attempt without retries.""" configure, port = scripted_http_server configure([status_code, 200]) adapter = _adapter_against_local_server(port) try: - result = adapter.get(endpoint="sports") + if status_code == 404: + result = adapter.get(endpoint="sports") + assert result.status_code == status_code + assert result.data == {} + else: + with pytest.raises(MlbHttpError) as exc_info: + adapter.get(endpoint="sports") + assert exc_info.value.status_code == status_code finally: adapter.close() - assert result.status_code == status_code - assert result.data == {} assert _ScriptedHandler.request_count == 1 @@ -306,7 +315,6 @@ def test_final_429_returns_empty_mlb_result_in_compatibility_mode( assert _ScriptedHandler.request_count == 4 -@XFAIL_PENDING_STRICT_DEFAULT def test_final_429_raises_mlb_http_error_after_retry_exhaustion_default_adapter( scripted_http_server, no_retry_sleep, @@ -327,7 +335,6 @@ def test_final_429_raises_mlb_http_error_after_retry_exhaustion_default_adapter( assert exc_info.value.method == "GET" -@XFAIL_PENDING_STRICT_DEFAULT def test_final_429_raises_via_default_mlb_client_after_retry_exhaustion( scripted_http_server, no_retry_sleep,