diff --git a/src/datastore.py b/src/datastore.py index c6f200bb081ac6ad5ab7661d5cc4419098ca0c1d..348f28549e716a30605ff2be5894fd5b03b4dc95 100644 --- a/src/datastore.py +++ b/src/datastore.py @@ -2,7 +2,8 @@ __author__ = 'Lukas Leufen' __date__ = '2019-11-22' -from typing import Any, List +from typing import Any, List, Tuple + from abc import ABC @@ -109,7 +110,7 @@ class DataStoreByVariable(AbstractDataStore): self._store[name] = {} self._store[name][scope] = obj - def get(self, name: str, scope: str, depth: int = None) -> Any: + def get(self, name: str, scope: str) -> Any: """ Retrieve an object with `name` from `scope`. If no object can be found in the exact scope, take an iterative look on the levels above. Raises a NameNotFoundInDataStore error, if no object with given name can be found in @@ -117,23 +118,24 @@ class DataStoreByVariable(AbstractDataStore): given scope and its levels above (could be either included in another scope or a more detailed sub-scope). :param name: Name to look for :param scope: scope to search the name for - :param depth: counter to check, if all roots of the scope have been visited to trigger an error. :return: the stored object """ - if depth is None: - depth = scope.count(".") - if depth >= 0: + return self._stride_through_scopes(name, scope)[2] + + def _stride_through_scopes(self, name, scope, depth=0): + if depth <= scope.count("."): + local_scope = scope.rsplit(".", maxsplit=depth)[0] try: - return self._store[name][scope] + return name, local_scope, self._store[name][local_scope] except KeyError: - return self.get(name, scope.rsplit(".", maxsplit=depth)[0], depth-1) + return self._stride_through_scopes(name, scope, depth + 1) else: - try: - occurrences = self.search_name(name) - raise NameNotFoundInScope(f"Couldn't find {name} in scope {scope}. {name} is only defined in " - f"{occurrences}") - except KeyError: + occurrences = self.search_name(name) + if len(occurrences) == 0: raise NameNotFoundInDataStore(f"Couldn't find {name} in data store") + else: + raise NameNotFoundInScope(f"Couldn't find {name} in scope {scope} . {name} is only defined in " + f"{occurrences}") def search_name(self, name: str) -> List[str]: """ @@ -141,23 +143,47 @@ class DataStoreByVariable(AbstractDataStore): :param name: Name to look for :return: list with all scopes and sub-scopes containing an object stored as `name` """ - return sorted(self._store[name]) + return sorted(self._store[name] if name in self._store.keys() else []) - def search_scope(self, scope: str) -> List[str]: + def search_scope(self, scope: str, current_scope_only=True, return_all=False) -> List[str or Tuple]: """ - Search for given `scope` and list all object names stored under this scope. + Search for given `scope` and list all object names stored under this scope. To look also for all superior scopes + set `current_scope_only=False`. To return the scope and the object's value too, set `return_all=True`. :param scope: scope to look for - :return: list with all object names + :param current_scope_only: look only for all names for given scope if true, else search for names from superior + scopes too. + :param return_all: return name, definition scope and value if True, else just the name + :return: list with all object names (if `return_all=False`) or list with tuple of object name, object scope and + object value ordered by name (if `return_all=True`) """ - names = [] - for (k, v) in self._store.items(): - if scope in v.keys(): - names.append(k) - if len(names) > 0: - return sorted(names) + if current_scope_only: + names = [] + for (k, v) in self._store.items(): + if scope in v.keys(): + names.append(k) + if len(names) > 0: + if return_all: + return sorted([(name, scope, self._store[name][scope]) for name in names], key=lambda tup: tup[0]) + else: + return sorted(names) + else: + raise EmptyScope(f"Given scope {scope} is not part of the data store. Available scopes are: " + f"{self.list_all_scopes()}") else: - raise EmptyScope(f"Given scope {scope} is not part of the data store. Available scopes are: " - f"{self.list_all_scopes()}") + results = [] + for name in self.list_all_names(): + try: + res = self._stride_through_scopes(name, scope) + if return_all: + results.append(res) + else: + results.append(res[0]) + except (NameNotFoundInDataStore, NameNotFoundInScope): + pass + if return_all: + return sorted(results, key=lambda tup: tup[0]) + else: + return sorted(results) def list_all_scopes(self) -> List[str]: """ @@ -171,6 +197,13 @@ class DataStoreByVariable(AbstractDataStore): scopes.append(scope) return sorted(scopes) + def list_all_names(self) -> List[str]: + """ + List all names available in the data store. + :return: all names + """ + return sorted(self._store.keys()) + class DataStoreByScope(AbstractDataStore): @@ -200,7 +233,7 @@ class DataStoreByScope(AbstractDataStore): self._store[scope] = {} self._store[scope][name] = obj - def get(self, name: str, scope: str, depth: int = None) -> Any: + def get(self, name: str, scope: str) -> Any: """ Retrieve an object with `name` from `scope`. If no object can be found in the exact scope, take an iterative look on the levels above. Raises a NameNotFoundInDataStore error, if no object with given name can be found in @@ -208,26 +241,25 @@ class DataStoreByScope(AbstractDataStore): given scope and its levels above (could be either included in another scope or a more detailed sub-scope). :param name: Name to look for :param scope: scope to search the name for - :param depth: counter to check, if all roots of the scope have been visited to trigger an error. :return: the stored object """ - if depth is None: - depth = scope.count(".") - if depth >= 0: + return self._stride_through_scopes(name, scope)[2] + + def _stride_through_scopes(self, name, scope, depth=0): + if depth <= scope.count("."): + local_scope = scope.rsplit(".", maxsplit=depth)[0] try: - return self._store[scope][name] + return name, local_scope, self._store[local_scope][name] except KeyError: - return self.get(name, scope.rsplit(".", maxsplit=1)[0], depth-1) + return self._stride_through_scopes(name, scope, depth + 1) else: occurrences = self.search_name(name) if len(occurrences) == 0: raise NameNotFoundInDataStore(f"Couldn't find {name} in data store") else: - raise NameNotFoundInScope(f"Couldn't find {name} in scope {scope}. {name} is only defined in " + raise NameNotFoundInScope(f"Couldn't find {name} in scope {scope} . {name} is only defined in " f"{occurrences}") - - def search_name(self, name: str) -> List[str]: """ Search for all occurrences of given `name` in the entire data store. @@ -240,17 +272,41 @@ class DataStoreByScope(AbstractDataStore): keys.append(key) return sorted(keys) - def search_scope(self, scope: str, current_scope_only=True) -> List[str]: + def search_scope(self, scope: str, current_scope_only: bool = True, return_all: bool = False) -> List[str or Tuple]: """ - Search for given `scope` and list all object names stored under this scope. + Search for given `scope` and list all object names stored under this scope. To look also for all superior scopes + set `current_scope_only=False`. To return the scope and the object's value too, set `return_all=True`. :param scope: scope to look for - :return: list with all object names + :param current_scope_only: look only for all names for given scope if true, else search for names from superior + scopes too. + :param return_all: return name, definition scope and value if True, else just the name + :return: list with all object names (if `return_all=False`) or list with tuple of object name, object scope and + object value ordered by name (if `return_all=True`) """ - try: - return sorted(self._store[scope].keys()) - except KeyError: - raise EmptyScope(f"Given scope {scope} is not part of the data store. Available scopes are: " - f"{self.list_all_scopes()}") + if current_scope_only: + try: + if return_all: + return [(name, scope, self._store[scope][name]) for name in sorted(self._store[scope].keys())] + else: + return sorted(self._store[scope].keys()) + except KeyError: + raise EmptyScope(f"Given scope {scope} is not part of the data store. Available scopes are: " + f"{self.list_all_scopes()}") + else: + results = [] + for name in self.list_all_names(): + try: + res = self._stride_through_scopes(name, scope) + if return_all: + results.append(res) + else: + results.append(res[0]) + except (NameNotFoundInDataStore, NameNotFoundInScope): + pass + if return_all: + return sorted(results, key=lambda tup: tup[0]) + else: + return sorted(results) def list_all_scopes(self) -> List[str]: """ @@ -259,3 +315,17 @@ class DataStoreByScope(AbstractDataStore): """ return sorted(self._store.keys()) + def list_all_names(self) -> List[str]: + """ + List all names available in the data store. + :return: all names + """ + names = [] + scopes = self.list_all_scopes() + for scope in scopes: + for name in self._store[scope].keys(): + if name not in names: + names.append(name) + return sorted(names) + + diff --git a/test/test_datastore.py b/test/test_datastore.py index 1434508b6d033aa0e915ace6bcc1eafb0febe638..5ba76bf8fc9c21553723cba3b2125be2d758e23b 100644 --- a/test/test_datastore.py +++ b/test/test_datastore.py @@ -58,12 +58,13 @@ class TestDataStoreByVariable: def test_raise_not_in_scope(self, ds): ds.put("number", 11, "general.sub") with pytest.raises(NameNotFoundInScope) as e: - ds.get("number", "general") - assert "Couldn't find number in scope general. number is only defined in ['general.sub']" in e.value.args[0] + ds.get("number", "general.sub2") + assert "Couldn't find number in scope general.sub2 . number is only defined in ['general.sub']" in e.value.args[0] def test_list_all_scopes(self, ds): ds.put("number", 22, "general2") ds.put("number", 11, "general.sub") + ds.put("number2", 2, "general.sub") ds.put("number", 3, "general.sub3") ds.put("number", 1, "general") assert ds.list_all_scopes() == ['general', 'general.sub', 'general.sub3', 'general2'] @@ -83,6 +84,40 @@ class TestDataStoreByVariable: assert "Given scope general.sub2 is not part of the data store." in e.value.args[0] assert "Available scopes are: ['general.sub', 'general2']" in e.value.args[0] + def test_list_all_names(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + assert ds.list_all_names() == ["number", "number1", "number2"] + + def test_search_scope_and_all_superiors(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + assert ds.search_scope("general.sub", current_scope_only=False) == ["number", "number1"] + assert ds.search_scope("general.sub.sub", current_scope_only=False) == ["number", "number1", "number2"] + + def test_search_scope_return_all(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + assert ds.search_scope("general.sub", return_all=True) == [("number", "general.sub", 11), + ("number1", "general.sub", 22)] + + def test_search_scope_and_all_superiors_return_all(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + ds.put("number", "ABC", "general.sub.sub") + assert ds.search_scope("general.sub", current_scope_only=False, return_all=True) == \ + [("number", "general.sub", 11), ("number1", "general.sub", 22)] + assert ds.search_scope("general.sub.sub", current_scope_only=False, return_all=True) == \ + [("number", "general.sub.sub", "ABC"), ("number1", "general.sub", 22), ("number2", "general.sub.sub", 3)] + class TestDataStoreByScope: @@ -125,12 +160,13 @@ class TestDataStoreByScope: def test_raise_not_in_scope(self, ds): ds.put("number", 11, "general.sub") with pytest.raises(NameNotFoundInScope) as e: - ds.get("number", "general") - assert "Couldn't find number in scope general. number is only defined in ['general.sub']" in e.value.args[0] + ds.get("number", "general.sub2") + assert "Couldn't find number in scope general.sub2 . number is only defined in ['general.sub']" in e.value.args[0] def test_list_all_scopes(self, ds): ds.put("number", 22, "general2") ds.put("number", 11, "general.sub") + ds.put("number2", 2, "general.sub") ds.put("number", 3, "general.sub3") ds.put("number", 1, "general") assert ds.list_all_scopes() == ['general', 'general.sub', 'general.sub3', 'general2'] @@ -150,6 +186,13 @@ class TestDataStoreByScope: assert "Given scope general.sub2 is not part of the data store." in e.value.args[0] assert "Available scopes are: ['general.sub', 'general2']" in e.value.args[0] + def test_list_all_names(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + assert ds.list_all_names() == ["number", "number1", "number2"] + def test_search_scope_and_all_superiors(self, ds): ds.put("number", 22, "general") ds.put("number", 11, "general.sub") @@ -158,3 +201,22 @@ class TestDataStoreByScope: assert ds.search_scope("general.sub", current_scope_only=False) == ["number", "number1"] assert ds.search_scope("general.sub.sub", current_scope_only=False) == ["number", "number1", "number2"] + def test_search_scope_return_all(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + assert ds.search_scope("general.sub", return_all=True) == [("number", "general.sub", 11), + ("number1", "general.sub", 22)] + + def test_search_scope_and_all_superiors_return_all(self, ds): + ds.put("number", 22, "general") + ds.put("number", 11, "general.sub") + ds.put("number1", 22, "general.sub") + ds.put("number2", 3, "general.sub.sub") + ds.put("number", "ABC", "general.sub.sub") + assert ds.search_scope("general.sub", current_scope_only=False, return_all=True) == \ + [("number", "general.sub", 11), ("number1", "general.sub", 22)] + assert ds.search_scope("general.sub.sub", current_scope_only=False, return_all=True) == \ + [("number", "general.sub.sub", "ABC"), ("number1", "general.sub", 22), ("number2", "general.sub.sub", 3)] +