Repository: pantsbuild/pants

Issue:
Add support for passthrough args to the python repl
In particular, the `ipython` repl supports CLI arguments like:

```
ipython notebook
```

which launches a visual repl.

Supporting passthrough args would mean that:

```
./pants repl.py --ipython $target -- notebook $file
```

would work (assuming you added the relevant 'notebook' deps to `--ipython-requirements`)

---

To do this, you would enable passthru args on the `PythonRepl` task by overriding the `supports_passthru_args` property to True ([example](https://github.com/pantsbuild/pants/blob/9f2bd17640a29ee7ad34d353efdd979222a75f70/src/python/pants//backend/jvm/tasks/jvm_run.py#L49-L51)), and then pass any additional `self.get_passthru_args()` as args [while launching the repl](https://github.com/pantsbuild/pants/blob/9f2bd17640a29ee7ad34d353efdd979222a75f70/src/python/pants/backend/python/tasks/python_repl.py#L32).


You could then add either a [unit](https://github.com/pantsbuild/pants/blob/7ae9a31508e61a237a1dcf650d0626b1f1b07af6/tests/python/pants_test/backend/python/tasks/test_python_repl.py#L25) or [integration](https://github.com/pantsbuild/pants/blob/7ae9a31508e61a237a1dcf650d0626b1f1b07af6/tests/python/pants_test/backend/python/tasks/test_python_repl_integration.py#L11) test for the passthru.


Discussion:
Thanks @stuhood  

I believe this works with both the v1 and v2 implementations. See https://pants.readme.io/docs/python-repl-goal.
Oh, nevermind. Confused with `run`. Reopening.

Resolving patch:
diff --git a/src/python/pants/backend/python/goals/repl.py b/src/python/pants/backend/python/goals/repl.py
--- a/src/python/pants/backend/python/goals/repl.py
+++ b/src/python/pants/backend/python/goals/repl.py
@@ -71,6 +71,7 @@ def maybe_get_resolve(t: Target) -> str | None:
 
 class PythonRepl(ReplImplementation):
     name = "python"
+    supports_args = False
 
 
 @rule(level=LogLevel.DEBUG)
@@ -124,6 +125,7 @@ async def create_python_repl_request(
 
 class IPythonRepl(ReplImplementation):
     name = "ipython"
+    supports_args = True
 
 
 @rule(level=LogLevel.DEBUG)
diff --git a/src/python/pants/backend/scala/goals/repl.py b/src/python/pants/backend/scala/goals/repl.py
--- a/src/python/pants/backend/scala/goals/repl.py
+++ b/src/python/pants/backend/scala/goals/repl.py
@@ -25,6 +25,7 @@
 
 class ScalaRepl(ReplImplementation):
     name = "scala"
+    supports_args = False
 
 
 @rule(level=LogLevel.DEBUG)
diff --git a/src/python/pants/core/goals/repl.py b/src/python/pants/core/goals/repl.py
--- a/src/python/pants/core/goals/repl.py
+++ b/src/python/pants/core/goals/repl.py
@@ -5,7 +5,7 @@
 import os
 from abc import ABC
 from dataclasses import dataclass
-from typing import ClassVar, Iterable, Mapping, Optional, Sequence, Tuple
+from typing import ClassVar, Iterable, Mapping, Optional, Sequence, Tuple, Type, cast
 
 from pants.core.util_rules.environments import _warn_on_non_local_environments
 from pants.engine.addresses import Addresses
@@ -18,7 +18,7 @@
 from pants.engine.rules import Effect, Get, collect_rules, goal_rule
 from pants.engine.target import FilteredTargets, Target
 from pants.engine.unions import UnionMembership, union
-from pants.option.option_types import BoolOption, StrOption
+from pants.option.option_types import ArgsListOption, BoolOption, StrOption
 from pants.util.frozendict import FrozenDict
 from pants.util.memo import memoized_property
 from pants.util.strutil import softwrap
@@ -33,6 +33,7 @@ class ReplImplementation(ABC):
     """
 
     name: ClassVar[str]
+    supports_args: ClassVar[bool]
 
     targets: Sequence[Target]
 
@@ -56,6 +57,12 @@ def activated(cls, union_membership: UnionMembership) -> bool:
         default=None,
         help="Override the automatically-detected REPL program for the target(s) specified.",
     )
+    args = ArgsListOption(
+        example="-i helloworld/main.py",
+        tool_name="the repl program",
+        passthrough=True,
+        extra_help="Currently supported only for the ipython shell.",
+    )
     restartable = BoolOption(
         default=False,
         help="True if the REPL should be restarted if its inputs have changed.",
@@ -110,7 +117,9 @@ async def run_repl(
     #  on the targets.  For now we default to the python repl.
     repl_shell_name = repl_subsystem.shell or "python"
     implementations = {impl.name: impl for impl in union_membership[ReplImplementation]}
-    repl_implementation_cls = implementations.get(repl_shell_name)
+    repl_implementation_cls = cast(
+        Optional[Type[ReplImplementation]], implementations.get(repl_shell_name)
+    )
     if repl_implementation_cls is None:
         available = sorted(implementations.keys())
         console.print_stderr(
@@ -123,14 +132,25 @@ async def run_repl(
         )
         return Repl(-1)
 
+    if repl_subsystem.args and not repl_implementation_cls.supports_args:
+        console.print_stderr(
+            softwrap(
+                f"""
+                REPL goal does not support passing args to a {repr(repl_shell_name)} shell.
+                """
+            )
+        )
+        return Repl(-1)
+
     repl_impl = repl_implementation_cls(targets=specified_targets)
     request = await Get(ReplRequest, ReplImplementation, repl_impl)
 
     env = {**complete_env, **request.extra_env}
+
     result = await Effect(
         InteractiveProcessResult,
         InteractiveProcess(
-            argv=request.args,
+            argv=(*request.args, *repl_subsystem.args),
             env=env,
             input_digest=request.digest,
             run_in_workspace=request.run_in_workspace,


Test patch:
diff --git a/src/python/pants/core/goals/repl_test.py b/src/python/pants/core/goals/repl_test.py
--- a/src/python/pants/core/goals/repl_test.py
+++ b/src/python/pants/core/goals/repl_test.py
@@ -13,6 +13,7 @@
 
 class MockRepl(ReplImplementation):
     name = "mock"
+    supports_args = False
 
 
 @rule