We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
nbformat v2 notebook with missing 'stream' key in stream outputs
@stefanv just sent me an nbformat 2 notebook which was missing the 'stream' keys in stream outputs, and failed to open in current master because of this.

@minrk , can you remember if that was ever a valid option in the notebook format? If so, we should support it. If not, we can write it off as having been saved by some broken development version of IPython. 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `IPython/nbformat/v4/convert.py`
Content:
```
1 """Code for converting notebooks to and from v3."""
2 
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 
6 import json
7 import re
8 
9 from .nbbase import (
10     nbformat, nbformat_minor,
11     NotebookNode,
12 )
13 
14 from IPython.nbformat import v3
15 from IPython.utils.log import get_logger
16 
17 def _warn_if_invalid(nb, version):
18     """Log validation errors, if there are any."""
19     from IPython.nbformat import validate, ValidationError
20     try:
21         validate(nb, version=version)
22     except ValidationError as e:
23         get_logger().error("Notebook JSON is not valid v%i: %s", version, e)
24 
25 def upgrade(nb, from_version=3, from_minor=0):
26     """Convert a notebook to v4.
27 
28     Parameters
29     ----------
30     nb : NotebookNode
31         The Python representation of the notebook to convert.
32     from_version : int
33         The original version of the notebook to convert.
34     from_minor : int
35         The original minor version of the notebook to convert (only relevant for v >= 3).
36     """
37     if from_version == 3:
38         # Validate the notebook before conversion
39         _warn_if_invalid(nb, from_version)
40 
41         # Mark the original nbformat so consumers know it has been converted
42         orig_nbformat = nb.pop('orig_nbformat', None)
43         nb.metadata.orig_nbformat = orig_nbformat or 3
44 
45         # Mark the new format
46         nb.nbformat = nbformat
47         nb.nbformat_minor = nbformat_minor
48 
49         # remove worksheet(s)
50         nb['cells'] = cells = []
51         # In the unlikely event of multiple worksheets,
52         # they will be flattened
53         for ws in nb.pop('worksheets', []):
54             # upgrade each cell
55             for cell in ws['cells']:
56                 cells.append(upgrade_cell(cell))
57         # upgrade metadata
58         nb.metadata.pop('name', '')
59         nb.metadata.pop('signature', '')
60         # Validate the converted notebook before returning it
61         _warn_if_invalid(nb, nbformat)
62         return nb
63     elif from_version == 4:
64         # nothing to do
65         if from_minor != nbformat_minor:
66             nb.metadata.orig_nbformat_minor = from_minor
67         nb.nbformat_minor = nbformat_minor
68 
69         return nb
70     else:
71         raise ValueError('Cannot convert a notebook directly from v%s to v4.  ' \
72                 'Try using the IPython.nbformat.convert module.' % from_version)
73 
74 def upgrade_cell(cell):
75     """upgrade a cell from v3 to v4
76 
77     heading cell:
78         - -> markdown heading
79     code cell:
80         - remove language metadata
81         - cell.input -> cell.source
82         - cell.prompt_number -> cell.execution_count
83         - update outputs
84     """
85     cell.setdefault('metadata', NotebookNode())
86     if cell.cell_type == 'code':
87         cell.pop('language', '')
88         if 'collapsed' in cell:
89             cell.metadata['collapsed'] = cell.pop('collapsed')
90         cell.source = cell.pop('input', '')
91         cell.execution_count = cell.pop('prompt_number', None)
92         cell.outputs = upgrade_outputs(cell.outputs)
93     elif cell.cell_type == 'heading':
94         cell.cell_type = 'markdown'
95         level = cell.pop('level', 1)
96         cell.source = u'{hashes} {single_line}'.format(
97             hashes='#' * level,
98             single_line = ' '.join(cell.get('source', '').splitlines()),
99         )
100     elif cell.cell_type == 'html':
101         # Technically, this exists. It will never happen in practice.
102         cell.cell_type = 'markdown'
103     return cell
104 
105 def downgrade_cell(cell):
106     """downgrade a cell from v4 to v3
107 
108     code cell:
109         - set cell.language
110         - cell.input <- cell.source
111         - cell.prompt_number <- cell.execution_count
112         - update outputs
113     markdown cell:
114         - single-line heading -> heading cell
115     """
116     if cell.cell_type == 'code':
117         cell.language = 'python'
118         cell.input = cell.pop('source', '')
119         cell.prompt_number = cell.pop('execution_count', None)
120         cell.collapsed = cell.metadata.pop('collapsed', False)
121         cell.outputs = downgrade_outputs(cell.outputs)
122     elif cell.cell_type == 'markdown':
123         source = cell.get('source', '')
124         if '\n' not in source and source.startswith('#'):
125             prefix, text = re.match(r'(#+)\s*(.*)', source).groups()
126             cell.cell_type = 'heading'
127             cell.source = text
128             cell.level = len(prefix)
129     return cell
130 
131 _mime_map = {
132     "text" : "text/plain",
133     "html" : "text/html",
134     "svg" : "image/svg+xml",
135     "png" : "image/png",
136     "jpeg" : "image/jpeg",
137     "latex" : "text/latex",
138     "json" : "application/json",
139     "javascript" : "application/javascript",
140 };
141 
142 def to_mime_key(d):
143     """convert dict with v3 aliases to plain mime-type keys"""
144     for alias, mime in _mime_map.items():
145         if alias in d:
146             d[mime] = d.pop(alias)
147     return d
148 
149 def from_mime_key(d):
150     """convert dict with mime-type keys to v3 aliases"""
151     for alias, mime in _mime_map.items():
152         if mime in d:
153             d[alias] = d.pop(mime)
154     return d
155 
156 def upgrade_output(output):
157     """upgrade a single code cell output from v3 to v4
158 
159     - pyout -> execute_result
160     - pyerr -> error
161     - output.type -> output.data.mime/type
162     - mime-type keys
163     - stream.stream -> stream.name
164     """
165     if output['output_type'] in {'pyout', 'display_data'}:
166         output.setdefault('metadata', NotebookNode())
167         if output['output_type'] == 'pyout':
168             output['output_type'] = 'execute_result'
169             output['execution_count'] = output.pop('prompt_number', None)
170 
171         # move output data into data sub-dict
172         data = {}
173         for key in list(output):
174             if key in {'output_type', 'execution_count', 'metadata'}:
175                 continue
176             data[key] = output.pop(key)
177         to_mime_key(data)
178         output['data'] = data
179         to_mime_key(output.metadata)
180         if 'application/json' in data:
181             data['application/json'] = json.loads(data['application/json'])
182         # promote ascii bytes (from v2) to unicode
183         for key in ('image/png', 'image/jpeg'):
184             if key in data and isinstance(data[key], bytes):
185                 data[key] = data[key].decode('ascii')
186     elif output['output_type'] == 'pyerr':
187         output['output_type'] = 'error'
188     elif output['output_type'] == 'stream':
189         output['name'] = output.pop('stream')
190     return output
191 
192 def downgrade_output(output):
193     """downgrade a single code cell output to v3 from v4
194 
195     - pyout <- execute_result
196     - pyerr <- error
197     - output.data.mime/type -> output.type
198     - un-mime-type keys
199     - stream.stream <- stream.name
200     """
201     if output['output_type'] in {'execute_result', 'display_data'}:
202         if output['output_type'] == 'execute_result':
203             output['output_type'] = 'pyout'
204             output['prompt_number'] = output.pop('execution_count', None)
205 
206         # promote data dict to top-level output namespace
207         data = output.pop('data', {})
208         if 'application/json' in data:
209             data['application/json'] = json.dumps(data['application/json'])
210         from_mime_key(data)
211         output.update(data)
212         from_mime_key(output.get('metadata', {}))
213     elif output['output_type'] == 'error':
214         output['output_type'] = 'pyerr'
215     elif output['output_type'] == 'stream':
216         output['stream'] = output.pop('name')
217     return output
218 
219 def upgrade_outputs(outputs):
220     """upgrade outputs of a code cell from v3 to v4"""
221     return [upgrade_output(op) for op in outputs]
222 
223 def downgrade_outputs(outputs):
224     """downgrade outputs of a code cell to v3 from v4"""
225     return [downgrade_output(op) for op in outputs]
226 
227 def downgrade(nb):
228     """Convert a v4 notebook to v3.
229 
230     Parameters
231     ----------
232     nb : NotebookNode
233         The Python representation of the notebook to convert.
234     """
235     if nb.nbformat != nbformat:
236         return nb
237 
238     # Validate the notebook before conversion
239     _warn_if_invalid(nb, nbformat)
240 
241     nb.nbformat = v3.nbformat
242     nb.nbformat_minor = v3.nbformat_minor
243     cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]
244     nb.worksheets = [v3.new_worksheet(cells=cells)]
245     nb.metadata.setdefault('name', '')
246     
247     # Validate the converted notebook before returning it
248     _warn_if_invalid(nb, v3.nbformat)
249     
250     nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat)
251     nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor)
252     
253     return nb
254
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/IPython/nbformat/v4/convert.py b/IPython/nbformat/v4/convert.py
--- a/IPython/nbformat/v4/convert.py
+++ b/IPython/nbformat/v4/convert.py
@@ -186,7 +186,7 @@
     elif output['output_type'] == 'pyerr':
         output['output_type'] = 'error'
     elif output['output_type'] == 'stream':
-        output['name'] = output.pop('stream')
+        output['name'] = output.pop('stream', 'stdout')
     return output
 
 def downgrade_output(output):

{"golden_diff": "diff --git a/IPython/nbformat/v4/convert.py b/IPython/nbformat/v4/convert.py\n--- a/IPython/nbformat/v4/convert.py\n+++ b/IPython/nbformat/v4/convert.py\n@@ -186,7 +186,7 @@\n     elif output['output_type'] == 'pyerr':\n         output['output_type'] = 'error'\n     elif output['output_type'] == 'stream':\n-        output['name'] = output.pop('stream')\n+        output['name'] = output.pop('stream', 'stdout')\n     return output\n \n def downgrade_output(output):\n", "issue": "nbformat v2 notebook with missing 'stream' key in stream outputs\n@stefanv just sent me an nbformat 2 notebook which was missing the 'stream' keys in stream outputs, and failed to open in current master because of this.\n\n@minrk , can you remember if that was ever a valid option in the notebook format? If so, we should support it. If not, we can write it off as having been saved by some broken development version of IPython. \n\n", "before_files": [{"content": "\"\"\"Code for converting notebooks to and from v3.\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nimport json\nimport re\n\nfrom .nbbase import (\n    nbformat, nbformat_minor,\n    NotebookNode,\n)\n\nfrom IPython.nbformat import v3\nfrom IPython.utils.log import get_logger\n\ndef _warn_if_invalid(nb, version):\n    \"\"\"Log validation errors, if there are any.\"\"\"\n    from IPython.nbformat import validate, ValidationError\n    try:\n        validate(nb, version=version)\n    except ValidationError as e:\n        get_logger().error(\"Notebook JSON is not valid v%i: %s\", version, e)\n\ndef upgrade(nb, from_version=3, from_minor=0):\n    \"\"\"Convert a notebook to v4.\n\n    Parameters\n    ----------\n    nb : NotebookNode\n        The Python representation of the notebook to convert.\n    from_version : int\n        The original version of the notebook to convert.\n    from_minor : int\n        The original minor version of the notebook to convert (only relevant for v >= 3).\n    \"\"\"\n    if from_version == 3:\n        # Validate the notebook before conversion\n        _warn_if_invalid(nb, from_version)\n\n        # Mark the original nbformat so consumers know it has been converted\n        orig_nbformat = nb.pop('orig_nbformat', None)\n        nb.metadata.orig_nbformat = orig_nbformat or 3\n\n        # Mark the new format\n        nb.nbformat = nbformat\n        nb.nbformat_minor = nbformat_minor\n\n        # remove worksheet(s)\n        nb['cells'] = cells = []\n        # In the unlikely event of multiple worksheets,\n        # they will be flattened\n        for ws in nb.pop('worksheets', []):\n            # upgrade each cell\n            for cell in ws['cells']:\n                cells.append(upgrade_cell(cell))\n        # upgrade metadata\n        nb.metadata.pop('name', '')\n        nb.metadata.pop('signature', '')\n        # Validate the converted notebook before returning it\n        _warn_if_invalid(nb, nbformat)\n        return nb\n    elif from_version == 4:\n        # nothing to do\n        if from_minor != nbformat_minor:\n            nb.metadata.orig_nbformat_minor = from_minor\n        nb.nbformat_minor = nbformat_minor\n\n        return nb\n    else:\n        raise ValueError('Cannot convert a notebook directly from v%s to v4.  ' \\\n                'Try using the IPython.nbformat.convert module.' % from_version)\n\ndef upgrade_cell(cell):\n    \"\"\"upgrade a cell from v3 to v4\n\n    heading cell:\n        - -> markdown heading\n    code cell:\n        - remove language metadata\n        - cell.input -> cell.source\n        - cell.prompt_number -> cell.execution_count\n        - update outputs\n    \"\"\"\n    cell.setdefault('metadata', NotebookNode())\n    if cell.cell_type == 'code':\n        cell.pop('language', '')\n        if 'collapsed' in cell:\n            cell.metadata['collapsed'] = cell.pop('collapsed')\n        cell.source = cell.pop('input', '')\n        cell.execution_count = cell.pop('prompt_number', None)\n        cell.outputs = upgrade_outputs(cell.outputs)\n    elif cell.cell_type == 'heading':\n        cell.cell_type = 'markdown'\n        level = cell.pop('level', 1)\n        cell.source = u'{hashes} {single_line}'.format(\n            hashes='#' * level,\n            single_line = ' '.join(cell.get('source', '').splitlines()),\n        )\n    elif cell.cell_type == 'html':\n        # Technically, this exists. It will never happen in practice.\n        cell.cell_type = 'markdown'\n    return cell\n\ndef downgrade_cell(cell):\n    \"\"\"downgrade a cell from v4 to v3\n\n    code cell:\n        - set cell.language\n        - cell.input <- cell.source\n        - cell.prompt_number <- cell.execution_count\n        - update outputs\n    markdown cell:\n        - single-line heading -> heading cell\n    \"\"\"\n    if cell.cell_type == 'code':\n        cell.language = 'python'\n        cell.input = cell.pop('source', '')\n        cell.prompt_number = cell.pop('execution_count', None)\n        cell.collapsed = cell.metadata.pop('collapsed', False)\n        cell.outputs = downgrade_outputs(cell.outputs)\n    elif cell.cell_type == 'markdown':\n        source = cell.get('source', '')\n        if '\\n' not in source and source.startswith('#'):\n            prefix, text = re.match(r'(#+)\\s*(.*)', source).groups()\n            cell.cell_type = 'heading'\n            cell.source = text\n            cell.level = len(prefix)\n    return cell\n\n_mime_map = {\n    \"text\" : \"text/plain\",\n    \"html\" : \"text/html\",\n    \"svg\" : \"image/svg+xml\",\n    \"png\" : \"image/png\",\n    \"jpeg\" : \"image/jpeg\",\n    \"latex\" : \"text/latex\",\n    \"json\" : \"application/json\",\n    \"javascript\" : \"application/javascript\",\n};\n\ndef to_mime_key(d):\n    \"\"\"convert dict with v3 aliases to plain mime-type keys\"\"\"\n    for alias, mime in _mime_map.items():\n        if alias in d:\n            d[mime] = d.pop(alias)\n    return d\n\ndef from_mime_key(d):\n    \"\"\"convert dict with mime-type keys to v3 aliases\"\"\"\n    for alias, mime in _mime_map.items():\n        if mime in d:\n            d[alias] = d.pop(mime)\n    return d\n\ndef upgrade_output(output):\n    \"\"\"upgrade a single code cell output from v3 to v4\n\n    - pyout -> execute_result\n    - pyerr -> error\n    - output.type -> output.data.mime/type\n    - mime-type keys\n    - stream.stream -> stream.name\n    \"\"\"\n    if output['output_type'] in {'pyout', 'display_data'}:\n        output.setdefault('metadata', NotebookNode())\n        if output['output_type'] == 'pyout':\n            output['output_type'] = 'execute_result'\n            output['execution_count'] = output.pop('prompt_number', None)\n\n        # move output data into data sub-dict\n        data = {}\n        for key in list(output):\n            if key in {'output_type', 'execution_count', 'metadata'}:\n                continue\n            data[key] = output.pop(key)\n        to_mime_key(data)\n        output['data'] = data\n        to_mime_key(output.metadata)\n        if 'application/json' in data:\n            data['application/json'] = json.loads(data['application/json'])\n        # promote ascii bytes (from v2) to unicode\n        for key in ('image/png', 'image/jpeg'):\n            if key in data and isinstance(data[key], bytes):\n                data[key] = data[key].decode('ascii')\n    elif output['output_type'] == 'pyerr':\n        output['output_type'] = 'error'\n    elif output['output_type'] == 'stream':\n        output['name'] = output.pop('stream')\n    return output\n\ndef downgrade_output(output):\n    \"\"\"downgrade a single code cell output to v3 from v4\n\n    - pyout <- execute_result\n    - pyerr <- error\n    - output.data.mime/type -> output.type\n    - un-mime-type keys\n    - stream.stream <- stream.name\n    \"\"\"\n    if output['output_type'] in {'execute_result', 'display_data'}:\n        if output['output_type'] == 'execute_result':\n            output['output_type'] = 'pyout'\n            output['prompt_number'] = output.pop('execution_count', None)\n\n        # promote data dict to top-level output namespace\n        data = output.pop('data', {})\n        if 'application/json' in data:\n            data['application/json'] = json.dumps(data['application/json'])\n        from_mime_key(data)\n        output.update(data)\n        from_mime_key(output.get('metadata', {}))\n    elif output['output_type'] == 'error':\n        output['output_type'] = 'pyerr'\n    elif output['output_type'] == 'stream':\n        output['stream'] = output.pop('name')\n    return output\n\ndef upgrade_outputs(outputs):\n    \"\"\"upgrade outputs of a code cell from v3 to v4\"\"\"\n    return [upgrade_output(op) for op in outputs]\n\ndef downgrade_outputs(outputs):\n    \"\"\"downgrade outputs of a code cell to v3 from v4\"\"\"\n    return [downgrade_output(op) for op in outputs]\n\ndef downgrade(nb):\n    \"\"\"Convert a v4 notebook to v3.\n\n    Parameters\n    ----------\n    nb : NotebookNode\n        The Python representation of the notebook to convert.\n    \"\"\"\n    if nb.nbformat != nbformat:\n        return nb\n\n    # Validate the notebook before conversion\n    _warn_if_invalid(nb, nbformat)\n\n    nb.nbformat = v3.nbformat\n    nb.nbformat_minor = v3.nbformat_minor\n    cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]\n    nb.worksheets = [v3.new_worksheet(cells=cells)]\n    nb.metadata.setdefault('name', '')\n    \n    # Validate the converted notebook before returning it\n    _warn_if_invalid(nb, v3.nbformat)\n    \n    nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat)\n    nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor)\n    \n    return nb\n", "path": "IPython/nbformat/v4/convert.py"}], "after_files": [{"content": "\"\"\"Code for converting notebooks to and from v3.\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nimport json\nimport re\n\nfrom .nbbase import (\n    nbformat, nbformat_minor,\n    NotebookNode,\n)\n\nfrom IPython.nbformat import v3\nfrom IPython.utils.log import get_logger\n\ndef _warn_if_invalid(nb, version):\n    \"\"\"Log validation errors, if there are any.\"\"\"\n    from IPython.nbformat import validate, ValidationError\n    try:\n        validate(nb, version=version)\n    except ValidationError as e:\n        get_logger().error(\"Notebook JSON is not valid v%i: %s\", version, e)\n\ndef upgrade(nb, from_version=3, from_minor=0):\n    \"\"\"Convert a notebook to v4.\n\n    Parameters\n    ----------\n    nb : NotebookNode\n        The Python representation of the notebook to convert.\n    from_version : int\n        The original version of the notebook to convert.\n    from_minor : int\n        The original minor version of the notebook to convert (only relevant for v >= 3).\n    \"\"\"\n    if from_version == 3:\n        # Validate the notebook before conversion\n        _warn_if_invalid(nb, from_version)\n\n        # Mark the original nbformat so consumers know it has been converted\n        orig_nbformat = nb.pop('orig_nbformat', None)\n        nb.metadata.orig_nbformat = orig_nbformat or 3\n\n        # Mark the new format\n        nb.nbformat = nbformat\n        nb.nbformat_minor = nbformat_minor\n\n        # remove worksheet(s)\n        nb['cells'] = cells = []\n        # In the unlikely event of multiple worksheets,\n        # they will be flattened\n        for ws in nb.pop('worksheets', []):\n            # upgrade each cell\n            for cell in ws['cells']:\n                cells.append(upgrade_cell(cell))\n        # upgrade metadata\n        nb.metadata.pop('name', '')\n        nb.metadata.pop('signature', '')\n        # Validate the converted notebook before returning it\n        _warn_if_invalid(nb, nbformat)\n        return nb\n    elif from_version == 4:\n        # nothing to do\n        if from_minor != nbformat_minor:\n            nb.metadata.orig_nbformat_minor = from_minor\n        nb.nbformat_minor = nbformat_minor\n\n        return nb\n    else:\n        raise ValueError('Cannot convert a notebook directly from v%s to v4.  ' \\\n                'Try using the IPython.nbformat.convert module.' % from_version)\n\ndef upgrade_cell(cell):\n    \"\"\"upgrade a cell from v3 to v4\n\n    heading cell:\n        - -> markdown heading\n    code cell:\n        - remove language metadata\n        - cell.input -> cell.source\n        - cell.prompt_number -> cell.execution_count\n        - update outputs\n    \"\"\"\n    cell.setdefault('metadata', NotebookNode())\n    if cell.cell_type == 'code':\n        cell.pop('language', '')\n        if 'collapsed' in cell:\n            cell.metadata['collapsed'] = cell.pop('collapsed')\n        cell.source = cell.pop('input', '')\n        cell.execution_count = cell.pop('prompt_number', None)\n        cell.outputs = upgrade_outputs(cell.outputs)\n    elif cell.cell_type == 'heading':\n        cell.cell_type = 'markdown'\n        level = cell.pop('level', 1)\n        cell.source = u'{hashes} {single_line}'.format(\n            hashes='#' * level,\n            single_line = ' '.join(cell.get('source', '').splitlines()),\n        )\n    elif cell.cell_type == 'html':\n        # Technically, this exists. It will never happen in practice.\n        cell.cell_type = 'markdown'\n    return cell\n\ndef downgrade_cell(cell):\n    \"\"\"downgrade a cell from v4 to v3\n\n    code cell:\n        - set cell.language\n        - cell.input <- cell.source\n        - cell.prompt_number <- cell.execution_count\n        - update outputs\n    markdown cell:\n        - single-line heading -> heading cell\n    \"\"\"\n    if cell.cell_type == 'code':\n        cell.language = 'python'\n        cell.input = cell.pop('source', '')\n        cell.prompt_number = cell.pop('execution_count', None)\n        cell.collapsed = cell.metadata.pop('collapsed', False)\n        cell.outputs = downgrade_outputs(cell.outputs)\n    elif cell.cell_type == 'markdown':\n        source = cell.get('source', '')\n        if '\\n' not in source and source.startswith('#'):\n            prefix, text = re.match(r'(#+)\\s*(.*)', source).groups()\n            cell.cell_type = 'heading'\n            cell.source = text\n            cell.level = len(prefix)\n    return cell\n\n_mime_map = {\n    \"text\" : \"text/plain\",\n    \"html\" : \"text/html\",\n    \"svg\" : \"image/svg+xml\",\n    \"png\" : \"image/png\",\n    \"jpeg\" : \"image/jpeg\",\n    \"latex\" : \"text/latex\",\n    \"json\" : \"application/json\",\n    \"javascript\" : \"application/javascript\",\n};\n\ndef to_mime_key(d):\n    \"\"\"convert dict with v3 aliases to plain mime-type keys\"\"\"\n    for alias, mime in _mime_map.items():\n        if alias in d:\n            d[mime] = d.pop(alias)\n    return d\n\ndef from_mime_key(d):\n    \"\"\"convert dict with mime-type keys to v3 aliases\"\"\"\n    for alias, mime in _mime_map.items():\n        if mime in d:\n            d[alias] = d.pop(mime)\n    return d\n\ndef upgrade_output(output):\n    \"\"\"upgrade a single code cell output from v3 to v4\n\n    - pyout -> execute_result\n    - pyerr -> error\n    - output.type -> output.data.mime/type\n    - mime-type keys\n    - stream.stream -> stream.name\n    \"\"\"\n    if output['output_type'] in {'pyout', 'display_data'}:\n        output.setdefault('metadata', NotebookNode())\n        if output['output_type'] == 'pyout':\n            output['output_type'] = 'execute_result'\n            output['execution_count'] = output.pop('prompt_number', None)\n\n        # move output data into data sub-dict\n        data = {}\n        for key in list(output):\n            if key in {'output_type', 'execution_count', 'metadata'}:\n                continue\n            data[key] = output.pop(key)\n        to_mime_key(data)\n        output['data'] = data\n        to_mime_key(output.metadata)\n        if 'application/json' in data:\n            data['application/json'] = json.loads(data['application/json'])\n        # promote ascii bytes (from v2) to unicode\n        for key in ('image/png', 'image/jpeg'):\n            if key in data and isinstance(data[key], bytes):\n                data[key] = data[key].decode('ascii')\n    elif output['output_type'] == 'pyerr':\n        output['output_type'] = 'error'\n    elif output['output_type'] == 'stream':\n        output['name'] = output.pop('stream', 'stdout')\n    return output\n\ndef downgrade_output(output):\n    \"\"\"downgrade a single code cell output to v3 from v4\n\n    - pyout <- execute_result\n    - pyerr <- error\n    - output.data.mime/type -> output.type\n    - un-mime-type keys\n    - stream.stream <- stream.name\n    \"\"\"\n    if output['output_type'] in {'execute_result', 'display_data'}:\n        if output['output_type'] == 'execute_result':\n            output['output_type'] = 'pyout'\n            output['prompt_number'] = output.pop('execution_count', None)\n\n        # promote data dict to top-level output namespace\n        data = output.pop('data', {})\n        if 'application/json' in data:\n            data['application/json'] = json.dumps(data['application/json'])\n        from_mime_key(data)\n        output.update(data)\n        from_mime_key(output.get('metadata', {}))\n    elif output['output_type'] == 'error':\n        output['output_type'] = 'pyerr'\n    elif output['output_type'] == 'stream':\n        output['stream'] = output.pop('name')\n    return output\n\ndef upgrade_outputs(outputs):\n    \"\"\"upgrade outputs of a code cell from v3 to v4\"\"\"\n    return [upgrade_output(op) for op in outputs]\n\ndef downgrade_outputs(outputs):\n    \"\"\"downgrade outputs of a code cell to v3 from v4\"\"\"\n    return [downgrade_output(op) for op in outputs]\n\ndef downgrade(nb):\n    \"\"\"Convert a v4 notebook to v3.\n\n    Parameters\n    ----------\n    nb : NotebookNode\n        The Python representation of the notebook to convert.\n    \"\"\"\n    if nb.nbformat != nbformat:\n        return nb\n\n    # Validate the notebook before conversion\n    _warn_if_invalid(nb, nbformat)\n\n    nb.nbformat = v3.nbformat\n    nb.nbformat_minor = v3.nbformat_minor\n    cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ]\n    nb.worksheets = [v3.new_worksheet(cells=cells)]\n    nb.metadata.setdefault('name', '')\n    \n    # Validate the converted notebook before returning it\n    _warn_if_invalid(nb, v3.nbformat)\n    \n    nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat)\n    nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor)\n    \n    return nb\n", "path": "IPython/nbformat/v4/convert.py"}]}