Repository: projectmesa/mesa
Pull request #1839 (2023-10-19 04:14:45)

docs: Rename useful-snippets to how-to guide

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/projectmesa/mesa/pull/1839?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
All modified lines are covered by tests :white_check_mark:
> Comparison is base [(`d40bc5b`)](https://app.codecov.io/gh/projectmesa/mesa/commit/d40bc5b475f667f65efc3c6dd256f56b1fe3f724?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 79.01% compared to head [(`600cf53`)](https://app.codecov.io/gh/projectmesa/mesa/pull/1839?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 78.90%.

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


```diff
@@            Coverage Diff             @@
##             main    #1839      +/-   ##
==========================================
- Coverage   79.01%   78.90%   -0.11%     
==========================================
  Files          15       15              
  Lines         910      910              
  Branches      193      193              
==========================================
- Hits          719      718       -1     
- Misses        168      169       +1     
  Partials       23       23              
```


[see 1 file with indirect coverage changes](https://app.codecov.io/gh/projectmesa/mesa/pull/1839/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/projectmesa/mesa/pull/1839?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).   
:loudspeaker: Have feedback on the report? [Share it here](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/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml
index 9e89ad99..ab9f22cd 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.11"]
+        python-version: ["3.12"]
         include:
+          - os: ubuntu
+            python-version: "3.11"
           - os: ubuntu
             python-version: "3.10"
           - os: ubuntu
@@ -64,24 +66,24 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v4
-    - name: Set up Python 3.10
+    - name: Set up Python 3.12
       uses: actions/setup-python@v4
       with:
-        python-version: "3.10"
-    - run: pip install ruff==0.0.275
+        python-version: "3.12"
+    - run: pip install ruff~=0.1.1  # Update periodically
     - 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/*'
+      run: ruff . --output-format=github --extend-exclude 'mesa/cookiecutter-mesa/*'
 
   lint-black:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v4
-    - name: Set up Python 3.10
+    - name: Set up Python 3.12
       uses: actions/setup-python@v4
       with:
-        python-version: "3.10"
+        python-version: "3.12"
     - run: pip install black[jupyter]
     - name: Lint with black
       run: black --check --exclude=mesa/cookiecutter-mesa/* .
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a16e3920..7600a15d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
-          python-version: '3.10'
+          python-version: "3.12"
       - name: Install dependencies
         run: pip install -U pip wheel setuptools
       - name: Build package
diff --git a/docs/useful-snippets/snippets.rst b/docs/howto.rst
similarity index 94%
rename from docs/useful-snippets/snippets.rst
rename to docs/howto.rst
index 728c4e65..09792af5 100644
--- a/docs/useful-snippets/snippets.rst
+++ b/docs/howto.rst
@@ -1,7 +1,7 @@
-Useful Snippets
-===============
+How-to Guide
+============
 
-A collection of useful code snippets. Here you can find code that allows you to get to get started on common tasks in Mesa.
+Here you can find code that allows you to get to get started on common tasks in Mesa.
 
 Models with Discrete Time
 -------------------------
diff --git a/docs/index.rst b/docs/index.rst
index 6b2e4831..83382e1b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -98,7 +98,7 @@ ABM features users have shared that you may want to use in your model
    tutorials/intro_tutorial
    tutorials/visualization_tutorial
    Best Practices <best-practices>
-   Useful Snippets <useful-snippets/snippets>
+   How-to Guide <howto>
    API Documentation <apis/api_main>
    Mesa Packages <packages>
    tutorials/adv_tutorial_legacy.ipynb
diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb
index 1aa8d4d1..a77f4a13 100644
--- a/docs/tutorials/intro_tutorial.ipynb
+++ b/docs/tutorials/intro_tutorial.ipynb
@@ -1263,7 +1263,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's collection of useful snippets](https://github.com/projectmesa/mesa/blob/main/docs/useful-snippets/snippets.rst), in 'Using multi-process `batch_run` on Windows' section for how to do it."
+    "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's how-to guide](https://github.com/projectmesa/mesa/blob/main/docs/howto.rst), in 'Using multi-process `batch_run` on Windows' section for how to do it."
    ]
   },
   {
diff --git a/mesa/datacollection.py b/mesa/datacollection.py
index 7f3dc847..8bddfa23 100644
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -56,44 +56,49 @@ class DataCollector:
         agent_reporters=None,
         tables=None,
     ):
-        """Instantiate a DataCollector with lists of model and agent reporters.
+        """
+        Instantiate a DataCollector with lists of model and agent reporters.
         Both model_reporters and agent_reporters accept a dictionary mapping a
-        variable name to either an attribute name, or a method.
-        For example, if there was only one model-level reporter for number of
-        agents, it might look like:
-            {"agent_count": lambda m: m.schedule.get_agent_count() }
-        If there was only one agent-level reporter (e.g. the agent's energy),
-        it might look like this:
-            {"energy": "energy"}
-        or like this:
-            {"energy": lambda a: a.energy}
+        variable name to either an attribute name, a function, a method of a class/instance,
+        or a function with parameters placed in a list.
+
+        Model reporters can take four types of arguments:
+        1. Lambda function:
+           {"agent_count": lambda m: m.schedule.get_agent_count()}
+        2. 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
+        3. Class attributes of a model:
+           {"model_attribute": "model_attribute"}
+        4. Functions with parameters that have been placed in a list:
+           {"Model_Function": [function, [param_1, param_2]]}
+
+        Agent reporters can similarly take:
+        1. Attribute name (string) referring to agent's attribute:
+           {"energy": "energy"}
+        2. Lambda function:
+           {"energy": lambda a: a.energy}
+        3. Method of an agent class/instance:
+           {"agent_action": self.do_action} # self here is an agent class instance
+           {"agent_action": Agent.do_action} # Agent here is a class
+        4. Functions with parameters placed in a list:
+           {"Agent_Function": [function, [param_1, param_2]]}
 
         The tables arg accepts a dictionary mapping names of tables to lists of
         columns. For example, if we want to allow agents to write their age
         when they are destroyed (to keep track of lifespans), it might look
         like:
-            {"Lifespan": ["unique_id", "age"]}
+           {"Lifespan": ["unique_id", "age"]}
 
         Args:
-            model_reporters: Dictionary of reporter names and attributes/funcs
-            agent_reporters: Dictionary of reporter names and attributes/funcs.
+            model_reporters: Dictionary of reporter names and attributes/funcs/methods.
+            agent_reporters: Dictionary of reporter names and attributes/funcs/methods.
             tables: Dictionary of table names to lists of column names.
 
         Notes:
-            If you want to pickle your model you must not use lambda functions.
-            If your model includes a large number of agents, you should *only*
-            use attribute names for the agent reporter, it will be much faster.
-
-            Model reporters can take four types of arguments:
-            lambda like above:
-            {"agent_count": lambda m: m.schedule.get_agent_count() }
-            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]]}
+            - If you want to pickle your model you must not use lambda functions.
+            - If your model includes a large number of agents, it is recommended to
+              use attribute names for the agent reporter, as it will be faster.
         """
         self.model_reporters = {}
         self.agent_reporters = {}
@@ -130,16 +135,31 @@ class DataCollector:
 
         Args:
             name: Name of the agent-level variable to collect.
-            reporter: Attribute string, or function object that returns the
-                      variable when given a model instance.
+            reporter: Attribute string, function object, method of a class/instance, or
+                      function with parameters placed in a list that returns the
+                      variable when given an agent instance.
         """
+        # Check if the reporter is an attribute string
         if isinstance(reporter, str):
             attribute_name = reporter
 
-            def reporter(agent):
+            def attr_reporter(agent):
                 return getattr(agent, attribute_name, None)
 
-            reporter.attribute_name = attribute_name
+            reporter = attr_reporter
+
+        # Check if the reporter is a function with arguments placed in a list
+        elif isinstance(reporter, list):
+            func, params = reporter[0], reporter[1]
+
+            def func_with_params(agent):
+                return func(agent, *params)
+
+            reporter = func_with_params
+
+        # For other types (like lambda functions, method of a class/instance),
+        # it's already suitable to be used as a reporter directly.
+
         self.agent_reporters[name] = reporter
 
     def _new_table(self, table_name, table_columns):
diff --git a/setup.py b/setup.py
index 73e82f80..6024165d 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ requires = [
 extras_require = {
     "dev": [
         "black",
-        "ruff==0.0.275",
+        "ruff~=0.1.1",  # Update periodically
         "coverage",
         "pytest >= 4.6",
         "pytest-cov",


Test patch:
diff --git a/tests/test_datacollector.py b/tests/test_datacollector.py
index 8cdee440..a45d8891 100644
--- a/tests/test_datacollector.py
+++ b/tests/test_datacollector.py
@@ -24,6 +24,9 @@ class MockAgent(Agent):
         self.val += 1
         self.val2 += 1
 
+    def double_val(self):
+        return self.val * 2
+
     def write_final_values(self):
         """
         Write the final value to the appropriate table.
@@ -32,6 +35,10 @@ class MockAgent(Agent):
         self.model.datacollector.add_table_row("Final_Values", row)
 
 
+def agent_function_with_params(agent, multiplier, offset):
+    return (agent.val * multiplier) + offset
+
+
 class DifferentMockAgent(MockAgent):
     # We define a different MockAgent to test for attributes that are present
     # only in 1 type of agent, but not the other.
@@ -54,17 +61,21 @@ class MockModel(Model):
         self.n = 10
         for i in range(self.n):
             self.schedule.add(MockAgent(i, self, val=i))
-        agent_reporters = {"value": lambda a: a.val, "value2": "val2"}
         self.initialize_data_collector(
-            {
+            model_reporters={
                 "total_agents": lambda m: m.schedule.get_agent_count(),
                 "model_value": "model_val",
                 "model_calc": self.schedule.get_agent_count,
                 "model_calc_comp": [self.test_model_calc_comp, [3, 4]],
                 "model_calc_fail": [self.test_model_calc_comp, [12, 0]],
             },
-            agent_reporters,
-            {"Final_Values": ["agent_id", "final_value"]},
+            agent_reporters={
+                "value": lambda a: a.val,
+                "value2": "val2",
+                "double_value": MockAgent.double_val,
+                "value_with_params": [agent_function_with_params, [2, 3]],
+            },
+            tables={"Final_Values": ["agent_id", "final_value"]},
         )
 
     def test_model_calc_comp(self, input1, input2):
@@ -132,6 +143,19 @@ class TestDataCollector(unittest.TestCase):
         data_collector = self.model.datacollector
         agent_table = data_collector.get_agent_vars_dataframe()
 
+        assert "double_value" in list(agent_table.columns)
+        assert "value_with_params" in list(agent_table.columns)
+
+        # Check the double_value column
+        for (step, agent_id), value in agent_table["double_value"].items():
+            expected_value = (step + agent_id) * 2
+            self.assertEqual(value, expected_value)
+
+        # Check the value_with_params column
+        for (step, agent_id), value in agent_table["value_with_params"].items():
+            expected_value = ((step + agent_id) * 2) + 3
+            self.assertEqual(value, expected_value)
+
         assert len(data_collector._agent_records) == 8
         for step, records in data_collector._agent_records.items():
             if step < 5:
@@ -140,7 +164,7 @@ class TestDataCollector(unittest.TestCase):
                 assert len(records) == 9
 
             for values in records:
-                assert len(values) == 4
+                assert len(values) == 6
 
         assert "value" in list(agent_table.columns)
         assert "value2" in list(agent_table.columns)
@@ -175,7 +199,7 @@ class TestDataCollector(unittest.TestCase):
         agent_vars = data_collector.get_agent_vars_dataframe()
         table_df = data_collector.get_table_dataframe("Final_Values")
         assert model_vars.shape == (8, 5)
-        assert agent_vars.shape == (77, 2)
+        assert agent_vars.shape == (77, 4)
         assert table_df.shape == (9, 2)
 
         with self.assertRaises(Exception):