From 5c21c87932903c17c383b9559acdd6ccb37a83f4 Mon Sep 17 00:00:00 2001 From: Shawnhdx0710 Date: Sat, 8 Aug 2026 22:44:00 +0800 Subject: [PATCH] fix: compute default seasons from the season calendar --- soccerdata/_common.py | 22 ++++++++++++++++++++-- tests/test_common.py | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/soccerdata/_common.py b/soccerdata/_common.py index 15c99903..f75ea4ac 100644 --- a/soccerdata/_common.py +++ b/soccerdata/_common.py @@ -460,12 +460,30 @@ def seasons(self) -> list[str]: def seasons(self, seasons: str | int | Iterable[str | int] | None) -> None: if seasons is None: logger.info("No seasons provided. Will retrieve data for the last 5 seasons.") - year = datetime.now(tz=timezone.utc).year - seasons = [f"{y - 1}-{y}" for y in range(year, year - 6, -1)] + seasons = self._default_seasons(datetime.now(tz=timezone.utc)) if isinstance(seasons, (str, int)): seasons = [seasons] self._season_ids = [self._season_code.parse(s) for s in seasons] + def _default_seasons(self, now: datetime) -> list[str]: + """Return the last 5 seasons, ending with the current one. + + The current season is derived from the season calendar, not from the + calendar year. For a multi-year league (e.g. Aug-May), the current + season ends in the next calendar year during the second half of the + year, so the end year is shifted by one during that period. For a + single-year league, the season id equals the start year of the + generated range, so the end year is one ahead of the calendar year. + """ + end_year = now.year + if ( + self._season_code == SeasonCode.SINGLE_YEAR + or self._season_code == SeasonCode.MULTI_YEAR + and now.month >= 8 + ): + end_year += 1 + return [f"{y - 1}-{y}" for y in range(end_year, end_year - 5, -1)] + class BaseRequestsReader(BaseReader): """Base class for readers that use the Python requests module.""" diff --git a/tests/test_common.py b/tests/test_common.py index 186b2780..34b8d6c9 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -268,6 +268,50 @@ def test_is_complete_undefined_league(mocker): reader._is_complete("FAKE-Dummy League", "1920") +def test_default_seasons(mocker): + """It should default to the last 5 seasons ending with the current season.""" + reader = BaseRequestsReader(no_store=True) + october = datetime(2024, 10, 30, 12, 0, tzinfo=timezone.utc) + march = datetime(2024, 3, 30, 12, 0, tzinfo=timezone.utc) + + # multi-year leagues: the current season ends next year after August + mocker.patch.object( + BaseRequestsReader, + "_season_code", + new_callable=mocker.PropertyMock, + return_value=SeasonCode.MULTI_YEAR, + ) + assert [SeasonCode.MULTI_YEAR.parse(s) for s in reader._default_seasons(october)] == [ + "2425", + "2324", + "2223", + "2122", + "2021", + ] + assert [SeasonCode.MULTI_YEAR.parse(s) for s in reader._default_seasons(march)] == [ + "2324", + "2223", + "2122", + "2021", + "1920", + ] + + # single-year leagues: seasons are single calendar years + mocker.patch.object( + BaseRequestsReader, + "_season_code", + new_callable=mocker.PropertyMock, + return_value=SeasonCode.SINGLE_YEAR, + ) + assert [SeasonCode.SINGLE_YEAR.parse(s) for s in reader._default_seasons(october)] == [ + "2024", + "2023", + "2022", + "2021", + "2020", + ] + + # Season codes def test_season_pattern1a(): assert SeasonCode.MULTI_YEAR.parse("9495") == "9495"