Repository: ipython/ipython

Issue:
%%timeit -o does not store its results to a variable
When using the timeit magic in cell mode:

```
elapsed_time_ = %%timeit -o
```

The timeit cell mode magic does not store the IPython.core.magics.execution.TimeitResult to the elapsed_time_ variable.

The timeit magic command's line mode and cell mode -o option should exhibit a standard behavior between the two modes.


Discussion:
A workaround is to use the `_` variable in the subsequent cell as described in [this stackoverflow](https://stackoverflow.com/questions/41351041/how-to-store-the-result-from-timeit-cell-magic) post.

But, this does not make the subsequent cell idempotent -- the subsequent cell has to be executed right after the timeit cell and not at any other time.  Being able to assign a variable makes the cell idempotent (to the extent that the variable will always be the output of the timeit command).
I'm not sure if `%%` magics ever do this. It looks valid enough to legitimize it with a label though, so...
To be clear, the behaviour is a SyntaxError:
```ipython
In [1]: elapsed_time_ = %%timeit -o
   ...: 15
  Cell In[1], line 1
    elapsed_time_ = %%timeit -o
                    ^
SyntaxError: invalid syntax
```

The expected behaviour is to store the result in a variable, but this doesn't seem to be possible, syntactically:
```
In [1]: elapsed_time_ = %%timeit -o
   ...: 15
12.8 ns ± 0.119 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

In [2]: elapsed_time_
Out[2]: <TimeitResult : 12.8 ns ± 0.119 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)>
```

Cf. the line-magic version:
```ipython
In [1]: elapsed_time_ = %timeit -o 15
12.8 ns ± 0.119 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

In [2]: elapsed_time_
Out[2]: <TimeitResult : 12.8 ns ± 0.119 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)>
```
It's true that cell magic can't really return things, If a cell magic it should likely be `-o <variable name>`.



Patch:
diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py
index 4e54149bf5..a90cf14e46 100644
--- a/IPython/core/magics/execution.py
+++ b/IPython/core/magics/execution.py
@@ -1055,11 +1055,11 @@ def timeit(self, line='', cell=None, local_ns=None):
 
         **Usage, in line mode**::
 
-          %timeit [-n<N> -r<R> [-t|-c] -q -p<P> -o] statement
+          %timeit [-n<N> -r<R> [-t|-c] -q -p<P> [-o|-v <V>]] statement
 
         **or in cell mode**::
 
-          %%timeit [-n<N> -r<R> [-t|-c] -q -p<P> -o] setup_code
+          %%timeit [-n<N> -r<R> [-t|-c] -q -p<P> [-o|-v <V>]] setup_code
           code
           code...
 
@@ -1104,6 +1104,9 @@ def timeit(self, line='', cell=None, local_ns=None):
           Return a ``TimeitResult`` that can be stored in a variable to inspect
           the result in more details.
 
+        -v <V>
+          Like ``-o``, but save the ``TimeitResult`` directly to variable <V>.
+
         .. versionchanged:: 7.3
             User variables are no longer expanded,
             the magic line is always left unmodified.
@@ -1135,7 +1138,7 @@ def timeit(self, line='', cell=None, local_ns=None):
         those from ``%timeit``."""
 
         opts, stmt = self.parse_options(
-            line, "n:r:tcp:qo", posix=False, strict=False, preserve_non_opts=True
+            line, "n:r:tcp:qov:", posix=False, strict=False, preserve_non_opts=True
         )
         if stmt == "" and cell is None:
             return
@@ -1145,8 +1148,9 @@ def timeit(self, line='', cell=None, local_ns=None):
         default_repeat = 7 if timeit.default_repeat < 7 else timeit.default_repeat
         repeat = int(getattr(opts, "r", default_repeat))
         precision = int(getattr(opts, "p", 3))
-        quiet = 'q' in opts
-        return_result = 'o' in opts
+        quiet = "q" in opts
+        return_result = "o" in opts
+        save_result = "v" in opts
         if hasattr(opts, "t"):
             timefunc = time.time
         if hasattr(opts, "c"):
@@ -1207,7 +1211,7 @@ def timeit(self, line='', cell=None, local_ns=None):
                 if var_name in local_ns:
                     conflict_globs[var_name] = var_val
             glob.update(local_ns)
-            
+
         exec(code, glob, ns)
         timer.inner = ns["inner"]
 
@@ -1229,9 +1233,9 @@ def timeit(self, line='', cell=None, local_ns=None):
 
         # Restore global vars from conflict_globs
         if conflict_globs:
-           glob.update(conflict_globs)
-                
-        if not quiet :
+            glob.update(conflict_globs)
+
+        if not quiet:
             # Check best timing is greater than zero to avoid a
             # ZeroDivisionError.
             # In cases where the slowest timing is lesser than a microsecond
@@ -1241,11 +1245,15 @@ def timeit(self, line='', cell=None, local_ns=None):
                 print("The slowest run took %0.2f times longer than the "
                       "fastest. This could mean that an intermediate result "
                       "is being cached." % (worst / best))
-           
+
             print( timeit_result )
 
             if tc > tc_min:
                 print("Compiler time: %.2f s" % tc)
+
+        if save_result:
+            self.shell.user_ns[opts.v] = timeit_result
+
         if return_result:
             return timeit_result
 


Test patch:
diff --git a/tests/test_magic.py b/tests/test_magic.py
index a2e6f80e5e..b89c5e8fb5 100644
--- a/tests/test_magic.py
+++ b/tests/test_magic.py
@@ -827,6 +827,16 @@ def test_timeit_return():
     assert res is not None
 
 
+def test_timeit_save():
+    """
+    test whether timeit -v save object
+    """
+
+    name = "some_variable_name"
+    _ip.run_line_magic("timeit", "-n10 -r10 -v %s 1" % name)
+    assert _ip.user_ns[name] is not None
+
+
 def test_timeit_quiet():
     """
     test quiet option of timeit magic