Repository: projectmesa/mesa (python)

Issue:
UX: Simplify TextElement usage
What if, in the Schelling example, instead of
```python
class HappyElement(TextElement):
    """
    Display a text count of how many happy agents there are.
    """

    def __init__(self):
        pass

    def render(self, model):
        return "Happy agents: " + str(model.happy)
happy_element = HappyElement()
```

we instead do
```python
def happy_element(model):
    return "Happy agents: " + str(model.happy)
```

In the `ModularServer` class, we detect if a visualization element is a function, if so, we then convert it to a `TextElement` automatically.

Pull request description:
feat: Implement auto-conversion of function to TextElement
Fixes #1233.
Note: the `TextElement` class definition is moved to be inside ModularVisualization.py, because otherwise, I get circular import.

Patch:
diff --git a/examples/schelling/server.py b/examples/schelling/server.py
index 12377491..fd643096 100644
--- a/examples/schelling/server.py
+++ b/examples/schelling/server.py
@@ -3,16 +3,11 @@ import mesa
 from model import Schelling
 
 
-class HappyElement(mesa.visualization.TextElement):
+def get_happy_agents(model):
     """
     Display a text count of how many happy agents there are.
     """
-
-    def __init__(self):
-        pass
-
-    def render(self, model):
-        return "Happy agents: " + str(model.happy)
+    return f"Happy agents: {model.happy}"
 
 
 def schelling_draw(agent):
@@ -32,7 +27,6 @@ def schelling_draw(agent):
     return portrayal
 
 
-happy_element = HappyElement()
 canvas_element = mesa.visualization.CanvasGrid(schelling_draw, 20, 20, 500, 500)
 happy_chart = mesa.visualization.ChartModule([{"Label": "happy", "Color": "Black"}])
 
@@ -45,5 +39,8 @@ model_params = {
 }
 
 server = mesa.visualization.ModularServer(
-    Schelling, [canvas_element, happy_element, happy_chart], "Schelling", model_params
+    Schelling,
+    [canvas_element, get_happy_agents, happy_chart],
+    "Schelling",
+    model_params,
 )
diff --git a/examples/virus_on_network/virus_on_network/server.py b/examples/virus_on_network/virus_on_network/server.py
index 3aa6e4f9..a8f47c61 100644
--- a/examples/virus_on_network/virus_on_network/server.py
+++ b/examples/virus_on_network/virus_on_network/server.py
@@ -59,15 +59,14 @@ chart = mesa.visualization.ChartModule(
 )
 
 
-class MyTextElement(mesa.visualization.TextElement):
-    def render(self, model):
-        ratio = model.resistant_susceptible_ratio()
-        ratio_text = "&infin;" if ratio is math.inf else f"{ratio:.2f}"
-        infected_text = str(number_infected(model))
-
-        return "Resistant/Susceptible Ratio: {}<br>Infected Remaining: {}".format(
-            ratio_text, infected_text
-        )
+def get_resistant_susceptible_ratio(model):
+    ratio = model.resistant_susceptible_ratio()
+    ratio_text = "&infin;" if ratio is math.inf else f"{ratio:.2f}"
+    infected_text = str(number_infected(model))
+
+    return "Resistant/Susceptible Ratio: {}<br>Infected Remaining: {}".format(
+        ratio_text, infected_text
+    )
 
 
 model_params = {
@@ -126,6 +125,9 @@ model_params = {
 }
 
 server = mesa.visualization.ModularServer(
-    VirusOnNetwork, [network, MyTextElement(), chart], "Virus Model", model_params
+    VirusOnNetwork,
+    [network, get_resistant_susceptible_ratio, chart],
+    "Virus Model",
+    model_params,
 )
 server.port = 8521
diff --git a/mesa/visualization/ModularVisualization.py b/mesa/visualization/ModularVisualization.py
index 63dea4ea..4f6b98e2 100644
--- a/mesa/visualization/ModularVisualization.py
+++ b/mesa/visualization/ModularVisualization.py
@@ -164,6 +164,15 @@ class VisualizationElement:
         return "<b>VisualizationElement goes here</b>."
 
 
+class TextElement(VisualizationElement):
+    """
+    Module for drawing live-updating text.
+    """
+
+    package_includes = ["TextModule.js"]
+    js_code = "elements.push(new TextModule());"
+
+
 # =============================================================================
 # Actual Tornado code starts here:
 
@@ -273,7 +282,9 @@ class ModularServer(tornado.web.Application):
         if model_params is None:
             model_params = {}
         # Prep visualization elements:
-        self.visualization_elements = visualization_elements
+        self.visualization_elements = self._auto_convert_functions_to_TextElements(
+            visualization_elements
+        )
         self.package_js_includes = set()
         self.package_css_includes = set()
         self.local_js_includes = set()
@@ -357,3 +368,26 @@ class ModularServer(tornado.web.Application):
     @staticmethod
     def _is_stylesheet(filename):
         return filename.lower().endswith(".css")
+
+    def _auto_convert_fn_to_TextElement(self, x):
+        """
+        Automatically convert a function to a TextElement object.
+        See https://github.com/projectmesa/mesa/issues/1233.
+        """
+
+        # Note: a class constructor is also a callable.
+        if not callable(x):
+            # i.e. not a function
+            return x
+
+        class MyTextElement(TextElement):
+            def render(self, model):
+                return x(model)
+
+        return MyTextElement()
+
+    def _auto_convert_functions_to_TextElements(self, visualization_elements):
+        out_elements = [
+            self._auto_convert_fn_to_TextElement(e) for e in visualization_elements
+        ]
+        return out_elements
diff --git a/mesa/visualization/UserParam.py b/mesa/visualization/UserParam.py
index 62b187e8..3e391e7a 100644
--- a/mesa/visualization/UserParam.py
+++ b/mesa/visualization/UserParam.py
@@ -1,3 +1,7 @@
+from numbers import Number
+from warnings import warn
+
+
 NUMBER = "number"
 CHECKBOX = "checkbox"
 CHOICE = "choice"
@@ -63,6 +67,13 @@ class UserSettableParameter:
         choices=None,
         description=None,
     ):
