Repository: BAMWelDX/weldx

Issue:
`WeldxFile` doesn't raise `KeyError` on missing key
When trying to access a non existing key from `WeldxFile`, `None` is returned instead of raising a  `KeyError`.
```python
from weldx import WeldxFile

with WeldxFile(tree={"key":"value"}, mode="rw") as wxf:
    print(wxf["key"])
    print(wxf["anything"])
# value
# None
```

Is this the intended behavior @marscher ?

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

For more details on these failures, see [this check](https://github.com/BAMWelDX/weldx/runs/6471177753).

Results for commit 8dbf560c. ± Comparison against base commit 9af804a4.

codecov[bot]: # [Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/759?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) Report
> Merging [#759](https://codecov.io/gh/BAMWelDX/weldx/pull/759?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (e51a5fe) into [master](https://codecov.io/gh/BAMWelDX/weldx/commit/9af804a48c158637d171c0a8a1a4aa2e28ddcd2a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX) (9af804a) will **increase** coverage by `0.09%`.
> The diff coverage is `91.66%`.

```diff
@@            Coverage Diff             @@
##           master     #759      +/-   ##
==========================================
+ Coverage   96.76%   96.86%   +0.09%     
==========================================
  Files          88       88              
  Lines        6006     6005       -1     
==========================================
+ Hits         5812     5817       +5     
+ Misses        194      188       -6     
```


| [Impacted Files](https://codecov.io/gh/BAMWelDX/weldx/pull/759?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/759/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvYXNkZi9maWxlLnB5) | `96.20% <ø> (ø)` | |
| [weldx/asdf/util.py](https://codecov.io/gh/BAMWelDX/weldx/pull/759/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX#diff-d2VsZHgvYXNkZi91dGlsLnB5) | `92.13% <91.66%> (+1.85%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/BAMWelDX/weldx/pull/759?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/759?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BAMWelDX). Last update [9af804a...e51a5fe](https://codecov.io/gh/BAMWelDX/weldx/pull/759?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).


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 10fea55..8241115 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -15,6 +15,10 @@ removed
 changes
 =======
 
+-  `WeldxFile` now raises a `KeyError`, if the user tries to directly read or manipulate a protected ASDF keyword
+   within the file. [:pull:`759`]
+
+
 fixes
 =====
 
diff --git a/README.rst b/README.rst
index 1061202..e72588d 100644
--- a/README.rst
+++ b/README.rst
@@ -176,8 +176,8 @@ Documentation build
 .. |Anaconda-Server Badge| image:: https://anaconda.org/conda-forge/weldx/badges/version.svg
    :target: https://anaconda.org/conda-forge/weldx
 
-.. |Zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5710040.svg
-   :target: https://doi.org/10.5281/zenodo.5710040
+.. |Zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.6504615.svg
+   :target: https://doi.org/10.5281/zenodo.6504615
 
 .. |License| image:: https://img.shields.io/badge/License-BSD%203--Clause-orange.svg
    :target: https://opensource.org/licenses/BSD-3-Clause
diff --git a/weldx/asdf/file.py b/weldx/asdf/file.py
index 1f92f1c..869b1ef 100644
--- a/weldx/asdf/file.py
+++ b/weldx/asdf/file.py
@@ -629,7 +629,7 @@ class WeldxFile(_ProtectedViewDict):
         super().update(mapping, **kwargs)
 
     def items(self) -> Set[tuple[Any, Any]]:
-        """Return a set-like object providing a view on this files items.
+        """Return a set-like object providing a view on this file's items.
 
         Returns
         -------
diff --git a/weldx/asdf/util.py b/weldx/asdf/util.py
index 7a39c5e..f897d99 100644
--- a/weldx/asdf/util.py
+++ b/weldx/asdf/util.py
@@ -587,29 +587,31 @@ def _get_instance_units(
 
 
 class _ProtectedViewDict(MutableMapping):
+    """A mutable mapping which protects given keys from manipulation."""
+
     def __init__(self, protected_keys, data=None):
         super(_ProtectedViewDict, self).__init__()
-        self.__data = data
+        self.__data = data if data is not None else dict()
         self.protected_keys = protected_keys
 
-    @property
-    def _data(self):
-        return self
+    def _wrap_protected_non_existent(self, key, method: str):
+        if key in self.protected_keys:
+            self._warn_protected_keys()
+            raise KeyError(f"'{key}' is protected.")
+        elif key not in self.__data:
+            raise KeyError(f"'{key}' not contained.")
+
+        method_obj = getattr(self.__data, method)
+        return method_obj(key)
 
     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)
+        return self._wrap_protected_non_existent(key, "__getitem__")
 
     def __delitem__(self, key):
-        if key in self.protected_keys:
-            self._warn_protected_keys()
-            return
-        del self.__data[key]
+        return self._wrap_protected_non_existent(key, "__delitem__")
 
     def __setitem__(self, key, value):
         if key in self.protected_keys:
@@ -643,7 +645,7 @@ class _ProtectedViewDict(MutableMapping):
             if k not in self.protected_keys:
                 return k, self.pop(k)
 
-        raise KeyError
+        raise KeyError("empty")
 
     def clear(self):
         """Clear all data except the protected keys."""


Test patch:
diff --git a/weldx/tests/asdf_tests/test_asdf_util.py b/weldx/tests/asdf_tests/test_asdf_util.py
index db5cd63..eb9338f 100644
--- a/weldx/tests/asdf_tests/test_asdf_util.py
+++ b/weldx/tests/asdf_tests/test_asdf_util.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import io
+import unittest
 from dataclasses import dataclass
 
 import numpy as np
@@ -9,6 +10,7 @@ import pytest
 
 from weldx.asdf.file import WeldxFile
 from weldx.asdf.util import (
+    _ProtectedViewDict,
     dataclass_serialization_class,
     get_highest_tag_version,
     get_schema_tree,
@@ -186,3 +188,46 @@ def test_get_highest_tag_version():
 def test_get_schema_tree():
     d = get_schema_tree("single_pass_weld-0.1.0")
     assert isinstance(d, dict)
+
+
+class TestProtectedView(unittest.TestCase):
+    def setUp(self):
+        data = dict(foo="blub", bar=42)
+        self.protected_key = "bar"
+        self.view = _ProtectedViewDict(protected_keys=[self.protected_key], data=data)
+
+    def test_protected_keys_hidden(self):
+        assert self.protected_key not in self.view.keys()
+        assert self.protected_key not in self.view
+        assert (self.protected_key, 42) not in self.view.items()
+
+    def test_allowed_access(self):
+        assert self.view["foo"] == "blub"
+
+    def test_illegal_access(self):
+        with pytest.raises(KeyError):
+            _ = self.view["bar"]
+
+        with pytest.raises(KeyError):
+            del self.view["bar"]
+
+    def test_access_non_existent(self):
+        with pytest.raises(KeyError):
+            _ = self.view["no"]
+
+    def test_update(self):
+        expected_match = "manipulate an ASDF internal structure"
+        warning_type = UserWarning
+
+        with pytest.warns(warning_type, match=expected_match):
+            self.view.update(dict(bar=None), foo=1)
+        assert self.view["foo"] == 1
+
+    def test_popitem(self):
+        k, v = self.view.popitem()
+        assert k == "foo"
+        assert v == "blub"
+
+    def test_clear(self):
+        self.view.clear()
+        assert len(self.view) == 0
diff --git a/weldx/tests/asdf_tests/test_weldx_file.py b/weldx/tests/asdf_tests/test_weldx_file.py
index 21acbb6..ac6911a 100644
--- a/weldx/tests/asdf_tests/test_weldx_file.py
+++ b/weldx/tests/asdf_tests/test_weldx_file.py
@@ -594,10 +594,12 @@ properties:
         expected_match = "manipulate an ASDF internal structure"
         warning_type = UserWarning
 
+        # reading is also forbidden
+        with pytest.raises(KeyError):
+            _ = self.fh[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):
@@ -612,7 +614,7 @@ properties:
             keys.append(key)
         assert keys == [META_ATTR]
 
-    def test_len_proteced_keys(self):
+    def test_len_protected_keys(self):
         """Should only contain key 'wx_metadata'."""
         assert len(self.fh) == 1