Repository: mwouts/jupytext
Pull request #569 (2020-07-21 18:02:08)

Preserve the indentation of magic commands
Cf. #437 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1851555..c54b4579 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+1.5.1+dev
+---------
+
+**Fixed**
+- Preserve indentation when commenting out magic commands (#437)
+
+
 1.5.1 (2020-07-05)
 ------------------
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a836e97b..6b818b94 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -39,6 +39,7 @@ Most of Jupytext's code is written in Python. To develop the Python part of Jupy
 cd jupytext
 conda env create --file environment.yml  # or conda env update --file ...
 conda activate jupytext-dev
+python -m ipykernel install --name jupytext-dev --user
 pip install -e .
 ```
 
diff --git a/jupytext/cell_metadata.py b/jupytext/cell_metadata.py
index a7249f39..0df74a90 100644
--- a/jupytext/cell_metadata.py
+++ b/jupytext/cell_metadata.py
@@ -64,10 +64,6 @@ _IGNORE_CELL_METADATA = ",".join(
 _IDENTIFIER_RE = re.compile(r"^[a-zA-Z_\.]+[a-zA-Z0-9_\.]*$")
 
 
-def _r_logical_values(pybool):
-    return "TRUE" if pybool else "FALSE"
-
-
 class RLogicalValueError(Exception):
     """Incorrect value for R boolean"""
 
diff --git a/jupytext/magics.py b/jupytext/magics.py
index aa5ac72c..858e0f8e 100644
--- a/jupytext/magics.py
+++ b/jupytext/magics.py
@@ -40,7 +40,7 @@ _MAGIC_FORCE_ESC_RE["csharp"] = re.compile(r"^(// |//)*#![a-zA-Z](.*)//\s*escape
 _MAGIC_FORCE_ESC_RE["csharp"] = re.compile(r"^(// |//)*#![a-zA-Z](.*)//\s*noescape")
 
 # Commands starting with a question or exclamation mark have to be escaped
-_PYTHON_HELP_OR_BASH_CMD = re.compile(r"^(# |#)*\s*(\?|!)\s*[A-Za-z]")
+_PYTHON_HELP_OR_BASH_CMD = re.compile(r"^\s*(# |#)*\s*(\?|!)\s*[A-Za-z]")
 
 # A bash command not followed by an equal sign or a parenthesis is a magic command
 _PYTHON_MAGIC_CMD = re.compile(
@@ -55,7 +55,7 @@ _PYTHON_MAGIC_CMD = re.compile(
     )
 )
 # Python help commands end with ?
-_IPYTHON_MAGIC_HELP = re.compile(r"^(# )*[^\s]*\?\s*$")
+_IPYTHON_MAGIC_HELP = re.compile(r"^\s*(# )*[^\s]*\?\s*$")
 
 _SCRIPT_LANGUAGES = [_SCRIPT_EXTENSIONS[ext]["language"] for ext in _SCRIPT_EXTENSIONS]
 
@@ -109,7 +109,15 @@ def comment_magic(
             next_is_magic
             or is_magic(line, language, global_escape_flag, explicitly_code)
         ):
-            source[pos] = _COMMENT[language] + " " + line
+            if next_is_magic:
+                # this is the continuation line of a magic command on the previous line,
+                # so we don't want to indent the comment
+                unindented = line
+                indent = ""
+            else:
+                unindented = line.lstrip()
+                indent = line[: len(line) - len(unindented)]
+            source[pos] = indent + _COMMENT[language] + " " + unindented
             next_is_magic = language == "python" and _LINE_CONTINUATION_RE.match(line)
         parser.read_line(line)
     return source
@@ -118,10 +126,12 @@ def comment_magic(
 def unesc(line, language):
     """Uncomment once a commented line"""
     comment = _COMMENT[language]
-    if line.startswith(comment + " "):
-        return line[len(comment) + 1 :]
-    if line.startswith(comment):
-        return line[len(comment) :]
+    unindented = line.lstrip()
+    indent = line[: len(line) - len(unindented)]
+    if unindented.startswith(comment + " "):
+        return indent + unindented[len(comment) + 1 :]
+    if unindented.startswith(comment):
+        return indent + unindented[len(comment) :]
     return line
 
 
diff --git a/jupytext/version.py b/jupytext/version.py
index b6fdc6e6..9826bbd3 100644
--- a/jupytext/version.py
+++ b/jupytext/version.py
@@ -1,3 +1,3 @@
 """Jupytext's version number"""
 
-__version__ = "1.5.1"
+__version__ = "1.5.1+dev"


Test patch:
diff --git a/tests/test_cell_metadata.py b/tests/test_cell_metadata.py
index 6d311012..394d03d5 100644
--- a/tests/test_cell_metadata.py
+++ b/tests/test_cell_metadata.py
@@ -4,6 +4,7 @@ from jupytext.cell_metadata import (
     rmd_options_to_metadata,
     metadata_to_rmd_options,
     parse_rmd_options,
+    parse_key_equal_value,
 )
 from jupytext.cell_metadata import (
     _IGNORE_CELL_METADATA,
@@ -211,3 +212,17 @@ def test_incorrectly_encoded(text="this is an incorrect expression d={{4 b=3"):
 def test_incorrectly_encoded_json(text='this is an incorrect expression {"d": "}'):
     value = text_to_metadata(text, allow_title=True)
     assert metadata_to_text(*value) == text
+
+
+def test_parse_key_value():
+    assert parse_key_equal_value("key='value' key2=5.0") == {
+        "key": "value",
+        "key2": 5.0,
+    }
+
+
+def test_parse_key_value_key():
+    assert parse_key_equal_value("key='value' key2") == {
+        "key": "value",
+        "key2": None,
+    }
diff --git a/tests/test_escape_magics.py b/tests/test_escape_magics.py
index ba5d93d8..d6cec995 100644
--- a/tests/test_escape_magics.py
+++ b/tests/test_escape_magics.py
@@ -250,3 +250,10 @@ def test_configure_magic(no_jupytext_version_number):
 """,
     )
     compare_notebooks(jupytext.reads(text, "py"), nb)
+
+
+def test_indented_magic():
+    assert is_magic("    !rm file", "python")
+    assert is_magic("    # !rm file", "python")
+    assert comment_magic(["    !rm file"]) == ["    # !rm file"]
+    assert uncomment_magic(["    # !rm file"]) == ["    !rm file"]
diff --git a/tests/test_read_simple_python.py b/tests/test_read_simple_python.py
index a52e59a7..727b33fe 100644
--- a/tests/test_read_simple_python.py
+++ b/tests/test_read_simple_python.py
@@ -10,6 +10,7 @@ from jupytext.compare import compare
 import jupytext
 from jupytext.compare import compare_notebooks
 from .utils import skip_if_dict_is_not_ordered
+import pytest
 
 
 def test_read_simple_file(
@@ -1136,7 +1137,7 @@ except:
         ]
     ),
     text="""try:
-#     !echo jo
+    # !echo jo
     pass
 except:
     pass
@@ -1182,3 +1183,41 @@ def test_arg(arg):
         assert cell.cell_type == "code"
     assert nb.cells[0].source == "import pytest"
     assert nb.cells[0].metadata == {}
+
+
+@pytest.mark.parametrize(
+    "script,cell",
+    [
+        (
+            """if True:
+    # # !rm file 1
+    # !rm file 2
+""",
+            """if True:
+    # !rm file 1
+    !rm file 2""",
+        ),
+        (
+            """# +
+if True:
+    # help?
+    # ?help
+    # # ?help
+    # # help?
+""",
+            """if True:
+    help?
+    ?help
+    # ?help
+    # help?""",
+        ),
+    ],
+)
+def test_indented_magic_commands(script, cell):
+
+    nb = jupytext.reads(script, "py")
+    assert len(nb.cells) == 1
+    assert nb.cells[0].cell_type == "code"
+    compare(nb.cells[0].source, cell)
+    assert nb.cells[0].metadata == {}
+    compare(jupytext.writes(nb, "py"), script)