Repository: google/picatrix (language: python)

Issue:
Use typing to construct argument list
Right now all the arguments are constructed from parsing the docstring. This should be changed, to reduce the reliance on docstring parsing and instead make use of typing and the casting/verification that can be done there.
Detangle the issues with magic arguments
ATM when you initialize a magic class it has an optional argument of `arguments`, which is a list of MagicArgument.

If it is not supplied it is ignored and arguments are derived from a docstring.

This could be detangled, and MagicArgument be better defined and used throughout. Also it should not be done in the `__init__` function but arguments should be configured after initialization of the object.


Discussion / hints:

I would like to propose major simplification of the current magic framework. Please find some details below.

# Definition of Magic

```python
class MagicProtocol(typing.Protocol):
    def __call__(
        data: Text,
        *args:Union[Text, int, bool],
        **kwargs:Union[Text, int, bool]
    ) -> Any:
        pass
```

# Definition of Argument parsers arguments

Argument types would be defined separately for each type. For example argument of type int would be defined as:

```python 
class MagicIntArgument:
    flags: List[Text]
    default: Optional[Int]
    help: Text

    def add_to_argparse(self, parser argparse.ArgumentParser):
        # code to transform MagicIntArgument to argparse.ArgumentParser.add_argument invocation
        ...
```

Similar classes would be defined for other argument types (Text, bool and others as needed).

`picatrix_magic` decorator signature would stays the same:

```python
def picatrix_magic(
    function: Optional[MagicProtocol] = None,
    arguments: Optional[List[MagicArgument]] = None,
    name_func: Optional[Callable[[Text], Text]] = None,
    conditional: Optional[Callable[[None], bool]] = None
) -> MagicProtocol:
    ...
```

If `arguments` is provided, it will be used to parse magic arguments and provide them to decorated function. \
If it's not provided, arguments will get parsed out of decorated function definition

```python
def _magic_to_arguments(magic: MagicProcotol) -> List[MagicArgument]:
    hints = typing.get_typing_hints(magic)
    ...
```

Rest of the code will stay more or less the same, but will follow the path in which `arguments` is provided  - either explicitly by caller or parsed out of magic definition.

# Concerns:

@bladyjoker, @kiddinn - I would like to ask your opinion on following topics:
1. Magic return type
  `Any` type is a broad one and kind of defeats a purpose of having a type at all. Initially I was thinking about making it `pd.DataFrame`, but then I saw that Timesketch magics are using Dict and other types as output. Do you see any way to define all possible valid outputs of the Magic?
2. Generic MagicIntArgument
  Implementation that I proposed above is serving the purpose, but is very repetitive and inflexible. I think it can be improved through use of `typing.Generic`. Do you happen to have an experience with Python generics and can share your opinion here? Alternatively - can you suggest any environment where I can quickly check my assumptions about `typing`? My usual development cycle is to validate assumptions in REPL and then code the most succesful solution. Unfortunately type verification is disabled in runtime.
3. I would like to use https://pypi.org/project/docstring-parser/ for parsing docstrings and extracting descriptions of arguments. In the future I would like to also use it to enable more end-user-friendly `functools.partial`s (reformatting docstrings to take into the account that some attributes got already applied by partial). Do you have any objections against this dependency?
ok, to answer your questions here

1. Return type. It is hard to predict the future need of a return type, often times it is a DataFrame, but it can be a dict, it can be a single value, int, string, etc... it can also be none... as in `timesketch_set_active_sketch` which doesn't produce an output. You may want to write a magic that returns a bool to indicate whether it was successful or not, that's the reason for teh `Any`, I agree it is not ideal, and it would be good to have something else, but I'm not sure what we can do here without restricting the magics to a limited set/usability.

