Repository: mwouts/jupytext

Issue:
how to get rid of jupytext_version in the notebook metadata
Hey there

as the tile has it, my use case would require to get rid of `jupytext_version` in my notebooks metadata

for some time I have been successfully running in a mode where `language_info` is stripped from said notebook metadata (actually it's kind of the same need, differences in the version numbers between the authoring laptop and the runtime testbed creates tend to create headaches ;-)

so I have tried to tweak that same setting, but I can't seem to find the correct wording; I have tried for example this :

   `notebook_metadata_filter: all,-language_info,-jupyter.jupytext.text_representation.jupytext_version`

together with a few variants, but can't seem to get it right; what am I doing wrong ?

-- a jupytext lover :-)

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/425?src=pr&el=h1) Report
> Merging [#425](https://codecov.io/gh/mwouts/jupytext/pull/425?src=pr&el=desc) into [master](https://codecov.io/gh/mwouts/jupytext/commit/3a6512d8ee2141ff21b1f476b5d683267f4f8720?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master     #425      +/-   ##
==========================================
+ Coverage   98.93%   98.94%   +<.01%     
==========================================
  Files          75       75              
  Lines        7608     7645      +37     
==========================================
+ Hits         7527     7564      +37     
  Misses         81       81
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/425?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [jupytext/cell\_to\_text.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvY2VsbF90b190ZXh0LnB5) | `97.3% <100%> (ø)` | :arrow_up: |
| [jupytext/formats.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvZm9ybWF0cy5weQ==) | `98.66% <100%> (ø)` | :arrow_up: |
| [tests/test\_metadata\_filter.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9tZXRhZGF0YV9maWx0ZXIucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/version.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvdmVyc2lvbi5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/compare.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvY29tcGFyZS5weQ==) | `99.33% <100%> (-0.02%)` | :arrow_down: |
| [jupytext/metadata\_filter.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvbWV0YWRhdGFfZmlsdGVyLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/jupytext.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvanVweXRleHQucHk=) | `98.59% <100%> (ø)` | :arrow_up: |
| [tests/test\_contentsmanager.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jb250ZW50c21hbmFnZXIucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/combine.py](https://codecov.io/gh/mwouts/jupytext/pull/425/diff?src=pr&el=tree#diff-anVweXRleHQvY29tYmluZS5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

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


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60088d9..af4397d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,12 @@
-1.3.3 (2020-01-??)
+1.3.3 (2020-01-27)
 ------------------
 
 **Added**
 - Jupytext has a logo! Many thanks to Kyle Kelley for contributing the actual logo (#423), and to Chris Holdgraf for suggesting this (#260).
+- Nested metadata filtering is now supported! You can use this to rid of `jupytext_version` if you wish (#416).
 
 **Fixed**
-- Code cells in the Markdown format can contain triple backticks inside multiline strings (#419)
+- Code cells in the Markdown format can contain triple backticks inside multiline strings (#419).
 - Changes in the YAML header when running `jupytext --test` on text files are ignored (#414).
 
 1.3.2 (2020-01-08)
diff --git a/docs/using-server.md b/docs/using-server.md
index 0580b06..4e04f3a 100644
--- a/docs/using-server.md
+++ b/docs/using-server.md
@@ -27,7 +27,7 @@ If you want to pair a notebook to a python script in a subfolder named `scripts`
 
 Jupytext accepts a few additional options. These options should be added to the `"jupytext"` section in the metadata &mdash; use either the metadata editor or the `--opt/--format-options` argument on the command line.
 - `comment_magics`: By default, Jupyter magics are commented when notebooks are exported to any other format than markdown. If you prefer otherwise, use this boolean option, or is global counterpart (see below).
-- `notebook_metadata_filter`: By default, Jupytext only exports the `kernelspec` and `jupytext` metadata to the text files. Set `"jupytext": {"notebook_metadata_filter": "-all"}` if you want that the script has no notebook metadata at all. The value for `notebook_metadata_filter` is a comma separated list of additional/excluded (negated) entries, with `all` a keyword that allows to exclude all entries.
+- `notebook_metadata_filter`: By default, Jupytext only exports the `kernelspec` and `jupytext` metadata to the text files. Set `"jupytext": {"notebook_metadata_filter": "-all"}` if you want that the script has no notebook metadata at all. The value for `notebook_metadata_filter` is a comma separated list of additional/excluded (negated) entries, with `all` a keyword that allows to exclude all entries. Use dots to filter recursively the metadata. For instance, use `notebook_metadata_filter="-jupytext.text_representation.jupytext_version"` to remove the `jupytext_version` field in the `jupytext.text_representation` metadata.
 - `cell_metadata_filter`: By default, cell metadata `autoscroll`, `collapsed`, `scrolled`, `trusted` and `ExecuteTime` are not included in the text representation. Add or exclude more cell metadata with this option.
 
 ## Global configuration
@@ -70,6 +70,11 @@ c.ContentsManager.default_notebook_metadata_filter = "-all"
 c.ContentsManager.default_cell_metadata_filter = "-all"
 ```
 
+It is possible to filter nested metadata. For example, if you want to preserve the Jupytext metadata, but not the Jupytext version number, you can use:
+```python
+c.ContentsManager.default_notebook_metadata_filter = "-jupytext.text_representation.jupytext_version"
+```
+
 NB: All these global options (and more) are documented [here](https://github.com/mwouts/jupytext/blob/master/jupytext/contentsmanager.py).
 
 ## Can I edit a notebook simultaneously in Jupyter and in a text editor?
diff --git a/jupytext/cell_to_text.py b/jupytext/cell_to_text.py
index 0cf1e52..9dfb6c2 100644
--- a/jupytext/cell_to_text.py
+++ b/jupytext/cell_to_text.py
@@ -33,8 +33,8 @@ class BaseCellExporter(object):
         self.ext = self.fmt.get('extension')
         self.cell_type = cell.cell_type
         self.source = cell_source(cell)
-        self.unfiltered_metadata = copy(cell.metadata)
-        self.metadata = filter_metadata(copy(cell.metadata),
+        self.unfiltered_metadata = cell.metadata
+        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)
@@ -245,6 +245,7 @@ class LightScriptCellExporter(BaseCellExporter):
                 self.metadata['cell_type'] = self.cell_type
                 self.source = self.markdown_to_text(self.source)
                 self.cell_type = 'code'
+                self.unfiltered_metadata = copy(self.unfiltered_metadata)
                 self.unfiltered_metadata.pop('cell_marker', '')
             return True
         return super(LightScriptCellExporter, self).is_code()
diff --git a/jupytext/combine.py b/jupytext/combine.py
index da0ecc8..c30751a 100644
--- a/jupytext/combine.py
+++ b/jupytext/combine.py
@@ -1,10 +1,9 @@
 """Combine source and outputs from two notebooks
 """
 import re
-from copy import copy
-from .cell_metadata import _IGNORE_CELL_METADATA, _JUPYTEXT_CELL_METADATA
+from .cell_metadata import _IGNORE_CELL_METADATA
 from .header import _DEFAULT_NOTEBOOK_METADATA
-from .metadata_filter import filter_metadata
+from .metadata_filter import restore_filtered_metadata
 from .formats import long_form_one_format
 
 _BLANK_LINE = re.compile(r'^\s*$')
@@ -26,7 +25,7 @@ def same_content(ref, test):
 
 
 def combine_inputs_with_outputs(nb_source, nb_outputs, fmt=None):
-    """Copy outputs of the second notebook into
+    """Copy outputs and metadata of the second notebook into
     the first one, for cells that have matching inputs"""
 
     output_code_cells = [cell for cell in nb_outputs.cells if cell.cell_type == 'code']
@@ -43,14 +42,11 @@ def combine_inputs_with_outputs(nb_source, nb_outputs, fmt=None):
     ext = fmt.get('extension') or text_repr.get('extension')
     format_name = fmt.get('format_name') or text_repr.get('format_name')
 
-    nb_outputs_filtered_metadata = copy(nb_outputs.metadata)
-    filter_metadata(nb_outputs_filtered_metadata,
-                    nb_source.metadata.get('jupytext', {}).get('notebook_metadata_filter'),
-                    _DEFAULT_NOTEBOOK_METADATA)
-
-    for key in nb_outputs.metadata:
-        if key not in nb_outputs_filtered_metadata:
-            nb_source.metadata[key] = nb_outputs.metadata[key]
+    nb_source.metadata = restore_filtered_metadata(
+        nb_source.metadata,
+        nb_outputs.metadata,
+        nb_source.metadata.get('jupytext', {}).get('notebook_metadata_filter'),
+        _DEFAULT_NOTEBOOK_METADATA)
 
     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']:
@@ -59,6 +55,11 @@ def combine_inputs_with_outputs(nb_source, nb_outputs, fmt=None):
     if not nb_source.metadata.get('jupytext', {}):
         nb_source.metadata.pop('jupytext', {})
 
+    if format_name in ['nomarker', 'sphinx'] or source_is_md_version_one:
+        cell_metadata_filter = '-all'
+    else:
+        cell_metadata_filter = nb_source.metadata.get('jupytext', {}).get('cell_metadata_filter')
+
     for cell in nb_source.cells:
         # Remove outputs to warranty that trust of returned notebook is that of second notebook
         if cell.cell_type == 'code':
@@ -71,35 +72,19 @@ def combine_inputs_with_outputs(nb_source, nb_outputs, fmt=None):
                     cell.execution_count = ocell.execution_count
                     cell.outputs = ocell.outputs
 
-                    # Append cell metadata that was filtered
-                    if format_name in ['nomarker', 'sphinx'] or source_is_md_version_one:
-                        ocell_filtered_metadata = {}
-                    else:
-                        ocell_filtered_metadata = copy(ocell.metadata)
-                        filter_metadata(ocell_filtered_metadata,
-                                        nb_source.metadata.get('jupytext', {}).get('cell_metadata_filter'),
-                                        _IGNORE_CELL_METADATA)
-
-                    for key in ocell.metadata:
-                        if key not in ocell_filtered_metadata and key not in _JUPYTEXT_CELL_METADATA:
-                            cell.metadata[key] = ocell.metadata[key]
+                    # Restore the filtered output cell metadata
+                    cell.metadata = restore_filtered_metadata(cell.metadata, ocell.metadata,
+                                                              cell_metadata_filter, _IGNORE_CELL_METADATA)
 
                     output_code_cells = output_code_cells[(i + 1):]
                     break
         else:
             for i, ocell in enumerate(output_other_cells):
                 if cell.cell_type == ocell.cell_type and same_content(cell.source, ocell.source):
-                    if format_name in ['spin', 'nomarker', 'sphinx'] or source_is_md_version_one:
-                        ocell_filtered_metadata = {}
-                    else:
-                        ocell_filtered_metadata = copy(ocell.metadata)
-                        filter_metadata(ocell_filtered_metadata,
-                                        nb_source.metadata.get('jupytext', {}).get('cell_metadata_filter'),
-                                        _IGNORE_CELL_METADATA)
-
-                    for key in ocell.metadata:
-                        if key not in ocell_filtered_metadata:
-                            cell.metadata[key] = ocell.metadata[key]
+                    # The 'spin' format does not allow metadata on non-code cells
+                    cell.metadata = restore_filtered_metadata(cell.metadata, ocell.metadata,
+                                                              '-all' if format_name == 'spin' else cell_metadata_filter,
+                                                              _IGNORE_CELL_METADATA)
 
                     output_other_cells = output_other_cells[(i + 1):]
                     break
diff --git a/jupytext/compare.py b/jupytext/compare.py
index 63d0dde..91ff866 100644
--- a/jupytext/compare.py
+++ b/jupytext/compare.py
@@ -3,7 +3,6 @@
 import re
 import json
 import difflib
-from copy import copy
 from .cell_metadata import _IGNORE_CELL_METADATA
 from .header import _DEFAULT_NOTEBOOK_METADATA
 from .metadata_filter import filter_metadata
@@ -39,12 +38,9 @@ def compare(actual, expected, return_diff=False):
 
 def filtered_cell(cell, preserve_outputs, cell_metadata_filter):
     """Cell type, metadata and source from given cell"""
-    metadata = copy(cell.metadata)
-    filter_metadata(metadata, cell_metadata_filter, _IGNORE_CELL_METADATA)
-
     filtered = {'cell_type': cell.cell_type,
                 'source': cell.source,
-                'metadata': metadata}
+                'metadata': filter_metadata(cell.metadata, cell_metadata_filter, _IGNORE_CELL_METADATA)}
 
     if preserve_outputs:
         for key in ['execution_count', 'outputs']:
@@ -56,8 +52,7 @@ def filtered_cell(cell, preserve_outputs, cell_metadata_filter):
 
 def filtered_notebook_metadata(notebook):
     """Notebook metadata, filtered for metadata added by Jupytext itself"""
-    metadata = copy(notebook.metadata)
-    metadata = filter_metadata(metadata,
+    metadata = filter_metadata(notebook.metadata,
                                notebook.metadata.get('jupytext', {}).get('notebook_metadata_filter'),
                                _DEFAULT_NOTEBOOK_METADATA)
     if 'jupytext' in metadata:
diff --git a/jupytext/formats.py b/jupytext/formats.py
index c63d880..dcd5e4d 100644
--- a/jupytext/formats.py
+++ b/jupytext/formats.py
@@ -5,8 +5,8 @@ new formats here!
 
 import os
 import re
-import nbformat
 import warnings
+import nbformat
 from .header import header_to_metadata_and_cell, insert_or_test_version_number
 from .cell_reader import MarkdownCellReader, RMarkdownCellReader, \
     LightScriptCellReader, RScriptCellReader, DoublePercentScriptCellReader, HydrogenCellReader, \
diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py
index 97f0a94..7cbb74e 100644
--- a/jupytext/jupytext.py
+++ b/jupytext/jupytext.py
@@ -108,7 +108,7 @@ class TextNotebookConverter(NotebookReader, NotebookWriter):
 
             cells = []
             for cell in nb.cells:
-                cell_metadata = filter_metadata(copy(cell.metadata),
+                cell_metadata = filter_metadata(cell.metadata,
                                                 self.fmt.get('cell_metadata_filter'),
                                                 _IGNORE_CELL_METADATA)
                 if cell.cell_type == 'code':
diff --git a/jupytext/metadata_filter.py b/jupytext/metadata_filter.py
index fecb28a..5c083ab 100644
--- a/jupytext/metadata_filter.py
+++ b/jupytext/metadata_filter.py
@@ -1,5 +1,6 @@
 """Notebook and cell metadata filtering"""
 
+from copy import copy
 from .cell_metadata import _JUPYTEXT_CELL_METADATA
 
 _DEFAULT_NOTEBOOK_METADATA = ','.join([
@@ -106,46 +107,81 @@ def update_metadata_filters(metadata, jupyter_md, cell_metadata):
             metadata.setdefault('jupytext', {})['notebook_metadata_filter'] = ','.join(nb_md_filter)
 
 
-def apply_metadata_filters(user_filter, default_filter, actual_keys):
-    """Apply the filter and replace 'all' with the actual or filtered keys"""
-
+def filter_metadata(metadata, user_filter, default_filter=''):
+    """Filter the cell or notebook metadata, according to the user preference"""
     default_filter = metadata_filter_as_dict(default_filter) or {}
     user_filter = metadata_filter_as_dict(user_filter) or {}
 
-    for key in ['additional', 'excluded']:
-        default_filter.setdefault(key, [])
-        user_filter.setdefault(key, [])
+    default_exclude = default_filter.get('excluded', [])
+    default_include = default_filter.get('additional', [])
 
-    if user_filter.get('excluded') == 'all':
-        default_filter['additional'] = []
-    if user_filter.get('additional') == 'all':
-        default_filter['excluded'] = []
+    assert not (default_exclude == 'all' and default_include == 'all')
+    if isinstance(default_include, list) and default_include and default_exclude == []:
+        default_exclude = 'all'
 
-    # notebook default filter = only few metadata
-    if default_filter.get('additional'):
-        if user_filter.get('additional') == 'all':
-            return actual_keys.difference(user_filter.get('excluded'))
+    user_exclude = user_filter.get('excluded', [])
+    user_include = user_filter.get('additional', [])
 
-        return (actual_keys
-                .intersection(set(user_filter.get('additional')).union(default_filter.get('additional')))
-                .difference(user_filter.get('excluded')))
+    # notebook default filter = include only few metadata
+    if default_exclude == 'all':
+        if user_include == 'all':
+            return subset_metadata(metadata, exclude=user_exclude)
+        if user_exclude == 'all':
+            return subset_metadata(metadata, keep_only=user_include)
+        return subset_metadata(metadata,
+                               keep_only=set(user_include).union(default_include),
+                               exclude=user_exclude)
 
     # cell default filter = all metadata but removed ones
-    if user_filter.get('excluded') == 'all':
-        return actual_keys.intersection(user_filter.get('additional'))
+    if user_include == 'all':
+        return subset_metadata(metadata, exclude=user_exclude)
+    if user_exclude == 'all':
+        return subset_metadata(metadata, keep_only=user_include)
+    return subset_metadata(metadata,
+                           exclude=set(user_exclude).union(set(default_exclude).difference(user_include)))
+
+
+def second_level(keys):
+    """Return a dictionary with the nested keys, e.g. returns {'I':['a', 'b']} when keys=['I.a', 'I.b']"""
+    sub_keys = {}
+    for key in keys:
+        if '.' in key:
+            left, right = key.split('.', 1)
+            sub_keys.setdefault(left, []).append(right)
+
+    return sub_keys
+
+
+def subset_metadata(metadata, keep_only=None, exclude=None):
+    """Filter the metadata"""
+    if keep_only is not None:
+        filtered_metadata = {key: metadata[key] for key in metadata if key in keep_only}
+        sub_keep_only = second_level(keep_only)
+        for key in sub_keep_only:
+            if key in metadata:
+                filtered_metadata[key] = subset_metadata(metadata[key], keep_only=sub_keep_only[key])
+    else:
+        filtered_metadata = copy(metadata)
 
-    return (actual_keys.difference(
-        set(user_filter.get('excluded')).union(set(default_filter.get('excluded'))
-                                               .difference(user_filter.get('additional')))))
+    if exclude is not None:
+        for key in exclude:
+            if key in filtered_metadata:
+                filtered_metadata.pop(key)
+        sub_exclude = second_level(exclude)
+        for key in sub_exclude:
+            if key in filtered_metadata:
+                filtered_metadata[key] = subset_metadata(filtered_metadata[key], exclude=sub_exclude[key])
 
+    return filtered_metadata
 
-def filter_metadata(metadata, user_filter, default_filter):
-    """Filter the cell or notebook metadata, according to the user preference"""
-    actual_keys = set(metadata.keys())
-    keep_keys = apply_metadata_filters(user_filter, default_filter, actual_keys)
 
-    for key in actual_keys:
-        if key not in keep_keys:
-            metadata.pop(key)
+def restore_filtered_metadata(filtered_metadata, unfiltered_metadata, user_filter, default_filter):
+    """Update the filtered metadata with the part of the unfiltered one that matches the filter"""
+    filtered_unfiltered_metadata = filter_metadata(unfiltered_metadata, user_filter, default_filter)
+
+    metadata = copy(filtered_metadata)
+    for key in unfiltered_metadata:
+        if key not in filtered_unfiltered_metadata:
+            metadata[key] = unfiltered_metadata[key]
 
     return metadata
diff --git a/jupytext/version.py b/jupytext/version.py
index 224510c..6b2562e 100644
--- a/jupytext/version.py
+++ b/jupytext/version.py
@@ -1,3 +1,3 @@
 """Jupytext's version number"""
 
-__version__ = '1.3.3-dev'
+__version__ = '1.3.3'


Test patch:
diff --git a/tests/test_contentsmanager.py b/tests/test_contentsmanager.py
index bfd08a2..36864c0 100644
--- a/tests/test_contentsmanager.py
+++ b/tests/test_contentsmanager.py
@@ -1596,3 +1596,32 @@ def test_multiple_pairing(tmpdir):
     # so that we read cell inputs from the py file
     assert model_ipynb['last_modified'] < model_py['last_modified']
     assert model_py['last_modified'] < model_md['last_modified']
+
+
+@skip_if_dict_is_not_ordered
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_py'))
+def test_filter_jupytext_version_information_416(nb_file, tmpdir):
+    tmp_py = str(tmpdir.join('notebook.py'))
+
+    cm = jupytext.TextFileContentsManager()
+    cm.root_dir = str(tmpdir)
+    cm.default_notebook_metadata_filter = "-jupytext.text_representation.jupytext_version"
+
+    # load notebook
+    notebook = jupytext.read(nb_file)
+    notebook.metadata['jupytext_formats'] = 'ipynb,py'
+    model = dict(type='notebook', content=notebook)
+
+    # save to ipynb and py
+    cm.save(model=model, path='notebook.ipynb')
+
+    assert os.path.isfile(tmp_py)
+
+    # read py file
+    with open(tmp_py) as fp:
+        text = fp.read()
+
+    assert '---' in text
+    assert 'jupytext:' in text
+    assert 'kernelspec:' in text
+    assert 'jupytext_version:' not in text
diff --git a/tests/test_metadata_filter.py b/tests/test_metadata_filter.py
index db3785e..8be405f 100644
--- a/tests/test_metadata_filter.py
+++ b/tests/test_metadata_filter.py
@@ -82,3 +82,18 @@ def test_notebook_metadata_none():
     nb = new_notebook(metadata={'jupytext': {'notebook_metadata_filter': '-all'}})
     text = writes(nb, 'md')
     assert '---' not in text
+
+
+def test_filter_nested_metadata():
+    metadata = {'I': {'1': {'a': 1, 'b': 2}}}
+
+    assert filter_metadata(metadata, 'I', '-all') == {'I': {'1': {'a': 1, 'b': 2}}}
+    assert filter_metadata(metadata, '-I') == {}
+
+    assert filter_metadata(metadata, 'I.1.a', '-all') == {'I': {'1': {'a': 1}}}
+    assert filter_metadata(metadata, '-I.1.b') == {'I': {'1': {'a': 1}}}
+
+    assert filter_metadata(metadata, '-I.1.b', 'I') == {'I': {'1': {'a': 1}}}
+
+    # That one is not supported yet
+    # assert filter_metadata(metadata, 'I.1.a', '-I') == {'I': {'1': {'a': 1}}}