Repository: projectmesa/mesa
Pull request #1558 (2022-12-09 10:42:48)

Remove auto-update GH Actions for Pipfile.lock
With this, the Pipfile.lock is going to be frozen. We should decide whether to remove it for 1.2 or 2.0. I think 1.2 is fine, but this decision is not blocking this PR.

codecov[bot]: # [Codecov](https://codecov.io/gh/projectmesa/mesa/pull/1558?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
Base: **81.44**% // Head: **82.05**% // Increases project coverage by **`+0.60%`** :tada:
> Coverage data is based on head [(`ba03e82`)](https://codecov.io/gh/projectmesa/mesa/pull/1558?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) compared to base [(`336ae59`)](https://codecov.io/gh/projectmesa/mesa/commit/336ae595dbfe032c5a09183d7461d56561bbe66e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).
> Patch has no changes to coverable lines.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1558      +/-   ##
==========================================
+ Coverage   81.44%   82.05%   +0.60%     
==========================================
  Files          16       18       +2     
  Lines        1326     1371      +45     
  Branches      230      230              
==========================================
+ Hits         1080     1125      +45     
  Misses        203      203              
  Partials       43       43              
```


| [Impacted Files](https://codecov.io/gh/projectmesa/mesa/pull/1558?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) | Coverage Δ | |
|---|---|---|
| [mesa/visualization/ModularVisualization.py](https://codecov.io/gh/projectmesa/mesa/pull/1558/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS92aXN1YWxpemF0aW9uL01vZHVsYXJWaXN1YWxpemF0aW9uLnB5) | `68.12% <0.00%> (-0.20%)` | :arrow_down: |
| [mesa/space.py](https://codecov.io/gh/projectmesa/mesa/pull/1558/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9zcGFjZS5weQ==) | `91.55% <0.00%> (-0.03%)` | :arrow_down: |
| [mesa/\_\_init\_\_.py](https://codecov.io/gh/projectmesa/mesa/pull/1558/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9fX2luaXRfXy5weQ==) | `100.00% <0.00%> (ø)` | |
| [mesa/model.py](https://codecov.io/gh/projectmesa/mesa/pull/1558/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9tb2RlbC5weQ==) | `100.00% <0.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/projectmesa/mesa/pull/1558?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).


Patch:
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 8e554855..00000000
--- a/.flake8
+++ /dev/null
@@ -1,8 +0,0 @@
-[flake8]
-# Ignore list taken from https://github.com/psf/black/blob/master/.flake8
-# E203	Whitespace before ':'
-# E266	Too many leading '#' for block comment
-# E501	Line too long (82 > 79 characters)
-# W503	Line break occurred before a binary operator
-ignore = E203, E266, E501, W503
-exclude = docs, build
diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml
index 33d311b9..b8eb7dd0 100644
--- a/.github/workflows/build_lint.yml
+++ b/.github/workflows/build_lint.yml
@@ -30,8 +30,10 @@ jobs:
       fail-fast: False
       matrix:
         os: [windows, ubuntu, macos]
-        python-version: ["3.10"]
+        python-version: ["3.11"]
         include:
+          - os: ubuntu
+            python-version: "3.10"
           - os: ubuntu
             python-version: "3.9"
           - os: ubuntu
@@ -58,7 +60,7 @@ jobs:
       name: Codecov
       uses: codecov/codecov-action@v3
 
-  lint-flake:
+  lint-ruff:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v3
@@ -66,10 +68,11 @@ jobs:
       uses: actions/setup-python@v4
       with:
         python-version: "3.10"
