Source code for sunpy.util.metadata
"""
This module provides a generalized dictionary class that deals with header
parsing, normalization, and maintaining coherence between keys and keycomments.
"""
from collections import OrderedDict, namedtuple
from collections.abc import Mapping
__all__ = ['MetaDict']
ModifiedItem = namedtuple('ModifiedItem', ['original', 'current'])
ModifiedItem.__repr__ = lambda t: f"(original={t.original}, current={t.current})"
[docs]
class MetaDict(OrderedDict):
"""
A class to hold metadata associated with a `sunpy.map.Map` derivative.
This class handles everything in lower case. This allows case
insensitive indexing.
If the key 'keycomments' exists, its value must be a dictionary mapping
keys in the :class:`MetaDict` to their comments. The casing of keys in the
keycomments dictionary is not significant. If a key is removed from the
:class:`MetaDict`, it will also be removed from the keycomments dictionary.
Additionally, any extraneous keycomments will be removed when the
:class:`MetaDict` is instantiated.
Parameters
----------
save_original : bool, optional
If `True` (the default), a copy of the original metadata will be saved to the
`original_meta` property. Note that this keyword argument is
required as an implementation detail to avoid recursion when saving
the original contents.
"""
def __init__(self, *args, save_original=True):
# Store all keys as lower-case to allow for case-insensitive indexing
# OrderedDict can be instantiated from a list of lists or a tuple of tuples
tags = dict()
if args:
args = list(args)
adict = args[0]
if isinstance(adict, MetaDict):
self._original_meta = adict._original_meta
if isinstance(adict, list) or isinstance(adict, tuple):
items = adict
elif isinstance(adict, Mapping):
# Cast to a dict here, since a Mapping doesn't have to define
# a .items() method (but has enough methods to be converted to
# a dict)
items = dict(adict).items()
else:
raise TypeError(f"Can not create a MetaDict from this input of type {type(adict)}")
self._check_str_keys(items)
tags = OrderedDict((k.lower(), v) for k, v in items)
args[0] = tags
super().__init__(*args)
# Use `copy=True` to avoid mutating the caller's keycomments
# dictionary (if they provided one).
self._prune_keycomments(copy=True)
if save_original and not hasattr(self, '_original_meta'):
self._original_meta = MetaDict(*args, save_original=False)
def __str__(self):
return '\n'.join([f"('{key}': '{item}')" for key, item in self.items()])
def __repr__(self):
return f"{self.__class__.__name__}([{self}])"
# Deliberately a property to prevent external modification
@property
def original_meta(self):
"""
A copy of the dictionary from when it was initialised.
"""
return self._original_meta
@property
def added_items(self):
"""
Items added since initialisation.
"""
return {k: self[k] for k in set(self) - set(self.original_meta)}
@property
def removed_items(self):
"""
Items removed since initialisation.
"""
return {k: self.original_meta[k] for k in set(self.original_meta) - set(self)}
@property
def modified_items(self):
"""
Items modified since initialisation.
This is a mapping from ``key`` to ``[original_value, current_value]``.
"""
keys = set(self).intersection(set(self.original_meta))
return {k: ModifiedItem(self.original_meta[k], self[k]) for k in keys if
self[k] != self.original_meta[k]}
[docs]
def copy(self):
"""
Shallow copies this instance.
"""
copied = super().copy()
# By default the above line will overwrite original_meta, so manually re-instate it
copied._original_meta = self.original_meta
return copied
@staticmethod
def _check_str_keys(items):
bad_keys = []
for k, v in items:
if not isinstance(k, str):
bad_keys.append(str(k))
if len(bad_keys):
raise ValueError('All MetaDict keys must be strings, '
'found the following non-compliant keys: ' +
', '.join(bad_keys))
def _prune_keycomments(self, copy=False):
"""
Remove keycomments for keys that are not contained in the MetaDict.
Parameters
----------
copy : `bool`, optional
Make a copy of the current keycomments dict before removing keys.
"""
if 'keycomments' not in self:
return
keycomments = self['keycomments']
if not isinstance(keycomments, dict):
raise TypeError(
"'keycomments' key must have a value of type `dict`. Found "
f"the following type: {type(keycomments)!r}")
if copy:
keycomments = keycomments.copy()
for key in list(keycomments.keys()):
if key not in self:
del keycomments[key]
self['keycomments'] = keycomments
def __contains__(self, key):
"""
Override ``__contains__``.
"""
return OrderedDict.__contains__(self, key.lower())
def __getitem__(self, key):
"""
Override ``[]`` indexing.
"""
return OrderedDict.__getitem__(self, key.lower())
def __setitem__(self, key, value):
"""
Override ``[]`` indexing.
"""
return OrderedDict.__setitem__(self, key.lower(), value)
[docs]
def popitem(self, last):
key, value = super().popitem(last)
self._prune_keycomments()
return key, value
def __delitem__(self, key):
"""
Override ``del dict[key]`` key deletion.
"""
OrderedDict.__delitem__(self, key.lower())
self._prune_keycomments()
[docs]
def item_hash(self):
"""
Create a hash based on the stored items.
This relies on all the items themselves being hashable. For this reason
the 'keycomments' item, which is a dict, is excluded from the hash.
If creating the hash fails, returns `None`.
"""
self_copy = self.copy()
self_copy.pop('keycomments', None)
try:
return hash(frozenset(self_copy.items()))
except Exception:
return
[docs]
def get(self, key, default=None):
"""
Override ``.get()`` indexing.
"""
return OrderedDict.get(self, key.lower(), default)
[docs]
def has_key(self, key):
"""
Override ``.has_key()`` to perform case-insensitively.
"""
return key.lower() in self
[docs]
def pop(self, key, default=None):
"""
Override ``.pop()`` to perform case-insensitively.
"""
has_key = key in self
result = OrderedDict.pop(self, key.lower(), default)
if has_key:
self._prune_keycomments()
return result
[docs]
def update(self, d2):
"""
Override ``.update()`` to perform case-insensitively.
"""
return OrderedDict.update(self, OrderedDict((k.lower(), v) for k, v in d2.items()))
[docs]
def setdefault(self, key, default=None):
"""
Override ``.setdefault()`` to perform case-insensitively.
"""
return OrderedDict.setdefault(self, key.lower(), default)