Repository: BAMWelDX/weldx

Issue:
weldxfile: consider removing asdf metadata from user dictionary like operations.
During writing examples (doctests) for WeldxFile, I noticed that it would be bad, if users are able to delete basic asdf meta data like history, software versions etc.
So I'd suggest to remove these data from the users view, restricting it to the data he or she has actually wanted to be in the file (dictionary). Of course the data is still there and will be written as such, but just became immutable. For `history` we already have an interface. So we'd need to another getter for software.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) Report
> Merging [#625](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (b05328c) into [master](https://codecov.io/gh/BAMWelDX/weldx/commit/69ed4dbc87e8fd01a0125732b2c8012aea7446c4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (69ed4db) will **decrease** coverage by `0.07%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/BAMWelDX/weldx/pull/625/graphs/tree.svg?width=650&height=150&src=pr&token=wdof1qQTsn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)

```diff
@@            Coverage Diff             @@
##           master     #625      +/-   ##
==========================================
- Coverage   94.65%   94.57%   -0.08%     
==========================================
  Files          93       93              
  Lines        5814     5954     +140     
==========================================
+ Hits         5503     5631     +128     
- Misses        311      323      +12     
```


| [Impacted Files](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) | Coverage Δ | |
|---|---|---|
| [weldx/asdf/file.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvYXNkZi9maWxlLnB5) | `94.40% <83.33%> (-1.11%)` | :arrow_down: |
| [weldx/tags/units/pint\_quantity.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdGFncy91bml0cy9waW50X3F1YW50aXR5LnB5) | `92.10% <0.00%> (-2.19%)` | :arrow_down: |
| [weldx/visualization/k3d\_impl.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdmlzdWFsaXphdGlvbi9rM2RfaW1wbC5weQ==) | `78.37% <0.00%> (-0.14%)` | :arrow_down: |
| [weldx/constants.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvY29uc3RhbnRzLnB5) | `100.00% <0.00%> (ø)` | |
| [weldx/welding/util.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvd2VsZGluZy91dGlsLnB5) | `100.00% <0.00%> (ø)` | |
| [weldx/transformations/cs\_manager.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdHJhbnNmb3JtYXRpb25zL2NzX21hbmFnZXIucHk=) | `98.76% <0.00%> (+0.01%)` | :arrow_up: |
| [weldx/geometry.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvZ2VvbWV0cnkucHk=) | `97.47% <0.00%> (+0.01%)` | :arrow_up: |
| [weldx/util/xarray.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdXRpbC94YXJyYXkucHk=) | `98.38% <0.00%> (+0.08%)` | :arrow_up: |
| [weldx/visualization/colors.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdmlzdWFsaXphdGlvbi9jb2xvcnMucHk=) | `88.67% <0.00%> (+0.21%)` | :arrow_up: |
| [weldx/tags/groove/iso\_9692\_1.py](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvdGFncy9ncm9vdmUvaXNvXzk2OTJfMS5weQ==) | `96.66% <0.00%> (+0.37%)` | :arrow_up: |
| ... and [2 more](https://codecov.io/gh/BAMWelDX/weldx/pull/625/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Last update [69ed4db...b05328c](https://codecov.io/gh/BAMWelDX/weldx/pull/625?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX).

github-actions[bot]: ## Unit Test Results
       1 files  ±0         1 suites  ±0   2m 49s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "duration of all tests") -19s
1 924 tests +1  1 924 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "passed tests") +1  0 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "skipped / disabled tests") ±0  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v1.20/README.md#the-symbols "failed tests") ±0 

Results for commit b05328c6. ± Comparison against base commit 704f5c1a.

review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/BAMWelDX/weldx/pull/625"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
marscher: does this "arguments differ" warnings raised by codacy origin from pylint? I cannot figure out which tool they use. The argument has been renamed from the top-level typing interface named "__m" to "mapping" for better user-experience. I can also add an ignore for this.
Another possibility which might have triggered this is the different (more sane) type annotation. I would also refuse to change that, because it makes more sense to require a key to be hashable in contrast to the super broad TypeVar("_K").
CagtayFabry: it looks like the pylint run but I am fine with this kind of error (or adding a simple ignore 👍 )
vhirtham: If you expand the error box, the second line is: 

~~~
Reported by Pylint (Python 3) Time to fix: 5 minutes
~~~

So as @CagtayFabry  said, its pylint. Just add `# pylint: disable=W0221` if you think we can ignore the issue

