Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion IPython/core/magic_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def my_cell_magic(line, cell):
#-----------------------------------------------------------------------------
import argparse
import re
import shlex

# Our own imports
from IPython.core.error import UsageError
Expand All @@ -92,6 +93,10 @@ def my_cell_magic(line, cell):
from IPython.utils.text import dedent

NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
QUOTED_ARG_VALUE_RE = re.compile(
r"""(?<!\S)(?P<option>[^\s=]+)=(?P<value>"(?:\\.|[^"\\])*"|'[^']*')(?=\s|$)"""
)


@undoc
class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter):
Expand Down Expand Up @@ -164,7 +169,26 @@ def error(self, message):
def parse_argstring(self, argstring, *, partial=False):
""" Split a string into an argument list and parse that argument list.
"""
argv = arg_split(argstring, strict=not partial)
replacements: dict[str, str] = {}

def replace_quoted_value(match):
action = self._option_string_actions.get(match.group("option"))
if action is None or action.nargs == 0:
return match.group()
# Keep the platform splitter from breaking an attached quoted value.
marker = f"__IPYTHON_MAGIC_ARG_{len(replacements)}__"
while marker in argstring:
marker += "_"
replacements[marker] = shlex.split(match.group("value"), posix=True)[0]
return f"{match.group('option')}={marker}"

masked = QUOTED_ARG_VALUE_RE.sub(replace_quoted_value, argstring)
argv = arg_split(masked, strict=not partial)
for index, token in enumerate(argv):
for marker, value in replacements.items():
if marker in token:
argv[index] = token.replace(marker, value)
break
if partial:
return self.parse_known_args(argv)
return self.parse_args(argv)
Expand Down
61 changes: 61 additions & 0 deletions tests/test_magic_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ def foo(self, args):
return parse_argstring(foo, args)


@magic_arguments()
@argument("--path", action="append", default=[])
@argument("remainder", nargs="*")
def magic_paths(self, args):
return parse_argstring(magic_paths, args)


def test_magic_arguments():
assert (
magic_foo1.__doc__
Expand Down Expand Up @@ -143,3 +150,57 @@ def test_magic_arguments():
assert real_name(foo) == "foo"
assert foo(None, "") == argparse.Namespace(foo=None)
assert hasattr(foo, "has_arguments")


def test_quoted_option_values_with_spaces(monkeypatch):
assert magic_paths(
None, '--path="-L/Users/carlos/Desktop/Test LGBM API"'
) == argparse.Namespace(
path=["-L/Users/carlos/Desktop/Test LGBM API"], remainder=[]
)

# Exercise the quote-preserving splitter used on POSIX on every platform.
from IPython.utils._process_common import arg_split

monkeypatch.setattr("IPython.core.magic_arguments.arg_split", arg_split)

assert magic_paths(
None,
'--path="-L/Users/carlos/Desktop/Test LGBM API" '
"--path='single quoted path' "
'--path="say \\"hello\\" here" '
'--path="C:\\Program Files\\project" '
'--path="héllo 世界" tail',
) == argparse.Namespace(
path=[
"-L/Users/carlos/Desktop/Test LGBM API",
"single quoted path",
'say "hello" here',
"C:\\Program Files\\project",
"héllo 世界",
],
remainder=["tail"],
)


def test_magic_argument_existing_quote_behavior(monkeypatch):
from IPython.utils._process_common import arg_split

monkeypatch.setattr("IPython.core.magic_arguments.arg_split", arg_split)

assert magic_paths(
None, '--path plain --path "separate quoted value" "quoted positional"'
) == argparse.Namespace(
path=["plain", '"separate quoted value"'],
remainder=['"quoted positional"'],
)


def test_non_option_equals_quote_behavior(monkeypatch):
from IPython.utils._process_common import arg_split

monkeypatch.setattr("IPython.core.magic_arguments.arg_split", arg_split)

assert magic_paths(None, 'expression="still split apart"') == argparse.Namespace(
path=[], remainder=['expression="still', "split", 'apart"']
)
Loading