diff --git a/README.md b/README.md index ccc8ec7f6..0fca2fe3a 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It can also convert these documents **into** Jupyter Notebooks, allowing you to synchronize content in both directions. -The languages that are currently supported by Jupytext are: Julia, Python, R, Bash, Scheme, Clojure, Matlab, Octave, C++, q/kdb+, IDL, TypeScript, Javascript, Scala, Rust/Evxcr, PowerShell and Robot Framework. Extending Jupytext to more languages should be easy - read more at [CONTRIBUTING.md](https://github.com/mwouts/jupytext/blob/master/CONTRIBUTING.md). In addition, jupytext users can choose between two formats for notebooks as scripts: +The languages that are currently supported by Jupytext are: Julia, Python, R, Bash, Scheme, Clojure, Matlab, Octave, C++, q/kdb+, IDL, TypeScript, Javascript, Scala, Rust/Evxcr, PowerShell, C#, F#, and Robot Framework. Extending Jupytext to more languages should be easy - read more at [CONTRIBUTING.md](https://github.com/mwouts/jupytext/blob/master/CONTRIBUTING.md). In addition, jupytext users can choose between two formats for notebooks as scripts: - The `percent` format, compatible with several IDEs, including Spyder, Hydrogen, VScode and PyCharm. In that format, cells are delimited with a commented `%%`. - The `light` format, designed for this project. Use that format to open standard scripts as notebooks, or to save notebooks as scripts with few cell markers - none when possible. diff --git a/docs/introduction.md b/docs/introduction.md index 1401dfac0..a694e03dd 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -10,7 +10,7 @@ Jupytext can save Jupyter notebooks as - Markdown and R Markdown documents, - Scripts in many languages. -The languages that are currently supported by Jupytext are: Julia, Python, R, Bash, Scheme, Clojure, Matlab, Octave, C++, q/kdb+, IDL, TypeScript, Javascript, Scala, Rust/Evxcr, PowerShell and Robot Framework. Extending Jupytext to more languages should be easy - read more at [CONTRIBUTING.md](https://github.com/mwouts/jupytext/blob/master/CONTRIBUTING.md#). In addition, jupytext users can choose between two formats for notebooks as scripts: +The languages that are currently supported by Jupytext are: Julia, Python, R, Bash, Scheme, Clojure, Matlab, Octave, C++, q/kdb+, IDL, TypeScript, Javascript, Scala, Rust/Evxcr, PowerShell, C#, F#,P and Robot Framework. Extending Jupytext to more languages should be easy - read more at [CONTRIBUTING.md](https://github.com/mwouts/jupytext/blob/master/CONTRIBUTING.md#). In addition, jupytext users can choose between two formats for notebooks as scripts: - The `percent` format, compatible with several IDEs, including Spyder, Hydrogen, VScode and PyCharm. In that format, cells are delimited with a commented `%%`. - The `light` format, designed for this project. Use that format to open standard scripts as notebooks, or to save notebooks as scripts with few cell markers - none when possible. diff --git a/jupytext/cell_metadata.py b/jupytext/cell_metadata.py index 1bbd3e79a..3ff12937e 100644 --- a/jupytext/cell_metadata.py +++ b/jupytext/cell_metadata.py @@ -49,7 +49,7 @@ 'autoscroll', 'collapsed', 'scrolled', 'trusted', 'ExecuteTime'] + _JUPYTEXT_CELL_METADATA) -_IDENTIFIER_RE = re.compile(r'^[a-zA-Z_\\.]+[a-zA-Z0-9_\\.]*$') +_IDENTIFIER_RE = re.compile(r'^[a-zA-Z_\.]+[a-zA-Z0-9_\.]*$') def _r_logical_values(pybool): @@ -345,7 +345,7 @@ def parse_key_equal_value(text): last_space_pos = text.rfind(' ') # Just an identifier? - if isidentifier(text[last_space_pos + 1:]): + if not text.startswith('--') and isidentifier(text[last_space_pos + 1:]): key = text[last_space_pos + 1:] value = None result = {key: value} diff --git a/jupytext/cell_reader.py b/jupytext/cell_reader.py index 2afb99cc9..7bb10ef03 100644 --- a/jupytext/cell_reader.py +++ b/jupytext/cell_reader.py @@ -263,7 +263,7 @@ def extract_content(self, lines): class MarkdownCellReader(BaseCellReader): """Read notebook cells from Markdown documents""" comment = '' - start_code_re = re.compile(r"^```({})($|\s(.*)$)".format( + start_code_re = re.compile(r"^```(\s*)({})($|\s.*$)".format( '|'.join(_JUPYTER_LANGUAGES_LOWER_AND_UPPER).replace('+', '\\+'))) non_jupyter_code_re = re.compile(r"^```") end_code_re = re.compile(r"^```\s*$") diff --git a/jupytext/cell_to_text.py b/jupytext/cell_to_text.py index 9dfb6c21a..71a233546 100644 --- a/jupytext/cell_to_text.py +++ b/jupytext/cell_to_text.py @@ -3,7 +3,7 @@ import re import warnings from copy import copy -from .languages import cell_language, comment_lines +from .languages import cell_language, comment_lines, same_language from .cell_metadata import is_active, _IGNORE_CELL_METADATA from .cell_metadata import metadata_to_text, metadata_to_rmd_options, metadata_to_double_percent_options from .metadata_filter import filter_metadata @@ -37,16 +37,18 @@ def __init__(self, cell, default_language, fmt=None): self.metadata = filter_metadata(cell.metadata, self.fmt.get('cell_metadata_filter'), _IGNORE_CELL_METADATA) - self.language, magic_args = cell_language(self.source) if self.parse_cell_language else (None, None) + if self.parse_cell_language: + self.language, magic_args = cell_language(self.source, default_language) - if self.language: if magic_args: self.metadata['magic_args'] = magic_args + else: + self.language = None - if not self.ext.endswith('.Rmd'): - self.metadata['language'] = self.language + if self.language and not self.ext.endswith('.Rmd'): + self.metadata['language'] = self.language - self.language = self.language or default_language + self.language = self.language or cell.metadata.get('language', default_language) self.default_language = default_language self.comment = _SCRIPT_EXTENSIONS.get(self.ext, {}).get('comment', '#') self.comment_magics = self.fmt.get('comment_magics', self.default_comment_magics) @@ -252,7 +254,7 @@ def is_code(self): def code_to_text(self): """Return the text representation of a code cell""" - active = is_active(self.ext, self.metadata, self.language == self.default_language) + active = is_active(self.ext, self.metadata, same_language(self.language, self.default_language)) source = copy(self.source) escape_code_start(source, self.ext, self.language) comment_questions = self.metadata.pop('comment_questions', True) @@ -378,7 +380,7 @@ def cell_to_text(self): if self.cell_type != 'code': self.metadata['cell_type'] = self.cell_type - active = is_active(self.ext, self.metadata, self.language == self.default_language) + active = is_active(self.ext, self.metadata, same_language(self.language, self.default_language)) if self.cell_type == 'raw' and 'active' in self.metadata and self.metadata['active'] == '': del self.metadata['active'] diff --git a/jupytext/formats.py b/jupytext/formats.py index dcd5e4de9..5ee6756ad 100644 --- a/jupytext/formats.py +++ b/jupytext/formats.py @@ -583,6 +583,9 @@ def auto_ext_from_metadata(metadata): if auto_ext == '.r': return '.R' + if auto_ext == '.fs': + return '.fsx' + return auto_ext diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py index 7cbb74e02..17baab9ad 100644 --- a/jupytext/jupytext.py +++ b/jupytext/jupytext.py @@ -71,21 +71,22 @@ def reads(self, s, **_): if self.implementation.format_name and self.implementation.format_name.startswith('sphinx'): cells.append(new_code_cell(source='%matplotlib inline')) - cell_metadata = set() cell_metadata_json = False while lines: reader = self.implementation.cell_reader_class(self.fmt, default_language) cell, pos = reader.read(lines) cells.append(cell) - cell_metadata.update(cell.metadata.keys()) cell_metadata_json = cell_metadata_json or reader.cell_metadata_json if pos <= 0: raise Exception('Blocked at lines ' + '\n'.join(lines[:6])) # pragma: no cover lines = lines[pos:] - update_metadata_filters(metadata, jupyter_md, cell_metadata) set_main_and_cell_language(metadata, cells, self.implementation.extension) + cell_metadata = set() + for cell in cells: + cell_metadata.update(cell.metadata.keys()) + update_metadata_filters(metadata, jupyter_md, cell_metadata) if cell_metadata_json: metadata.setdefault('jupytext', {}).setdefault('cell_metadata_json', True) @@ -130,7 +131,9 @@ def writes(self, nb, metadata=None, **kwargs): cells=nb.cells) metadata = nb.metadata - default_language = default_language_from_metadata_and_ext(metadata, self.implementation.extension) or 'python' + default_language = default_language_from_metadata_and_ext(metadata, + self.implementation.extension, + True) or 'python' self.update_fmt_with_notebook_options(nb.metadata) if 'use_runtools' not in self.fmt: for cell in nb.cells: @@ -138,9 +141,6 @@ def writes(self, nb, metadata=None, **kwargs): self.fmt['use_runtools'] = True break - if 'main_language' in metadata.get('jupytext', {}): - del metadata['jupytext']['main_language'] - header = encoding_and_executable(nb, metadata, self.ext) header_content, header_lines_to_next_cell = metadata_and_cell_to_header(nb, metadata, self.implementation, self.ext) diff --git a/jupytext/languages.py b/jupytext/languages.py index cfeeea462..33aa25a02 100644 --- a/jupytext/languages.py +++ b/jupytext/languages.py @@ -24,23 +24,29 @@ '.ts': {'language': 'typescript', 'comment': '//'}, '.scala': {'language': 'scala', 'comment': '//'}, '.rs': {'language': 'rust', 'comment': '//'}, - '.robot': {'language': 'robotframework', 'comment': '#'}} + '.robot': {'language': 'robotframework', 'comment': '#'}, + '.cs': {'language': 'csharp', 'comment': '//'}, + '.fsx': {'language': 'fsharp', 'comment': '//'}, + '.fs': {'language': 'fsharp', 'comment': '//'}} _COMMENT_CHARS = [_SCRIPT_EXTENSIONS[ext]['comment'] for ext in _SCRIPT_EXTENSIONS if _SCRIPT_EXTENSIONS[ext]['comment'] != '#'] _COMMENT = {_SCRIPT_EXTENSIONS[ext]['language']: _SCRIPT_EXTENSIONS[ext]['comment'] for ext in _SCRIPT_EXTENSIONS} -_JUPYTER_LANGUAGES = set(_JUPYTER_LANGUAGES).union(_COMMENT.keys()) +_JUPYTER_LANGUAGES = set(_JUPYTER_LANGUAGES).union(_COMMENT.keys()).union(['c#', 'f#', 'cs', 'fs']) _JUPYTER_LANGUAGES_LOWER_AND_UPPER = _JUPYTER_LANGUAGES.union({str.upper(lang) for lang in _JUPYTER_LANGUAGES}) -def default_language_from_metadata_and_ext(metadata, ext): +def default_language_from_metadata_and_ext(metadata, ext, pop_main_language=False): """Return the default language given the notebook metadata, and a file extension""" default_from_ext = _SCRIPT_EXTENSIONS.get(ext, {}).get('language') - language = (metadata.get('jupytext', {}).get('main_language') - or metadata.get('kernelspec', {}).get('language') - or default_from_ext) + main_language = metadata.get('jupytext', {}).get('main_language') + default_language = metadata.get('kernelspec', {}).get('language') or default_from_ext + language = main_language or default_language + + if main_language is not None and main_language == default_language and pop_main_language: + metadata['jupytext'].pop('main_language') if language is None or language == 'R': return language @@ -48,20 +54,28 @@ def default_language_from_metadata_and_ext(metadata, ext): if language.startswith('C++'): return 'c++' - return language.lower() + return language.lower().replace('#', 'sharp') + + +def usual_language_name(language): + """Return the usual language name (one that may be found in _SCRIPT_EXTENSIONS above)""" + language = language.lower() + if language == 'r': + return 'R' + if language.startswith('c++'): + return 'c++' + if language == 'octave': + return 'matlab' + if language in ['cs', 'c#']: + return 'csharp' + if language in ['fs', 'f#']: + return 'fsharp' + return language def same_language(kernel_language, language): """Are those the same language?""" - if kernel_language == language: - return True - if kernel_language.lower() == language: - return True - if kernel_language.startswith('C++') and language == 'c++': - return True - if kernel_language == 'octave' and language == 'matlab': - return True - return False + return usual_language_name(kernel_language) == usual_language_name(language) def set_main_and_cell_language(metadata, cells, ext): @@ -73,7 +87,7 @@ def set_main_and_cell_language(metadata, cells, ext): languages = {'python': 0.5} for cell in cells: if 'language' in cell['metadata']: - language = cell['metadata']['language'] + language = usual_language_name(cell['metadata']['language']) languages[language] = languages.get(language, 0.0) + 1 main_language = max(languages, key=languages.get) @@ -85,20 +99,35 @@ def set_main_and_cell_language(metadata, cells, ext): # Remove 'language' meta data and add a magic if not main language for cell in cells: if 'language' in cell['metadata']: - language = cell['metadata'].pop('language') - if language != main_language and language in _JUPYTER_LANGUAGES: + language = cell['metadata']['language'] + if language == main_language: + cell['metadata'].pop('language') + continue + + if usual_language_name(language) == main_language: + continue + + if language in _JUPYTER_LANGUAGES: + cell['metadata'].pop('language') + magic = '%%' if main_language != 'csharp' else '#!' if 'magic_args' in cell['metadata']: magic_args = cell['metadata'].pop('magic_args') - cell['source'] = u'%%{} {}\n'.format(language, magic_args) + cell['source'] + cell['source'] = u'{}{} {}\n'.format(magic, language, magic_args) + cell['source'] else: - cell['source'] = u'%%{}\n'.format(language) + cell['source'] + cell['source'] = u'{}{}\n'.format(magic, language) + cell['source'] -def cell_language(source): +def cell_language(source, default_language): """Return cell language and language options, if any""" if source: line = source[0] - if line.startswith('%%'): + if default_language == 'csharp': + if line.startswith('#!'): + lang = line[2:].strip() + if lang in _JUPYTER_LANGUAGES: + source.pop(0) + return lang, '' + elif line.startswith('%%'): magic = line[2:] if ' ' in magic: lang, magic_args = magic.split(' ', 1) diff --git a/jupytext/magics.py b/jupytext/magics.py index 0b53ba615..4fc352778 100644 --- a/jupytext/magics.py +++ b/jupytext/magics.py @@ -2,7 +2,7 @@ import re from .stringparser import StringParser -from .languages import _SCRIPT_EXTENSIONS, _COMMENT +from .languages import _SCRIPT_EXTENSIONS, _COMMENT, usual_language_name # A magic expression is a line or cell or metakernel magic (#94, #61) escaped zero, or multiple times _MAGIC_RE = {_SCRIPT_EXTENSIONS[ext]['language']: re.compile( @@ -20,6 +20,11 @@ _MAGIC_FORCE_ESC_RE['rust'] = re.compile(r"^(// |//)*:[a-zA-Z](.*)//\s*escape") _MAGIC_FORCE_ESC_RE['rust'] = re.compile(r"^(// |//)*:[a-zA-Z](.*)//\s*noescape") +# C# magics start with '#!' +_MAGIC_RE['csharp'] = re.compile(r"^(// |//)*#![a-zA-Z]") +_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*[A-Za-z]") @@ -37,6 +42,7 @@ def is_magic(line, language, global_escape_flag=True, explicitly_code=False): """Is the current line a (possibly escaped) Jupyter magic, and should it be commented?""" + language = usual_language_name(language) if language in ['octave', 'matlab'] or language not in _SCRIPT_LANGUAGES: return False if _MAGIC_FORCE_ESC_RE[language].match(line): diff --git a/jupytext/metadata_filter.py b/jupytext/metadata_filter.py index 5c083ab94..d16fff08c 100644 --- a/jupytext/metadata_filter.py +++ b/jupytext/metadata_filter.py @@ -75,8 +75,6 @@ def metadata_filter_as_string(metadata_filter): def update_metadata_filters(metadata, jupyter_md, cell_metadata): """Update or set the notebook and cell metadata filters""" - cell_metadata = [m for m in cell_metadata if m not in ['language', 'magic_args']] - if 'cell_metadata_filter' in metadata.get('jupytext', {}): metadata_filter = metadata_filter_as_dict(metadata.get('jupytext', {})['cell_metadata_filter']) if isinstance(metadata_filter.get('excluded'), list): diff --git a/jupytext/version.py b/jupytext/version.py index 6b2562e31..285745267 100644 --- a/jupytext/version.py +++ b/jupytext/version.py @@ -1,3 +1,3 @@ """Jupytext's version number""" -__version__ = '1.3.3' +__version__ = '1.3.3+dev' diff --git a/tests/notebooks/ipynb_cs/csharp.ipynb b/tests/notebooks/ipynb_cs/csharp.ipynb new file mode 100644 index 000000000..897044187 --- /dev/null +++ b/tests/notebooks/ipynb_cs/csharp.ipynb @@ -0,0 +1,152 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We start with..." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello World!\r\n" + ] + } + ], + "source": [ + "Console.WriteLine(\"Hello World!\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we do a plot with Plotly, following the [Plotting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Plotting%20with%20Xplot.ipynb) example from `dotnet/interactive`:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\r\n", + "\r\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "using XPlot.Plotly;\n", + "\n", + "var bar = new Graph.Bar\n", + "{\n", + " name = \"Bar\",\n", + " x = new[] {'A', 'B', 'C'},\n", + " y = new[] {1, 3, 2}\n", + "};\n", + "\n", + "var chart = Chart.Plot(new[] {bar});\n", + "chart.WithTitle(\"A bar plot\");\n", + "display(chart);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also test the math outputs as in the [Math and Latex](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Math%20and%20LaTeX.ipynb) example:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "\\begin{align} e^{i \\pi} = -1\\end{align}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(LaTeXString)@\"\\begin{align} e^{i \\pi} = -1\\end{align}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$e^{i \\pi} = -1$$" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(MathString)@\"e^{i \\pi} = -1\"" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".NET (C#)", + "language": "C#", + "name": ".net-csharp" + }, + "language_info": { + "file_extension": ".cs", + "mimetype": "text/x-csharp", + "name": "C#", + "pygments_lexer": "csharp", + "version": "8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/tests/notebooks/ipynb_fs/fsharp.ipynb b/tests/notebooks/ipynb_fs/fsharp.ipynb new file mode 100644 index 000000000..a1048ca09 --- /dev/null +++ b/tests/notebooks/ipynb_fs/fsharp.ipynb @@ -0,0 +1,88 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook was inspired by [Plottting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/fsharp/Docs/Plotting%20with%20Xplot.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "open XPlot.Plotly" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\r\n", + "\r\n" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "let bar =\n", + " Bar(\n", + " name = \"Bar 1\",\n", + " x = [\"A\"; \"B\"; \"C\"],\n", + " y = [1; 3; 2])\n", + "\n", + "[bar]\n", + "|> Chart.Plot\n", + "|> Chart.WithTitle \"A sample bar plot\"" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".NET (F#)", + "language": "F#", + "name": ".net-fsharp" + }, + "language_info": { + "file_extension": ".fs", + "mimetype": "text/x-fsharp", + "name": "C#", + "pygments_lexer": "fsharp", + "version": "4.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/notebooks/mirror/ipynb_to_Rmd/csharp.Rmd b/tests/notebooks/mirror/ipynb_to_Rmd/csharp.Rmd new file mode 100644 index 000000000..04b7529aa --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_Rmd/csharp.Rmd @@ -0,0 +1,40 @@ +--- +jupyter: + kernelspec: + display_name: .NET (C#) + language: C# + name: .net-csharp +--- + +We start with... + +```{csharp} +Console.WriteLine("Hello World!"); +``` + +Then we do a plot with Plotly, following the [Plotting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Plotting%20with%20Xplot.ipynb) example from `dotnet/interactive`: + +```{csharp} +using XPlot.Plotly; + +var bar = new Graph.Bar +{ + name = "Bar", + x = new[] {'A', 'B', 'C'}, + y = new[] {1, 3, 2} +}; + +var chart = Chart.Plot(new[] {bar}); +chart.WithTitle("A bar plot"); +display(chart); +``` + +We also test the math outputs as in the [Math and Latex](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Math%20and%20LaTeX.ipynb) example: + +```{csharp} +(LaTeXString)@"\begin{align} e^{i \pi} = -1\end{align}" +``` + +```{csharp} +(MathString)@"e^{i \pi} = -1" +``` diff --git a/tests/notebooks/mirror/ipynb_to_Rmd/fsharp.Rmd b/tests/notebooks/mirror/ipynb_to_Rmd/fsharp.Rmd new file mode 100644 index 000000000..b3d886b8a --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_Rmd/fsharp.Rmd @@ -0,0 +1,25 @@ +--- +jupyter: + kernelspec: + display_name: .NET (F#) + language: F# + name: .net-fsharp +--- + +This notebook was inspired by [Plottting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/fsharp/Docs/Plotting%20with%20Xplot.ipynb). + +```{fsharp} +open XPlot.Plotly +``` + +```{fsharp} +let bar = + Bar( + name = "Bar 1", + x = ["A"; "B"; "C"], + y = [1; 3; 2]) + +[bar] +|> Chart.Plot +|> Chart.WithTitle "A sample bar plot" +``` diff --git a/tests/notebooks/mirror/ipynb_to_hydrogen/csharp.cs b/tests/notebooks/mirror/ipynb_to_hydrogen/csharp.cs new file mode 100644 index 000000000..c513c83d2 --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_hydrogen/csharp.cs @@ -0,0 +1,39 @@ +// --- +// jupyter: +// kernelspec: +// display_name: .NET (C#) +// language: C# +// name: .net-csharp +// --- + +// %% [markdown] +// We start with... + +// %% +Console.WriteLine("Hello World!"); + +// %% [markdown] +// Then we do a plot with Plotly, following the [Plotting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Plotting%20with%20Xplot.ipynb) example from `dotnet/interactive`: + +// %% +using XPlot.Plotly; + +var bar = new Graph.Bar +{ + name = "Bar", + x = new[] {'A', 'B', 'C'}, + y = new[] {1, 3, 2} +}; + +var chart = Chart.Plot(new[] {bar}); +chart.WithTitle("A bar plot"); +display(chart); + +// %% [markdown] +// We also test the math outputs as in the [Math and Latex](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Math%20and%20LaTeX.ipynb) example: + +// %% +(LaTeXString)@"\begin{align} e^{i \pi} = -1\end{align}" + +// %% +(MathString)@"e^{i \pi} = -1" diff --git a/tests/notebooks/mirror/ipynb_to_hydrogen/fsharp.fsx b/tests/notebooks/mirror/ipynb_to_hydrogen/fsharp.fsx new file mode 100644 index 000000000..94307eb6e --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_hydrogen/fsharp.fsx @@ -0,0 +1,24 @@ +// --- +// jupyter: +// kernelspec: +// display_name: .NET (F#) +// language: F# +// name: .net-fsharp +// --- + +// %% [markdown] +// This notebook was inspired by [Plottting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/fsharp/Docs/Plotting%20with%20Xplot.ipynb). + +// %% +open XPlot.Plotly + +// %% +let bar = + Bar( + name = "Bar 1", + x = ["A"; "B"; "C"], + y = [1; 3; 2]) + +[bar] +|> Chart.Plot +|> Chart.WithTitle "A sample bar plot" diff --git a/tests/notebooks/mirror/ipynb_to_md/csharp.md b/tests/notebooks/mirror/ipynb_to_md/csharp.md new file mode 100644 index 000000000..ce3cc59d1 --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_md/csharp.md @@ -0,0 +1,40 @@ +--- +jupyter: + kernelspec: + display_name: .NET (C#) + language: C# + name: .net-csharp +--- + +We start with... + +```csharp +Console.WriteLine("Hello World!"); +``` + +Then we do a plot with Plotly, following the [Plotting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Plotting%20with%20Xplot.ipynb) example from `dotnet/interactive`: + +```csharp +using XPlot.Plotly; + +var bar = new Graph.Bar +{ + name = "Bar", + x = new[] {'A', 'B', 'C'}, + y = new[] {1, 3, 2} +}; + +var chart = Chart.Plot(new[] {bar}); +chart.WithTitle("A bar plot"); +display(chart); +``` + +We also test the math outputs as in the [Math and Latex](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Math%20and%20LaTeX.ipynb) example: + +```csharp +(LaTeXString)@"\begin{align} e^{i \pi} = -1\end{align}" +``` + +```csharp +(MathString)@"e^{i \pi} = -1" +``` diff --git a/tests/notebooks/mirror/ipynb_to_md/fsharp.md b/tests/notebooks/mirror/ipynb_to_md/fsharp.md new file mode 100644 index 000000000..686325e4e --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_md/fsharp.md @@ -0,0 +1,25 @@ +--- +jupyter: + kernelspec: + display_name: .NET (F#) + language: F# + name: .net-fsharp +--- + +This notebook was inspired by [Plottting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/fsharp/Docs/Plotting%20with%20Xplot.ipynb). + +```fsharp +open XPlot.Plotly +``` + +```fsharp +let bar = + Bar( + name = "Bar 1", + x = ["A"; "B"; "C"], + y = [1; 3; 2]) + +[bar] +|> Chart.Plot +|> Chart.WithTitle "A sample bar plot" +``` diff --git a/tests/notebooks/mirror/ipynb_to_percent/csharp.cs b/tests/notebooks/mirror/ipynb_to_percent/csharp.cs new file mode 100644 index 000000000..c513c83d2 --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_percent/csharp.cs @@ -0,0 +1,39 @@ +// --- +// jupyter: +// kernelspec: +// display_name: .NET (C#) +// language: C# +// name: .net-csharp +// --- + +// %% [markdown] +// We start with... + +// %% +Console.WriteLine("Hello World!"); + +// %% [markdown] +// Then we do a plot with Plotly, following the [Plotting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Plotting%20with%20Xplot.ipynb) example from `dotnet/interactive`: + +// %% +using XPlot.Plotly; + +var bar = new Graph.Bar +{ + name = "Bar", + x = new[] {'A', 'B', 'C'}, + y = new[] {1, 3, 2} +}; + +var chart = Chart.Plot(new[] {bar}); +chart.WithTitle("A bar plot"); +display(chart); + +// %% [markdown] +// We also test the math outputs as in the [Math and Latex](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Math%20and%20LaTeX.ipynb) example: + +// %% +(LaTeXString)@"\begin{align} e^{i \pi} = -1\end{align}" + +// %% +(MathString)@"e^{i \pi} = -1" diff --git a/tests/notebooks/mirror/ipynb_to_percent/fsharp.fsx b/tests/notebooks/mirror/ipynb_to_percent/fsharp.fsx new file mode 100644 index 000000000..94307eb6e --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_percent/fsharp.fsx @@ -0,0 +1,24 @@ +// --- +// jupyter: +// kernelspec: +// display_name: .NET (F#) +// language: F# +// name: .net-fsharp +// --- + +// %% [markdown] +// This notebook was inspired by [Plottting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/fsharp/Docs/Plotting%20with%20Xplot.ipynb). + +// %% +open XPlot.Plotly + +// %% +let bar = + Bar( + name = "Bar 1", + x = ["A"; "B"; "C"], + y = [1; 3; 2]) + +[bar] +|> Chart.Plot +|> Chart.WithTitle "A sample bar plot" diff --git a/tests/notebooks/mirror/ipynb_to_script/csharp.cs b/tests/notebooks/mirror/ipynb_to_script/csharp.cs new file mode 100644 index 000000000..8856abe32 --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_script/csharp.cs @@ -0,0 +1,34 @@ +// --- +// jupyter: +// kernelspec: +// display_name: .NET (C#) +// language: C# +// name: .net-csharp +// --- + +// We start with... + +Console.WriteLine("Hello World!"); + +// Then we do a plot with Plotly, following the [Plotting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Plotting%20with%20Xplot.ipynb) example from `dotnet/interactive`: + +// + +using XPlot.Plotly; + +var bar = new Graph.Bar +{ + name = "Bar", + x = new[] {'A', 'B', 'C'}, + y = new[] {1, 3, 2} +}; + +var chart = Chart.Plot(new[] {bar}); +chart.WithTitle("A bar plot"); +display(chart); +// - + +// We also test the math outputs as in the [Math and Latex](https://github.com/dotnet/interactive/blob/master/NotebookExamples/csharp/Docs/Math%20and%20LaTeX.ipynb) example: + +(LaTeXString)@"\begin{align} e^{i \pi} = -1\end{align}" + +(MathString)@"e^{i \pi} = -1" diff --git a/tests/notebooks/mirror/ipynb_to_script/fsharp.fsx b/tests/notebooks/mirror/ipynb_to_script/fsharp.fsx new file mode 100644 index 000000000..45980feba --- /dev/null +++ b/tests/notebooks/mirror/ipynb_to_script/fsharp.fsx @@ -0,0 +1,22 @@ +// --- +// jupyter: +// kernelspec: +// display_name: .NET (F#) +// language: F# +// name: .net-fsharp +// --- + +// This notebook was inspired by [Plottting with XPlot](https://github.com/dotnet/interactive/blob/master/NotebookExamples/fsharp/Docs/Plotting%20with%20Xplot.ipynb). + +open XPlot.Plotly + +// + +let bar = + Bar( + name = "Bar 1", + x = ["A"; "B"; "C"], + y = [1; 3; 2]) + +[bar] +|> Chart.Plot +|> Chart.WithTitle "A sample bar plot" diff --git a/tests/test_active_cells.py b/tests/test_active_cells.py index 5f24fd0d5..712c9c3d5 100644 --- a/tests/test_active_cells.py +++ b/tests/test_active_cells.py @@ -53,12 +53,17 @@ 'outputs': []}} +def check_active_cell(ext, active_dict): + text = ('' if ext == '.py' else HEADER[ext]) + active_dict[ext] + nb = jupytext.reads(text, ext) + assert len(nb.cells) == 1 + compare(jupytext.writes(nb, ext), text) + compare(nb.cells[0], active_dict['.ipynb']) + + @pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R']) def test_active_all(ext, no_jupytext_version_number): - nb = jupytext.reads(HEADER[ext] + ACTIVE_ALL[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_ALL[ext]) - compare(nb.cells[0], ACTIVE_ALL['.ipynb']) + check_active_cell(ext, ACTIVE_ALL) ACTIVE_IPYNB = {'.py': """# + active="ipynb" @@ -90,10 +95,7 @@ def test_active_all(ext, no_jupytext_version_number): @skip_if_dict_is_not_ordered @pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R']) def test_active_ipynb(ext, no_jupytext_version_number): - nb = jupytext.reads(HEADER[ext] + ACTIVE_IPYNB[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_IPYNB[ext]) - compare(nb.cells[0], ACTIVE_IPYNB['.ipynb']) + check_active_cell(ext, ACTIVE_IPYNB) ACTIVE_IPYNB_RMD_USING_TAG = {'.py': """# + tags=["active-ipynb-Rmd"] @@ -125,10 +127,7 @@ def test_active_ipynb(ext, no_jupytext_version_number): @skip_if_dict_is_not_ordered @pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R']) def test_active_ipynb_rmd_using_tags(ext, no_jupytext_version_number): - nb = jupytext.reads(HEADER[ext] + ACTIVE_IPYNB_RMD_USING_TAG[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_IPYNB_RMD_USING_TAG[ext]) - compare(nb.cells[0], ACTIVE_IPYNB_RMD_USING_TAG['.ipynb']) + check_active_cell(ext, ACTIVE_IPYNB_RMD_USING_TAG) ACTIVE_IPYNB_RSPIN = {'.R': """#+ active="ipynb", eval=FALSE @@ -176,10 +175,7 @@ def test_active_ipynb_rspin(no_jupytext_version_number): @skip_if_dict_is_not_ordered @pytest.mark.parametrize('ext', ['.Rmd', '.md', '.py', '.R']) def test_active_py_ipynb(ext, no_jupytext_version_number): - nb = jupytext.reads(HEADER[ext] + ACTIVE_PY_IPYNB[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_PY_IPYNB[ext]) - compare(nb.cells[0], ACTIVE_PY_IPYNB['.ipynb']) + check_active_cell(ext, ACTIVE_PY_IPYNB) ACTIVE_PY_R_IPYNB = {'.py': """# + active="ipynb,py,R" @@ -203,10 +199,7 @@ def test_active_py_ipynb(ext, no_jupytext_version_number): @skip_if_dict_is_not_ordered @pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R']) def test_active_py_r_ipynb(ext, no_jupytext_version_number): - nb = jupytext.reads(HEADER[ext] + ACTIVE_PY_R_IPYNB[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_PY_R_IPYNB[ext]) - compare(nb.cells[0], ACTIVE_PY_R_IPYNB['.ipynb']) + check_active_cell(ext, ACTIVE_PY_R_IPYNB) ACTIVE_RMD = {'.py': """# + active="Rmd" @@ -227,10 +220,7 @@ def test_active_py_r_ipynb(ext, no_jupytext_version_number): @skip_if_dict_is_not_ordered @pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R']) def test_active_rmd(ext, no_jupytext_version_number): - nb = jupytext.reads(HEADER[ext] + ACTIVE_RMD[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_RMD[ext]) - compare(nb.cells[0], ACTIVE_RMD['.ipynb']) + check_active_cell(ext, ACTIVE_RMD) ACTIVE_NOT_INCLUDE_RMD = {'.py': """# + tags=["remove_cell"] active="Rmd" @@ -252,7 +242,4 @@ def test_active_rmd(ext, no_jupytext_version_number): @skip_if_dict_is_not_ordered @pytest.mark.parametrize('ext', ['.Rmd', '.py', '.R']) def test_active_not_include_rmd(ext, no_jupytext_version_number): - nb = jupytext.reads(ACTIVE_NOT_INCLUDE_RMD[ext], ext) - assert len(nb.cells) == 1 - compare(jupytext.writes(nb, ext), ACTIVE_NOT_INCLUDE_RMD[ext]) - compare(nb.cells[0], ACTIVE_NOT_INCLUDE_RMD['.ipynb']) + check_active_cell(ext, ACTIVE_NOT_INCLUDE_RMD) diff --git a/tests/test_auto_ext.py b/tests/test_auto_ext.py index 0813e8bd4..feaacdbca 100644 --- a/tests/test_auto_ext.py +++ b/tests/test_auto_ext.py @@ -32,6 +32,8 @@ def test_auto_from_kernelspecs_works(nb_file): pytest.skip('No file_extension in language_info') if expected_ext == '.r': expected_ext = '.R' + elif expected_ext == '.fs': + expected_ext = '.fsx' auto_ext = auto_ext_from_metadata(nb.metadata) assert auto_ext == expected_ext diff --git a/tests/test_read_dotnet_try_markdown.py b/tests/test_read_dotnet_try_markdown.py new file mode 100644 index 000000000..25296a397 --- /dev/null +++ b/tests/test_read_dotnet_try_markdown.py @@ -0,0 +1,39 @@ +import jupytext +from jupytext.cell_metadata import parse_key_equal_value +from jupytext.compare import compare + + +def test_parse_metadata(): + assert parse_key_equal_value('--key value --key-2 .\\a\\b.cs') == \ + {'incorrectly_encoded_metadata': '--key value --key-2 .\\a\\b.cs'} + + +def test_parse_double_hyphen_metadata(): + assert parse_key_equal_value('--key1 value1 --key2 value2') == \ + {'incorrectly_encoded_metadata': '--key1 value1 --key2 value2'} + + +def test_read_dotnet_try_markdown(md='''This is a dotnet/try Markdown file, inspired +from this [post](https://devblogs.microsoft.com/dotnet/creating-interactive-net-documentation/) + +``` cs --region methods --source-file .\\myapp\\Program.cs --project .\\myapp\\myapp.csproj +var name ="Rain"; +Console.WriteLine($"Hello {name.ToUpper()}!"); +``` +'''): + # Read the notebook + nb = jupytext.reads(md, fmt='.md') + assert nb.metadata['jupytext']['main_language'] == 'csharp' + assert len(nb.cells) == 2 + assert nb.cells[0].cell_type == 'markdown' + assert nb.cells[1].cell_type == 'code' + assert nb.cells[1].source == """var name ="Rain"; +Console.WriteLine($"Hello {name.ToUpper()}!");""" + compare(nb.cells[1].metadata, { + 'language': 'cs', + 'incorrectly_encoded_metadata': + '--region methods --source-file .\\myapp\\Program.cs --project .\\myapp\\myapp.csproj'}) + + # Round trip to Markdown + md2 = jupytext.writes(nb, 'md') + compare(md2, md.replace('``` cs', '```cs')) diff --git a/tests/test_read_simple_csharp.py b/tests/test_read_simple_csharp.py new file mode 100644 index 000000000..10fdfdc6d --- /dev/null +++ b/tests/test_read_simple_csharp.py @@ -0,0 +1,78 @@ +import pytest +import jupytext +from jupytext.compare import compare + + +@pytest.mark.parametrize('lang', ['cs', 'c#', 'csharp']) +def test_simple_cs(lang): + source = """// A Hello World! program in C#. +Console.WriteLine("Hello World!"); +""" + md = """```{lang} +{source} +``` +""".format(lang=lang, source=source) + nb = jupytext.reads(md, 'md') + assert nb.metadata['jupytext']['main_language'] == 'csharp' + assert len(nb.cells) == 1 + assert nb.cells[0].cell_type == 'code' + + cs = jupytext.writes(nb, 'cs') + assert source in cs + if lang != 'csharp': + assert cs.startswith('// + language="{lang}"'.format(lang=lang)) + + md2 = jupytext.writes(nb, 'md') + compare(md2, md) + + +@pytest.mark.parametrize('lang', ['cs', 'c#', 'csharp']) +def test_csharp_magics(no_jupytext_version_number, lang): + md = """```{lang} +#!html +Hello! +``` +""".format(lang=lang) + nb = jupytext.reads(md, 'md') + nb.metadata['jupytext'].pop('notebook_metadata_filter') + nb.metadata['jupytext'].pop('cell_metadata_filter') + assert nb.metadata['jupytext']['main_language'] == 'csharp' + assert len(nb.cells) == 1 + assert nb.cells[0].cell_type == 'code' + + cs = jupytext.writes(nb, 'cs') + assert all(line.startswith('//') for line in cs.splitlines()), cs + + md2 = jupytext.writes(nb, 'md') + md_expected = """--- +jupyter: + jupytext: + main_language: csharp +--- + +```html +Hello! +``` +""" + compare(md2, md_expected) + + +def test_read_html_cell_from_md(no_jupytext_version_number): + md = """--- +jupyter: + jupytext: + main_language: csharp +--- + +```html +Hello! +``` +""" + + nb = jupytext.reads(md, 'md') + assert len(nb.cells) == 1 + assert nb.cells[0].cell_type == 'code' + compare(nb.cells[0].source, "#!html\nHello!") + + md2 = jupytext.writes(nb, 'md') + compare(md2, md) diff --git a/tests/test_read_simple_markdown.py b/tests/test_read_simple_markdown.py index d358d7942..c821b3804 100644 --- a/tests/test_read_simple_markdown.py +++ b/tests/test_read_simple_markdown.py @@ -490,7 +490,7 @@ def test_read_markdown_IDL(no_jupytext_version_number, text='''--- assert nb.cells[1].source == 'a = 1' text2 = jupytext.writes(nb, 'md') - compare(text2, text.replace('```IDL', '```idl')) + compare(text2, text) def test_inactive_cell(text='''```python active="md"