AppTest `from_function` args
diff --git a/lib/streamlit/runtime/scriptrunner/script_runner.py b/lib/streamlit/runtime/scriptrunner/script_runner.py
index 203749e05fc1..c36bbd6d6548 100644
--- a/lib/streamlit/runtime/scriptrunner/script_runner.py
+++ b/lib/streamlit/runtime/scriptrunner/script_runner.py
@@ -510,7 +510,7 @@ def _run_script(self, rerun_data: RerunData) -> None:
                 #         ...
                 #     ```
                 # in their scripts.
-                module = _new_module("__main__")
+                module = self._new_module("__main__")
 
                 # Install the fake module as the __main__ module. This allows
                 # the pickle module to work inside the user's code, since it now
@@ -624,6 +624,10 @@ def _on_script_finished(
         if config.get_option("runner.postScriptGC"):
             gc.collect(2)
 
+    def _new_module(self, name: str) -> types.ModuleType:
+        """Create a new module with the given name."""
+        return types.ModuleType(name)
+
 
 class ScriptControlException(BaseException):
     """Base exception for ScriptRunner."""
@@ -674,11 +678,6 @@ def _clean_problem_modules() -> None:
             pass
 
 
-def _new_module(name: str) -> types.ModuleType:
-    """Create a new module with the given name."""
-    return types.ModuleType(name)
-
-
 # The reason this is not a decorator is because we want to make it clear at the
 # calling location that this function is being used.
 def _log_if_error(fn: Callable[[], None]) -> None:
diff --git a/lib/streamlit/testing/v1/app_test.py b/lib/streamlit/testing/v1/app_test.py
index ea1496a485c3..9743b22038fd 100644
--- a/lib/streamlit/testing/v1/app_test.py
+++ b/lib/streamlit/testing/v1/app_test.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 from __future__ import annotations
 
-import ast
 import hashlib
 import inspect
 import pathlib
@@ -142,12 +141,21 @@ class AppTest:
         dict-like syntax to set ``query_params`` values for the simulated app.
     """
 
-    def __init__(self, script_path: str, *, default_timeout: float):
+    def __init__(
+        self,
+        script_path: str,
+        *,
+        default_timeout: float,
+        args=None,
+        kwargs=None,
+    ):
         self._script_path = script_path
         self.default_timeout = default_timeout
         self.session_state = SafeSessionState(SessionState(), lambda: None)
         self.query_params: dict[str, Any] = {}
         self.secrets: dict[str, Any] = {}
+        self.args = args
+        self.kwargs = kwargs
 
         tree = ElementTree()
         tree._runner = self
@@ -180,17 +188,30 @@ def from_string(cls, script: str, *, default_timeout: float = 3) -> AppTest:
             executed via ``.run()``.
 
         """
+        return cls._from_string(script, default_timeout=default_timeout)
+
+    @classmethod
+    def _from_string(
+        cls, script: str, *, default_timeout: float = 3, args=None, kwargs=None
+    ) -> AppTest:
         hasher = hashlib.md5(bytes(script, "utf-8"), **HASHLIB_KWARGS)
         script_name = hasher.hexdigest()
 
         path = pathlib.Path(TMP_DIR.name, script_name)
         aligned_script = textwrap.dedent(script)
         path.write_text(aligned_script)
-        return AppTest(str(path), default_timeout=default_timeout)
+        return AppTest(
+            str(path), default_timeout=default_timeout, args=args, kwargs=kwargs
+        )
 
     @classmethod
     def from_function(
-        cls, script: Callable[[], None], *, default_timeout: float = 3
+        cls,
+        script: Callable[..., Any],
+        *,
+        default_timeout: float = 3,
+        args=None,
+        kwargs=None,
     ) -> AppTest:
         """
         Create an instance of ``AppTest`` to simulate an app page defined\
@@ -210,6 +231,12 @@ def from_function(
             Default time in seconds before a script run is timed out. Can be
             overridden for individual ``.run()`` calls.
 
+        args: tuple
+            An optional tuple of args to pass to the script function.
+
+        kwargs: dict
+            An optional dict of kwargs to pass to the script function.
+
         Returns
         -------
         AppTest
@@ -217,14 +244,12 @@ def from_function(
             executed via ``.run()``.
 
         """
-        # TODO: Simplify this using `ast.unparse()` once we drop 3.8 support
         source_lines, _ = inspect.getsourcelines(script)
         source = textwrap.dedent("".join(source_lines))
-        module = ast.parse(source)
-        fn_def = module.body[0]
-        body_lines = source_lines[fn_def.lineno :]
-        body = textwrap.dedent("".join(body_lines))
-        return cls.from_string(body, default_timeout=default_timeout)
+        module = source + f"\n{script.__name__}(*__args, **__kwargs)"
+        return cls._from_string(
+            module, default_timeout=default_timeout, args=args, kwargs=kwargs
+        )
 
     @classmethod
     def from_file(cls, script_path: str, *, default_timeout: float = 3) -> AppTest:
@@ -299,7 +324,9 @@ def _run(
             new_secrets._secrets = self.secrets
             st.secrets = new_secrets
 
-        script_runner = LocalScriptRunner(self._script_path, self.session_state)
+        script_runner = LocalScriptRunner(
+            self._script_path, self.session_state, args=self.args, kwargs=self.kwargs
+        )
         self._tree = script_runner.run(widget_state, self.query_params, timeout)
         self._tree._runner = self
         # Last event is SHUTDOWN, so the corresponding data includes query string
diff --git a/lib/streamlit/testing/v1/local_script_runner.py b/lib/streamlit/testing/v1/local_script_runner.py
index 545a6d2bc959..f2ec8ca60024 100644
--- a/lib/streamlit/testing/v1/local_script_runner.py
+++ b/lib/streamlit/testing/v1/local_script_runner.py
@@ -15,6 +15,7 @@
 
 import os
 import time
+import types
 from typing import Any
 from urllib import parse
 
@@ -37,6 +38,8 @@ def __init__(
         self,
         script_path: str,
         session_state: SafeSessionState,
+        args=None,
+        kwargs=None,
     ):
         """Initializes the ScriptRunner for the given script_path."""
 
@@ -45,6 +48,8 @@ def __init__(
         self.forward_msg_queue = ForwardMsgQueue()
         self.script_path = script_path
         self.session_state = session_state
+        self.args = args if args is not None else tuple()
+        self.kwargs = kwargs if kwargs is not None else dict()
 
         super().__init__(
             session_id="test session id",
@@ -135,6 +140,12 @@ def _on_script_finished(
         # are marked as active.
         runtime.get_instance().media_file_mgr.remove_orphaned_files()
 
+    def _new_module(self, name: str) -> types.ModuleType:
+        module = types.ModuleType(name)
+        module.__dict__["__args"] = self.args
+        module.__dict__["__kwargs"] = self.kwargs
+        return module
+
 
 def require_widgets_deltas(runner: LocalScriptRunner, timeout: float = 3) -> None:
     """Wait for the given ScriptRunner to emit a completion event. If the timeout
diff --git a/lib/tests/streamlit/testing/app_test_test.py b/lib/tests/streamlit/testing/app_test_test.py
index c8866fb8481a..8727adbdc500 100644
--- a/lib/tests/streamlit/testing/app_test_test.py
+++ b/lib/tests/streamlit/testing/app_test_test.py
@@ -205,3 +205,15 @@ def button_one_clicked(cont):
     assert at.markdown.len == 2
     assert at.info[0].value == "Hi!"
     assert at.markdown.values == ["FooBar", "BarFoo"]
+
+
+def test_from_function_kwargs():
+    def script(foo, baz):
+        import streamlit as st
+
+        st.text(foo)
+        st.text(baz)
+        return foo
+
+    at = AppTest.from_function(script, args=("bar",), kwargs={"baz": "baz"}).run()
+    assert at.text.values == ["bar", "baz"]



## Describe your changes
Allows `AppTest.from_function` to work with a function that takes arguments, and pass arguments to it.
The new implementation also allows it to work for functions that return values or otherwise do things that are not valid as top level expressions. 

## GitHub Issue Link (if applicable)
#7900 

## Testing Plan

Basic unit test added

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.
