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.

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

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

```diff
@@           Coverage Diff           @@
##           master     #472   +/-   ##
=======================================
  Coverage   98.85%   98.86%           
=======================================
  Files          80       80           
  Lines        7884     7899   +15     
=======================================
+ Hits         7794     7809   +15     
  Misses         90       90           
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/472?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [jupytext/stringparser.py](https://codecov.io/gh/mwouts/jupytext/pull/472/diff?src=pr&el=tree#diff-anVweXRleHQvc3RyaW5ncGFyc2VyLnB5) | `95.45% <100.00%> (+0.33%)` | :arrow_up: |
| [tests/test\_read\_simple\_percent.py](https://codecov.io/gh/mwouts/jupytext/pull/472/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9yZWFkX3NpbXBsZV9wZXJjZW50LnB5) | `100.00% <100.00%> (ø)` | |
| [tests/test\_stringparser.py](https://codecov.io/gh/mwouts/jupytext/pull/472/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9zdHJpbmdwYXJzZXIucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/472?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/472?src=pr&el=footer). Last update [271fa1c...4bea6fb](https://codecov.io/gh/mwouts/jupytext/pull/472?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 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]