Repository: mwouts/jupytext

Issue:
Wrong cell separation when creating .ipynb from .py
Let's say I work in a notebook file, paired to a `.py` file, and write one function per cell.

When I re-generate the `.ipynb` notebook from the `.py` file I have noticed that some functions now live in the same cell.

This is best illustrated by the images below. The following code:
![Screenshot 2020-03-17 at 21 43 10](https://user-images.githubusercontent.com/7881978/76904670-5777ad00-6898-11ea-8f0d-0e7ce50ecfa4.png)

becomes, after regenerating the `.ipynb` from the paired `.py`:
![Screenshot 2020-03-17 at 21 36 20](https://user-images.githubusercontent.com/7881978/76904593-34e59400-6898-11ea-95e2-fb2fb9c007d4.png)

The problem does not impact all functions/cells. I've inspected the code in the `.py` file and cannot find any obvious difference between the impacted cells/functions and the non impacted ones.

I'm using jupytext version `1.4.0` but had noticed this behaviour on previous versions too.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45aa942..f22230e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 **Fixed**
 - Fixed the CLI example for not commenting out magic commands: `--opt comment_magics=false`. In addition, most of the `jupytext` commands in `using-cli.md` are now tested! (#465)
 - `jupytext.read` and `jupytext.write` now give more meaningful errors when the format information is incorrect (#462)
+- Multiline comments starting with quadruple quotes should not cause issues anymore (#460)
 
 1.4.1 (2020-03-19)
 ------------------
diff --git a/jupytext/stringparser.py b/jupytext/stringparser.py
index 17b28fa..1b3c618 100644
--- a/jupytext/stringparser.py
+++ b/jupytext/stringparser.py
@@ -8,6 +8,7 @@ class StringParser:
     is quoted or not"""
     single = None
     triple = None
+    triple_start = None
 
     def __init__(self, language):
         self.ignore = language is None
@@ -29,6 +30,8 @@ class StringParser:
         if not self.is_quoted() and self.comment is not None and line.startswith(self.comment):
             return
 
+        self.triple_start = -1
+
         for i, char in enumerate(line):
             if char not in ['"', "'"]:
                 continue
@@ -46,13 +49,14 @@ class StringParser:
                 continue
 
             if self.triple == char:
-                if line[i - 2:i + 1] == 3 * char:
+                if line[i - 2:i + 1] == 3 * char and i >= self.triple_start + 3:
                     self.triple = None
                     continue
             if self.triple is not None:
                 continue
             if line[i - 2:i + 1] == 3 * char:
                 self.triple = char
+                self.triple_start = i
                 continue
             self.single = char
 


Test patch:
diff --git a/tests/test_read_simple_percent.py b/tests/test_read_simple_percent.py
index 42af388..1a2fa1b 100644
--- a/tests/test_read_simple_percent.py
+++ b/tests/test_read_simple_percent.py
@@ -405,3 +405,21 @@ def test_single_triple_quote_works(no_jupytext_version_number, text='''# ---
 print("hello")
 ''', notebook=new_notebook(cells=[new_code_cell('print("hello")')])):
     compare_notebooks(jupytext.reads(text, 'py'), notebook)
+
+
+def test_docstring_with_quadruple_quote(nb=new_notebook(cells=[
+    new_code_cell('''def fun_1(df):
+  """"
+  docstring starting with 4 double quotes and ending with 3
+  """
+  return df'''),
+    new_code_cell('''def fun_2(df):
+  """
+  docstring
+  """
+  return df''')
+])):
+    """Reproduces https://github.com/mwouts/jupytext/issues/460"""
+    py = jupytext.writes(nb, 'py:percent')
+    nb2 = jupytext.reads(py, 'py')
+    compare_notebooks(nb2, nb)
diff --git a/tests/test_stringparser.py b/tests/test_stringparser.py
index 985b31d..36307dc 100644
--- a/tests/test_stringparser.py
+++ b/tests/test_stringparser.py
@@ -37,3 +37,19 @@ def test_single_chars(text="""'This is a single line comment'''
     for line in text.splitlines():
         assert not sp.is_quoted()
         sp.read_line(line)
+
+
+def test_long_string_with_four_quotes(text="""''''This is a multiline
+comment that starts with four quotes
+'''
+
+1 + 1
+"""):
+    quoted = []
+    sp = StringParser('python')
+    for i, line in enumerate(text.splitlines()):
+        if sp.is_quoted():
+            quoted.append(i)
+        sp.read_line(line)
+
+    assert quoted == [1, 2]