diff --git a/.github/workflows/selective_cache_persist.yml b/.github/workflows/selective_cache_persist.yml index 8f9e9ec1b..90ab56054 100644 --- a/.github/workflows/selective_cache_persist.yml +++ b/.github/workflows/selective_cache_persist.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.9-minver', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10-minver', '3.10', '3.11', '3.12', '3.13', '3.14'] name: Persist cache for ${{ matrix.python-version }} steps: - name: Create cache directory diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f2c86832..3c05274f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,10 +22,9 @@ jobs: matrix: include: - name-suffix: "(Minimum Versions)" - python-version: "3.9" + python-version: "3.10" cache-suffix: "-minver" extra-requirements: "-c requirements/minver.txt" - - python-version: "3.9" - python-version: "3.10" - python-version: "3.11" - python-version: "3.12" diff --git a/fastf1/_api.py b/fastf1/_api.py index 4946d24b2..cd5c3fb89 100644 --- a/fastf1/_api.py +++ b/fastf1/_api.py @@ -3,10 +3,6 @@ import datetime import json import zlib -from typing import ( - Optional, - Union -) import numpy as np import pandas as pd @@ -105,7 +101,7 @@ def make_path(wname, wdate, sname, sdate): def timing_data(path: str, - response: Optional[str] = None, + response: str | None = None, livedata=None ) -> (pd.DataFrame, pd.DataFrame): """ @@ -1784,7 +1780,7 @@ def fetch_page(path, name): return None -def parse(text: str, zipped: bool = False) -> Union[str, dict]: +def parse(text: str, zipped: bool = False) -> str | dict: """ .. warning:: :mod:`fastf1.api` will be considered private in future releases and diff --git a/fastf1/core.py b/fastf1/core.py index 470075b0c..ac9cb6e39 100644 --- a/fastf1/core.py +++ b/fastf1/core.py @@ -1,11 +1,13 @@ import collections import re import warnings -from collections.abc import Iterable +from collections.abc import ( + Callable, + Iterable +) from functools import cached_property from typing import ( Any, - Callable, Literal, Optional, Union @@ -181,7 +183,7 @@ def __init__(self, drop_unknown_channels: bool = False, **kwargs): super().__init__(*args, **kwargs) - self.session: Optional[Session] = session + self.session: Session | None = session self.driver = driver if drop_unknown_channels: @@ -234,7 +236,7 @@ def merge(self, *args, **kwargs): def slice_by_mask( self, - mask: Union[list, pd.Series, np.ndarray], + mask: list | pd.Series | np.ndarray, pad: int = 0, pad_side: str = 'both' ) -> "Telemetry": @@ -297,7 +299,7 @@ def slice_by_lap( end_time = ref_laps['Time'].max() start_time = ref_laps['LapStartTime'].min() - elif isinstance(ref_laps, (Lap, Laps)): + elif isinstance(ref_laps, Lap | Laps): if isinstance(ref_laps, Laps): # one lap in Laps ref_laps = ref_laps.iloc[0] # handle as a single lap if 'DriverNumber' not in ref_laps.index: @@ -365,7 +367,7 @@ def slice_by_time( def merge_channels( self, other: Union["Telemetry", pd.DataFrame], - frequency: Union[int, Literal['original'], None] = None + frequency: int | Literal['original'] | None = None ): """Merge telemetry objects containing different telemetry channels. @@ -544,9 +546,9 @@ def merge_channels( def resample_channels( self, - rule: Optional[str] = None, - new_date_ref: Optional[pd.Series] = None, - **kwargs: Optional[Any] + rule: str | None = None, + new_date_ref: pd.Series | None = None, + **kwargs: Any | None ): """Resample telemetry data. @@ -671,7 +673,7 @@ def register_new_channel( cls, name: str, signal_type: str, - interpolation_method: Optional[str] = None + interpolation_method: str | None = None ): """Register a custom telemetry channel. @@ -1186,11 +1188,11 @@ def __init__(self, event, session_name, f1_api_support=False): self._track_status: pd.DataFrame - self._total_laps: Optional[int] + self._total_laps: int | None self._laps: Laps - self._t0_date: Optional[pd.Timestamp] - self._session_start_time: Optional[pd.Timedelta] + self._t0_date: pd.Timestamp | None + self._session_start_time: pd.Timedelta | None self._car_data: dict self._pos_data: dict @@ -1198,7 +1200,7 @@ def __init__(self, event, session_name, f1_api_support=False): self._weather_data: pd.DataFrame self._results: SessionResults - self._session_split_times: Optional[list] = None + self._session_split_times: list | None = None def __repr__(self): return (f"{self.event.year} Season Round {self.event.RoundNumber}: " @@ -2405,7 +2407,7 @@ def _drivers_from_f1_api(self, *, livedata=None): def _drivers_results_from_ergast( self, *, load_drivers=False, load_results=False - ) -> Optional[pd.DataFrame]: + ) -> pd.DataFrame | None: if self.name in self._RACE_LIKE_SESSIONS + self._QUALI_LIKE_SESSIONS: session_name = self.name else: @@ -2620,7 +2622,7 @@ def get_driver(self, identifier) -> "DriverResult": raise ValueError(f"Invalid driver identifier '{identifier}'") return self.results[mask].iloc[0] - def get_circuit_info(self) -> Optional[CircuitInfo]: + def get_circuit_info(self) -> CircuitInfo | None: """Returns additional information about the circuit that hosts this event. @@ -2740,7 +2742,7 @@ class Laps(BaseDataFrame): 'LapStartDate': 'datetime64[ns]', 'TrackStatus': str, 'Position': 'float64', # need to support NaN - 'Deleted': Optional[bool], + 'Deleted': bool | None, 'DeletedReason': str, 'FastF1Generated': bool, 'IsAccurate': bool @@ -2755,7 +2757,7 @@ class Laps(BaseDataFrame): def __init__(self, *args, - session: Optional[Session] = None, + session: Session | None = None, **kwargs): super().__init__(*args, **kwargs) @@ -2815,7 +2817,7 @@ def merge(self, *args, **kwargs): def get_telemetry(self, *, - frequency: Union[int, Literal['original'], None] = None + frequency: int | Literal['original'] | None = None ) -> Telemetry: """Telemetry data for all laps in `self` @@ -3037,7 +3039,7 @@ def pick_lap(self, lap_number: int) -> "Laps": FutureWarning) return self[self['LapNumber'] == lap_number] - def pick_laps(self, lap_numbers: Union[int, Iterable[int]]) -> "Laps": + def pick_laps(self, lap_numbers: int | Iterable[int]) -> "Laps": """Return all laps of a specific LapNumber or a list of LapNumbers in self. :: @@ -3051,7 +3053,7 @@ def pick_laps(self, lap_numbers: Union[int, Iterable[int]]) -> "Laps": Returns: instance of :class:`Laps` """ - if isinstance(lap_numbers, (int, float)): + if isinstance(lap_numbers, int | float): lap_numbers = [lap_numbers, ] for i in lap_numbers: @@ -3060,7 +3062,7 @@ def pick_laps(self, lap_numbers: Union[int, Iterable[int]]) -> "Laps": return self[self["LapNumber"].isin(lap_numbers)] - def pick_driver(self, identifier: Union[int, str]) -> "Laps": + def pick_driver(self, identifier: int | str) -> "Laps": """Return all laps of a specific driver in self based on the driver's three letters identifier or based on the driver number. @@ -3088,7 +3090,7 @@ def pick_driver(self, identifier: Union[int, str]) -> "Laps": return self[self['Driver'] == identifier] def pick_drivers(self, - identifiers: Union[int, str, Iterable[Union[int, str]]] + identifiers: int | str | Iterable[int | str] ) -> "Laps": """Return all laps of the specified driver or drivers in self based on the drivers' three letters identifier or the driver number. :: @@ -3103,7 +3105,7 @@ def pick_drivers(self, Returns: instance of :class:`Laps` """ - if isinstance(identifiers, (int, str)): + if isinstance(identifiers, int | str): identifiers = [identifiers, ] names = [n.upper() for n in identifiers if not str(n).isdigit()] @@ -3134,7 +3136,7 @@ def pick_team(self, name: str) -> "Laps": FutureWarning) return self[self['Team'] == name] - def pick_teams(self, names: Union[str, Iterable[str]]) -> "Laps": + def pick_teams(self, names: str | Iterable[str]) -> "Laps": """Return all laps of the specified team or teams in self based on the team names. :: @@ -3192,7 +3194,7 @@ def pick_fastest(self, only_by_time: bool = False) -> Optional["Lap"]: return lap - def pick_quicklaps(self, threshold: Optional[float] = None) -> "Laps": + def pick_quicklaps(self, threshold: float | None = None) -> "Laps": """Return all laps with `LapTime` faster than a certain limit. By default, the threshold is 107% of the best `LapTime` of all laps in self. @@ -3229,7 +3231,7 @@ def pick_tyre(self, compound: str) -> "Laps": FutureWarning) return self[self['Compound'] == compound.upper()] - def pick_compounds(self, compounds: Union[str, Iterable[str]]) -> "Laps": + def pick_compounds(self, compounds: str | Iterable[str]) -> "Laps": """Return all laps in self which were done on some specific compounds. :: @@ -3416,7 +3418,7 @@ def split_qualifying_sessions(self) -> list[Optional["Laps"]]: laps[i] = None return laps - def iterlaps(self, require: Optional[Iterable] = None) \ + def iterlaps(self, require: Iterable | None = None) \ -> Iterable[tuple[int, "Lap"]]: """Iterator for iterating over all laps in self. @@ -3481,7 +3483,7 @@ def telemetry(self) -> Telemetry: def get_telemetry(self, *, - frequency: Union[int, Literal['original'], None] = None + frequency: int | Literal['original'] | None = None ) -> Telemetry: """Telemetry data for this lap diff --git a/fastf1/ergast/interface.py b/fastf1/ergast/interface.py index 932cd466e..f99847f39 100644 --- a/fastf1/ergast/interface.py +++ b/fastf1/ergast/interface.py @@ -2,7 +2,6 @@ import json from typing import ( Literal, - Optional, Union ) @@ -127,8 +126,8 @@ class ErgastResultFrame(BaseDataFrame): _internal_names_set = set(_internal_names) def __init__(self, data=None, *, - category: Optional[dict] = None, - response: Optional[list] = None, + category: dict | None = None, + response: list | None = None, auto_cast: bool = True, **kwargs): if (data is not None) and (response is not None): @@ -408,7 +407,7 @@ class Ergast: def __init__(self, result_type: Literal['raw', 'pandas'] = 'pandas', auto_cast: bool = True, - limit: Optional[int] = None): + limit: int | None = None): self._default_result_type = result_type self._default_auto_cast = auto_cast self._limit = limit @@ -416,18 +415,18 @@ def __init__(self, @staticmethod def _build_url( endpoint: str, - season: Union[Literal['current'], int] = None, - round: Union[Literal['last'], int] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - lap_number: Optional[int] = None, - stop_number: Optional[int] = None, - standings_position: Optional[int] = None + season: Literal['current'] | int = None, + round: Literal['last'] | int = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + lap_number: int | None = None, + stop_number: int | None = None, + standings_position: int | None = None ) -> str: selectors = list() @@ -497,7 +496,7 @@ def _build_url( return BASE_URL + "".join(selectors) + ".json" @classmethod - def _get(cls, url: str, params: dict) -> Union[dict, list]: + def _get(cls, url: str, params: dict) -> dict | list: # request data from ergast and load the returned json data. r = Cache.requests_get(url, headers=HEADERS, params=params, timeout=TIMEOUT) @@ -521,15 +520,13 @@ def _build_result( endpoint: str, table: str, category: dict, - subcategory: Optional[dict], - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None, - selectors: Optional[dict] = None, - ) -> Union[ErgastSimpleResponse, - ErgastMultiResponse, - ErgastRawResponse]: + subcategory: dict | None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None, + selectors: dict | None = None, + ) -> ErgastSimpleResponse | ErgastMultiResponse | ErgastRawResponse: # query the Ergast database and # split the raw response into multiple parts, depending also on what # type was selected for the response data format. @@ -584,13 +581,11 @@ def _build_result( def _build_default_result( self, *, selectors: dict, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, **kwargs - ) -> Union[ErgastSimpleResponse, - ErgastMultiResponse, - ErgastRawResponse]: + ) -> ErgastSimpleResponse | ErgastMultiResponse | ErgastRawResponse: # use defaults or per-call overrides if specified if result_type is None: result_type = self._default_result_type @@ -612,18 +607,18 @@ def _build_default_result( # can be represented by a DataFrame-like object def get_seasons( self, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastSimpleResponse, ErgastRawResponse]: + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastSimpleResponse | ErgastRawResponse: """Get a list of seasons. See: https://ergast.com/mrd/methods/seasons/ @@ -673,20 +668,20 @@ def get_seasons( def get_race_schedule( self, - season: Union[Literal['current'], int], - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastSimpleResponse, ErgastRawResponse]: + season: Literal['current'] | int, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastSimpleResponse | ErgastRawResponse: """Get a list of races. See: https://ergast.com/mrd/methods/schedule/ @@ -740,20 +735,20 @@ def get_race_schedule( def get_driver_info( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastSimpleResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastSimpleResponse | ErgastRawResponse: """Get a list of drivers. See: https://ergast.com/mrd/methods/drivers/ @@ -807,20 +802,20 @@ def get_driver_info( def get_constructor_info( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastSimpleResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastSimpleResponse | ErgastRawResponse: """Get a list of constructors. See: https://ergast.com/mrd/methods/constructors/ @@ -874,19 +869,19 @@ def get_constructor_info( def get_circuits( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastSimpleResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastSimpleResponse | ErgastRawResponse: """Get a list of circuits. See: https://ergast.com/mrd/methods/circuits/ @@ -938,20 +933,20 @@ def get_circuits( def get_finishing_status( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastSimpleResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastSimpleResponse | ErgastRawResponse: """Get a list of finishing status codes. See: https://ergast.com/mrd/methods/status/ @@ -1011,20 +1006,20 @@ def get_finishing_status( # needs to be represented by multiple DataFrame-like objects def get_race_results( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get race results for one or multiple races. See: https://ergast.com/mrd/methods/results/ @@ -1079,20 +1074,20 @@ def get_race_results( def get_qualifying_results( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - fastest_rank: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + fastest_rank: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get qualifying results for one or multiple qualifying sessions. See: https://ergast.com/mrd/methods/qualifying/ @@ -1147,19 +1142,19 @@ def get_qualifying_results( def get_sprint_results( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - circuit: Optional[str] = None, - constructor: Optional[str] = None, - driver: Optional[str] = None, - grid_position: Optional[int] = None, - results_position: Optional[int] = None, - status: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + circuit: str | None = None, + constructor: str | None = None, + driver: str | None = None, + grid_position: int | None = None, + results_position: int | None = None, + status: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get sprint results for one or multiple sprints. See: https://ergast.com/mrd/methods/sprint/ @@ -1212,15 +1207,15 @@ def get_sprint_results( def get_driver_standings( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - driver: Optional[str] = None, - standings_position: Optional[int] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + driver: str | None = None, + standings_position: int | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get driver standings at specific points of a season. See: https://ergast.com/mrd/methods/standings/ @@ -1264,15 +1259,15 @@ def get_driver_standings( def get_constructor_standings( self, - season: Optional[Union[Literal['current'], int]] = None, - round: Optional[Union[Literal['last'], int]] = None, - constructor: Optional[str] = None, - standings_position: Optional[int] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int | None = None, + round: Literal['last'] | int | None = None, + constructor: str | None = None, + standings_position: int | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get constructor standings at specific points of a season. See: https://ergast.com/mrd/methods/standings/ @@ -1318,15 +1313,15 @@ def get_constructor_standings( ) def get_lap_times(self, - season: Union[Literal['current'], int], - round: Union[Literal['last'], int], - lap_number: Optional[int] = None, - driver: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int, + round: Literal['last'] | int, + lap_number: int | None = None, + driver: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get sprint results for one or multiple sprints. See: https://ergast.com/mrd/methods/laps/ @@ -1369,16 +1364,16 @@ def get_lap_times(self, selectors=selectors) def get_pit_stops(self, - season: Union[Literal['current'], int], - round: Union[Literal['last'], int], - stop_number: Optional[int] = None, - lap_number: Optional[int] = None, - driver: Optional[str] = None, - result_type: Optional[Literal['pandas', 'raw']] = None, - auto_cast: Optional[bool] = None, - limit: Optional[int] = None, - offset: Optional[int] = None - ) -> Union[ErgastMultiResponse, ErgastRawResponse]: + season: Literal['current'] | int, + round: Literal['last'] | int, + stop_number: int | None = None, + lap_number: int | None = None, + driver: str | None = None, + result_type: Literal['pandas', 'raw'] | None = None, + auto_cast: bool | None = None, + limit: int | None = None, + offset: int | None = None + ) -> ErgastMultiResponse | ErgastRawResponse: """Get pit stop information for one or multiple sessions. See: https://ergast.com/mrd/methods/standings/ diff --git a/fastf1/ergast/structure.py b/fastf1/ergast/structure.py index 55ddf9dd1..d4a8f848e 100644 --- a/fastf1/ergast/structure.py +++ b/fastf1/ergast/structure.py @@ -1,9 +1,6 @@ import datetime import re -from typing import ( - Any, - Optional -) +from typing import Any from fastf1.logger import get_logger @@ -20,7 +17,7 @@ # matches [hh:][mm:]ss[.micros][Z | +-hh:mm] timestring -def date_from_ergast(d_str: Any) -> Optional[datetime.datetime]: +def date_from_ergast(d_str: Any) -> datetime.datetime | None: """Create a ``datetime.datetime`` object from a date stamp formatted like 'YYYY-MM-DD'.""" try: @@ -31,7 +28,7 @@ def date_from_ergast(d_str: Any) -> Optional[datetime.datetime]: return None -def time_from_ergast(t_str: Any) -> Optional[datetime.time]: +def time_from_ergast(t_str: Any) -> datetime.time | None: """Create a ``datetime.time`` object from a string that is formatted mostly like a timestamp according to ISO 8601. The implementation here only implements a subset of ISO 8601 to work around some missing functionality @@ -83,7 +80,7 @@ def time_from_ergast(t_str: Any) -> Optional[datetime.time]: return None -def timedelta_from_ergast(t_str: Any) -> Optional[datetime.timedelta]: +def timedelta_from_ergast(t_str: Any) -> datetime.timedelta | None: """Create a ``datetime.timedelta`` object from a string that is formatted [+/-][hh:][mm:]ss[.micros], where all parts except for seconds are optional. diff --git a/fastf1/events.py b/fastf1/events.py index 72688eec4..69a9ba77c 100644 --- a/fastf1/events.py +++ b/fastf1/events.py @@ -2,11 +2,7 @@ import datetime import json import warnings -from typing import ( - Literal, - Optional, - Union -) +from typing import Literal import dateutil.parser import pandas as pd @@ -52,10 +48,10 @@ def get_session( year: int, - gp: Union[str, int], - identifier: Optional[Union[int, str]] = None, + gp: str | int, + identifier: int | str | None = None, *, - backend: Optional[Literal['fastf1', 'f1timing', 'ergast']] = None, + backend: Literal['fastf1', 'f1timing', 'ergast'] | None = None, force_ergast: bool = False, ) -> Session: """Create a :class:`~fastf1.core.Session` object based on year, event name @@ -128,7 +124,7 @@ def get_testing_session( test_number: int, session_number: int, *, - backend: Optional[Literal['fastf1', 'f1timing']] = None + backend: Literal['fastf1', 'f1timing'] | None = None ) -> Session: """Create a :class:`~fastf1.core.Session` object for testing sessions based on year, test event number and session number. @@ -161,9 +157,9 @@ def get_testing_session( def get_event( year: int, - gp: Union[int, str], + gp: int | str, *, - backend: Optional[Literal['fastf1', 'f1timing', 'ergast']] = None, + backend: Literal['fastf1', 'f1timing', 'ergast'] | None = None, force_ergast: bool = False, strict_search: bool = False, exact_match: bool = False @@ -231,7 +227,7 @@ def get_testing_event( year: int, test_number: int, *, - backend: Optional[Literal['fastf1', 'f1timing']] = None + backend: Literal['fastf1', 'f1timing'] | None = None ) -> "Event": """Create a :class:`fastf1.events.Event` object for testing sessions based on year and test event number. @@ -270,7 +266,7 @@ def get_event_schedule( year: int, *, include_testing: bool = True, - backend: Optional[Literal['fastf1', 'f1timing', 'ergast']] = None, + backend: Literal['fastf1', 'f1timing', 'ergast'] | None = None, force_ergast: bool = False ) -> "EventSchedule": """Create an :class:`~fastf1.events.EventSchedule` object for a specific @@ -339,10 +335,10 @@ def get_event_schedule( def get_events_remaining( - dt: Optional[datetime.datetime] = None, + dt: datetime.datetime | None = None, *, include_testing: bool = True, - backend: Optional[Literal['fastf1', 'f1timing', 'ergast']] = None, + backend: Literal['fastf1', 'f1timing', 'ergast'] | None = None, force_ergast: bool = False ) -> 'EventSchedule': """ @@ -902,7 +898,7 @@ def get_session_name(self, identifier) -> str: return session_name - def get_session_date(self, identifier: Union[str, int], utc=False) \ + def get_session_date(self, identifier: str | int, utc=False) \ -> pd.Timestamp: """Return the date and time (if available) at which a specific session of this event is or was held. @@ -932,7 +928,7 @@ def get_session_date(self, identifier: Union[str, int], utc=False) \ return date_utc return date - def get_session(self, identifier: Union[int, str]) -> "Session": + def get_session(self, identifier: int | str) -> "Session": """Return a session from this event. Args: diff --git a/fastf1/internals/pandas_base.py b/fastf1/internals/pandas_base.py index 17ae19a83..d32c9044a 100644 --- a/fastf1/internals/pandas_base.py +++ b/fastf1/internals/pandas_base.py @@ -1,9 +1,8 @@ -"""Base classes for objects that inherit form Pandas Series or DataFrame.""" +"""Base classes for objects that inherit from Pandas Series or DataFrame.""" import typing +from collections.abc import Callable from typing import ( Any, - Callable, - Optional, final ) @@ -54,7 +53,7 @@ class BaseDataFrame(pd.DataFrame): the correct type as defined in the _COLUMNS attribute. """ - _COLUMNS: Optional[dict[str, Any]] = None + _COLUMNS: dict[str, Any] | None = None """ A dictionary that defines the default columns of the DataFrame and their corresponding data types. The keys are the column names and the values are @@ -151,14 +150,13 @@ class that implements horizontal and vertical slicing of Pandas DataFrames from this class in its __new__ method. """ - __meta_created_from: Optional[BaseDataFrame] + __meta_created_from: BaseDataFrame | None def __new__(cls, data=None, index=None, *args, **kwargs) -> pd.Series: parent = getattr(cls, '__meta_created_from', None) - if ((index is None) and isinstance(data, (pd.Series, - pd.DataFrame, - SingleBlockManager))): + if (index is None) and isinstance( + data, pd.Series | pd.DataFrame | SingleBlockManager): # no index is explicitly given, try to get an index from the # data itself index = getattr(data, 'index', None) @@ -175,9 +173,7 @@ def __new__(cls, data=None, index=None, *args, **kwargs) -> pd.Series: # the data is a row of the parent DataFrame constructor = parent._constructor_sliced_horizontal - if (isinstance(data, SingleBlockManager) - and hasattr(constructor, '_from_mgr') - and pd.__version__.startswith('2.')): + if isinstance(data, SingleBlockManager): obj = constructor._from_mgr(data, axes=data.axes) else: obj = constructor(data=data, index=index, *args, **kwargs) diff --git a/fastf1/logger.py b/fastf1/logger.py index 1117f0d43..c376f9100 100644 --- a/fastf1/logger.py +++ b/fastf1/logger.py @@ -2,7 +2,6 @@ import logging import os import warnings -from typing import Union class LoggingManager: @@ -67,7 +66,7 @@ def get_logger(name: str): return LoggingManager.get_child(name) -def set_log_level(level: Union[str, int]): +def set_log_level(level: str | int): """Set the log level for all parts of FastF1. When setting the log level for FastF1, only messages with this level or diff --git a/fastf1/mvapi/api.py b/fastf1/mvapi/api.py index 6dc220710..2ae82c0cc 100644 --- a/fastf1/mvapi/api.py +++ b/fastf1/mvapi/api.py @@ -1,4 +1,3 @@ -from typing import Optional import requests.exceptions @@ -16,7 +15,7 @@ def _make_url(path: str): return f"{PROTO}://{HOST}{path}" -def get_circuit(*, year: int, circuit_key: int) -> Optional[dict]: +def get_circuit(*, year: int, circuit_key: int) -> dict | None: """:meta private: Request circuit data from the MultiViewer API and return the JSON response.""" diff --git a/fastf1/mvapi/data.py b/fastf1/mvapi/data.py index 8088c8038..5cf3f3373 100644 --- a/fastf1/mvapi/data.py +++ b/fastf1/mvapi/data.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional import numpy as np import pandas as pd @@ -117,7 +116,7 @@ def add_marker_distance(self, reference_lap: "fastf1.core.Lap"): df['Distance'] = distances -def get_circuit_info(*, year: int, circuit_key: int) -> Optional[CircuitInfo]: +def get_circuit_info(*, year: int, circuit_key: int) -> CircuitInfo | None: """:meta private: Load circuit information from the MultiViewer API and convert it into as :class:``SessionInfo`` object. diff --git a/fastf1/plotting/_interface.py b/fastf1/plotting/_interface.py index 83f2546f5..08b39b4fa 100644 --- a/fastf1/plotting/_interface.py +++ b/fastf1/plotting/_interface.py @@ -3,9 +3,7 @@ from collections.abc import Sequence from typing import ( Any, - Literal, - Optional, - Union + Literal ) import matplotlib.axes @@ -411,7 +409,7 @@ def _replace_magic_auto( style: dict, session: Session, colormap: str, - color_kws: Union[list, tuple] + color_kws: list | tuple ) -> dict[str, Any]: """ Recursively replace the 'auto' values with the team color of the driver if @@ -431,11 +429,11 @@ def _replace_magic_auto( def get_driver_style( identifier: str, - style: Union[str, Sequence[str], Sequence[dict]], + style: str | Sequence[str] | Sequence[dict], session: Session, *, colormap: str = 'default', - additional_color_kws: Union[list, tuple] = (), + additional_color_kws: list | tuple = (), exact_match: bool = False ) -> dict[str, Any]: """ @@ -841,9 +839,9 @@ def override_team_constants( identifier: str, session: Session, *, - short_name: Optional[str] = None, - official_color: Optional[str] = None, - fastf1_color: Optional[str] = None + short_name: str | None = None, + official_color: str | None = None, + fastf1_color: str | None = None ): """ Override the default team constants for a specific team. diff --git a/fastf1/plotting/_plotting.py b/fastf1/plotting/_plotting.py index 7bcc941ae..a5691f1dd 100644 --- a/fastf1/plotting/_plotting.py +++ b/fastf1/plotting/_plotting.py @@ -1,5 +1,4 @@ import warnings -from typing import Optional try: @@ -29,7 +28,7 @@ def setup_mpl( mpl_timedelta_support: bool = True, - color_scheme: Optional[str] = None, + color_scheme: str | None = None, *args, **kwargs # for backwards compatibility, do not use in new code ): """Setup matplotlib for use with fastf1. diff --git a/fastf1/req.py b/fastf1/req.py index 3a2202395..2a56572dc 100644 --- a/fastf1/req.py +++ b/fastf1/req.py @@ -8,10 +8,7 @@ import sys import time import warnings -from typing import ( - Literal, - Optional -) +from typing import Literal import requests from requests_cache import CacheMixin @@ -204,7 +201,7 @@ class Cache(metaclass=_MetaCache): _IGNORE_VERSION = False _FORCE_RENEW = False - _requests_session_cached: Optional[_CachedSessionWithRateLimiting] = None + _requests_session_cached: _CachedSessionWithRateLimiting | None = None _requests_session: requests.Session = _SessionWithRateLimiting() _default_cache_enabled = False # flag to ensure that warning about disabled cache is logged once only # noqa: E501 _tmp_disabled = False @@ -644,7 +641,7 @@ def ci_mode(cls, enabled: bool): cls._ci_mode = enabled @classmethod - def get_cache_info(cls) -> tuple[Optional[str], Optional[int]]: + def get_cache_info(cls) -> tuple[str | None, int | None]: """Returns information about the cache directory and its size. If the cache is not configured, None will be returned for both the diff --git a/fastf1/utils.py b/fastf1/utils.py index e6bbb3bd5..903ff51d0 100644 --- a/fastf1/utils.py +++ b/fastf1/utils.py @@ -2,10 +2,6 @@ import datetime import warnings from functools import reduce -from typing import ( - Optional, - Union -) import numpy as np import pandas as pd @@ -123,8 +119,8 @@ def recursive_dict_get(d: dict, *keys: str, default_none: bool = False): return ret -def to_timedelta(x: Union[str, datetime.timedelta]) \ - -> Optional[datetime.timedelta]: +def to_timedelta(x: str | datetime.timedelta) \ + -> datetime.timedelta | None: """Fast timedelta object creation from a time string Permissible string formats: @@ -181,8 +177,8 @@ def to_timedelta(x: Union[str, datetime.timedelta]) \ return None -def to_datetime(x: Union[str, datetime.datetime]) \ - -> Optional[datetime.datetime]: +def to_datetime(x: str | datetime.datetime) \ + -> datetime.datetime | None: """Fast datetime object creation from a date string. Permissible string formats: diff --git a/pyproject.toml b/pyproject.toml index 41bd610d1..ede112561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -25,19 +24,19 @@ classifiers = [ ] # minimum python version additionally needs to be changed in the test matrix -requires-python = ">=3.9" +requires-python = ">=3.10" # minimum package versions additionally need to be changed in requirements/minver.txt dependencies = [ "cryptography", - "matplotlib>=3.5.1,<4.0.0", - "numpy>=1.23.1,<3.0.0", - "pandas>=1.4.1,<3.0.0", + "matplotlib>=3.8.0,<4.0.0", + "numpy>=1.26.0,<3.0.0", + "pandas>=2.1.1,<3.0.0", "platformdirs", "pyjwt", "python-dateutil", - "requests>=2.28.1", + "requests>=2.30.0", "requests-cache>=1.0.0", - "scipy>=1.8.1,<2.0.0", + "scipy>=1.11.0,<2.0.0", "signalrcore", "rapidfuzz", "timple>=0.1.6", diff --git a/pytest.ini b/pytest.ini index 4369b54c7..42b0a71a1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -46,3 +46,7 @@ filterwarnings = # external, only relevant for pandas 2.2.1 due to incorrect deprecation warning # (01/2024) ignore:Passing a (Single)?BlockManager to.*:DeprecationWarning + # external, deprecation warnings in newer versions of pyparsing in combination with + # older versions of matplotlib, e.g. "'oneOf' deprecated - use 'one_of'" + # (01/2026) + ignore:'[a-z]+[A-Z][a-z]+' deprecated - use '[a-z]+_[a-z]+':DeprecationWarning diff --git a/requirements/minver.txt b/requirements/minver.txt index cd2f83f8f..51005207f 100644 --- a/requirements/minver.txt +++ b/requirements/minver.txt @@ -1,8 +1,8 @@ -matplotlib==3.5.1 -numpy==1.23.1 -pandas==1.4.1 -requests==2.28.1 +matplotlib==3.8.0 +numpy==1.26.0 +pandas==2.1.1 +requests==2.30.0 requests-cache==1.0.0 -scipy==1.8.1 +scipy==1.11.0 timple==0.1.6 websockets==10.3 \ No newline at end of file