Repository: mwouts/jupytext

Issue:
Be able to specify --execute run_path from CLI
So I have some ipynb files that only work if run the root of my project, but there doesn't seem to be an easy way to specify that the run_path should be the CWD instead of the path of input path or output path of the arguments. The only way to get something like this behavior appears to specify that the the output = '-', but this can be restrictive. Would you consider a CLI flag to specify what the run_path of the file should be? Perhaps as an optional parameter to`--execute`? 

Alternatively, it might be useful as override similar to --output. 

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=h1) Report
> Merging [#612](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=desc) into [master](https://codecov.io/gh/mwouts/jupytext/commit/d4e922778cf363a5ccbf2e8174f12f4496cc31cb?el=desc) will **decrease** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mwouts/jupytext/pull/612/graphs/tree.svg?width=650&height=150&src=pr&token=a1pNjsiILi)](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #612      +/-   ##
==========================================
- Coverage   98.99%   98.98%   -0.01%     
==========================================
  Files          94       94              
  Lines        9152     9184      +32     
==========================================
+ Hits         9060     9091      +31     
- Misses         92       93       +1     
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [jupytext/cli.py](https://codecov.io/gh/mwouts/jupytext/pull/612/diff?src=pr&el=tree#diff-anVweXRleHQvY2xpLnB5) | `96.96% <100.00%> (+0.11%)` | :arrow_up: |
| [tests/test\_execute.py](https://codecov.io/gh/mwouts/jupytext/pull/612/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9leGVjdXRlLnB5) | `100.00% <100.00%> (ø)` | |
| [jupytext/formats.py](https://codecov.io/gh/mwouts/jupytext/pull/612/diff?src=pr&el=tree#diff-anVweXRleHQvZm9ybWF0cy5weQ==) | `96.60% <0.00%> (-0.31%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=footer). Last update [d4e9227...78d8eb4](https://codecov.io/gh/mwouts/jupytext/pull/612?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dcc24b..b4b63aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-1.6.0-dev (2020-08-??)
+1.6.0 (2020-08-??)
 ----------------------
 
 **Added**
@@ -14,6 +14,7 @@ of R Markdown notebooks as a raw cell in Jupyter (#415)
 - Warn if 'Include Metadata' is off when saving text files in Jupyter (#561)
 - Test that notebooks paired through a configuration file are left unmodified (#598)
 - Test that metadata filters in the configuration files are taken into account when using `jupytext --to` (#543)
+- New argument `--run-path` to execute the notebooks at the desired location (#595)
 
 **Changed**
 - Install Jupytext from source on MyBinder to avoid cache issues (#567)
diff --git a/jupytext/cli.py b/jupytext/cli.py
index 828b663..5857964 100644
--- a/jupytext/cli.py
+++ b/jupytext/cli.py
@@ -267,6 +267,11 @@ def parse_jupytext_args(args=None):
         action="store_true",
         help="Execute the notebook with the given kernel",
     )
+    parser.add_argument(
+        "--run-path",
+        type=str,
+        help="Execute the notebook at the given path (defaults to the notebook parent directory)",
+    )
 
     parser.add_argument(
         "--quiet",
@@ -326,6 +331,9 @@ def jupytext(args=None):
         print_paired_paths(args.notebooks[0], args.input_format)
         return 1
 
+    if args.run_path:
+        args.execute = True
+
     if (
         (args.test or args.test_strict)
         and not args.output_format
@@ -564,11 +572,29 @@ def jupytext_single_file(nb_file, args, log):
         kernel_name = notebook.metadata.get("kernelspec", {}).get("name")
         log("[jupytext] Executing notebook with kernel {}".format(kernel_name))
         exec_proc = ExecutePreprocessor(timeout=None, kernel_name=kernel_name)
-        if nb_dest is not None and nb_dest != "-":
 
-            resources = {"metadata": {"path": str(os.path.dirname(nb_dest))}}
+        if nb_dest is not None and nb_dest != "-":
+            nb_path = os.path.dirname(nb_dest)
         elif nb_file != "-":
-            resources = {"metadata": {"path": str(os.path.dirname(nb_file))}}
+            nb_path = os.path.dirname(nb_file)
+        else:
+            nb_path = None
+
+        run_path = args.run_path or nb_path
+        if args.run_path and not os.path.isdir(run_path):
+            # is this a relative directory?
+            for base_dir in [nb_path, os.getcwd()]:
+                try_path = os.path.join(base_dir, run_path)
+                if os.path.isdir(try_path):
+                    run_path = try_path
+                    break
+            if not os.path.isdir(run_path):
+                raise ValueError(
+                    "--run-path={} is not a valid path".format(args.run_path)
+                )
+
+        if run_path:
+            resources = {"metadata": {"path": run_path}}
         else:
             resources = {}
         exec_proc.preprocess(notebook, resources=resources)


Test patch:
diff --git a/tests/test_execute.py b/tests/test_execute.py
index f6330a3..44db6e8 100644
--- a/tests/test_execute.py
+++ b/tests/test_execute.py
@@ -1,7 +1,8 @@
-from .utils import requires_nbconvert, requires_ir_kernel, skip_on_windows
+import shutil
 import pytest
 from jupytext import read
 from jupytext.cli import jupytext
+from .utils import requires_nbconvert, requires_ir_kernel, skip_on_windows
 
 
 @requires_nbconvert
@@ -168,11 +169,11 @@ def test_execute_r(tmpdir, caplog, capsys):  # pragma: no cover
 @requires_nbconvert
 @skip_on_windows
 def test_execute_in_subfolder(tmpdir, caplog, capsys):
-    tmpdir.mkdir("subfolder")
+    subfolder = tmpdir.mkdir("subfolder")
 
-    tmp_csv = str(tmpdir.join("subfolder", "inputs.csv"))
-    tmp_py = str(tmpdir.join("subfolder", "notebook.py"))
-    tmp_ipynb = str(tmpdir.join("subfolder", "notebook.ipynb"))
+    tmp_csv = str(subfolder.join("inputs.csv"))
+    tmp_py = str(subfolder.join("notebook.py"))
+    tmp_ipynb = str(subfolder.join("notebook.ipynb"))
 
     with open(tmp_csv, "w") as fp:
         fp.write("1\n2\n")
@@ -193,3 +194,33 @@ sum(ast.literal_eval(line) for line in text.splitlines())
     nb = read(tmp_ipynb)
     assert len(nb.cells) == 3
     assert nb.cells[2].outputs[0]["data"] == {"text/plain": "3"}
+
+    tmp2_py = str(tmpdir.mkdir("another_folder").join("notebook.py"))
+    tmp2_ipynb = str(tmpdir.join("another_folder", "notebook.ipynb"))
+
+    shutil.copy(tmp_py, tmp2_py)
+
+    # Executing without run-path fails
+    import nbconvert
+
+    with pytest.raises(
+        nbconvert.preprocessors.execute.CellExecutionError,
+        match="No such file or directory: 'inputs.csv'",
+    ):
+        jupytext(args=[tmp2_py, "--to", "ipynb", "--execute"])
+
+    # Raise if folder does not exists
+    with pytest.raises(ValueError, match="is not a valid path"):
+        jupytext(args=[tmp2_py, "--to", "ipynb", "--run-path", "wrong_path"])
+
+    # Execute in full path
+    jupytext(args=[tmp2_py, "--to", "ipynb", "--run-path", str(subfolder)])
+    nb = read(tmp2_ipynb)
+    assert len(nb.cells) == 3
+    assert nb.cells[2].outputs[0]["data"] == {"text/plain": "3"}
+
+    # Execute in path relative to notebook dir
+    jupytext(args=[tmp2_py, "--to", "ipynb", "--run-path", "../subfolder"])
+    nb = read(tmp2_ipynb)
+    assert len(nb.cells) == 3
+    assert nb.cells[2].outputs[0]["data"] == {"text/plain": "3"}