Patch:
diff --git a/.github/workflows/build_pkg.yml b/.github/workflows/build_pkg.yml
index 0b1d900..9812a0d 100644
--- a/.github/workflows/build_pkg.yml
+++ b/.github/workflows/build_pkg.yml
@@ -13,7 +13,7 @@ on:
       - '!.github/workflows/build.yml'
   release:
     types:
-      - created
+      - published
   schedule:
     - cron: '0 6 * * 1'
   workflow_dispatch:
@@ -25,7 +25,7 @@ jobs:
       (github.event_name == 'workflow_dispatch') ||
       (github.ref == 'refs/heads/master') ||
       startsWith(github.ref, 'refs/tags/') ||
-      (github.event_name == 'release' && github.event.action == 'created')
+      (github.event_name == 'release')
     name: conda build
     runs-on: ubuntu-latest
     steps:
@@ -70,7 +70,7 @@ jobs:
       (github.event.pull_request.draft == false) ||
       (github.event_name == 'workflow_dispatch') ||
       startsWith(github.ref, 'refs/tags/') ||
-      (github.event_name == 'release' && github.event.action == 'created')
+      (github.event_name == 'release')
     name: pypi build
     runs-on: ubuntu-latest
     steps:
@@ -124,12 +124,12 @@ jobs:
           echo "pypi_token=${{ secrets.TESTPYPI_UPLOAD }}" >> $GITHUB_ENV
 
       - name: set pypi main repo for release
-        if: github.event_name == 'release' && github.event.action == 'created'
+        if: github.event_name == 'release'
         run: |
           echo "pypi_repo=pypi" >> $GITHUB_ENV
           echo "pypi_token=${{ secrets.PYPI_UPLOAD }}" >> $GITHUB_ENV
 
       - name: pypi release