2. We need to start adding more typing verification... we can use tools like `mypy` to automatically run against the codebase (I've been experimenting with it, but I need to make some changes to have it work in the first place). This could run on every PR, just like pylint and unit tests, to make sure the codebase is enforcing typing.  My idea is also to get rid of the docstring parsing to `enforce` the typing and use the build-int typing to do the casting and enforcing of arguments.

3. I don't have any objections about that.

I was also thinking, do we need to have the magic arguments at all, or just have the typing in the functions and parsing of docstrings? Do we need the added flexibility but still a lot more complexity by also supporting defining these MagicArguments?

Fix patch:
diff --git a/.isort.cfg b/.isort.cfg
index 8cbb74f..5c7bdfa 100755
--- a/.isort.cfg
+++ b/.isort.cfg
@@ -5,4 +5,4 @@ force_grid_wrap = 0
 use_parentheses = True
 ensure_newline_before_comments = True
 line_length = 80
-known_third_party = IPython,dateutil,dfdatetime,ipyaggrid,mock,pandas,pytest,setuptools,timesketch_api_client,timesketch_import_client
+known_third_party = IPython,dateutil,dfdatetime,docstring_parser,ipyaggrid,mock,pandas,pytest,setuptools,timesketch_api_client,timesketch_import_client
diff --git a/picatrix/lib/magic.py b/picatrix/lib/magic.py
new file mode 100644
index 0000000..bf98e0d
--- /dev/null
+++ b/picatrix/lib/magic.py
@@ -0,0 +1,299 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Types and functions defining Picatrix integration with IPython magics."""
+
+import argparse
+import shlex
+from dataclasses import dataclass, field
+from enum import Enum, auto
+from inspect import getfullargspec
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Text,
+    Type,
+    Union,
+)
+
+from docstring_parser import parse
+
+from .error import Error
+
+
+class MagicError(Error):
+  """Generic Picatrix error related to IPython magics."""
+
+
+class MagicParsingError(MagicError, ValueError):
+  """Raised when invalid function is provided to magic framework."""
+
+
+class MagicArgParsingError(MagicError, ValueError):
+  """Raised when invalid arguments where provided to magic."""
+
+
+class MagicType(Enum):
+  """Enum indicating a type of the IPython magic."""
+  CELL = auto()
+  LINE = auto()
+
+
+_MagicArgValues = [bool, int, float, str]
+MagicArgValue = Union[bool, int, float, str]
+_MagicArgValueType = Union[Type[bool], Type[int], Type[float], Type[str]]
+
+
+def _validate_arg_value(arg: Text, v: Any) -> MagicArgValue:
+  for typ in _MagicArgValues:
+    if isinstance(v, typ):
+      return typ(v)
+  raise MagicArgParsingError(
+      f"{arg}=\"{v}\" is of invalid type; has to be {MagicArgValue}")
+
+
+@dataclass(frozen=True)
+class MagicArgs:
+  """Arguments parsed out of IPython magic invocation."""
+  bind_variable: Text = field(default="_")
+  kwargs: Dict[Text, MagicArgValue] = field(default_factory=dict)
+
+  def __post_init__(self):
+    if not self.bind_variable.isidentifier():  # pylint: disable=no-member
+      raise MagicArgParsingError(
+          f"\"{self.bind_variable}\" isn't valid Python "
+          "identifier, see: "
+          "https://docs.python.org/3/reference/"
+          "lexical_analysis.html#identifiers")
+
+
+class ArgParserNonZeroStatus(Exception):
+  """Raised when the argument parser has exited with non-zero status."""
+
+
+class MagicArgumentParser(argparse.ArgumentParser):
+  """Argument parser for picatrix magics."""
+
+  def exit(self, status: int = 0, message: Optional[Text] = None):
+    """Exiting method for argument parser.
+
+    Args:
+      status (int): exit status of the parser.
+      message (str): the error message.
+
+    Raises:
+      MagicArgParsingError: when the parser is unable to parse the arguments.
+      ArgParserNonZeroStatus: when the parser has successfully completed.
+    """
+    if not status:
+      raise ArgParserNonZeroStatus('Exiting.')
+
+    if message:
+      raise MagicArgParsingError(message.strip())
+
+    raise MagicArgParsingError('Wrong usage.')
+
+  def parse_magic_args(
+      self, line: Text, cell: Optional[Text] = None) -> MagicArgs:
+    """Parse arguments out of line and cell content as provided by IPython."""
+    line = line.strip()
+
+    if line and "-- " not in line:
+      return MagicArgs(bind_variable=line)
+    if line.endswith("--"):
+      return MagicArgs(bind_variable=line.rstrip("--"))
+
+    if not line:
+      bind_variable = "_"
+      raw = line
+    elif line.startswith("-- "):
+      bind_variable = "_"
+      raw = line[3:]
+    else:
+      bind_variable, raw = line.split(" -- ", maxsplit=1)
+
+    kwargs: Dict[Text, MagicArgValue] = {
+        str(arg): _validate_arg_value(arg, value)
+        for arg, value in self.parse_args(shlex.split(raw)).__dict__.items()
+    }
+    if cell:
+      kwargs["cell"] = cell
+    return MagicArgs(bind_variable, kwargs)
+
+
+def _usage_string(
+    mtyp: MagicType, args: Iterable[Text], kwargs: Iterable[Text]) -> Text:
+  arguments = (
+      " ".join(f"[--{key} {key.upper()}]" for key in kwargs) + " " +
+      " ".join(args))
+
+  if mtyp == MagicType.LINE:
+    return f"\n```%%%(prog)s [bind_variable] -- [-h] {arguments}```"
+  else:
+    return f"\n```\n%%%%%(prog)s [bind_variable] -- [-h] {arguments}\ndata\n```"
+
+
+@dataclass(frozen=True)
+class _MagicSpec:
+  """Magic function specification for argument parsing purposes."""
+  name: Text
+  docstring: Text
+  typ: MagicType
+
+  args_with_no_defaults: List[Text] = field(default_factory=list)
+  args_with_defaults: Dict[Text, Text] = field(default_factory=dict)
+  args_descriptions: Dict[Text, Text] = field(default_factory=dict)
+  args_types: Dict[Text, _MagicArgValueType] = field(default_factory=dict)
+
+  def __post_init__(self):
+    if (self.args_with_no_defaults or
+        self.args_with_defaults) and not self.args_descriptions:
+      raise MagicParsingError(
+          "Magics have to have docstring section describing their arguments.")
+    # pylint: disable=unsupported-membership-test
+    for arg, typ in self.args_types.items():
+      if typ == bool and arg in self.args_with_no_defaults:
+        raise MagicParsingError(
+            "Arguments of type bool have to have a default value specified.")
+    for arg in list(self.args_with_no_defaults) + list(self.args_with_defaults):
+      if arg not in self.args_descriptions:
+        raise MagicParsingError(
+            "Magics have to have docstring section describing all of their "
+            f"arguments; docstring missing for `{arg}`")
+    if self.typ == MagicType.CELL and "cell" not in self.args_with_no_defaults:
+      raise MagicParsingError(
+          "Cell magics have to have positional argument called `cell`")
+    # pylint: enable=unsupported-membership-test
+
+  @classmethod
+  def from_function(
+      cls,
+      typ: MagicType,
+      func: Callable[..., Any],
+      name: Optional[Text] = None) -> "_MagicSpec":
+    """Creates _MagicSpec from compatible function."""
+    name = name if name else func.__name__
+
+    if not name.isidentifier():
+      raise MagicParsingError(
+          f"\"{name}\" isn't valid Python identifier, see: "
+          "https://docs.python.org/3/reference/"
+          "lexical_analysis.html#identifiers")
+    if not func.__doc__:
+      raise MagicParsingError("Magics have to have docstring.")
+
+    spec = getfullargspec(func)
+    if spec.varargs or spec.varkw:
+      raise MagicParsingError(
+          "Magics can't have explicit variadic arguments, "
+          "i.e. `*args` or `**kwargs`")
+    if spec.kwonlyargs and not spec.kwonlydefaults:
+      raise MagicParsingError(
+          "Magics can't have keyword-only arguments without default value.")
+
+    args: List[Text] = spec.args
+    args_with_defaults: Dict[Text, Text] = {}
+    args_types: Dict[Text, _MagicArgValueType] = {}
+
+    if spec.annotations:
+      for arg, typ_ in spec.annotations.items():
+        if typ_ in _MagicArgValues:
+          args_types[arg] = typ_
+        else:
+          raise MagicParsingError(
+              f"Magics can only have arguments of type {MagicArgValue}; "
+              f"got {arg}: {typ_}")
+
+    if spec.defaults:
+      for default in reversed(spec.defaults):
+        args_with_defaults[args.pop()] = default
+
+    if spec.kwonlydefaults:
+      for arg, default in spec.kwonlydefaults.items():
+        args_with_defaults[arg] = default
+
+    docstring = func.__doc__ if func.__doc__ else ""
+
+    args_descriptions: Dict[Text, Text] = {
+        param.arg_name: param.description  # type: ignore
+        for param in parse(docstring).params
+    }
+
+    return cls(
+        name, docstring, typ, args, args_with_defaults, args_descriptions,
+        args_types)
+
+  def to_parser(self) -> MagicArgumentParser:
+    """Create an argument parser out of _MagicSpec."""
+    desc, *_ = self.docstring.split('\n')
+    parser = MagicArgumentParser(
+        prog=self.name,
+        description=desc,
+        usage=_usage_string(
+            self.typ, self.args_with_no_defaults,
+            self.args_with_defaults.keys()))  # pylint: disable=no-member
+
+    for arg in self.args_with_no_defaults:  # pylint: disable=not-an-iterable
+      if self.typ == MagicType.CELL and arg == "cell":
+        continue
+      typ = self.args_types.get(arg, str)
+      parser.add_argument(
+          arg,
+          type=typ,  # type: ignore
+          action="store",
+          help=self.args_descriptions[arg])
+    for arg, default in self.args_with_defaults.items():  # pylint: disable=no-member
+      typ = self.args_types.get(arg, str)
+      if typ == bool:
+        parser.add_argument(
+            f"--{arg}",
+            dest=arg,
+            action="store_false" if default else "store_true",
+            help=self.args_descriptions[arg],
+            default=default)
+      else:
+        parser.add_argument(
+            f"--{arg}",
+            dest=arg,
+            type=typ,  # type: ignore
+            action="store",
+            help=self.args_descriptions[arg],
+            default=default)
+    return parser
+
+
+@dataclass(frozen=True)
+class Magic:
+  """Wrapper for IPython magic."""
+  _spec: _MagicSpec
+  _parser: MagicArgumentParser
+  func: Callable[..., Any]
+  __doc__: Text
+
+  @classmethod
+  def wrap(
+      cls,
+      typ: MagicType,
+      func: Callable[..., Any],
+      name: Optional[Text] = None) -> "Magic":
+    """Wrap wraps a function to make it an IPython magic."""
+    spec = _MagicSpec.from_function(typ, func, name=name)
+    return cls(spec, spec.to_parser(), func, func.__doc__ or cls.__doc__)
+
+  def __call__(self, line: Text, cell: Optional[Text] = None) -> Any:
+    args = self._parser.parse_magic_args(line, cell)
+    globals()[args.bind_variable] = self.func(**args.kwargs)  #pylint: disable=not-a-mapping
diff --git a/requirements.txt b/requirements.txt
index d1e871f..73175be 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,4 @@ pandas>=1.1.3
 timesketch-api-client>=20201130
 timesketch-import-client>=20200910
 typing-extensions>=3.7.4.3
+docstring-parser>=0.12


Test patch:
diff --git a/picatrix/lib/magic_test.py b/picatrix/lib/magic_test.py
new file mode 100644
index 0000000..25b8bdc
--- /dev/null
+++ b/picatrix/lib/magic_test.py
@@ -0,0 +1,354 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# type: ignore
+# pylint: disable=W0212
+# pylint: disable=unused-argument
+"""Test for Picatrix integration with IPython magics."""
+
+from typing import Text
+
+import pytest
+
+from .magic import (
+    MagicArgParsingError,
+    MagicArgs,
+    MagicParsingError,
+    MagicType,
+    _MagicSpec,
+)
+
+
+# Test functions
+def magic(
+    a: Text,
+    b: Text,
+):
+  """Example function.
+
+    Args:
+      a: first argument
+      b: second argument
+    """
+
+
+def test_magicspec_valid():
+  """Test if _MagicSpec parses simple function."""
+
+  want = _MagicSpec(
+      name="magic",
+      docstring=magic.__doc__,
+      typ=MagicType.LINE,
+      args_with_no_defaults=["a", "b"],
+      args_descriptions={
+          "a": "first argument",
+          "b": "second argument",
+      },
+      args_types={
+          "a": str,
+          "b": str,
+      })
+  got = _MagicSpec.from_function(MagicType.LINE, magic)
+
+  assert want == got
+
+
+def magic_with_defaults(
+    a: Text,
+    b: Text = "dog",
+    c: Text = "cat",
+):
+  """Example function.
+
+    Args:
+      a: first argument
+      b: second argument
+      c: third argument
+    """
+
+
+def test_magicspec_valid_with_defaults():
+  """Test if _MagicSpec parses simple function with default value."""
+
+  want = _MagicSpec(
+      name="magic_with_defaults",
+      docstring=magic_with_defaults.__doc__,
+      typ=MagicType.LINE,
+      args_with_no_defaults=["a"],
+      args_with_defaults={
+          "b": "dog",
+          "c": "cat",
+      },
+      args_descriptions={
+          "a": "first argument",
+          "b": "second argument",
+          "c": "third argument",
+      },
+      args_types={
+          "a": str,
+          "b": str,
+          "c": str,
+      })
+  got = _MagicSpec.from_function(MagicType.LINE, magic_with_defaults)
+
+  assert want == got
+
+
+# pylint: disable=missing-function-docstring
+def magic_no_docstring(
+    a: Text,
+    b: Text = "dog",
+):
+  pass
+
+
+# pylint: enable=missing-function-docstring
+
+
+def test_magicspec_no_docstring():
+  """Test if _MagicSpec errors on function with no docstring."""
+
+  with pytest.raises(MagicParsingError):
+    _MagicSpec.from_function(MagicType.LINE, magic_no_docstring)
+
+
+def magic_bad_docstring(
+    a: Text,
+    b: Text = "dog",
+):
+  """Example function.
+
+    Args:
+      a: first argument
+    """
+
+
+def test_magicspec_bad_docstring():
+  """Test if _MagicSpec errors on function with no docstring for all args."""
+
+  with pytest.raises(MagicParsingError):
+    _MagicSpec.from_function(MagicType.LINE, magic_bad_docstring)
+
+
+def magic_notstring(
+    a: int,
+    b: Text = "dog",
+    c: float = 1.11,
+):
+  """Example function.
+
+    Args:
+      a: first argument
+      b: second argument
+      c: third argument
+    """
+
+
+def test_magicspec_nonstring():
+  """Test if _MagicSpec errors on non-string parameter."""
+
+  want = _MagicSpec(
+      name="magic_notstring",
+      docstring=magic_notstring.__doc__,
+      typ=MagicType.LINE,
+      args_with_no_defaults=["a"],
+      args_with_defaults={
+          "b": "dog",
+          "c": 1.11,
+      },
+      args_descriptions={
+          "a": "first argument",
+          "b": "second argument",
+          "c": "third argument",
+      },
+      args_types={
+          "a": int,
+          "b": str,
+          "c": float,
+      })
+  got = _MagicSpec.from_function(MagicType.LINE, magic_notstring)
+
+  assert want == got
+
+
+def magic_variadic(
+    a: Text,
+    *args: Text,
+) -> Text:
+  """Example function.
+
+    Args:
+      a: first argument
+      *args: all other arguments
+  """
+
+
+def test_magicspec_variadic():
+  """Test if _MagicSpec errors on functions with variadic params."""
+
+  with pytest.raises(MagicParsingError):
+    _MagicSpec.from_function(MagicType.LINE, magic_variadic)
+
+
+def magic_kwvariadic(
+    a: Text,
+    **kwargs: Text,
+) -> Text:
+  """Example function.
+
+    Args:
+      a: first argument
+      **kwargs: all other arguments
+    """
+
+
+def test_magicspec_kwvariadic():
+  """Test if _MagicSpec errors on functions with keyword variadic params."""
+
+  with pytest.raises(MagicParsingError):
+    _MagicSpec.from_function(MagicType.LINE, magic_kwvariadic)
+
+
+def magic_with_all_defaults(
+    a: Text = "dog",
+    b: Text = "cat",
+):
+  """Example function.
+
+    Args:
+      a: first argument
+      b: second argument
+  """
+
+
+def test_magicsargsparser_empty():
+  """Test parsing of empty string."""
+
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_with_all_defaults)
+  parser = spec.to_parser()
+
+  line = ""
+  want = MagicArgs(
+      bind_variable="_", kwargs={
+          "a": "dog",
+          "b": "cat",
+      })
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+
+def test_magicsargsparser_positional():
+  """Test parsing of a signal positional argument."""
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_with_defaults)
+  parser = spec.to_parser()
+
+  line = "v -- horse"
+  want = MagicArgs(
+      bind_variable="v", kwargs={
+          "a": "horse",
+          "b": "dog",
+          "c": "cat",
+      })
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+
+def test_magicsargsparser_mixed():
+  """Test parsing of a mix of positional and keyword arguments."""
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_with_defaults)
+  parser = spec.to_parser()
+
+  line = "-- --b=boar horse"
+  want = MagicArgs(
+      bind_variable="_", kwargs={
+          "a": "horse",
+          "b": "boar",
+          "c": "cat",
+      })
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+
+def test_magicsargsparser_notstring():
+  """Test parsing of non-string arguments."""
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_notstring)
+  parser = spec.to_parser()
+
+  line = "-- --b=boar --c=2.22 10"
+  want = MagicArgs(
+      bind_variable="_", kwargs={
+          "a": 10,
+          "b": "boar",
+          "c": 2.22,
+      })
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+
+def magic_bool_true(a: bool = True):
+  """Example function.
+
+    Args:
+      a: first argument
+  """
+
+
+def test_magicsargsparser_bool_true():
+  """Test parsing of boolean argument with default equal to true."""
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_bool_true)
+  parser = spec.to_parser()
+
+  line = "-- --a"
+  want = MagicArgs(bind_variable="_", kwargs={"a": False})
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+  line = ""
+  want = MagicArgs(bind_variable="_", kwargs={"a": True})
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+
+def magic_bool_false(a: bool = False):
+  """Example function.
+
+    Args:
+      a: first argument
+  """
+
+
+def test_magicsargsparser_bool_false():
+  """Test parsing of boolean argument with default equal to false."""
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_bool_false)
+  parser = spec.to_parser()
+
+  line = "-- --a"
+  want = MagicArgs(bind_variable="_", kwargs={"a": True})
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+  line = ""
+  want = MagicArgs(bind_variable="_", kwargs={"a": False})
+  got = parser.parse_magic_args(line, None)
+  assert want == got
+
+
+@pytest.mark.parametrize("line", ["--", " -- ", "-- --b=cat", "11 -- cat"])
+def test_magicsargsparser_error(line):
+  """Test parsing behavior for errornous inputs."""
+  spec = _MagicSpec.from_function(MagicType.LINE, magic_with_defaults)
+  parser = spec.to_parser()
+
+  with pytest.raises(MagicArgParsingError):
+    _ = parser.parse_magic_args(line, None)