diff --git a/src/pydase/utils/helpers.py b/src/pydase/utils/helpers.py index 998aee2..2c0db14 100644 --- a/src/pydase/utils/helpers.py +++ b/src/pydase/utils/helpers.py @@ -1,5 +1,6 @@ import inspect import logging +import re from collections.abc import Callable from itertools import chain from typing import Any @@ -7,6 +8,52 @@ from typing import Any logger = logging.getLogger(__name__) +def parse_full_access_path(path: str) -> list[str]: + """ + Splits a full access path into its atomic parts, separating attribute names, numeric + indices, and string keys within indices. + + The reverse function is given by `get_path_from_path_parts`. + + Args: + path: str + The full access path string to be split into components. + + Returns: + list[str] + A list of components that make up the path, including attribute names, + numeric indices, and string keys as separate elements. + """ + # | [] + # | [""] + # | [''] + pattern = r'\w+|\[\d+\]|\["[^"]*"\]|\[\'[^\']*\']' + return re.findall(pattern, path) + + +def get_path_from_path_parts(path_parts: list[str]) -> str: + """Creates the full access path from its atomic parts. + + The reverse function is given by `parse_full_access_path`. + + Args: + path_parts: list[str] + A list of components that make up the path, including attribute names, + numeric indices and string keys enclosed in square brackets as separate + elements. + Returns: + str + The full access path corresponding to the path_parts. + """ + + path = "" + for path_part in path_parts: + if not path_part.startswith("[") and path != "": + path += "." + path += path_part + return path + + def get_attribute_doc(attr: Any) -> str | None: """This function takes an input attribute attr and returns its documentation string if it's different from the documentation of its type, otherwise, diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index f6d47bd..80d4aba 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -4,11 +4,55 @@ import pydase import pytest from pydase.utils.helpers import ( get_object_attr_from_path, + get_path_from_path_parts, is_property_attribute, + parse_full_access_path, parse_keyed_attribute, ) +@pytest.mark.parametrize( + "full_access_path, expected", + [ + ("attr_name", ["attr_name"]), + ("parent.attr_name", ["parent", "attr_name"]), + ("nested.parent.attr_name", ["nested", "parent", "attr_name"]), + ("nested.parent.attr_name", ["nested", "parent", "attr_name"]), + ("attr_name[0]", ["attr_name", "[0]"]), + ("parent.attr_name[0]", ["parent", "attr_name", "[0]"]), + ("attr_name[0][1]", ["attr_name", "[0]", "[1]"]), + ('attr_name[0]["some_key"]', ["attr_name", "[0]", '["some_key"]']), + ( + 'dict_attr["some_key"].attr_name["other_key"]', + ["dict_attr", '["some_key"]', "attr_name", '["other_key"]'], + ), + ], +) +def test_parse_full_access_path(full_access_path: str, expected: list[str]) -> None: + assert parse_full_access_path(full_access_path) == expected + + +@pytest.mark.parametrize( + "path_parts, expected", + [ + (["attr_name"], "attr_name"), + (["parent", "attr_name"], "parent.attr_name"), + (["nested", "parent", "attr_name"], "nested.parent.attr_name"), + (["nested", "parent", "attr_name"], "nested.parent.attr_name"), + (["attr_name", "[0]"], "attr_name[0]"), + (["parent", "attr_name", "[0]"], "parent.attr_name[0]"), + (["attr_name", "[0]", "[1]"], "attr_name[0][1]"), + (["attr_name", "[0]", '["some_key"]'], 'attr_name[0]["some_key"]'), + ( + ["dict_attr", '["some_key"]', "attr_name", '["other_key"]'], + 'dict_attr["some_key"].attr_name["other_key"]', + ), + ], +) +def test_get_path_from_path_parts(path_parts: list[str], expected: str) -> None: + assert get_path_from_path_parts(path_parts) == expected + + @pytest.mark.parametrize( "attr_name, expected", [