-        if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'release' && github.event.action == 'created')
+        if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'release')
         run: |
           python -m twine upload --repository $pypi_repo dist/* -u __token__ -p $pypi_token
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6fb82d8..5844db8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,7 +24,7 @@ repos:
     hooks:
       - id: black
   - repo: https://github.com/pycqa/isort
-    rev: 5.9.3
+    rev: 5.10.0
     hooks:
       - id: isort
   - repo: https://github.com/PyCQA/flake8
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5abe3ff..5fdff2d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -15,6 +15,10 @@ removed
 changes
 =======
 
+-  `WeldxFile` now hides ASDF added fields like history and asdf_library
+   from the dictionary interface. To access these, there are separate
+   properties `[#625] <https://github.com/BAMWeldX/weldx/pull/625>`__.
+
 fixes
 =====
 
diff --git a/tutorials/weldxfile.ipynb b/tutorials/weldxfile.ipynb
index c5dfc2e..24a885d 100644
--- a/tutorials/weldxfile.ipynb
+++ b/tutorials/weldxfile.ipynb
@@ -318,7 +318,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "WeldxFile(filename_hist)[\"history\"]"
+    "WeldxFile(filename_hist).history"
    ]
   },
   {
@@ -347,7 +347,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Lets now inspect how we wrote history."
+    "Let's now inspect how we wrote history."
    ]
   },
   {
@@ -356,7 +356,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "WeldxFile(filename_hist)[\"history\"][\"entries\"][-1]"
+    "WeldxFile(filename_hist).history[-1]"
    ]
   },
   {
@@ -473,7 +473,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "We receive a ValidationError from the ASDF library, which tells us exactly what the missing information is. The same will happen, if we accidentially pass the wrong type."
+    "We receive a ValidationError from the ASDF library, which tells us exactly what the missing information is. The same will happen, if we accidentally pass the wrong type."
    ]
   },
   {
diff --git a/weldx/asdf/cli/welding_schema.py b/weldx/asdf/cli/welding_schema.py
index d3c36fe..258003c 100644
--- a/weldx/asdf/cli/welding_schema.py
+++ b/weldx/asdf/cli/welding_schema.py
@@ -1,11 +1,11 @@
 """single_pass_weld schema."""
 import sys
 from io import BytesIO
-from typing import Optional, Tuple
+from typing import Optional, Tuple, Union
 
 
 def single_pass_weld_example(
-    out_file: str = "single_pass_weld_example.asdf",
+    out_file: Optional[Union[str, BytesIO]] = "single_pass_weld_example.asdf",
 ) -> Optional[Tuple[BytesIO, dict]]:
     """Create ASDF file containing all required fields of the single_pass_weld schema.
 
diff --git a/weldx/asdf/file.py b/weldx/asdf/file.py
index 32856cd..f022058 100644
--- a/weldx/asdf/file.py
+++ b/weldx/asdf/file.py
@@ -1,7 +1,7 @@
 """`WeldxFile` wraps creation and updating of ASDF files and underlying files."""
 import copy
+import io
 import pathlib
-from collections import UserDict
 from collections.abc import MutableMapping, ValuesView
 from contextlib import contextmanager
 from io import BytesIO, IOBase
@@ -10,6 +10,7 @@ from typing import (
     AbstractSet,
     Any,
     Dict,
+    Hashable,
     Iterable,
     List,
     Mapping,
@@ -22,21 +23,31 @@ import asdf
 import numpy as np
 from asdf import AsdfFile, generic_io
 from asdf import open as open_asdf
-from asdf import util
 from asdf.tags.core import Software
-from asdf.util import get_file_type
+from asdf.util import FileType, get_file_type
 from jsonschema import ValidationError
 
-from weldx.asdf.util import get_schema_path, get_yaml_header, view_tree
+from weldx.asdf.util import (
+    _ProtectedViewDict,
+    get_schema_path,
+    get_yaml_header,
+    view_tree,
+)
 from weldx.types import SupportsFileReadWrite, types_file_like, types_path_and_file_like
-from weldx.util import inherit_docstrings
+from weldx.util import (
+    deprecated,
+    inherit_docstrings,
+    is_interactive_session,
+    is_jupyterlab_session,
+)
 
 __all__ = [
     "WeldxFile",
+    "DEFAULT_ARRAY_COMPRESSION",
+    "DEFAULT_ARRAY_COPYING",
+    "_PROTECTED_KEYS",
 ]
 
-from weldx.util import is_interactive_session, is_jupyterlab_session
-
 
 @contextmanager
 def reset_file_position(fh: SupportsFileReadWrite):
@@ -59,9 +70,15 @@ DEFAULT_ARRAY_COMPRESSION = "input"
 DEFAULT_ARRAY_COPYING = True
 """Stored Arrays will be copied to memory, or not. If False, use memory mapping."""
 
+_PROTECTED_KEYS = (
+    "history",
+    "asdf_library",
+)
+"""These keys are not seen, nor can they be manipulated."""
+
 
 @inherit_docstrings
-class WeldxFile(UserDict):
+class WeldxFile(_ProtectedViewDict):
     """Expose an ASDF file as a dictionary like object and handle underlying files.
 
     The WeldxFile class makes it easy to work with ASDF files. Creating, validating,
@@ -237,14 +254,12 @@ class WeldxFile(UserDict):
             else:
                 self._in_memory = False
 
-            # TODO: this could be inefficient for large files.
-            # For buffers is fast, but not for real files.
-            # real files should be probed over the filesystem!
             if mode == "rw" and isinstance(
                 filename_or_file_like, SupportsFileReadWrite
             ):
                 with reset_file_position(filename_or_file_like):
-                    new_file_created = len(filename_or_file_like.read()) == 0
+                    filename_or_file_like.seek(0, io.SEEK_END)
+                    new_file_created = filename_or_file_like.tell() == 0
 
             # the user passed a raw file handle, its their responsibility to close it.
             self._close = False
@@ -274,10 +289,8 @@ class WeldxFile(UserDict):
             )
         self._asdf_handle: AsdfFile = asdf_file
 
-        # UserDict interface: we want to store a reference to the tree, but the ctor
-        # of UserDict takes a copy, so we do it manually here.
-        super().__init__()
-        self.data = self._asdf_handle.tree
+        # initialize protected key interface.
+        super().__init__(protected_keys=_PROTECTED_KEYS, data=self._asdf_handle.tree)
 
     def _write_tree(
         self, filename_or_path_like, tree, asdffile_kwargs, write_kwargs, created
@@ -333,7 +346,7 @@ class WeldxFile(UserDict):
             else:
                 generic_file = generic_io.get_file(filename, mode="r")
                 file_type = get_file_type(generic_file)
-                if not file_type == util.FileType.ASDF:
+                if not file_type == FileType.ASDF:
                     raise FileExistsError(
                         f"given file {filename} is not an ASDF file and "
                         "could be overwritten because of read/write mode!"
@@ -444,6 +457,16 @@ class WeldxFile(UserDict):
 
     sync.__doc__ = AsdfFile.update.__doc__
 
+    def keys(self) -> AbstractSet:
+        """Return a set of keys/attributes stored in this file.
+
+        Returns
+        -------
+        KeysView :
+            all keys stored at the root of this file.
+        """
+        return super(WeldxFile, self).keys()
+
     @classmethod
     def fromkeys(cls, iterable, default=None) -> "WeldxFile":
         """Create a new file with keys from iterable and values set to value.
@@ -476,21 +499,6 @@ class WeldxFile(UserDict):
         """
         return super().values()
 
-    def keys(self) -> AbstractSet:
-        """Return a set of keys/attributes stored in this file.
-
-        Returns
-        -------
-        KeysView :
-            all keys stored at the root of this file.
-        """
-        return super().keys()
-
-    def clear(self):
-        """Remove all data from file."""
-        # TODO: we do not want to delete the history, software.
-        super().clear()
-
     def get(self, key, default=None):
         """Get data attached to given key from file.
 
@@ -508,7 +516,7 @@ class WeldxFile(UserDict):
         """
         return super().get(key, default=default)
 
-    def update(self, mapping: Union[Mapping, Iterable] = (), **kwargs):
+    def update(self, mapping: Union[Mapping, Iterable] = (), **kwargs: Any):
         """Update this file from mapping or iterable mapping and kwargs.
 
         Parameters
@@ -535,23 +543,17 @@ class WeldxFile(UserDict):
 
         >>> a.update(b, foo="bar")
 
-        # remove asdf meta data for easy comparision
-        >>> del a["asdf_library"], a["history"]
-        >>> a.data
-        {'x': 23, 'y': 0, 'foo': 'bar'}
-
         Or we can update with an iterable of key, value tuples.
         >>> data = [('x', 0), ('y', -1)]
         >>> a.update(data)
-        >>> a.data
-        {'x': 0, 'y': -1, 'foo': 'bar'}
+        >>> a["foo"], a["x"], a["y"]
+        ('bar', 0, -1)
 
         Another possibility is to directly pass keyword arguments.
         >>> a.update(x=-1, z=42)
-        >>> a.data
-        {'x': -1, 'y': -1, 'foo': 'bar', 'z': 42}
+        >>> a["x"], a["z"]
+        (-1, 42)
         """
-        # TODO: we do not want to manipulate the history, software.
         super().update(mapping, **kwargs)
 
     def items(self) -> AbstractSet[Tuple[Any, Any]]:
@@ -604,23 +606,23 @@ class WeldxFile(UserDict):
         >>> "x" not in a.keys()
         True
         """
-        # TODO: we do not want to delete the history, software.
         return super().pop(key, default=default)
 
-    def popitem(self) -> Any:
+    def popitem(self) -> Tuple[Hashable, Any]:
         """Remove the item that was last inserted into the file.
 
         Notes
         -----
-        In versions before 3.7, the popitem() method removes a random item.
+        The assumption of an ordered dictionary, that this method returns
+        the key inserted lastly, does not hold necessarily. E.g. if the file has been
+        written to disk and is loaded again the previous order has been lost eventually.
 
         Returns
         -------
         object :
             the last item.
         """
-        # TODO: we do not want to delete the history, software.
-        return super().popitem()
+        return super(WeldxFile, self).popitem()
 
     def add_history_entry(self, change_desc: str, software: dict = None) -> None:
         """Add an history_entry to the file.
@@ -645,7 +647,12 @@ class WeldxFile(UserDict):
     @property
     def history(self) -> List:
         """Return a list of all history entries in this file."""
-        return self._asdf_handle.get_history_entries()
+        return self._asdf_handle.get_history_entries().copy()
+
+    @property
+    def asdf_library(self) -> dict:
+        """Get version information about the ASDF library lastly modifying this file."""
+        return self._asdf_handle["asdf_library"].copy()
 
     @property
     def custom_schema(self) -> Optional[str]:
@@ -694,6 +701,7 @@ class WeldxFile(UserDict):
                 if not overwrite:
                     raise
 
+        # TODO: we could try to optimize this, e.g. avoid the extra copy?
         file = self.write_to(filename_or_file_like)
         wx = WeldxFile(
             file,
@@ -782,6 +790,11 @@ class WeldxFile(UserDict):
             fd.seek(0)
         return fd
 
+    @property
+    @deprecated(since="0.5.2", removed="0.6", message="Please do not use this anymore.")
+    def data(self):
+        return self._data
+
     def show_asdf_header(
         self, use_widgets: bool = None, _interactive: Optional[bool] = None
     ):
diff --git a/weldx/asdf/util.py b/weldx/asdf/util.py
index 8853195..84eb25d 100644
--- a/weldx/asdf/util.py
+++ b/weldx/asdf/util.py
@@ -1,16 +1,27 @@
 """Utilities for asdf files."""
-import io
-from collections.abc import Mapping
+
 from distutils.version import LooseVersion
-from io import BytesIO
+from io import BytesIO, TextIOBase
 from pathlib import Path
-from typing import Any, Callable, Dict, List, Tuple, Type, Union
+from typing import (
+    AbstractSet,
+    Any,
+    Callable,
+    Dict,
+    Hashable,
+    List,
+    Mapping,
+    MutableMapping,
+    Tuple,
+    Type,
+    Union,
+)
 from warnings import warn
 
 import asdf
 from asdf.asdf import SerializationContext
 from asdf.config import AsdfConfig, get_config
-from asdf.extension._extension import Extension
+from asdf.extension import Extension
 from asdf.tagged import TaggedDict
 from asdf.util import uri_match as asdf_uri_match
 from boltons.iterutils import get_path, remap
@@ -240,7 +251,7 @@ def get_yaml_header(file: types_path_and_file_like, parse=False) -> Union[str, d
         return b"".join(iter(readline_replace_eol, None))
 
     if isinstance(file, types_file_like.__args__):
-        if isinstance(file, io.TextIOBase):
+        if isinstance(file, TextIOBase):
             raise ValueError(
                 "cannot read files opened in text mode. " "Please open in binary mode."
             )
@@ -542,6 +553,82 @@ def _get_instance_shape(
     return None
 
 
+class _ProtectedViewDict(MutableMapping):
+    def __init__(self, protected_keys, data=None):
+        super(_ProtectedViewDict, self).__init__()
+        self.__data = data
+        self.protected_keys = protected_keys
+
+    @property
+    def _data(self):
+        return self
+
+    def __len__(self) -> int:
+        return len(self.keys())
+
+    def __getitem__(self, key):
+        if key in self.protected_keys:
+            self._warn_protected_keys()
+            raise KeyError
+        return self.__data.get(key)
+
+    def __delitem__(self, key):
+        if key in self.protected_keys:
+            self._warn_protected_keys()
+            return
+        del self.__data[key]
+
+    def __setitem__(self, key, value):
+        if key in self.protected_keys:
+            self._warn_protected_keys()
+            return
+        self.__data[key] = value
+
+    def keys(self) -> AbstractSet:
+        return {k for k in self.__data.keys() if k not in self.protected_keys}
+
+    def __iter__(self):
+        return (k for k in self.keys())
+
+    def __contains__(self, item):
+        return item in self.keys()
+
+    def update(
+        self, mapping: Mapping[Hashable, Any], **kwargs: Any
+    ):  # pylint: disable=W0221
+        _mapping = dict(mapping, **kwargs)  # merge mapping and kwargs
+        if any(key in self.protected_keys for key in _mapping.keys()):
+            self._warn_protected_keys()
+            _mapping = {
+                k: v for k, v in _mapping.items() if k not in self.protected_keys
+            }
+
+        self.__data.update(_mapping)
+
+    def popitem(self) -> Tuple[Hashable, Any]:
+        for k in self.keys():
+            if k not in self.protected_keys:
+                return k, self.pop(k)
+
+        raise KeyError
+
+    def clear(self):
+        """Clear all data except the protected keys."""
+        _protected_data = {k: self.__data.pop(k) for k in self.protected_keys}
+        self.__data.clear()
+        self.__data.update(_protected_data)  # re-add protected data.
+        assert len(self) == 0
+
+    def _warn_protected_keys(self, stacklevel=3):
+        import warnings
+
+        warnings.warn(
+            "You tried to manipulate an ASDF internal structure"
+            f" (currently protected: {self.protected_keys}",
+            stacklevel=stacklevel,
+        )
+
+
 def get_schema_tree(schemafile: Union[str, Path], *, drop: set = None) -> dict:
     """Get a dictionary representation of a weldx schema file with custom formatting.
 


Test patch:
diff --git a/weldx/tests/asdf_tests/test_weldx_file.py b/weldx/tests/asdf_tests/test_weldx_file.py
index 53854cb..efc9037 100644
--- a/weldx/tests/asdf_tests/test_weldx_file.py
+++ b/weldx/tests/asdf_tests/test_weldx_file.py
@@ -14,6 +14,7 @@ from jsonschema import ValidationError
 
 from weldx import WeldxFile
 from weldx.asdf.cli.welding_schema import single_pass_weld_example
+from weldx.asdf.file import _PROTECTED_KEYS
 from weldx.asdf.util import get_schema_path
 from weldx.types import SupportsFileReadWrite
 from weldx.util import compare_nested
@@ -21,7 +22,7 @@ from weldx.util import compare_nested
 SINGLE_PASS_SCHEMA = "single_pass_weld-0.1.0"
 
 
-class ReadOnlyFile:
+class _ReadOnlyFile:
     """Simulate a read-only file."""
 
     def __init__(self, tmpdir):  # noqa: D107
@@ -42,7 +43,7 @@ class ReadOnlyFile:
         return True
 
 
-class WritableFile:
+class _WritableFile:
     """Example of a class implementing SupportsFileReadWrite."""
 
     def __init__(self):  # noqa: D107
@@ -70,7 +71,7 @@ class WritableFile:
 
 def test_protocol_check(tmpdir):
     """Instance checks."""
-    assert isinstance(WritableFile(), SupportsFileReadWrite)
+    assert isinstance(_WritableFile(), SupportsFileReadWrite)
     assert isinstance(BytesIO(), SupportsFileReadWrite)
 
     # real file:
@@ -176,7 +177,7 @@ class TestWeldXFile:
     @staticmethod
     def test_create_writable_protocol():
         """Interface test for writable files."""
-        f = WritableFile()
+        f = _WritableFile()
         WeldxFile(f, tree=dict(test="yes"), mode="rw")
         new_file = TestWeldXFile.make_copy(f.to_wrap)
         assert WeldxFile(new_file)["test"] == "yes"
@@ -184,13 +185,13 @@ class TestWeldXFile:
     @staticmethod
     def test_create_readonly_protocol(tmpdir):
         """A read-only file should be supported by ASDF."""
-        f = ReadOnlyFile(tmpdir)
+        f = _ReadOnlyFile(tmpdir)
         WeldxFile(f)
 
     @staticmethod
     def test_read_only_raise_on_write(tmpdir):
         """Read-only files cannot be written to."""
-        f = ReadOnlyFile(tmpdir)
+        f = _ReadOnlyFile(tmpdir)
         with pytest.raises(ValueError):
             WeldxFile(f, mode="rw")
 
@@ -512,3 +513,40 @@ class TestWeldXFile:
                 return fh.read()
 
         assert _read("test.asdf") == _read("test.wx")
+
+    @pytest.mark.parametrize("protected_key", _PROTECTED_KEYS)
+    def test_cannot_update_del_protected_keys(self, protected_key):
+        """Ensure we cannot manipulate protected keys."""
+        expected_match = "manipulate an ASDF internal structure"
+        warning_type = UserWarning
+        with pytest.raises(KeyError):  # try to obtain key from underlying dict.
+            _ = self.fh.data[protected_key]
+
+        with pytest.warns(warning_type, match=expected_match):
+            self.fh.update({protected_key: None})
+        with pytest.warns(warning_type, match=expected_match):
+            del self.fh[protected_key]
+        with pytest.warns(warning_type, match=expected_match):
+            self.fh.pop(protected_key)
+        with pytest.warns(warning_type, match=expected_match):
+            self.fh[protected_key] = NotImplemented
+
+    def test_popitem_remain_protected_keys(self):
+        """Ensure we cannot manipulate protected keys."""
+        keys = []
+
+        while len(self.fh):
+            key, _ = self.fh.popitem()
+            keys.append(key)
+        assert keys == ["wx_metadata"]
+
+    def test_len_proteced_keys(self):
+        """Should only contain key 'wx_metadata'."""
+        assert len(self.fh) == 1
+
+    def test_keys_not_in_protected_keys(self):
+        """Protected keys do not show up in keys()."""
+        assert self.fh.keys() not in set(_PROTECTED_KEYS)
+
+        for x in iter(self.fh):
+            assert x not in _PROTECTED_KEYS