From f4ae9539df21c293cd9807693e6ff7a0f5dc6cca Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Wed, 8 May 2024 07:11:07 +0200 Subject: [PATCH] Drop support for Python 3.9, require Python >= 3.10 (#2132) * Drop support for Python 3.9, require Python >= 3.10 Starting development for Mesa 3.0 and following SPEC 0 we can drop Python 3.9 support and require Python 3.10 or higher for future development. This allows adopting more modern Python features and simplifies the testing and CI configurations. See the Python 3.10 release notes: https://docs.python.org/3/whatsnew/3.10.html The 2.3.x release series will keep supporting Python 3.9. * Update type hinting for Python 3.10 `X | Y` can now be used for type annotations, including making a variable optional with `X | None` * intro_tutorial.ipynb: Require Python 3.10 * typing: Replace Union with pipe (|) We can do this now in Python 3.10! --- .github/workflows/build_lint.yml | 2 -- .pre-commit-config.yaml | 2 +- docs/tutorials/intro_tutorial.ipynb | 2 +- mesa/agent.py | 4 ++-- mesa/batchrunner.py | 8 ++++---- mesa/datacollection.py | 2 +- mesa/experimental/cell_space/cell_collection.py | 4 ++-- mesa/experimental/cell_space/grid.py | 2 +- mesa/experimental/cell_space/network.py | 6 +++--- mesa/experimental/components/altair.py | 3 +-- mesa/experimental/components/matplotlib.py | 8 +++----- mesa/experimental/devs/eventlist.py | 3 ++- mesa/experimental/devs/simulator.py | 3 ++- mesa/model.py | 4 ++-- mesa/space.py | 10 +++++----- mesa/time.py | 4 +--- pyproject.toml | 7 +++---- 17 files changed, 34 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml index 2edadaedcbe..2beefecb313 100644 --- a/.github/workflows/build_lint.yml +++ b/.github/workflows/build_lint.yml @@ -37,8 +37,6 @@ jobs: python-version: "3.11" - os: ubuntu python-version: "3.10" - - os: ubuntu - python-version: "3.9" # Disabled for now. See https://github.com/projectmesa/mesa/issues/1253 #- os: ubuntu # python-version: 'pypy-3.8' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d00bb6d49f6..fa4ea7fa879 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: rev: v3.15.2 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py310-plus] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 # Use the ref you want to point at hooks: diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb index 65f2a586561..a9fe56875e1 100644 --- a/docs/tutorials/intro_tutorial.ipynb +++ b/docs/tutorials/intro_tutorial.ipynb @@ -60,7 +60,7 @@ "source": [ "### Tutorial Setup\n", "\n", - "Create and activate a [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/). *Python version 3.9 or higher is required*.\n", + "Create and activate a [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/). *Python version 3.10 or higher is required*.\n", "\n", "Install Mesa:\n", "\n", diff --git a/mesa/agent.py b/mesa/agent.py index ab5e1800138..2f0e842d382 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -14,11 +14,11 @@ import warnings import weakref from collections import defaultdict -from collections.abc import Iterable, Iterator, MutableSet, Sequence +from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence from random import Random # mypy -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # We ensure that these are not imported during runtime to prevent cyclic diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py index aba96855c66..a74eddbeacc 100644 --- a/mesa/batchrunner.py +++ b/mesa/batchrunner.py @@ -2,7 +2,7 @@ from collections.abc import Iterable, Mapping from functools import partial from multiprocessing import Pool -from typing import Any, Optional, Union +from typing import Any from tqdm.auto import tqdm @@ -11,9 +11,9 @@ def batch_run( model_cls: type[Model], - parameters: Mapping[str, Union[Any, Iterable[Any]]], + parameters: Mapping[str, Any | Iterable[Any]], # We still retain the Optional[int] because users may set it to None (i.e. use all CPUs) - number_processes: Optional[int] = 1, + number_processes: int | None = 1, iterations: int = 1, data_collection_period: int = -1, max_steps: int = 1000, @@ -76,7 +76,7 @@ def batch_run( def _make_model_kwargs( - parameters: Mapping[str, Union[Any, Iterable[Any]]], + parameters: Mapping[str, Any | Iterable[Any]], ) -> list[dict[str, Any]]: """Create model kwargs from parameters dictionary. diff --git a/mesa/datacollection.py b/mesa/datacollection.py index 59247953d82..3082a67db6c 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -196,7 +196,7 @@ def collect(self, model): if self.model_reporters: for var, reporter in self.model_reporters.items(): # Check if lambda or partial function - if isinstance(reporter, (types.LambdaType, partial)): + if isinstance(reporter, types.LambdaType | partial): self.model_vars[var].append(reporter(model)) # Check if model attribute elif isinstance(reporter, str): diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 9ca36589849..114301db100 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -1,10 +1,10 @@ from __future__ import annotations import itertools -from collections.abc import Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping from functools import cached_property from random import Random -from typing import TYPE_CHECKING, Callable, Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar if TYPE_CHECKING: from mesa.experimental.cell_space.cell import Cell diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index e73d18f46fd..f08657d2107 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -60,7 +60,7 @@ def _validate_parameters(self): raise ValueError("Dimensions must be a list of positive integers.") if not isinstance(self.torus, bool): raise ValueError("Torus must be a boolean.") - if self.capacity is not None and not isinstance(self.capacity, (float, int)): + if self.capacity is not None and not isinstance(self.capacity, float | int): raise ValueError("Capacity must be a number or None.") def select_random_empty_cell(self) -> T: diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 57c4d492bb0..3983287e4ef 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -1,5 +1,5 @@ from random import Random -from typing import Any, Optional +from typing import Any from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.discrete_space import DiscreteSpace @@ -11,8 +11,8 @@ class Network(DiscreteSpace): def __init__( self, G: Any, # noqa: N803 - capacity: Optional[int] = None, - random: Optional[Random] = None, + capacity: int | None = None, + random: Random | None = None, cell_klass: type[Cell] = Cell, ) -> None: """A Networked grid diff --git a/mesa/experimental/components/altair.py b/mesa/experimental/components/altair.py index 6952c03f336..f9d1a81c172 100644 --- a/mesa/experimental/components/altair.py +++ b/mesa/experimental/components/altair.py @@ -1,5 +1,4 @@ import contextlib -from typing import Optional import solara @@ -8,7 +7,7 @@ @solara.component -def SpaceAltair(model, agent_portrayal, dependencies: Optional[list[any]] = None): +def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None): space = getattr(model, "grid", None) if space is None: # Sometimes the space is defined as model.space instead of model.grid diff --git a/mesa/experimental/components/matplotlib.py b/mesa/experimental/components/matplotlib.py index 85f8df9cb1d..6284944aa7f 100644 --- a/mesa/experimental/components/matplotlib.py +++ b/mesa/experimental/components/matplotlib.py @@ -1,5 +1,3 @@ -from typing import Optional - import networkx as nx import solara from matplotlib.figure import Figure @@ -9,7 +7,7 @@ @solara.component -def SpaceMatplotlib(model, agent_portrayal, dependencies: Optional[list[any]] = None): +def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None): space_fig = Figure() space_ax = space_fig.subplots() space = getattr(model, "grid", None) @@ -116,7 +114,7 @@ def portray(space): @solara.component -def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None): +def PlotMatplotlib(model, measure, dependencies: list[any] | None = None): fig = Figure() ax = fig.subplots() df = model.datacollector.get_model_vars_dataframe() @@ -127,7 +125,7 @@ def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None): for m, color in measure.items(): ax.plot(df.loc[:, m], label=m, color=color) fig.legend() - elif isinstance(measure, (list, tuple)): + elif isinstance(measure, list | tuple): for m in measure: ax.plot(df.loc[:, m], label=m) fig.legend() diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py index 48af72a4315..a0ec9337d66 100644 --- a/mesa/experimental/devs/eventlist.py +++ b/mesa/experimental/devs/eventlist.py @@ -1,10 +1,11 @@ from __future__ import annotations import itertools +from collections.abc import Callable from enum import IntEnum from heapq import heapify, heappop, heappush from types import MethodType -from typing import Any, Callable +from typing import Any from weakref import WeakMethod, ref diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py index 11c33f4e074..74f018e883d 100644 --- a/mesa/experimental/devs/simulator.py +++ b/mesa/experimental/devs/simulator.py @@ -1,7 +1,8 @@ from __future__ import annotations import numbers -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from mesa import Model diff --git a/mesa/model.py b/mesa/model.py index 724fea2ee77..d998e112f26 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -14,12 +14,12 @@ from collections import defaultdict # mypy -from typing import Any, Union +from typing import Any from mesa.agent import Agent, AgentSet from mesa.datacollection import DataCollector -TimeT = Union[float, int] +TimeT = float | int class Model: diff --git a/mesa/space.py b/mesa/space.py index 2be9503eef7..a6d070f0291 100644 --- a/mesa/space.py +++ b/mesa/space.py @@ -23,9 +23,9 @@ import itertools import math import warnings -from collections.abc import Iterable, Iterator, Sequence +from collections.abc import Callable, Iterable, Iterator, Sequence from numbers import Real -from typing import Any, Callable, TypeVar, Union, cast, overload +from typing import Any, TypeVar, cast, overload from warnings import warn with contextlib.suppress(ImportError): @@ -42,12 +42,12 @@ Coordinate = tuple[int, int] # used in ContinuousSpace -FloatCoordinate = Union[tuple[float, float], npt.NDArray[float]] +FloatCoordinate = tuple[float, float] | npt.NDArray[float] NetworkCoordinate = int -Position = Union[Coordinate, FloatCoordinate, NetworkCoordinate] +Position = Coordinate | FloatCoordinate | NetworkCoordinate -GridContent = Union[Agent, None] +GridContent = Agent | None MultiGridContent = list[Agent] F = TypeVar("F", bound=Callable[..., Any]) diff --git a/mesa/time.py b/mesa/time.py index 10fa4005ac2..e47563b4880 100644 --- a/mesa/time.py +++ b/mesa/time.py @@ -30,14 +30,12 @@ from collections.abc import Iterable # mypy -from typing import Union - from mesa.agent import Agent, AgentSet from mesa.model import Model # BaseScheduler has a self.time of int, while # StagedActivation has a self.time of float -TimeT = Union[float, int] +TimeT = float | int class BaseScheduler: diff --git a/pyproject.toml b/pyproject.toml index ebc685a0d74..0a94f49104e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "Mesa" description = "Agent-based modeling (ABM) in Python" license = { text = "Apache 2.0" } -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name = "Project Mesa Team", email = "projectmesa@googlegroups.com" }, ] @@ -25,7 +25,6 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -81,9 +80,9 @@ path = "mesa/__init__.py" [tool.ruff] # See https://github.com/charliermarsh/ruff#rules for error code definitions. -# Hardcode to Python 3.9. +# Hardcode to Python 3.10. # Reminder to update mesa-examples if the value below is changed. -target-version = "py39" +target-version = "py310" extend-exclude = ["docs", "build"] [tool.ruff.lint]