Repository: mwouts/jupytext
Pull request #332 (2019-09-16 20:29:19)

1.2.4
**Improvements**

- The documentation includes a mention on how to set metadata filters at the command line (#330)
- Jupytext will not catch any error when the flag `--warn-only` is not set (#327)

**BugFixes**

- Now the flag `--warn-only` catches every possible error (#263)
- `.md` and `.markdown` files are treated identically (#325)
- Fixed `--set-kernel` when using pipes (#326)
- Fixed utf-8 encoding on stdout on Python 2 (#331)

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

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

```diff
@@            Coverage Diff            @@
##           master     #332     +/-   ##
=========================================
- Coverage   99.18%   98.98%   -0.2%     
=========================================
  Files          71       71             
  Lines        6772     6625    -147     
=========================================
- Hits         6717     6558    -159     
- Misses         55       67     +12
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/332?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [jupytext/cell\_reader.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvY2VsbF9yZWFkZXIucHk=) | `96.37% <ø> (-0.21%)` | :arrow_down: |
| [jupytext/magics.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvbWFnaWNzLnB5) | `100% <ø> (ø)` | :arrow_up: |
| [jupytext/kernels.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQva2VybmVscy5weQ==) | `89.65% <100%> (-2.35%)` | :arrow_down: |
| [tests/test\_cli.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jbGkucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/formats.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvZm9ybWF0cy5weQ==) | `98.92% <100%> (ø)` | :arrow_up: |
| [jupytext/contentsmanager.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvY29udGVudHNtYW5hZ2VyLnB5) | `98.44% <100%> (ø)` | :arrow_up: |
| [jupytext/version.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvdmVyc2lvbi5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/header.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvaGVhZGVyLnB5) | `99.23% <100%> (ø)` | :arrow_up: |
| [jupytext/combine.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvY29tYmluZS5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/jupytext.py](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree#diff-anVweXRleHQvanVweXRleHQucHk=) | `98.54% <66.66%> (-1.46%)` | :arrow_down: |
| ... and [31 more](https://codecov.io/gh/mwouts/jupytext/pull/332/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/332?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/332?src=pr&el=footer). Last update [0fe2c0c...22fdf2f](https://codecov.io/gh/mwouts/jupytext/pull/332?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 00000000..a7ceb05c
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,7 @@
+
+coverage:
+  status:
+    # pull-requests only
+    patch:
+      default:
+        threshold: 0.1%
diff --git a/.travis.yml b/.travis.yml
index f726639d..6ffee1b3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -55,6 +55,8 @@ before_script:
   - flake8 . --count --exit-zero --max-complexity=10 --statistics
 script:
   - coverage run --source=. -m py.test
+  # Test HTML display of Utf-8 chars with pipes
+  - echo "from IPython.core.display import HTML; HTML(u'\xd7')" | jupytext --from py --to ipynb --set-kernel - --execute > out.ipynb
 after_success:
   - coverage report -m
   - codecov
diff --git a/HISTORY.rst b/HISTORY.rst
index 31425e72..5d01ce6d 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,21 @@
 Release History
 ---------------
 
+1.2.4 (2019-09-??)
+++++++++++++++++++++++
+
+**Improvements**
+
+- The documentation includes a mention on how to set metadata filters at the command line (#330)
+- Jupytext will not catch any error when the flag ``--warn-only`` is not set (#327)
+
+**BugFixes**
+
+- Now the flag ``--warn-only`` catches every possible error (#263)
+- ``.md`` and ``.markdown`` files are treated identically (#325)
+- Fixed ``--set-kernel`` when using pipes (#326)
+- Fixed utf-8 encoding on stdout on Python 2 (#331)
+
 1.2.3 (2019-09-02)
 ++++++++++++++++++++++
 
diff --git a/jupytext/cell_reader.py b/jupytext/cell_reader.py
index b7e08596..b4e9e305 100644
--- a/jupytext/cell_reader.py
+++ b/jupytext/cell_reader.py
@@ -191,7 +191,7 @@ class BaseCellReader(object):
 
         # Is this a raw cell?
         if ('active' in self.metadata and not is_active('ipynb', self.metadata)) or \
-                (self.ext == '.md' and self.cell_type == 'code' and self.language is None):
+                (self.ext in ['.md', '.markdown'] and self.cell_type == 'code' and self.language is None):
             if self.metadata.get('active') == '':
                 del self.metadata['active']
             self.cell_type = 'raw'
diff --git a/jupytext/cli.py b/jupytext/cli.py
index f6a93069..56a25333 100644
--- a/jupytext/cli.py
+++ b/jupytext/cli.py
@@ -159,17 +159,8 @@ def parse_jupytext_args(args=None):
     return parser.parse_args(args)
 
 
-def jupytext_cli(args=None):
-    """Entry point for the jupytext script"""
-    try:
-        jupytext(args)
-    except (ValueError, TypeError, IOError) as err:
-        sys.stderr.write('[jupytext] Error: ' + str(err) + '\n')
-        exit(1)
-
-
 def jupytext(args=None):
-    """Internal implementation of Jupytext command line"""
+    """Entry point for the jupytext script"""
     args = parse_jupytext_args(args)
 
     def log(text):
@@ -188,11 +179,6 @@ def jupytext(args=None):
         for nb_file in args.notebooks:
             log(nb_file)
 
-    def writef_git_add(notebook_, nb_file_, fmt_):
-        write(notebook_, nb_file_, fmt=fmt_)
-        if args.pre_commit:
-            system('git', 'add', nb_file_)
-
     # Read notebook from stdin
     if not args.notebooks:
         if not args.pre_commit:
@@ -227,9 +213,6 @@ def jupytext(args=None):
         args.to = long_form_one_format(args.to)
         set_format_options(args.to, args.format_options)
 
-    # Main loop
-    round_trip_conversion_errors = 0
-
     # Wildcard extension on Windows #202
     notebooks = []
     for pattern in args.notebooks:
@@ -238,183 +221,198 @@ def jupytext(args=None):
         else:
             notebooks.append(pattern)
 
+    # Count how many file have round-trip issues when testing
+    exit_code = 0
     for nb_file in notebooks:
+        if not args.warn_only:
+            exit_code += jupytext_single_file(nb_file, args, log)
+        else:
+            try:
+                exit_code += jupytext_single_file(nb_file, args, log)
+            except Exception as err:
+                sys.stderr.write('[jupytext] Error: {}\n'.format(str(err)))
+
+    return exit_code
+
+
+def jupytext_single_file(nb_file, args, log):
+    """Apply the jupytext commmand, with given arguments, to a single file"""
+    if nb_file == '-' and args.sync:
+        raise ValueError('Cannot sync a notebook on stdin')
+
+    nb_dest = args.output or (None if not args.to
+                              else ('-' if nb_file == '-' else
+                                    full_path(base_path(nb_file, args.input_format), args.to)))
+
+    # Just acting on metadata / pipe => save in place
+    if not nb_dest and not args.sync:
+        nb_dest = nb_file
+
+    if nb_dest == '-':
+        args.quiet = True
+
+    # I. ### Read the notebook ###
+    fmt = copy(args.input_format) or {}
+    set_format_options(fmt, args.format_options)
+    log('[jupytext] Reading {}{}'.format(
+        nb_file if nb_file != '-' else 'stdin',
+        ' in format {}'.format(short_form_one_format(fmt)) if 'extension' in fmt else ''))
+
+    notebook = read(nb_file, fmt=fmt)
+    if not fmt:
+        text_representation = notebook.metadata.get('jupytext', {}).get('text_representation', {})
+        ext = os.path.splitext(nb_file)[1]
+        if text_representation.get('extension') == ext:
+            fmt = {key: text_representation[key] for key in text_representation if
+                   key in ['extension', 'format_name']}
+        elif ext:
+            fmt = {'extension': ext}
+
+    # Compute actual extension when using script/auto, and update nb_dest if necessary
+    dest_fmt = args.to
+    if dest_fmt and dest_fmt['extension'] == '.auto':
+        auto_ext = auto_ext_from_metadata(notebook.metadata)
+        if not auto_ext:
+            raise ValueError('The notebook has no language information. '
+                             'Please provide an explicit script extension.')
+        dest_fmt['extension'] = auto_ext
+        if not args.output and nb_file != '-':
+            nb_dest = full_path(base_path(nb_file, args.input_format), dest_fmt)
+
+    # Set the kernel
+    set_kernel = args.set_kernel
+    if (not set_kernel) and args.execute and notebook.metadata.get('kernelspec', {}).get('name') is None:
+        set_kernel = '-'
+
+    if set_kernel:
+        if set_kernel == '-':
+            language = notebook.metadata.get('jupytext', {}).get('main_language') \
+                       or notebook.metadata['kernelspec']['language']
+
+            if not language:
+                raise ValueError('Cannot infer a kernel as notebook language is not defined')
+
+            kernelspec = kernelspec_from_language(language)
+
+            if not kernelspec:
+                raise ValueError('Found no kernel for {}'.format(language))
+        else:
+            try:
+                kernelspec = get_kernel_spec(set_kernel)
+            except KeyError:
+                raise KeyError('Please choose a kernel name among {}'
+                               .format([name for name in find_kernel_specs()]))
+
+            kernelspec = {'name': args.set_kernel,
+                          'language': kernelspec.language,
+                          'display_name': kernelspec.display_name}
+
+        log("[jupytext] Setting kernel {}".format(kernelspec.get('name')))
+        args.update_metadata['kernelspec'] = kernelspec
+
+    # Update the metadata
+    if args.update_metadata:
+        log("[jupytext] Updating notebook metadata with '{}'".format(json.dumps(args.update_metadata)))
+        # Are we updating a text file that has a metadata filter? #212
+        if notebook.metadata.get('jupytext', {}).get('notebook_metadata_filter') == '-all':
+            notebook.metadata.get('jupytext', {}).pop('notebook_metadata_filter')
+        recursive_update(notebook.metadata, args.update_metadata)
+
+        if 'kernelspec' in args.update_metadata and 'main_language' in notebook.metadata.get('jupytext', {}):
+            notebook.metadata['jupytext'].pop('main_language')
+
+    # Read paired notebooks
+    if args.sync:
+        set_prefix_and_suffix(fmt, notebook, nb_file)
         try:
-            if nb_file == '-' and args.sync:
-                raise ValueError('Cannot sync a notebook on stdin')
-
-            nb_dest = args.output or (None if not args.to
-                                      else ('-' if nb_file == '-' else
-                                            full_path(base_path(nb_file, args.input_format), args.to)))
-
-            # Just acting on metadata / pipe => save in place
-            if not nb_dest and not args.sync:
-                nb_dest = nb_file
-
-            if nb_dest == '-':
-                args.quiet = True
-
-            # I. ### Read the notebook ###
-            fmt = copy(args.input_format) or {}
-            set_format_options(fmt, args.format_options)
-            log('[jupytext] Reading {}{}'.format(
-                nb_file if nb_file != '-' else 'stdin',
-                ' in format {}'.format(short_form_one_format(fmt)) if 'extension' in fmt else ''))
-
-            notebook = read(nb_file, fmt=fmt)
-            if not fmt:
-                text_representation = notebook.metadata.get('jupytext', {}).get('text_representation', {})
-                ext = os.path.splitext(nb_file)[1]
-                if text_representation.get('extension') == ext:
-                    fmt = {key: text_representation[key] for key in text_representation if
-                           key in ['extension', 'format_name']}
-                elif ext:
-                    fmt = {'extension': ext}
-
-            # Compute actual extension when using script/auto, and update nb_dest if necessary
-            dest_fmt = args.to
-            if dest_fmt and dest_fmt['extension'] == '.auto':
-                auto_ext = auto_ext_from_metadata(notebook.metadata)
-                if not auto_ext:
-                    raise ValueError('The notebook has no language information. '
-                                     'Please provide an explicit script extension.')
-                dest_fmt['extension'] = auto_ext
-                if not args.output and nb_file != '-':
-                    nb_dest = full_path(base_path(nb_file, args.input_format), dest_fmt)
-
-            # Set the kernel
-            set_kernel = args.set_kernel
-            if args.execute and notebook.metadata.get('kernelspec', {}).get('name') is None:
-                log("[jupytext] Setting default kernel with --set-kernel -")
-                set_kernel = '-'
-
-            if set_kernel:
-                if set_kernel == '-':
-                    language = notebook.metadata.get('jupytext', {}).get('main_language') \
-                               or notebook.metadata['kernelspec']['language']
-                    if not language:
-                        raise ValueError('Cannot infer a kernel as notebook language is not defined')
-
-                    kernelspec = kernelspec_from_language(language)
-                    if not kernelspec:
-                        raise ValueError('Found no kernel for {}'.format(language))
-                else:
-                    try:
-                        kernelspec = get_kernel_spec(set_kernel)
-                    except KeyError:
-                        raise KeyError('Please choose a kernel name among {}'
-                                       .format([name for name in find_kernel_specs()]))
-                    kernelspec = {'name': args.set_kernel,
-                                  'language': kernelspec.language,
-                                  'display_name': kernelspec.display_name}
-
-                args.update_metadata['kernelspec'] = kernelspec
-
-            # Update the metadata
-            if args.update_metadata:
-                log("[jupytext] Updating notebook metadata with '{}'".format(json.dumps(args.update_metadata)))
-                # Are we updating a text file that has a metadata filter? #212
-                if fmt['extension'] != '.ipynb' and \
-                        notebook.metadata.get('jupytext', {}).get('notebook_metadata_filter') == '-all':
-                    notebook.metadata.get('jupytext', {}).pop('notebook_metadata_filter')
-                recursive_update(notebook.metadata, args.update_metadata)
-
-                if 'kernelspec' in args.update_metadata and 'main_language' in notebook.metadata.get('jupytext', {}):
-                    notebook.metadata['jupytext'].pop('main_language')
-
-            # Read paired notebooks
-            if args.sync:
-                set_prefix_and_suffix(fmt, notebook, nb_file)
-                try:
-                    notebook, inputs_nb_file, outputs_nb_file = load_paired_notebook(notebook, fmt, nb_file, log)
-                except NotAPairedNotebook as err:
-                    sys.stderr.write('[jupytext] Warning: ' + str(err) + '\n')
-                    continue
+            notebook, inputs_nb_file, outputs_nb_file = load_paired_notebook(notebook, fmt, nb_file, log)
+        except NotAPairedNotebook as err:
+            sys.stderr.write('[jupytext] Warning: ' + str(err) + '\n')
+            return 0
 
-            # II. ### Apply commands onto the notebook ###
-            # Pipe the notebook into the desired commands
-            for cmd in args.pipe or []:
-                notebook = pipe_notebook(notebook, cmd, args.pipe_fmt)
-
-            # and/or test the desired commands onto the notebook
-            for cmd in args.check or []:
-                pipe_notebook(notebook, cmd, args.pipe_fmt, update=False)
-
-            # Execute the notebook
-            if args.execute:
-                log("[jupytext] Executing notebook")
-                kernel_name = notebook.metadata.get('kernelspec', {}).get('name')
-                exec_proc = ExecutePreprocessor(timeout=None, kernel_name=kernel_name)
-                exec_proc.preprocess(notebook, resources={})
-
-            # III. ### Possible actions ###
-            modified = args.update_metadata or args.pipe or args.execute
-            # a. Test round trip conversion
-            if args.test or args.test_strict:
-                try:
-                    test_round_trip_conversion(notebook, dest_fmt,
-                                               update=args.update,
-                                               allow_expected_differences=not args.test_strict,
-                                               stop_on_first_error=args.stop_on_first_error)
-                except NotebookDifference as err:
-                    round_trip_conversion_errors += 1
-                    sys.stdout.write('{}: {}'.format(nb_file, str(err)))
-                continue
+    # II. ### Apply commands onto the notebook ###
+    # Pipe the notebook into the desired commands
+    for cmd in args.pipe or []:
+        notebook = pipe_notebook(notebook, cmd, args.pipe_fmt)
 
-            # b. Output to the desired file or format
-            if nb_dest:
-                if nb_dest == nb_file and not dest_fmt:
-                    dest_fmt = fmt
-
-                # Test consistency between dest name and output format
-                if dest_fmt and nb_dest != '-':
-                    base_path(nb_dest, dest_fmt)
-
-                # Describe what jupytext is doing
-                if os.path.isfile(nb_dest) and args.update:
-                    if not nb_dest.endswith('.ipynb'):
-                        raise ValueError('--update is only for ipynb files')
-                    action = ' (destination file updated)'
-                    check_file_version(notebook, nb_file, nb_dest)
-                    combine_inputs_with_outputs(notebook, read(nb_dest), fmt=fmt)
-                elif os.path.isfile(nb_dest):
-                    action = ' (destination file replaced)'
-                else:
-                    action = ''
-
-                log('[jupytext] Writing {nb_dest}{format}{action}'
-                    .format(nb_dest=nb_dest,
-                            format=' in format ' + short_form_one_format(
-                                dest_fmt) if dest_fmt and 'format_name' in dest_fmt else '',
-                            action=action))
-                writef_git_add(notebook, nb_dest, dest_fmt)
-
-            # c. Synchronize paired notebooks
-            if args.sync:
-                # Also update the original notebook if the notebook was modified
-                if modified:
-                    inputs_nb_file = outputs_nb_file = None
-                formats = notebook.metadata['jupytext']['formats']
-
-                for ipynb in [True, False]:
-                    # Write first format last so that it is the most recent file
-                    for alt_path, alt_fmt in paired_paths(nb_file, fmt, formats)[::-1]:
-                        # Write ipynb first for compatibility with our contents manager
-                        if alt_path.endswith('.ipynb') != ipynb:
-                            continue
-                        # Do not write the ipynb file if it was not modified
-                        # But, always write text representations to make sure they are the most recent
-                        if alt_path == inputs_nb_file and alt_path == outputs_nb_file:
-                            continue
-                        log("[jupytext] Updating '{}'".format(alt_path))
-                        writef_git_add(notebook, alt_path, alt_fmt)
-        except (ValueError, TypeError, IOError) as err:
-            if args.warn_only:
-                sys.stderr.write('[jupytext] Error: {}\n'.format(str(err)))
-            else:
-                raise err
+    # and/or test the desired commands onto the notebook
+    for cmd in args.check or []:
+        pipe_notebook(notebook, cmd, args.pipe_fmt, update=False)
+
+    # Execute the notebook
+    if args.execute:
+        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)
+        exec_proc.preprocess(notebook, resources={})
+
+    # III. ### Possible actions ###
+    modified = args.update_metadata or args.pipe or args.execute
+    # a. Test round trip conversion
+    if args.test or args.test_strict:
+        try:
+            test_round_trip_conversion(notebook, dest_fmt,
+                                       update=args.update,
+                                       allow_expected_differences=not args.test_strict,
+                                       stop_on_first_error=args.stop_on_first_error)
+        except NotebookDifference as err:
+            sys.stdout.write('{}: {}'.format(nb_file, str(err)))
+            return 1
+        return 0
+
+    # b. Output to the desired file or format
+    if nb_dest:
+        if nb_dest == nb_file and not dest_fmt:
+            dest_fmt = fmt
+
+        # Test consistency between dest name and output format
+        if dest_fmt and nb_dest != '-':
+            base_path(nb_dest, dest_fmt)
+
+        # Describe what jupytext is doing
+        if os.path.isfile(nb_dest) and args.update:
+            if not nb_dest.endswith('.ipynb'):
+                raise ValueError('--update is only for ipynb files')
+            action = ' (destination file updated)'
+            check_file_version(notebook, nb_file, nb_dest)
+            combine_inputs_with_outputs(notebook, read(nb_dest), fmt=fmt)
+        elif os.path.isfile(nb_dest):
+            action = ' (destination file replaced)'
+        else:
+            action = ''
+
+        log('[jupytext] Writing {nb_dest}{format}{action}'
+            .format(nb_dest=nb_dest,
+                    format=' in format ' + short_form_one_format(
+                        dest_fmt) if dest_fmt and 'format_name' in dest_fmt else '',
+                    action=action))
+        write(notebook, nb_dest, fmt=dest_fmt)
+        if args.pre_commit:
+            system('git', 'add', nb_dest)
+
+    # c. Synchronize paired notebooks
+    if args.sync:
+        # Also update the original notebook if the notebook was modified
+        if modified:
+            inputs_nb_file = outputs_nb_file = None
+        formats = notebook.metadata['jupytext']['formats']
+
+        for ipynb in [True, False]:
+            # Write first format last so that it is the most recent file
+            for alt_path, alt_fmt in paired_paths(nb_file, fmt, formats)[::-1]:
+                # Write ipynb first for compatibility with our contents manager
+                if alt_path.endswith('.ipynb') != ipynb:
+                    continue
+                # Do not write the ipynb file if it was not modified
+                # But, always write text representations to make sure they are the most recent
+                if alt_path == inputs_nb_file and alt_path == outputs_nb_file:
+                    continue
+                log("[jupytext] Updating '{}'".format(alt_path))
+                write(notebook, alt_path, fmt=alt_fmt)
+                if args.pre_commit:
+                    system('git', 'add', alt_path)
 
-    return round_trip_conversion_errors
+    return 0
 
 
 def notebooks_in_git_index(fmt):
@@ -480,7 +478,6 @@ def set_prefix_and_suffix(fmt, notebook, nb_file):
 
 class NotAPairedNotebook(ValueError):
     """An error raised when a notebook is not a paired notebook"""
-    pass
 
 
 def load_paired_notebook(notebook, fmt, nb_file, log):
diff --git a/jupytext/combine.py b/jupytext/combine.py
index 0c76c845..51fd2c66 100644
--- a/jupytext/combine.py
+++ b/jupytext/combine.py
@@ -46,8 +46,8 @@ def combine_inputs_with_outputs(nb_source, nb_outputs, fmt=None):
         if key not in nb_outputs_filtered_metadata:
             nb_source.metadata[key] = nb_outputs.metadata[key]
 
-    source_is_md_version_one = ext in ['.md', '.Rmd'] and text_repr.get('format_version') == '1.0'
-    if nb_source.metadata.get('jupytext', {}).get('formats') or ext in ['.md', '.Rmd']:
+    source_is_md_version_one = ext in ['.md', '.markdown', '.Rmd'] and text_repr.get('format_version') == '1.0'
+    if nb_source.metadata.get('jupytext', {}).get('formats') or ext in ['.md', '.markdown', '.Rmd']:
         nb_source.metadata.get('jupytext', {}).pop('text_representation', None)
 
     if not nb_source.metadata.get('jupytext', {}):
diff --git a/jupytext/contentsmanager.py b/jupytext/contentsmanager.py
index 9fb64b57..56394625 100644
--- a/jupytext/contentsmanager.py
+++ b/jupytext/contentsmanager.py
@@ -36,7 +36,7 @@ def preferred_format(incomplete_format, preferred_formats):
     for fmt in long_form_multiple_formats(preferred_formats):
         if ((incomplete_format['extension'] == fmt['extension'] or (
                 fmt['extension'] == '.auto' and
-                incomplete_format['extension'] not in ['.md', '.Rmd', '.ipynb'])) and
+                incomplete_format['extension'] not in ['.md', '.markdown', '.Rmd', '.ipynb'])) and
                 incomplete_format.get('suffix') == fmt.get('suffix', incomplete_format.get('suffix')) and
                 incomplete_format.get('prefix') == fmt.get('prefix', incomplete_format.get('prefix'))):
             fmt.update(incomplete_format)
@@ -264,7 +264,7 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
 
                     alt_path = full_path(base, fmt)
                     self.create_prefix_dir(alt_path, fmt)
-                    if 'format_name' in fmt and fmt['extension'] not in ['.Rmd', '.md']:
+                    if 'format_name' in fmt and fmt['extension'] not in ['.md', '.markdown', '.Rmd']:
                         self.log.info("Saving %s in format %s:%s",
                                       os.path.basename(alt_path), fmt['extension'][1:], fmt['format_name'])
                     else:
diff --git a/jupytext/formats.py b/jupytext/formats.py
index 683c7b6c..d5e2f0b7 100644
--- a/jupytext/formats.py
+++ b/jupytext/formats.py
@@ -170,7 +170,7 @@ def get_format_implementation(ext, format_name=None):
             formats_for_extension.append(fmt.format_name)
 
     if formats_for_extension:
-        if ext == '.md' and format_name == 'pandoc':
+        if ext in ['.md', '.markdown'] and format_name == 'pandoc':
             raise JupytextFormatError('Please install pandoc>=2.7.2')
 
         raise JupytextFormatError("Format '{}' is not associated to extension '{}'. "
@@ -184,7 +184,7 @@ def read_metadata(text, ext):
     ext = '.' + ext.split('.')[-1]
     lines = text.splitlines()
 
-    if ext in ['.md', '.Rmd']:
+    if ext in ['.md', '.markdown', '.Rmd']:
         comment = ''
     else:
         comment = _SCRIPT_EXTENSIONS.get(ext, {}).get('comment', '#')
@@ -274,7 +274,7 @@ def guess_format(text, ext):
         if rspin_comment_count >= 1:
             return 'spin', {}
 
-    if ext == '.md':
+    if ext in ['.md', '.markdown']:
         for line in lines:
             if line.startswith(':::'):  # Pandoc div
                 return 'pandoc', {}
@@ -355,7 +355,7 @@ def format_name_for_ext(metadata, ext, cm_default_formats=None, explicit_default
             if (not explicit_default) or fmt.get('format_name'):
                 return fmt.get('format_name')
 
-    if (not explicit_default) or ext in ['.Rmd', '.md']:
+    if (not explicit_default) or ext in ['.md', '.markdown', '.Rmd']:
         return None
 
     return get_format_implementation(ext).format_name
@@ -509,7 +509,8 @@ def short_form_one_format(jupytext_format):
         fmt = jupytext_format['prefix'] + '/' + fmt
 
     if jupytext_format.get('format_name'):
-        if jupytext_format['extension'] not in ['.md', '.Rmd'] or jupytext_format['format_name'] == 'pandoc':
+        if jupytext_format['extension'] not in ['.md', '.markdown', '.Rmd'] or \
+                jupytext_format['format_name'] == 'pandoc':
             fmt = fmt + ':' + jupytext_format['format_name']
 
     return fmt
diff --git a/jupytext/header.py b/jupytext/header.py
index 22994230..47ae5af8 100644
--- a/jupytext/header.py
+++ b/jupytext/header.py
@@ -54,12 +54,12 @@ def encoding_and_executable(notebook, metadata, ext):
     comment = _SCRIPT_EXTENSIONS.get(ext, {}).get('comment')
     jupytext_metadata = metadata.get('jupytext', {})
 
-    if ext not in ['.Rmd', '.md'] and 'executable' in jupytext_metadata:
+    if comment is not None and 'executable' in jupytext_metadata:
         lines.append(comment + '!' + jupytext_metadata.pop('executable'))
 
     if 'encoding' in jupytext_metadata:
         lines.append(jupytext_metadata.pop('encoding'))
-    elif ext not in ['.Rmd', '.md']:
+    elif comment is not None:
         for cell in notebook.cells:
             try:
                 cell.source.encode('ascii')
diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py
index 1c8135a0..9cd5b307 100644
--- a/jupytext/jupytext.py
+++ b/jupytext/jupytext.py
@@ -161,7 +161,7 @@ class TextNotebookConverter(NotebookReader, NotebookWriter):
             text.extend([''] * lines_to_next_cell)
 
             # two blank lines between markdown cells in Rmd when those do not have explicit region markers
-            if self.ext in ['.Rmd', '.md'] and not cell.is_code():
+            if self.ext in ['.md', '.markdown', '.Rmd'] and not cell.is_code():
                 if (i + 1 < len(cell_exporters) and not cell_exporters[i + 1].is_code() and
                         not texts[i][0].startswith('<!-- #region') and
                         not texts[i + 1][0].startswith('<!-- #region') and
@@ -318,7 +318,13 @@ def write(nb, fp, version=nbformat.NO_CONVERT, fmt=None, **kwargs):
     :param kwargs: (not used) additional parameters for nbformat.write
     """
     if fp == '-':
-        write(nb, sys.stdout, version=version, fmt=fmt, **kwargs)
+        # Use sys.stdout.buffer when possible, and explicit utf-8 encoding, cf. #331
+        content = writes(nb, version=version, fmt=fmt, **kwargs)
+        try:
+            # Python 3
+            sys.stdout.buffer.write(content.encode('utf-8'))
+        except AttributeError:
+            sys.stdout.write(content.encode('utf-8'))
         return
 
     if not hasattr(fp, 'write'):
diff --git a/jupytext/kernels.py b/jupytext/kernels.py
index f2ea5079..ed736b69 100644
--- a/jupytext/kernels.py
+++ b/jupytext/kernels.py
@@ -31,6 +31,12 @@ def kernelspec_from_language(language):
                 if kernel_specs.language == 'python' and kernel_specs.argv[0] == sys.executable:
                     return {'name': name, 'language': language, 'display_name': kernel_specs.display_name}
 
+            # If none was found, return the first kernel that has 'python' as argv[0]
+            for name in find_kernel_specs():
+                kernel_specs = get_kernel_spec(name)
+                if kernel_specs.language == 'python' and kernel_specs.argv[0] == 'python':
+                    return {'name': name, 'language': language, 'display_name': kernel_specs.display_name}
+
         for name in find_kernel_specs():
             kernel_specs = get_kernel_spec(name)
             if kernel_specs.language == language or (language == 'c++' and kernel_specs.language.startswith('C++')):
diff --git a/jupytext/magics.py b/jupytext/magics.py
index 361ebaaf..a946e96c 100644
--- a/jupytext/magics.py
+++ b/jupytext/magics.py
@@ -77,7 +77,8 @@ def uncomment_magic(source, language='python', global_escape_flag=True):
 
 
 _ESCAPED_CODE_START = {'.Rmd': re.compile(r"^(# |#)*```{.*}"),
-                       '.md': re.compile(r"^(# |#)*```")}
+                       '.md': re.compile(r"^(# |#)*```"),
+                       '.markdown': re.compile(r"^(# |#)*```")}
 _ESCAPED_CODE_START.update({ext: re.compile(
     r"^({0} |{0})*({0}|{0} )\+".format(_SCRIPT_EXTENSIONS[ext]['comment'])) for ext in _SCRIPT_EXTENSIONS})
 
diff --git a/jupytext/version.py b/jupytext/version.py
index 11e7b8b0..0cb2020b 100644
--- a/jupytext/version.py
+++ b/jupytext/version.py
@@ -1,3 +1,3 @@
 """Jupytext's version number"""
 
-__version__ = '1.2.3'
+__version__ = '1.2.4-dev'
diff --git a/setup.py b/setup.py
index a2c5662a..68e228d0 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@ setup(
                                                          'jupytext/nbextension/jupytext_menu_zoom.png',
                                                          'jupytext/nbextension/jupytext.yml']),
                 ('share/jupyter/lab/extensions', ['packages/labextension/jupyterlab-jupytext-1.0.2.tgz'])],
-    entry_points={'console_scripts': ['jupytext = jupytext.cli:jupytext_cli']},
+    entry_points={'console_scripts': ['jupytext = jupytext.cli:jupytext']},
     tests_require=['pytest'],
     install_requires=['nbformat>=4.0.0', 'pyyaml', 'mock;python_version<"3"'],
     license='MIT',


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 778be4c0..f7221ce1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,7 +1,10 @@
+# -*- coding: utf-8 -*-
+
 import os
 import sys
 import time
 import stat
+from io import StringIO
 
 try:
     import unittest.mock as mock
@@ -16,8 +19,8 @@ from argparse import ArgumentTypeError
 from nbformat.v4.nbbase import new_notebook, new_markdown_cell, new_code_cell
 from jupyter_client.kernelspec import find_kernel_specs, get_kernel_spec
 from jupytext import __version__
-from jupytext import read, write, writes
-from jupytext.cli import parse_jupytext_args, jupytext, jupytext_cli, system, str2bool
+from jupytext import read, reads, write, writes
+from jupytext.cli import parse_jupytext_args, jupytext, system, str2bool
 from jupytext.compare import compare_notebooks
 from jupytext.paired_paths import paired_paths, InconsistentPath
 from jupytext.formats import long_form_one_format, JupytextFormatError
@@ -542,7 +545,7 @@ def test_set_kernel_inplace(py_file, tmpdir):
 
     nb = read(tmp_py)
     kernel_name = nb.metadata['kernelspec']['name']
-    assert get_kernel_spec(kernel_name).argv[0].lower() in ['python', sys.executable]
+    assert get_kernel_spec(kernel_name).argv[0].lower() in ['python', sys.executable.lower()]
 
 
 @pytest.mark.parametrize('py_file', list_notebooks('python'))
@@ -556,7 +559,7 @@ def test_set_kernel_auto(py_file, tmpdir):
 
     nb = read(tmp_ipynb)
     kernel_name = nb.metadata['kernelspec']['name']
-    assert get_kernel_spec(kernel_name).argv[0].lower() in ['python', sys.executable]
+    assert get_kernel_spec(kernel_name).argv[0].lower() in ['python', sys.executable.lower()]
 
 
 @pytest.mark.parametrize('py_file', list_notebooks('python'))
@@ -753,6 +756,34 @@ def test_cli_can_infer_jupytext_format_from_stdin(nb_file, tmpdir):
     compare_notebooks(nb, nb2, 'Rmd')
 
 
+def test_set_kernel_works_with_pipes_326(capsys):
+    md = u"""```python
+1 + 1
+```"""
+
+    with mock.patch('sys.stdin', StringIO(md)):
+        jupytext(['--to', 'ipynb', '--set-kernel', '-', '-'])
+
+    out, err = capsys.readouterr()
+    assert err == ''
+    nb = reads(out, 'ipynb')
+    assert 'kernelspec' in nb.metadata
+
+
+def test_utf8_out_331(capsys):
+    py = u"from IPython.core.display import HTML; HTML(u'\xd7')"
+
+    with mock.patch('sys.stdin', StringIO(py)):
+        jupytext(['--to', 'ipynb', '--execute', '-'])
+
+    out, err = capsys.readouterr()
+    assert err == ''
+    nb = reads(out, 'ipynb')
+    assert len(nb.cells) == 1
+    print(nb.cells[0].outputs)
+    assert nb.cells[0].outputs[0]['data']['text/html'] == u'\xd7'
+
+
 @requires_jupytext_installed
 def test_cli_expect_errors(tmp_ipynb):
     with pytest.raises(ValueError):
@@ -765,8 +796,8 @@ def test_cli_expect_errors(tmp_ipynb):
         jupytext(['--pre-commit', 'notebook.ipynb'])
     with pytest.raises(ValueError):
         jupytext(['notebook.ipynb', '--from', 'py:percent', '--to', 'md'])
-    with pytest.raises(SystemExit):
-        jupytext_cli([])
+    with pytest.raises(ValueError):
+        jupytext([])
     with pytest.raises((SystemExit, TypeError)):  # SystemExit on Windows, TypeError on Linux
         system('jupytext', ['notebook.ipynb', '--from', 'py:percent', '--to', 'md'])