-    - run: pip install flake8
-    - name: Lint with flake8
-      # Use settings from mesas .flake8 file
-      run: flake8 . --count --show-source --statistics
+    - run: pip install ruff
+    - name: Lint with ruff
+      # Include `--format=github` to enable automatic inline annotations.
+      # Use settings from pyproject.toml.
+      run: ruff . --format=github --extend-exclude mesa/cookiecutter-mesa/*
 
   lint-black:
     runs-on: ubuntu-latest
diff --git a/.github/workflows/update-pipfile.yml b/.github/workflows/update-pipfile.yml
deleted file mode 100644
index 59a90d52..00000000
--- a/.github/workflows/update-pipfile.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: "Update Pipfile.lock"
-on:
-  schedule:
-    - cron: '0 6 1 * *' # 1st day of each month at 06:00 UTC
-  push:
-    paths:
-    - 'Pipfile'
-    - '.github/workflows/update-pipfile.yml'
-
-jobs:
-  piplock:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - uses: actions/setup-python@v4
-      with:
-        python-version: '3.9'
-    - run: pip install -U pip
-    - run: pip install -U wheel
-    - run: pip install -U pipenv
-    - run: pipenv lock
-    - uses: actions/upload-artifact@v3
-      with:
-        name: "Pipfile lock"
-        path: Pipfile.lock
-    - uses: peter-evans/create-pull-request@v4
-      with:
-        title: "Update Pipfile.lock (dependencies)"
-        branch: update-pipfile
-        base: main
-        commit-message: "[Bot] Update Pipfile.lock dependencies"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2285b3af..0b70964c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,12 +4,12 @@ ci:
 
 repos:
 -   repo: https://github.com/psf/black
-    rev: 22.10.0
+    rev: 22.12.0
     hooks:
       - id: black-jupyter
         exclude: ^mesa/cookiecutter-mesa/
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v3.3.0
+    rev: v3.3.1
     hooks:
     -   id: pyupgrade
         args: [--py38-plus]
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 5ed0feef..f65b7baa 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -68,11 +68,11 @@ If you're changing previous Mesa features, please make sure of the following:
 - Additional features or rewrites of current features are accompanied by tests.
 - New features are demonstrated in a model, so folks can understand more easily.
 
-To ensure that your submission will not break the build, you will need to install Flake8 and pytest.
+To ensure that your submission will not break the build, you will need to install Ruff and pytest.
 
 .. code-block:: bash
 
-    pip install flake8 pytest pytest-cov
+    pip install ruff pytest pytest-cov
 
 We test by implementing simple models and through traditional unit tests in the tests/ folder. The following only covers unit tests coverage. Ensure that your test coverage has not gone down. If it has and you need help, we will offer advice on how to structure tests for the contribution.
 
@@ -91,7 +91,7 @@ You should no longer have to worry about code formatting. If still in doubt you
 
 .. code-block:: bash
 
-    flake8 . --ignore=F403,E501,E123,E128,W504,W503 --exclude=docs,build
+    ruff .
 
 
 .. _`PEP8` : https://www.python.org/dev/peps/pep-0008
diff --git a/mesa/__init__.py b/mesa/__init__.py
index 56bf8a31..596e28f8 100644
--- a/mesa/__init__.py
+++ b/mesa/__init__.py
@@ -5,14 +5,13 @@ Core Objects: Model, and Agent.
 """
 import datetime
 
-from mesa.model import Model
-from mesa.agent import Agent
-
-import mesa.time as time
-import mesa.space as space
 import mesa.flat.visualization as visualization
-from mesa.datacollection import DataCollector
+import mesa.space as space
+import mesa.time as time
+from mesa.agent import Agent
 from mesa.batchrunner import batch_run  # noqa
+from mesa.datacollection import DataCollector
+from mesa.model import Model
 
 __all__ = [
     "Model",
diff --git a/mesa/agent.py b/mesa/agent.py
index 51c2f1fe..e4347c42 100644
--- a/mesa/agent.py
+++ b/mesa/agent.py
@@ -7,9 +7,10 @@ Core Objects: Agent
 # Remove this __future__ import once the oldest supported Python is 3.10
 from __future__ import annotations
 
+from random import Random
+
 # mypy
 from typing import TYPE_CHECKING
-from random import Random
 
 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 e856b097..d3939f88 100644
--- a/mesa/batchrunner.py
+++ b/mesa/batchrunner.py
@@ -10,7 +10,6 @@ import random
 from functools import partial
 from itertools import count, product
 from multiprocessing import Pool, cpu_count
-from warnings import warn
 from typing import (
     Any,
     Dict,
@@ -22,6 +21,7 @@ from typing import (
     Type,
     Union,
 )
+from warnings import warn
 
 import pandas as pd
 from tqdm import tqdm
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py
index 64023f8c..a72014b2 100644
--- a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-from setuptools import setup, find_packages
+from setuptools import find_packages, setup
 
 requires = ["mesa"]
 
diff --git a/mesa/datacollection.py b/mesa/datacollection.py
index ba25c80e..f2ada296 100644
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -34,11 +34,12 @@ The default DataCollector here makes several assumptions:
     * The schedule has an agent list called agents
     * For collecting agent-level variables, agents must have a unique_id
 """
-from functools import partial
 import itertools
+import types
+from functools import partial
 from operator import attrgetter
+
 import pandas as pd
-import types
 
 
 class DataCollector:
@@ -83,9 +84,10 @@ class DataCollector:
             Model reporters can take four types of arguments:
             lambda like above:
             {"agent_count": lambda m: m.schedule.get_agent_count() }
-            method with @property decorators
-            {"agent_count": schedule.get_agent_count()
-            class attributes of model
+            method of a class/instance:
+            {"agent_count": self.get_agent_count} # self here is a class instance
+            {"agent_count": Model.get_agent_count} # Model here is a class
+            class attributes of a model
             {"model_attribute": "model_attribute"}
             functions with parameters that have placed in a list
             {"Model_Function":[function, [param_1, param_2]]}
@@ -117,8 +119,6 @@ class DataCollector:
             reporter: Attribute string, or function object that returns the
                       variable when given a model instance.
         """
-        if type(reporter) is str:
-            reporter = partial(self._getattr, reporter)
         self.model_reporters[name] = reporter
         self.model_vars[name] = []
 
@@ -163,9 +163,6 @@ class DataCollector:
         agent_records = map(get_reports, model.schedule.agents)
         return agent_records
 
-    def _reporter_decorator(self, reporter):
-        return reporter()
-
     def collect(self, model):
         """Collect all the data for the given model object."""
         if self.model_reporters:
@@ -175,13 +172,15 @@ class DataCollector:
                 if isinstance(reporter, types.LambdaType):
                     self.model_vars[var].append(reporter(model))
                 # Check if model attribute
-                elif isinstance(reporter, partial):
-                    self.model_vars[var].append(reporter(model))
+                elif isinstance(reporter, str):
+                    self.model_vars[var].append(getattr(model, reporter, None))
                 # Check if function with arguments
                 elif isinstance(reporter, list):
                     self.model_vars[var].append(reporter[0](*reporter[1]))
+                # TODO: Check if method of a class, as of now it is assumed
+                # implicitly if the other checks fail.
                 else:
-                    self.model_vars[var].append(self._reporter_decorator(reporter))
+                    self.model_vars[var].append(reporter())
 
         if self.agent_reporters:
             agent_records = self._record_agents(model)
@@ -232,8 +231,8 @@ class DataCollector:
         df = pd.DataFrame.from_records(
             data=all_records,
             columns=["Step", "AgentID"] + rep_names,
+            index=["Step", "AgentID"],
         )
-        df = df.set_index(["Step", "AgentID"])
         return df
 
     def get_table_dataframe(self, table_name):
diff --git a/mesa/main.py b/mesa/main.py
index 51f0639a..1537e885 100644
--- a/mesa/main.py
+++ b/mesa/main.py
@@ -1,8 +1,9 @@
-import sys
 import os
-import click
+import sys
 from subprocess import call
 
+import click
+
 PROJECT_PATH = click.Path(
     exists=True, file_okay=False, dir_okay=True, resolve_path=True
 )
diff --git a/mesa/model.py b/mesa/model.py
index 0851a4ea..5225f066 100644
--- a/mesa/model.py
+++ b/mesa/model.py
@@ -9,11 +9,11 @@ from __future__ import annotations
 
 import random
 
-from mesa.datacollection import DataCollector
-
 # mypy
 from typing import Any
 
+from mesa.datacollection import DataCollector
+
 
 class Model:
     """Base class for models."""
diff --git a/mesa/space.py b/mesa/space.py
index 42dd29e3..3bbd45df 100644
--- a/mesa/space.py
+++ b/mesa/space.py
@@ -20,19 +20,16 @@ NetworkGrid: a network where each node contains zero or more agents.
 # Remove this __future__ import once the oldest supported Python is 3.10
 from __future__ import annotations
 
-import itertools
 import collections
+import itertools
 import math
-from warnings import warn
-
-import numpy as np
-
+from numbers import Real
 from typing import (
     Any,
     Callable,
-    List,
     Iterable,
     Iterator,
+    List,
     Sequence,
     Tuple,
     TypeVar,
@@ -40,11 +37,14 @@ from typing import (
     cast,
     overload,
 )
+from warnings import warn
+
+import networkx as nx
+import numpy as np
+import numpy.typing as npt
 
 # For Mypy
 from .agent import Agent
-from numbers import Real
-import numpy.typing as npt
 
 Coordinate = Tuple[int, int]
 # used in ContinuousSpace
@@ -78,17 +78,16 @@ def is_integer(x: Real) -> bool:
     return isinstance(x, (int, np.integer))
 
 
-class Grid:
+class _Grid:
     """Base class for a rectangular grid.
 
-    Grid cells are indexed by [x][y], where [0][0] is assumed to be the
-    bottom-left and [width-1][height-1] is the top-right. If a grid is
+    Grid cells are indexed by [x, y], where [0, 0] is assumed to be the
+    bottom-left and [width-1, height-1] is the top-right. If a grid is
     toroidal, the top and bottom, and left and right, edges wrap to each other
 
     Properties:
         width, height: The grid's width and height.
         torus: Boolean which determines whether to treat the grid as a torus.
-        grid: Internal list-of-lists which holds the grid cells themselves.
     """
 
     def __init__(self, width: int, height: int, torus: bool) -> None:
@@ -103,13 +102,16 @@ class Grid:
         self.torus = torus
         self.num_cells = height * width
 
-        self.grid: list[list[GridContent]]
-        self.grid = [
+        # Internal list-of-lists which holds the grid cells themselves
+        self._grid: list[list[GridContent]]
+        self._grid = [
             [self.default_val() for _ in range(self.height)] for _ in range(self.width)
         ]
 
-        # Add all cells to the empties list.
-        self.empties = set(itertools.product(range(self.width), range(self.height)))
+        # Flag to check if the empties set has been created. Better than initializing
+        # _empties as set() because in this case it would become impossible to discern
+        # if the set hasn't still being built or if it has become empty after creation.
+        self._empties_built = False
 
         # Neighborhood Cache
         self._neighborhood_cache: dict[Any, list[Coordinate]] = dict()
@@ -119,6 +121,21 @@ class Grid:
         """Default value for new cell elements."""
         return None
 
+    @property
+    def empties(self) -> set:
+        if not self._empties_built:
+            self.build_empties()
+        return self._empties
+
+    def build_empties(self) -> None:
+        self._empties = set(
+            filter(
+                self.is_cell_empty,
+                itertools.product(range(self.width), range(self.height)),
+            )
+        )
+        self._empties_built = True
+
     @overload
     def __getitem__(self, index: int) -> list[GridContent]:
         ...
@@ -141,11 +158,11 @@ class Grid:
 
         if isinstance(index, int):
             # grid[x]
-            return self.grid[index]
+            return self._grid[index]
         elif isinstance(index[0], tuple):
             # grid[(x1, y1), (x2, y2), ...]
             index = cast(Sequence[Coordinate], index)
-            return [self.grid[x][y] for x, y in map(self.torus_adj, index)]
+            return [self._grid[x][y] for x, y in map(self.torus_adj, index)]
 
         x, y = index
         x_int, y_int = is_integer(x), is_integer(y)
@@ -154,32 +171,32 @@ class Grid:
             # grid[x, y]
             index = cast(Coordinate, index)
             x, y = self.torus_adj(index)
-            return self.grid[x][y]
+            return self._grid[x][y]
         elif x_int:
             # grid[x, :]
             x, _ = self.torus_adj((x, 0))
             y = cast(slice, y)
-            return self.grid[x][y]
+            return self._grid[x][y]
         elif y_int:
             # grid[:, y]
             _, y = self.torus_adj((0, y))
             x = cast(slice, x)
-            return [rows[y] for rows in self.grid[x]]
+            return [rows[y] for rows in self._grid[x]]
         else:
             # grid[:, :]
             x, y = (cast(slice, x), cast(slice, y))
-            return [cell for rows in self.grid[x] for cell in rows[y]]
+            return [cell for rows in self._grid[x] for cell in rows[y]]
 
     def __iter__(self) -> Iterator[GridContent]:
         """Create an iterator that chains the rows of the grid together
         as if it is one list:"""
-        return itertools.chain(*self.grid)
+        return itertools.chain(*self._grid)
 
     def coord_iter(self) -> Iterator[tuple[GridContent, int, int]]:
         """An iterator that returns coordinates as well as cell contents."""
         for row in range(self.width):
             for col in range(self.height):
-                yield self.grid[row][col], row, col  # agent, x, y
+                yield self._grid[row][col], row, col  # agent, x, y
 
     def neighbor_iter(self, pos: Coordinate, moore: bool = True) -> Iterator[Agent]:
         """Iterate over position neighbors.
@@ -379,32 +396,40 @@ class Grid:
     def iter_cell_list_contents(
         self, cell_list: Iterable[Coordinate]
     ) -> Iterator[Agent]:
-        """Returns an iterator of the contents of the cells
-        identified in cell_list.
+        """Returns an iterator of the agents contained in the cells identified
+        in `cell_list`; cells with empty content are excluded.
 
         Args:
             cell_list: Array-like of (x, y) tuples, or single tuple.
 
         Returns:
-            An iterator of the contents of the cells identified in cell_list
+            An iterator of the agents contained in the cells identified in `cell_list`.
         """
         # iter_cell_list_contents returns only non-empty contents.
-        return (self.grid[x][y] for x, y in cell_list if self.grid[x][y])
+        return (
+            self._grid[x][y]
+            for x, y in itertools.filterfalse(self.is_cell_empty, cell_list)
+        )
 
     @accept_tuple_argument
     def get_cell_list_contents(self, cell_list: Iterable[Coordinate]) -> list[Agent]:
-        """Returns a list of the contents of the cells
-        identified in cell_list.
-        Note: this method returns a list of `Agent`'s; `None` contents are excluded.
+        """Returns an iterator of the agents contained in the cells identified
+        in `cell_list`; cells with empty content are excluded.
 
         Args:
             cell_list: Array-like of (x, y) tuples, or single tuple.
 
         Returns:
-            A list of the contents of the cells identified in cell_list
+            A list of the agents contained in the cells identified in `cell_list`.
         """
         return list(self.iter_cell_list_contents(cell_list))
 
+    def place_agent(self, agent: Agent, pos: Coordinate) -> None:
+        ...
+
+    def remove_agent(self, agent: Agent) -> None:
+        ...
+
     def move_agent(self, agent: Agent, pos: Coordinate) -> None:
         """Move an agent from its current position to a new position.
 
@@ -417,22 +442,6 @@ class Grid:
         self.remove_agent(agent)
         self.place_agent(agent, pos)
 
-    def place_agent(self, agent: Agent, pos: Coordinate) -> None:
-        """Place the agent at the specified location, and set its pos variable."""
-        x, y = pos
-        self.grid[x][y] = agent
-        self.empties.discard(pos)
-        agent.pos = pos
-
-    def remove_agent(self, agent: Agent) -> None:
-        """Remove the agent from the grid and set its pos attribute to None."""
-        if (pos := agent.pos) is None:
-            return
-        x, y = pos
-        self.grid[x][y] = self.default_val()
-        self.empties.add(pos)
-        agent.pos = None
-
     def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None:
         """Swap agents positions"""
         agents_no_pos = []
@@ -456,7 +465,7 @@ class Grid:
     def is_cell_empty(self, pos: Coordinate) -> bool:
         """Returns a bool of the contents of a cell."""
         x, y = pos
-        return self.grid[x][y] == self.default_val()
+        return self._grid[x][y] == self.default_val()
 
     def move_to_empty(
         self, agent: Agent, cutoff: float = 0.998, num_agents: int | None = None
@@ -517,8 +526,17 @@ class Grid:
         return len(self.empties) > 0
 
 
-class SingleGrid(Grid):
-    """Grid where each cell contains exactly at most one object."""
+class SingleGrid(_Grid):
+    """Rectangular grid where each cell contains exactly at most one agent.
+
+    Grid cells are indexed by [x, y], where [0, 0] is assumed to be the
+    bottom-left and [width-1, height-1] is the top-right. If a grid is
+    toroidal, the top and bottom, and left and right, edges wrap to each other.
+
+    Properties:
+        width, height: The grid's width and height.
+        torus: Boolean which determines whether to treat the grid as a torus.
+    """
 
     def position_agent(
         self, agent: Agent, x: int | str = "random", y: int | str = "random"
@@ -551,38 +569,43 @@ class SingleGrid(Grid):
             )
 
         if x == "random" or y == "random":
-            if len(self.empties) == 0:
-                raise Exception("ERROR: Grid full")
             self.move_to_empty(agent)
         else:
             coords = (x, y)
             self.place_agent(agent, coords)
 
     def place_agent(self, agent: Agent, pos: Coordinate) -> None:
+        """Place the agent at the specified location, and set its pos variable."""
         if self.is_cell_empty(pos):
-            super().place_agent(agent, pos)
+            x, y = pos
+            self._grid[x][y] = agent
+            if self._empties_built:
+                self._empties.discard(pos)
+            agent.pos = pos
         else:
             raise Exception("Cell not empty")
 
+    def remove_agent(self, agent: Agent) -> None:
+        """Remove the agent from the grid and set its pos attribute to None."""
+        if (pos := agent.pos) is None:
+            return
+        x, y = pos
+        self._grid[x][y] = self.default_val()
+        if self._empties_built:
+            self._empties.add(pos)
+        agent.pos = None
 
-class MultiGrid(Grid):
-    """Grid where each cell can contain more than one object.
 
-    Grid cells are indexed by [x][y], where [0][0] is assumed to be at
-    bottom-left and [width-1][height-1] is the top-right. If a grid is
-    toroidal, the top and bottom, and left and right, edges wrap to each other.
+class MultiGrid(_Grid):
+    """Rectangular grid where each cell can contain more than one agent.
 
-    Each grid cell holds a set object.
+    Grid cells are indexed by [x, y], where [0, 0] is assumed to be at
+    bottom-left and [width-1, height-1] is the top-right. If a grid is
+    toroidal, the top and bottom, and left and right, edges wrap to each other.
 
     Properties:
         width, height: The grid's width and height.
-
         torus: Boolean which determines whether to treat the grid as a torus.
-
-        grid: Internal list-of-lists which holds the grid cells themselves.
-
-    Methods:
-        get_neighbors: Returns the objects surrounding a given cell.
     """
 
     grid: list[list[MultiGridContent]]
@@ -595,40 +618,42 @@ class MultiGrid(Grid):
     def place_agent(self, agent: Agent, pos: Coordinate) -> None:
         """Place the agent at the specified location, and set its pos variable."""
         x, y = pos
-        if agent.pos is None or agent not in self.grid[x][y]:
-            self.grid[x][y].append(agent)
+        if agent.pos is None or agent not in self._grid[x][y]:
+            self._grid[x][y].append(agent)
             agent.pos = pos
-            self.empties.discard(pos)
+            if self._empties_built:
+                self._empties.discard(pos)
 
     def remove_agent(self, agent: Agent) -> None:
         """Remove the agent from the given location and set its pos attribute to None."""
         pos = agent.pos
         x, y = pos
-        self.grid[x][y].remove(agent)
-        if self.is_cell_empty(pos):
-            self.empties.add(pos)
+        self._grid[x][y].remove(agent)
+        if self._empties_built and self.is_cell_empty(pos):
+            self._empties.add(pos)
         agent.pos = None
 
     @accept_tuple_argument
     def iter_cell_list_contents(
         self, cell_list: Iterable[Coordinate]
-    ) -> Iterator[MultiGridContent]:
-        """Returns an iterator of the contents of the
-        cells identified in cell_list.
+    ) -> Iterator[Agent]:
+        """Returns an iterator of the agents contained in the cells identified
+        in `cell_list`; cells with empty content are excluded.
 
         Args:
             cell_list: Array-like of (x, y) tuples, or single tuple.
 
         Returns:
-            A iterator of the contents of the cells identified in cell_list
+            An iterator of the agents contained in the cells identified in `cell_list`.
         """
         return itertools.chain.from_iterable(
-            self[x][y] for x, y in cell_list if not self.is_cell_empty((x, y))
+            self._grid[x][y]
+            for x, y in itertools.filterfalse(self.is_cell_empty, cell_list)
         )
 
 
-class HexGrid(Grid):
-    """Hexagonal Grid: Extends Grid to handle hexagonal neighbors.
+class HexGrid(SingleGrid):
+    """Hexagonal Grid: Extends SingleGrid to handle hexagonal neighbors.
 
     Functions according to odd-q rules.
     See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
@@ -1015,11 +1040,21 @@ class NetworkGrid:
         self.G.nodes[node_id]["agent"].append(agent)
         agent.pos = node_id
 
-    def get_neighbors(self, node_id: int, include_center: bool = False) -> list[int]:
-        """Get all adjacent nodes"""
-        neighbors = list(self.G.neighbors(node_id))
-        if include_center:
-            neighbors.append(node_id)
+    def get_neighbors(
+        self, node_id: int, include_center: bool = False, radius: int = 1
+    ) -> list[int]:
+        """Get all adjacent nodes within a certain radius"""
+        if radius == 1:
+            neighbors = list(self.G.neighbors(node_id))
+            if include_center:
+                neighbors.append(node_id)
+        else:
+            neighbors_with_distance = nx.single_source_shortest_path_length(
+                self.G, node_id, radius
+            )
+            if not include_center:
+                del neighbors_with_distance[node_id]
+            neighbors = sorted(neighbors_with_distance.keys())
         return neighbors
 
     def move_agent(self, agent: Agent, node_id: int) -> None:
@@ -1041,12 +1076,7 @@ class NetworkGrid:
         """Returns a list of the agents contained in the nodes identified
         in `cell_list`; nodes with empty content are excluded.
         """
-        list_of_lists = [
-            self.G.nodes[node_id]["agent"]
-            for node_id in cell_list
-            if not self.is_cell_empty(node_id)
-        ]
-        return [item for sublist in list_of_lists for item in sublist]
+        return list(self.iter_cell_list_contents(cell_list))
 
     def get_all_cell_contents(self) -> list[Agent]:
         """Returns a list of all the agents in the network."""
@@ -1056,4 +1086,7 @@ class NetworkGrid:
         """Returns an iterator of the agents contained in the nodes identified
         in `cell_list`; nodes with empty content are excluded.
         """
-        yield from self.get_cell_list_contents(cell_list)
+        return itertools.chain.from_iterable(
+            self.G.nodes[node_id]["agent"]
+            for node_id in itertools.filterfalse(self.is_cell_empty, cell_list)
+        )
diff --git a/mesa/time.py b/mesa/time.py
index 4c332e3e..7982eeec 100644
--- a/mesa/time.py
+++ b/mesa/time.py
@@ -29,10 +29,10 @@ from collections import defaultdict
 
 # mypy
 from typing import Iterator, Union
+
 from mesa.agent import Agent
 from mesa.model import Model
 
-
 # BaseScheduler has a self.time of int, while
 # StagedActivation has a self.time of float
 TimeT = Union[float, int]
diff --git a/mesa/visualization/ModularVisualization.py b/mesa/visualization/ModularVisualization.py
index 6345db24..d723a50b 100644
--- a/mesa/visualization/ModularVisualization.py
+++ b/mesa/visualization/ModularVisualization.py
@@ -97,15 +97,16 @@ Client -> Server:
 import asyncio
 import os
 import platform
+import webbrowser
+
 import tornado.autoreload
+import tornado.escape
+import tornado.gen
 import tornado.ioloop
 import tornado.web
 import tornado.websocket
-import tornado.escape
-import tornado.gen
-import webbrowser
 
-from mesa.visualization.UserParam import UserSettableParameter, UserParam
+from mesa.visualization.UserParam import UserParam, UserSettableParameter
 
 # Suppress several pylint warnings for this file.
 # Attributes being defined outside of init is a Tornado feature.
diff --git a/mesa/visualization/UserParam.py b/mesa/visualization/UserParam.py
index 0a1c6ac7..f08fd8e9 100644
--- a/mesa/visualization/UserParam.py
+++ b/mesa/visualization/UserParam.py
@@ -1,6 +1,5 @@
-from warnings import warn
 import numbers
-
+from warnings import warn
 
 NUMBER = "number"
 CHECKBOX = "checkbox"
@@ -92,7 +91,7 @@ class UserSettableParameter:
         valid = True
 
         if self.param_type == self.NUMBER:
-            valid = not (self.value is None)
+            valid = self.value is not None
 
         elif self.param_type == self.SLIDER:
             valid = not (
diff --git a/mesa/visualization/modules/BarChartVisualization.py b/mesa/visualization/modules/BarChartVisualization.py
index a0c3cedb..bec182bd 100644
--- a/mesa/visualization/modules/BarChartVisualization.py
+++ b/mesa/visualization/modules/BarChartVisualization.py
@@ -5,7 +5,8 @@ Pie Chart Module
 Module for drawing live-updating bar charts using d3.js
 """
 import json
-from mesa.visualization.ModularVisualization import VisualizationElement, D3_JS_FILE
+
+from mesa.visualization.ModularVisualization import D3_JS_FILE, VisualizationElement
 
 
 class BarChartModule(VisualizationElement):
diff --git a/mesa/visualization/modules/CanvasGridVisualization.py b/mesa/visualization/modules/CanvasGridVisualization.py
index 281cd574..b8b4b462 100644
--- a/mesa/visualization/modules/CanvasGridVisualization.py
+++ b/mesa/visualization/modules/CanvasGridVisualization.py
@@ -5,6 +5,7 @@ Modular Canvas Rendering
 Module for visualizing model objects in grid cells.
 """
 from collections import defaultdict
+
 from mesa.visualization.ModularVisualization import VisualizationElement
 
 
diff --git a/mesa/visualization/modules/ChartVisualization.py b/mesa/visualization/modules/ChartVisualization.py
index fc6ee1fe..8f356fa4 100644
--- a/mesa/visualization/modules/ChartVisualization.py
+++ b/mesa/visualization/modules/ChartVisualization.py
@@ -5,7 +5,8 @@ Chart Module
 Module for drawing live-updating line charts using Charts.js
 """
 import json
-from mesa.visualization.ModularVisualization import VisualizationElement, CHART_JS_FILE
+
+from mesa.visualization.ModularVisualization import CHART_JS_FILE, VisualizationElement
 
 
 class ChartModule(VisualizationElement):
diff --git a/mesa/visualization/modules/HexGridVisualization.py b/mesa/visualization/modules/HexGridVisualization.py
index ddd26a97..8e318baf 100644
--- a/mesa/visualization/modules/HexGridVisualization.py
+++ b/mesa/visualization/modules/HexGridVisualization.py
@@ -5,6 +5,7 @@ Modular Canvas Rendering
 Module for visualizing model objects in hexagonal grid cells.
 """
 from collections import defaultdict
+
 from mesa.visualization.ModularVisualization import VisualizationElement
 
 
diff --git a/mesa/visualization/modules/NetworkVisualization.py b/mesa/visualization/modules/NetworkVisualization.py
index 85a35306..fa0ea61d 100644
--- a/mesa/visualization/modules/NetworkVisualization.py
+++ b/mesa/visualization/modules/NetworkVisualization.py
@@ -4,7 +4,7 @@ Network Visualization Module
 
 Module for rendering the network, using [d3.js](https://d3js.org/) framework.
 """
-from mesa.visualization.ModularVisualization import VisualizationElement, D3_JS_FILE
+from mesa.visualization.ModularVisualization import D3_JS_FILE, VisualizationElement
 
 
 class NetworkModule(VisualizationElement):
diff --git a/mesa/visualization/modules/PieChartVisualization.py b/mesa/visualization/modules/PieChartVisualization.py
index 671d53cb..b9762922 100644
--- a/mesa/visualization/modules/PieChartVisualization.py
+++ b/mesa/visualization/modules/PieChartVisualization.py
@@ -5,7 +5,8 @@ Pie Chart Module
 Module for drawing live-updating pie charts using d3.js
 """
 import json
-from mesa.visualization.ModularVisualization import VisualizationElement, D3_JS_FILE
+
+from mesa.visualization.ModularVisualization import D3_JS_FILE, VisualizationElement
 
 
 class PieChartModule(VisualizationElement):
diff --git a/mesa/visualization/templates/js/GridDraw.js b/mesa/visualization/templates/js/GridDraw.js
index 5e1603de..614ed6eb 100644
--- a/mesa/visualization/templates/js/GridDraw.js
+++ b/mesa/visualization/templates/js/GridDraw.js
@@ -57,7 +57,7 @@ const GridVisualization = function (
   // Calls the appropriate shape(agent)
   this.drawLayer = function (portrayalLayer) {
     // Re-initialize the lookup table
-    interactionHandler ? interactionHandler.mouseoverLookupTable.init() : null;
+    if (interactionHandler) interactionHandler.mouseoverLookupTable.init();
 
     for (const i in portrayalLayer) {
       const p = portrayalLayer[i];
@@ -72,9 +72,7 @@ const GridVisualization = function (
       p.y = gridHeight - p.y - 1;
 
       // if a handler exists, add coordinates for the portrayalLayer index
-      interactionHandler
-        ? interactionHandler.mouseoverLookupTable.set(p.x, p.y, i)
-        : null;
+      if (interactionHandler) interactionHandler.mouseoverLookupTable.set(p.x, p.y, i);
 
       // If the stroke color is not defined, then the first color in the colors array is the stroke color.
       if (!p.stroke_color) p.stroke_color = p.Color[0];
@@ -127,9 +125,7 @@ const GridVisualization = function (
         this.drawCustomImage(p.Shape, p.x, p.y, p.scale, p.text, p.text_color);
     }
     // if a handler exists, update its mouse listeners with the new data
-    interactionHandler
-      ? interactionHandler.updateMouseListeners(portrayalLayer)
-      : null;
+    if (interactionHandler) interactionHandler.updateMouseListeners(portrayalLayer);
   };
 
   // DRAWING METHODS
diff --git a/mesa/visualization/templates/js/HexDraw.js b/mesa/visualization/templates/js/HexDraw.js
index a09bbbff..ad496389 100644
--- a/mesa/visualization/templates/js/HexDraw.js
+++ b/mesa/visualization/templates/js/HexDraw.js
@@ -55,12 +55,12 @@ const HexVisualization = function (
   const maxR = Math.min(cellHeight, cellWidth) / 2 - 1;
 
   // Configure the interaction handler to use a hex coordinate mapper
-  interactionHandler ? interactionHandler.setCoordinateMapper("hex") : null;
+  if (interactionHandler) interactionHandler.setCoordinateMapper("hex");
 
   // Calls the appropriate shape(agent)
   this.drawLayer = function (portrayalLayer) {
     // Re-initialize the lookup table
-    interactionHandler ? interactionHandler.mouseoverLookupTable.init() : null;
+    if (interactionHandler) interactionHandler.mouseoverLookupTable.init();
     for (const i in portrayalLayer) {
       const p = portrayalLayer[i];
       // Does the inversion of y positioning because of html5
@@ -69,9 +69,7 @@ const HexVisualization = function (
       p.y = gridHeight - p.y - 1;
 
       // if a handler exists, add coordinates for the portrayalLayer index
-      interactionHandler
-        ? interactionHandler.mouseoverLookupTable.set(p.x, p.y, i)
-        : null;
+      if (interactionHandler) interactionHandler.mouseoverLookupTable.set(p.x, p.y, i);
 
       if (p.Shape == "hex")
         this.drawHex(p.x, p.y, p.r, p.Color, p.Filled, p.text, p.text_color);
@@ -93,9 +91,7 @@ const HexVisualization = function (
         this.drawCustomImage(p.Shape, p.x, p.y, p.scale, p.text, p.text_color);
     }
     // if a handler exists, update its mouse listeners with the new data
-    interactionHandler
-      ? interactionHandler.updateMouseListeners(portrayalLayer)
-      : null;
+    if (interactionHandler) interactionHandler.updateMouseListeners(portrayalLayer);
   };
 
   // DRAWING METHODS
@@ -158,7 +154,7 @@ const HexVisualization = function (
     } else {
       cy = (y + 0.5) * cellHeight + cellHeight / 2;
     }
-    maxHexRadius = cellHeight / Math.sqrt(3);
+    const maxHexRadius = cellHeight / Math.sqrt(3);
     const r = radius * maxHexRadius;
 
     function hex_corner(x, y, size, i) {
diff --git a/mesa/visualization/templates/js/InteractionHandler.js b/mesa/visualization/templates/js/InteractionHandler.js
index 93f1a6b6..f98906d1 100644
--- a/mesa/visualization/templates/js/InteractionHandler.js
+++ b/mesa/visualization/templates/js/InteractionHandler.js
@@ -125,8 +125,6 @@ const InteractionHandler = function (width, height, gridWidth, gridHeight, ctx)
 
       // map the event to x,y coordinates
       const position = coordinateMapper(event);
-      const yPosition = Math.floor(event.offsetY / cellHeight);
-      const xPosition = Math.floor(event.offsetX / cellWidth);
 
       // look up the portrayal items the coordinates refer to and draw a tooltip
       mouseoverLookupTable
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..6a5c4e33
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+[tool.ruff]
+select = ["E", "F", "I"]
+# Ignore list taken from https://github.com/psf/black/blob/master/.flake8
+# E203	Whitespace before ':'
+# E266	Too many leading '#' for block comment
+# E501	Line too long (82 > 79 characters)
+# W503	Line break occurred before a binary operator
+# But we don't specify them because ruff's Black already
+# checks for it.
+# See https://github.com/charliermarsh/ruff/issues/1842#issuecomment-1381210185
+extend-ignore = ["E501"]
+extend-exclude = ["docs", "build"]
+# Hardcode to Python 3.10.
+target-version = "py310"
diff --git a/setup.py b/setup.py
index 8a59a0f8..6e2292dd 100644
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,17 @@
 #!/usr/bin/env python
-import re
 import os
+import re
+import shutil
 import urllib.request
 import zipfile
-import shutil
-
-from setuptools import setup, find_packages
 from codecs import open
 
+from setuptools import find_packages, setup
+
 requires = ["click", "cookiecutter", "networkx", "numpy", "pandas", "tornado", "tqdm"]
 
 extras_require = {
-    "dev": ["black", "coverage", "flake8", "pytest >= 4.6", "pytest-cov", "sphinx"],
+    "dev": ["black", "ruff", "coverage", "pytest >= 4.6", "pytest-cov", "sphinx"],
     "docs": ["sphinx", "ipython"],
 }
 


Test patch:
diff --git a/tests/test_batchrunner.py b/tests/test_batchrunner.py
index 2bc7dc6f..761598b4 100644
--- a/tests/test_batchrunner.py
+++ b/tests/test_batchrunner.py
@@ -1,20 +1,19 @@
 """
 Test the BatchRunner
 """
+import unittest
 from functools import reduce
 from operator import mul
-import unittest
 
 from mesa import Agent, Model
-from mesa.time import BaseScheduler
-from mesa.datacollection import DataCollector
 from mesa.batchrunner import (
     BatchRunner,
     FixedBatchRunner,
     ParameterProduct,
     ParameterSampler,
 )
-
+from mesa.datacollection import DataCollector
+from mesa.time import BaseScheduler
 
 NUM_AGENTS = 7
 
diff --git a/tests/test_batchrunnerMP.py b/tests/test_batchrunnerMP.py
index d6ff3001..935e61ed 100644
--- a/tests/test_batchrunnerMP.py
+++ b/tests/test_batchrunnerMP.py
@@ -1,15 +1,15 @@
 """
 Test the BatchRunner
 """
+import unittest
 from functools import reduce
+from multiprocessing import cpu_count, freeze_support
 from operator import mul
-import unittest
 
 from mesa import Agent, Model
-from mesa.time import BaseScheduler
-from mesa.datacollection import DataCollector
 from mesa.batchrunner import BatchRunnerMP, ParameterProduct, ParameterSampler
-from multiprocessing import freeze_support, cpu_count
+from mesa.datacollection import DataCollector
+from mesa.time import BaseScheduler
 
 NUM_AGENTS = 7
 
diff --git a/tests/test_datacollector.py b/tests/test_datacollector.py
index 0f2bd4c0..565f9e95 100644
--- a/tests/test_datacollector.py
+++ b/tests/test_datacollector.py
@@ -3,7 +3,7 @@ Test the DataCollector
 """
 import unittest
 
-from mesa import Model, Agent
+from mesa import Agent, Model
 from mesa.time import BaseScheduler
 
 
diff --git a/tests/test_examples.py b/tests/test_examples.py
index 73285b5c..bb2c31c3 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -1,8 +1,8 @@
-import sys
-import os.path
-import unittest
 import contextlib
 import importlib
+import os.path
+import sys
+import unittest
 
 
 def classcase(name):
diff --git a/tests/test_grid.py b/tests/test_grid.py
index 007aa6c4..e5148246 100644
--- a/tests/test_grid.py
+++ b/tests/test_grid.py
@@ -3,8 +3,9 @@ Test the Grid objects.
 """
 import random
 import unittest
-from unittest.mock import patch, Mock
-from mesa.space import Grid, SingleGrid, MultiGrid, HexGrid
+from unittest.mock import Mock, patch
+
+from mesa.space import HexGrid, MultiGrid, SingleGrid
 
 # Initial agent positions for testing
 #
@@ -29,9 +30,9 @@ class MockAgent:
         self.pos = pos
 
 
-class TestBaseGrid(unittest.TestCase):
+class TestSingleGrid(unittest.TestCase):
     """
-    Testing a non-toroidal grid.
+    Testing a non-toroidal singlegrid.
     """
 
     torus = False
@@ -43,7 +44,7 @@ class TestBaseGrid(unittest.TestCase):
         # The height needs to be even to test the edge case described in PR #1517
         height = 6  # height of grid
         width = 3  # width of grid
-        self.grid = Grid(width, height, self.torus)
+        self.grid = SingleGrid(width, height, self.torus)
         self.agents = []
         counter = 0
         for x in range(width):
@@ -149,8 +150,8 @@ class TestBaseGrid(unittest.TestCase):
     def test_agent_move(self):
         # get the agent at [0, 1]
         agent = self.agents[0]
-        self.grid.move_agent(agent, (1, 1))
-        assert agent.pos == (1, 1)
+        self.grid.move_agent(agent, (1, 0))
+        assert agent.pos == (1, 0)
         # move it off the torus and check for the exception
         if not self.torus:
             with self.assertRaises(Exception):
@@ -158,17 +159,17 @@ class TestBaseGrid(unittest.TestCase):
             with self.assertRaises(Exception):
                 self.grid.move_agent(agent, [1, self.grid.height + 1])
         else:
-            self.grid.move_agent(agent, [-1, 1])
-            assert agent.pos == (self.grid.width - 1, 1)
-            self.grid.move_agent(agent, [1, self.grid.height + 1])
-            assert agent.pos == (1, 1)
+            self.grid.move_agent(agent, [0, -1])
+            assert agent.pos == (0, self.grid.height - 1)
+            self.grid.move_agent(agent, [1, self.grid.height])
+            assert agent.pos == (1, 0)
 
     def test_agent_remove(self):
         agent = self.agents[0]
         x, y = agent.pos
         self.grid.remove_agent(agent)
         assert agent.pos is None
-        assert self.grid.grid[x][y] is None
+        assert self.grid[x][y] is None
 
     def test_swap_pos(self):
 
@@ -201,9 +202,9 @@ class TestBaseGrid(unittest.TestCase):
             self.grid.swap_pos(agent_a, agent_b)
 
 
-class TestBaseGridTorus(TestBaseGrid):
+class TestSingleGridTorus(TestSingleGrid):
     """
-    Testing the toroidal base grid.
+    Testing the toroidal singlegrid.
     """
 
     torus = True
@@ -243,12 +244,9 @@ class TestBaseGridTorus(TestBaseGrid):
         assert len(neighbors) == 3
 
 
-class TestSingleGrid(unittest.TestCase):
+class TestSingleGridEnforcement(unittest.TestCase):
     """
-    Test the SingleGrid object.
-
-    Since it inherits from Grid, all the functionality tested above should
-    work here too. Instead, this tests the enforcement.
+    Test the enforcement in SingleGrid.
     """
 
     def setUp(self):
@@ -402,7 +400,7 @@ class TestMultiGrid(unittest.TestCase):
 
 class TestHexGrid(unittest.TestCase):
     """
-    Testing a hexagonal grid.
+    Testing a hexagonal singlegrid.
     """
 
     def setUp(self):
@@ -455,9 +453,9 @@ class TestHexGrid(unittest.TestCase):
         assert sum(x + y for x, y in neighborhood) == 39
 
 
-class TestHexGridTorus(TestBaseGrid):
+class TestHexGridTorus(TestSingleGrid):
     """
-    Testing a hexagonal toroidal grid.
+    Testing a hexagonal toroidal singlegrid.
     """
 
     torus = True
@@ -508,9 +506,9 @@ class TestHexGridTorus(TestBaseGrid):
 
 class TestIndexing:
     # Create a grid where the content of each coordinate is a tuple of its coordinates
-    grid = Grid(3, 5, True)
+    grid = SingleGrid(3, 5, True)
     for _, x, y in grid.coord_iter():
-        grid.grid[x][y] = (x, y)
+        grid._grid[x][y] = (x, y)
 
     def test_int(self):
         assert self.grid[0][0] == (0, 0)
diff --git a/tests/test_import_namespace.py b/tests/test_import_namespace.py
index e405dc8f..09c8162d 100644
--- a/tests/test_import_namespace.py
+++ b/tests/test_import_namespace.py
@@ -3,7 +3,6 @@ def test_import():
     # https://github.com/projectmesa/mesa/pull/1294.
     import mesa
     import mesa.flat as mf
-
     from mesa.time import RandomActivation
 
     mesa.time.RandomActivation
diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py
index 280b8bee..cfd60cde 100644
--- a/tests/test_lifespan.py
+++ b/tests/test_lifespan.py
@@ -1,10 +1,11 @@
 import unittest
 
-from mesa.time import RandomActivation
-from mesa.datacollection import DataCollector
-from mesa import Model, Agent
 import numpy as np
 
+from mesa import Agent, Model
+from mesa.datacollection import DataCollector
+from mesa.time import RandomActivation
+
 
 class LifeTimeModel(Model):
     """Simple model for running models with a finite life"""
diff --git a/tests/test_main.py b/tests/test_main.py
index 6aa38502..730a2189 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -2,6 +2,7 @@ import os
 import sys
 import unittest
 from unittest.mock import patch
+
 from click.testing import CliRunner
 
 from mesa.main import cli
diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py
index b2ed1130..627552eb 100644
--- a/tests/test_scaffold.py
+++ b/tests/test_scaffold.py
@@ -1,5 +1,6 @@
-import unittest
 import os
+import unittest
+
 from click.testing import CliRunner
 
 from mesa.main import cli
diff --git a/tests/test_space.py b/tests/test_space.py
index 769214cf..765c3662 100644
--- a/tests/test_space.py
+++ b/tests/test_space.py
@@ -4,12 +4,9 @@ import networkx as nx
 import numpy as np
 import pytest
 
-from mesa.space import ContinuousSpace
-from mesa.space import SingleGrid
-from mesa.space import NetworkGrid
+from mesa.space import ContinuousSpace, NetworkGrid, SingleGrid
 from tests.test_grid import MockAgent
 
-
 TEST_AGENTS = [(-20, -20), (-20, -20.05), (65, 18)]
 TEST_AGENTS_GRID = [(1, 1), (10, 0), (10, 10)]
 TEST_AGENTS_NETWORK_SINGLE = [0, 1, 5]
@@ -302,10 +299,10 @@ class TestSingleGrid(unittest.TestCase):
         for i, pos in enumerate(TEST_AGENTS_GRID):
             a = self.agents[i]
             assert a.pos == pos
-            assert self.space.grid[pos[0]][pos[1]] == a
+            assert self.space[pos[0]][pos[1]] == a
             self.space.remove_agent(a)
             assert a.pos is None
-            assert self.space.grid[pos[0]][pos[1]] is None
+            assert self.space[pos[0]][pos[1]] is None
 
     def test_empty_cells(self):
         if self.space.exists_empty_cells():
@@ -325,12 +322,12 @@ class TestSingleGrid(unittest.TestCase):
         _agent = self.agents[agent_number]
 
         assert _agent.pos == initial_pos
-        assert self.space.grid[initial_pos[0]][initial_pos[1]] == _agent
-        assert self.space.grid[final_pos[0]][final_pos[1]] is None
+        assert self.space[initial_pos[0]][initial_pos[1]] == _agent
+        assert self.space[final_pos[0]][final_pos[1]] is None
         self.space.move_agent(_agent, final_pos)
         assert _agent.pos == final_pos
-        assert self.space.grid[initial_pos[0]][initial_pos[1]] is None
-        assert self.space.grid[final_pos[0]][final_pos[1]] == _agent
+        assert self.space[initial_pos[0]][initial_pos[1]] is None
+        assert self.space[final_pos[0]][final_pos[1]] == _agent
 
 
 class TestSingleNetworkGrid(unittest.TestCase):
@@ -340,7 +337,7 @@ class TestSingleNetworkGrid(unittest.TestCase):
         """
         Create a test network grid and populate with Mock Agents.
         """
-        G = nx.complete_graph(TestSingleNetworkGrid.GRAPH_SIZE)
+        G = nx.cycle_graph(TestSingleNetworkGrid.GRAPH_SIZE)
         self.space = NetworkGrid(G)
         self.agents = []
         for i, pos in enumerate(TEST_AGENTS_NETWORK_SINGLE):
@@ -357,14 +354,10 @@ class TestSingleNetworkGrid(unittest.TestCase):
             assert a.pos == pos
 
     def test_get_neighbors(self):
-        assert (
-            len(self.space.get_neighbors(0, include_center=True))
-            == TestSingleNetworkGrid.GRAPH_SIZE
-        )
-        assert (
-            len(self.space.get_neighbors(0, include_center=False))
-            == TestSingleNetworkGrid.GRAPH_SIZE - 1
-        )
+        assert len(self.space.get_neighbors(0, include_center=True)) == 3
+        assert len(self.space.get_neighbors(0, include_center=False)) == 2
+        assert len(self.space.get_neighbors(2, include_center=True, radius=3)) == 7
+        assert len(self.space.get_neighbors(2, include_center=False, radius=3)) == 6
 
     def test_move_agent(self):
         initial_pos = 1
diff --git a/tests/test_time.py b/tests/test_time.py
index 7bf198e9..903d3f2d 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -4,13 +4,14 @@ Test the advanced schedulers.
 
 import unittest
 from unittest import TestCase, mock
-from mesa import Model, Agent
+
+from mesa import Agent, Model
 from mesa.time import (
     BaseScheduler,
-    StagedActivation,
     RandomActivation,
-    SimultaneousActivation,
     RandomActivationByType,
+    SimultaneousActivation,
+    StagedActivation,
 )
 
 RANDOM = "random"
diff --git a/tests/test_tornado.py b/tests/test_tornado.py
index e0f50cf5..24025a58 100644
--- a/tests/test_tornado.py
+++ b/tests/test_tornado.py
@@ -1,8 +1,10 @@
-from tornado.testing import AsyncHTTPTestCase
+import json
+
 import tornado
+from tornado.testing import AsyncHTTPTestCase
+
 from mesa import Model
 from mesa.visualization.ModularVisualization import ModularServer
-import json
 
 
 class TestServer(AsyncHTTPTestCase):
diff --git a/tests/test_usersettableparam.py b/tests/test_usersettableparam.py
index 6dc52774..9106d050 100644
--- a/tests/test_usersettableparam.py
+++ b/tests/test_usersettableparam.py
@@ -1,11 +1,12 @@
 from unittest import TestCase
+
 from mesa.visualization.UserParam import (
-    UserSettableParameter,
-    Slider,
     Checkbox,
     Choice,
-    StaticText,
     NumberInput,
+    Slider,
+    StaticText,
+    UserSettableParameter,
 )
 
 
diff --git a/tests/test_visualization.py b/tests/test_visualization.py
index 3f33bee9..76022f01 100644
--- a/tests/test_visualization.py
+++ b/tests/test_visualization.py
@@ -1,13 +1,12 @@
-from unittest import TestCase
 from collections import defaultdict
+from unittest import TestCase
 
 from mesa.model import Model
-from mesa.space import Grid
+from mesa.space import MultiGrid
 from mesa.time import SimultaneousActivation
 from mesa.visualization.ModularVisualization import ModularServer
 from mesa.visualization.modules import CanvasGrid, TextElement
 from mesa.visualization.UserParam import UserSettableParameter
-
 from tests.test_batchrunner import MockAgent
 
 
@@ -21,7 +20,7 @@ class MockModel(Model):
         self.key1 = (key1,)
         self.key2 = key2
         self.schedule = SimultaneousActivation(self)
-        self.grid = Grid(width, height, torus=True)
+        self.grid = MultiGrid(width, height, torus=True)
 
         for (c, x, y) in self.grid.coord_iter():
             a = MockAgent(x + y * 100, self, x * y * 3)