From b290c19ed59a1ead7472efb5d02c8bfdae6f837e Mon Sep 17 00:00:00 2001 From: Dan Porter Date: Mon, 24 Aug 2026 18:22:54 +0100 Subject: [PATCH] use bracket counting ## eval_functions.py - remove regex for OR expressions (a|b) as this was causing issues with brakets. - replace with a new function *find_or_expressions()* that returns unique inner expression - update *replace_expression_vars()* and *prepare_expression()* to use this new function. - add additional tests to check this fix. - Fix for #61 All tests pass. --- src/hdfmap/__init__.py | 4 +-- src/hdfmap/eval_functions.py | 64 +++++++++++++++++++++++++----------- tests/test_edge_cases.py | 17 ++++++++++ tests/test_eval_functions.py | 5 +++ 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/src/hdfmap/__init__.py b/src/hdfmap/__init__.py index 67eabc5..a3fdc06 100644 --- a/src/hdfmap/__init__.py +++ b/src/hdfmap/__init__.py @@ -66,8 +66,8 @@ 'set_all_logging_level', 'version_info', 'module_info' ] -__version__ = "1.3.0" -__date__ = "2026/08/14" +__version__ = "1.4.0" +__date__ = "2026/08/24" __author__ = "Dan Porter" diff --git a/src/hdfmap/eval_functions.py b/src/hdfmap/eval_functions.py index 9ec12b8..af1d708 100644 --- a/src/hdfmap/eval_functions.py +++ b/src/hdfmap/eval_functions.py @@ -26,7 +26,6 @@ re_long_floats = re.compile(r'\d+\.\d{5,}') # finds floats with long trailing decimals re_dataset_attributes = re.compile(r'([a-zA-Z_]\w*)@([a-zA-Z_]\w*)') # finds 'name@attribute' in expressions re_dataset_default = re.compile(r'(\w+)\?\((.+?)\)') # finds 'name?('noname'), return (name, 'noname') -re_dataset_alternate = re.compile(r'\((\w\S*\|\w\S*)\)') # finds '(name1|name2|name3)', return 'name1|name2|name3' # fromisoformat requires python 3.11+ datetime_converter = np.vectorize(lambda x: datetime.datetime.fromisoformat(x.decode() if hasattr(x, 'decode') else x)) @@ -239,6 +238,34 @@ def find_identifiers(expression: str) -> list[str]: return [name for name in asteval.get_ast_names(ast.parse(expression))] +def find_or_expressions(expr: str) -> set[str]: + """ + Returns instances of OR-expressions within the expression, + + An OR-expression is given as '(a|b|c)' + + Brackets are counted correctly so only the OR-expression will be returned. + + :param expr: string expression + :return: set of unique string expressions + """ + stack = [] + alternates = [] + + for i, ch in enumerate(expr): + if ch == '(': + stack.append(i) + elif ch == ')': + if not stack: + continue + start = stack.pop() + sub_str = expr[start:i + 1] + if '|' in sub_str: + alternates.append(sub_str) + stack.clear() + return set(alternates) + + def replace_expression_vars(expr: str, mapping: dict[str, str]) -> str: """ Replace variable names in an expression @@ -249,23 +276,20 @@ def replace_expression_vars(expr: str, mapping: dict[str, str]) -> str: :param mapping: mapping from variable names to replacements :return: replaced expression """ - # Remove (alternate|names) if any part would be replaced - hidden = [] - def hide(match): - sub_str = match.group(0) - if any(name in sub_str for name in mapping): - hidden.append(match.group(0)) - return f"__ALT{len(hidden)-1}__" - return sub_str - expr = re_dataset_alternate.sub(hide, expr) + # Find OR expressions (a|b), don't replace anything in these + sub_expr = find_or_expressions(expr) + + for n, sub_str in enumerate(sub_expr): + expr = expr.replace(sub_str, f"__ALT{n}__") + # Replace names in mapping for name, repl in mapping.items(): pattern = r'\b' + re.escape(name) + r'\b' expr = re.sub(pattern, repl, expr) # recover original expression parts - for ii, match in enumerate(hidden): - expr = expr.replace(f"__ALT{ii}__", match) + for n, sub_str in enumerate(sub_expr): + expr = expr.replace(f"__ALT{n}__", sub_str) return expr @@ -383,14 +407,14 @@ def prepare_expression(hdf_file: h5py.File, expression: str, hdf_namespace: dict else: expression = expression.replace(match.group(), name) # find alternate names '(opt1|opt2|opt3)' - for alt_names in re_dataset_alternate.findall(expression): # alt_names = 'opt1|opt2|opt3' - names = alt_names.split('|') - # first available name in hdf_namespace or last name - name = next( - (n for n in names if n in attributes), - next((n for n in names if hdf_namespace.get(n, '') in hdf_file), names[-1]) - ) - expression = expression.replace(f"({alt_names})", name) # replace parentheses + while or_expressions := find_or_expressions(expression): + for or_exp in or_expressions: + names = or_exp.strip('()').split('|') + name = next( + (n for n in names if n in attributes), + next((n for n in names if hdf_namespace.get(n, '') in hdf_file), names[-1]) + ) + expression = expression.replace(or_exp, name) return expression diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 4308d7d..6925aff 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -2,6 +2,7 @@ import json import hdfmap import hdfmap.hdf_loader +from pytest import approx from . import only_dls_file_system @@ -171,3 +172,19 @@ def test_i06_pol_scan(): assert len(axes_paths) == len(axes_names) # assert axes_names[1] == 'ds' + +@only_dls_file_system +def test_complex_eval(): + f = '/dls/science/groups/das/ExampleData/hdfmap_tests/i16/1109527.nxs' + m = hdfmap.create_nexus_map(f) + + m.add_named_expression(**{ + '_t': '(count_time|counttime|t?(1.0))', + '_cmd': '(cmd|user_input_command|user_command|scan_command)', + 'cmd': '(cmd|user_input_command|user_command|scan_command)', + }) + + with m.load_hdf() as hdf: + assert m.eval(hdf, '_cmd') == 'flyscancn eta_fly 0.005 61 pil3_100k 0.1 0.5 roi1 roi2' + assert m.eval(hdf, 'max(signal / Transmission / (rc/300.) / _t)') == approx(1215483134.5953412) + assert m.eval(hdf, 'cmd') == 'flyscancn eta_fly 0.005 61 pil3_100k 0.1 0.5 roi1 roi2' \ No newline at end of file diff --git a/tests/test_eval_functions.py b/tests/test_eval_functions.py index da4e7db..97a819f 100644 --- a/tests/test_eval_functions.py +++ b/tests/test_eval_functions.py @@ -22,6 +22,11 @@ def test_replace_expression_vars(): new_expr = replace_expression_vars(expression, mapping) assert new_expr == "(cmd|command|scan_command?(''))\nstr((cmd|command|scan_command?('no_cmd')))\npath" + expression = "max(_t)" + mapping = {'_t': '(count_time|counttime|t?(1.0))'} + new_expr = replace_expression_vars(expression, mapping) + assert new_expr == "max((count_time|counttime|t?(1.0)))" + def test_prepare_expression_load_data(): expression = 'idgap@units, x*y'