Add Metadata methods to ProxyMetadata when reasonable to avoid AttributeError exceptions.

Add tests for the new methods.
This commit is contained in:
Charles Haley 2023-10-05 15:35:31 +01:00
parent fedafa4978
commit fe5a546367
2 changed files with 150 additions and 12 deletions

View File

@ -372,16 +372,30 @@ class ProxyMetadata(Metadata):
if extra is not None:
cache[field + '_index'] = val
def get_user_metadata(self, field, make_copy=False):
um = ga(self, '_user_metadata')
try:
ans = um[field]
except KeyError:
pass
else:
if make_copy:
ans = deepcopy(ans)
return ans
# Replacements (overrides) for methods in the Metadata base class.
# ProxyMetadata cannot set attributes.
def _unimplemented_exception(self, method, add_txt):
raise NotImplementedError(f"{method}() cannot be used in this context. "
"{'ProxyMetadata is read only' if add_txt else ''}")
# Metadata returns a seemingly arbitrary set of items. Rather than attempt
# compatibility, flag __iter__ as unimplemented. This won't break anything
# because the Metadata version raises AttributeError
def __iter__(self):
raise NotImplementedError(f"__iter__() cannot be used in this context. "
"Use the explicit methods such as all_field_keys()")
def has_key(self, key):
return key in self.all_field_keys()
def deepcopy(self, **kwargs):
self._unimplemented_exception('deepcopy', add_txt=False)
def deepcopy_metadata(self):
return deepcopy(ga('_user_metadata'))
# def get(self, field, default=None)
def get_extra(self, field, default=None):
um = ga(self, '_user_metadata')
@ -393,10 +407,37 @@ class ProxyMetadata(Metadata):
raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))
def set(self, *args, **kwargs):
self._unimplemented_exception('set', add_txt=True)
def get_identifiers(self):
res = self.get('identifiers')
return {} if res is None else res
def set_identifiers(self, *args):
self._unimplemented_exception('set_identifiers', add_txt=True)
def set_identifier(self, *args):
self._unimplemented_exception('set_identifier', add_txt=True)
def has_identifier(self, typ):
return typ in self.get('identifiers', {})
# def standard_field_keys(self)
def custom_field_keys(self):
um = ga(self, '_user_metadata')
return iter(um.custom_field_keys())
def all_field_keys(self):
um = ga(self, '_user_metadata')
return ALL_METADATA_FIELDS.union(frozenset(um.all_field_keys()))
def all_non_none_fields(self):
self._unimplemented_exception('all_non_none_fields', add_txt=False)
# This version can return custom column metadata while the Metadata version
# won't.
def get_standard_metadata(self, field, make_copy=False):
field_metadata = ga(self, '_user_metadata')
if field in field_metadata and field_metadata[field]['kind'] == 'field':
@ -405,9 +446,47 @@ class ProxyMetadata(Metadata):
return field_metadata[field]
return None
def all_field_keys(self):
# def get_all_standard_metadata(self, make_copy)
def get_all_user_metadata(self, make_copy):
um = ga(self, '_user_metadata')
return frozenset(ALL_METADATA_FIELDS.union(frozenset(um)))
if make_copy:
res = {k: deepcopy(um[k]) for k in um.custom_field_keys()}
else:
res = {k: um[k] for k in um.custom_field_keys()}
return res
# The Metadata version of this method works only with custom field keys. It
# isn't clear how this method differs from get_standard_metadata other than
# it will return non-'field' metadata. Leave it in case someone depends on
# that.
def get_user_metadata(self, field, make_copy=False):
um = ga(self, '_user_metadata')
try:
ans = um[field]
except KeyError:
pass
else:
if make_copy:
ans = deepcopy(ans)
return ans
def set_all_user_metadata(self, *args):
self._unimplemented_exception('set_all_user_metadata', add_txt=True)
def set_user_metadata(self, *args):
self._unimplemented_exception('set_user_metadata', add_txt=True)
def remove_stale_user_metadata(self, *args):
self._unimplemented_exception('remove_stale_user_metadata', add_txt=True)
def template_to_attribute(self, *args):
self._unimplemented_exception('template_to_attribute', add_txt=True)
def smart_update(self, *args, **kwargs):
self._unimplemented_exception('smart_update', add_txt=True)
# The rest of the methods in Metadata can be used as is.
@property
def _proxy_metadata(self):

View File

@ -649,6 +649,65 @@ class ReadingTest(BaseTest):
mi, pmi = cache.get_metadata(1), cache.get_proxy_metadata(1)
self.assertEqual(mi.get('#comp1'), pmi.get('#comp1'))
# Test overridden Metadata methods
self.assertTrue(pmi.has_key('tags') == mi.has_key('tags'))
self.assertFalse(pmi.has_key('taggs'), 'taggs attribute')
self.assertTrue(pmi.has_key('taggs') == mi.has_key('taggs'))
self.assertSetEqual(set(pmi.custom_field_keys()), set(mi.custom_field_keys()))
self.assertEqual(pmi.get_extra('#series', 0), 3)
self.assertEqual(pmi.get_extra('#series', 0), mi.get_extra('#series', 0))
self.assertDictEqual(pmi.get_identifiers(), {'test': 'two'})
self.assertDictEqual(pmi.get_identifiers(), mi.get_identifiers())
self.assertTrue(pmi.has_identifier('test'))
self.assertTrue(pmi.has_identifier('test') == mi.has_identifier('test'))
self.assertListEqual(list(pmi.custom_field_keys()), list(mi.custom_field_keys()))
# ProxyMetadata has the virtual fields while Metadata does not.
self.assertSetEqual(set(pmi.all_field_keys())-{'id', 'series_sort', 'path',
'in_tag_browser', 'sort', 'ondevice',
'au_map', 'marked', '#series_index'},
set(mi.all_field_keys()))
# mi.get_standard_metadata() doesn't include the rec_index metadata key
fm_pmi = pmi.get_standard_metadata('series')
fm_pmi.pop('rec_index')
self.assertDictEqual(fm_pmi, mi.get_standard_metadata('series', make_copy=False))
# The ProxyMetadata versions don't include the values. Note that the mi
# version of get_standard_metadata won't return custom columns while the
# ProxyMetadata version will
fm_mi = mi.get_user_metadata('#series', make_copy=False)
fm_mi.pop('#extra#')
fm_mi.pop('#value#')
self.assertDictEqual(pmi.get_standard_metadata('#series'), fm_mi)
self.assertDictEqual(pmi.get_user_metadata('#series'), fm_mi)
fm_mi = mi.get_all_user_metadata(make_copy=False)
for one in fm_mi:
fm_mi[one].pop('#extra#', None)
fm_mi[one].pop('#value#', None)
self.assertDictEqual(pmi.get_all_user_metadata(make_copy=False), fm_mi)
# Check the unimplemented methods
self.assertRaises(NotImplementedError, lambda: 'foo' in pmi)
self.assertRaises(NotImplementedError, pmi.set, 'a', 'a')
self.assertRaises(NotImplementedError, pmi.set_identifiers, 'a', 'a')
self.assertRaises(NotImplementedError, pmi.set_identifier, 'a', 'a')
self.assertRaises(NotImplementedError, pmi.all_non_none_fields)
self.assertRaises(NotImplementedError, pmi.set_all_user_metadata, {})
self.assertRaises(NotImplementedError, pmi.set_user_metadata, 'a', {})
self.assertRaises(NotImplementedError, pmi.remove_stale_user_metadata, {})
self.assertRaises(NotImplementedError, pmi.template_to_attribute, {}, {})
self.assertRaises(NotImplementedError, pmi.smart_update, {})
# }}}
def test_marked_field(self): # {{{