+
+        warn(
+            "UserSettableParameter is deprecated in favor of UserParam objects "
+            "such as Slider, Checkbox, Choice, StaticText, NumberInput. "
+            "See the examples folder for how to use them. "
+            "UserSettableParameter will be removed in the next major release."
+        )
         if choices is None:
             choices = list()
         if param_type not in self.TYPES:
@@ -268,3 +279,19 @@ class StaticText(UserParam):
         self._value = value
         valid = isinstance(self.value, str)
         self.maybe_raise_error(valid)
+
+
+class NumberInput(UserParam):
+    """
+    a simple numerical input
+
+    Example:
+    number_option = NumberInput("My Number", value=123)
+    """
+
+    def __init__(self, name="", value=None, description=None):
+        self.param_type = NUMBER
+        self.name = name
+        self._value = value
+        valid = isinstance(self.value, Number)
+        self.maybe_raise_error(valid)
diff --git a/mesa/visualization/modules/TextVisualization.py b/mesa/visualization/modules/TextVisualization.py
deleted file mode 100644
index 81a49a30..00000000
--- a/mesa/visualization/modules/TextVisualization.py
+++ /dev/null
@@ -1,13 +0,0 @@
-"""
-Text Module
-============
-
-Module for drawing live-updating text.
-
-"""
-from mesa.visualization.ModularVisualization import VisualizationElement
-
-
-class TextElement(VisualizationElement):
-    package_includes = ["TextModule.js"]
-    js_code = "elements.push(new TextModule());"
diff --git a/mesa/visualization/modules/__init__.py b/mesa/visualization/modules/__init__.py
index 250b9705..8cc89e23 100644
--- a/mesa/visualization/modules/__init__.py
+++ b/mesa/visualization/modules/__init__.py
@@ -6,6 +6,9 @@ from mesa.visualization.modules.CanvasGridVisualization import CanvasGrid  # noq
 from mesa.visualization.modules.ChartVisualization import ChartModule  # noqa
 from mesa.visualization.modules.PieChartVisualization import PieChartModule  # noqa
 from mesa.visualization.modules.BarChartVisualization import BarChartModule  # noqa
-from mesa.visualization.modules.TextVisualization import TextElement  # noqa
 from mesa.visualization.modules.HexGridVisualization import CanvasHexGrid  # noqa
 from mesa.visualization.modules.NetworkVisualization import NetworkModule  # noqa
+
+# Delete this line in the next major release, once the simpler namespace has
+# become widely adopted.
+from mesa.visualization.ModularVisualization import TextElement  # noqa


Test patch:
diff --git a/tests/test_usersettableparam.py b/tests/test_usersettableparam.py
index deb4da42..6dc52774 100644
--- a/tests/test_usersettableparam.py
+++ b/tests/test_usersettableparam.py
@@ -5,12 +5,14 @@ from mesa.visualization.UserParam import (
     Checkbox,
     Choice,
     StaticText,
+    NumberInput,
 )
 
 
 class TestOption(TestCase):
     def setUp(self):
         self.number_option = UserSettableParameter("number", value=123)
+        self.number_option_standalone = NumberInput("number", value=123)
         self.checkbox_option = UserSettableParameter("checkbox", value=True)
         self.checkbox_option_standalone = Checkbox(value=True)
         self.choice_option = UserSettableParameter(
@@ -29,9 +31,10 @@ class TestOption(TestCase):
         self.static_text_option = StaticText("Hurr, Durr Im'a Sheep")
 
     def test_number(self):
-        assert self.number_option.value == 123
-        self.number_option.value = 321
-        assert self.number_option.value == 321
+        for option in [self.number_option, self.number_option_standalone]:
+            assert option.value == 123
+            option.value = 321
+            assert option.value == 321
 
     def test_checkbox(self):
         for option in [self.checkbox_option, self.checkbox_option_standalone]: