From 496dfdabcfce401219238c64a190802719aa90b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20M=2E=20Gimenez?= Date: Tue, 10 Sep 2024 18:24:34 +0200 Subject: [PATCH 1/5] Improve context storage to share data through whole run (#396) * Improve storage in context * Fix in changelog * add run storage * Fix in changelog * Update CHANGELOG.rst * fix linter * fix linter * Improve _get_initial_value_from_context * Improve _get_initial_value_from_context * Rename store_key_in_storage * Improve store_key_in_storage * Added test to check storage capabilities * Fix in test * Added test to check storage capabilities * Added test to check storage capabilities * Added test to check storage capabilities * Improved managing of storages using ChainMap * Update version and changelog * Debugging * Improved managing of storages when dynamic env are not used * Fix lint warning * Fix lint warning * Fix lint warning * Fix lint warning * Updated Changelog * Remove dict() from a ChainMap in before_feature --------- Co-authored-by: Jose Miguel Rodriguez Naranjo --- CHANGELOG.rst | 4 + VERSION | 3 +- toolium/behave/environment.py | 15 +++- .../utils/test_dataset_map_param_context.py | 73 +++++++++++++++++++ toolium/utils/dataset.py | 59 ++++++++++++--- 5 files changed, 137 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8599d043..2208938a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,10 @@ v3.1.6 *Release date: In development* +- Added `run_storage` to store information during the whole test execution +- ChainMap storages (context.storage, context.feature_storage and context.run_storage) +- In steps, be able to store values into desire storage by using [key], [FEATURE:key] and [RUN:key] + v3.1.5 ------ diff --git a/VERSION b/VERSION index 63300303..488d53f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1,2 @@ -3.1.6.dev0 +3.1.6.dev1 + diff --git a/toolium/behave/environment.py b/toolium/behave/environment.py index b6f66f09..57ba8f46 100644 --- a/toolium/behave/environment.py +++ b/toolium/behave/environment.py @@ -19,6 +19,7 @@ import logging import os import re +import collections from toolium.utils import dataset from toolium.config_files import ConfigFiles @@ -50,6 +51,13 @@ def before_all(context): context.global_status = {'test_passed': True} create_and_configure_wrapper(context) + # Dictionary to store information during the whole test execution + context.run_storage = dict() + context.storage = context.run_storage + + # Method in context to store values in context.storage, context.feature_storage or context.run_storage from steps + context.store_key_in_storage = dataset.store_key_in_storage + # Behave dynamic environment context.dyn_env = DynamicEnvironment(logger=context.logger) @@ -72,10 +80,9 @@ def before_feature(context, feature): no_driver = 'no_driver' in feature.tags start_driver(context, no_driver) - # Dictionary to store information between steps - context.storage = dict() # Dictionary to store information between features context.feature_storage = dict() + context.storage = collections.ChainMap(context.feature_storage, context.run_storage) # Behave dynamic environment context.dyn_env.get_steps_from_feature_description(feature.description) @@ -129,8 +136,8 @@ def before_scenario(context, scenario): context.logger.info("Running new scenario: %s", scenario.name) - # Make sure storage dict are empty in each scenario - context.storage = dict() + # Make sure context storage dict is empty in each scenario and merge with the rest of storages + context.storage = collections.ChainMap(dict(), context.feature_storage, context.run_storage) # Behave dynamic environment context.dyn_env.execute_before_scenario_steps(context) diff --git a/toolium/test/utils/test_dataset_map_param_context.py b/toolium/test/utils/test_dataset_map_param_context.py index 39873535..222d91ff 100644 --- a/toolium/test/utils/test_dataset_map_param_context.py +++ b/toolium/test/utils/test_dataset_map_param_context.py @@ -90,6 +90,79 @@ class Context(object): assert expected_st == result_st +def test_a_context_param_storage_and_run_storage(): + """ + Verification of a mapped parameter as CONTEXT saved in storage and run storage + """ + class Context(object): + pass + context = Context() + context.attribute = "attribute value" + context.storage = {"storage_key": "storage entry value"} + context.run_storage = {"storage_key": "run storage entry value"} + dataset.behave_context = context + + result_st = map_param("[CONTEXT:storage_key]") + expected_st = "storage entry value" + assert expected_st == result_st + + +def test_store_key_in_feature_storage(): + """ + Verification of method store_key_in_storage with a mapped parameter as FEATURE saved in feature storage + """ + class Context(object): + pass + context = Context() + context.attribute = "attribute value" + context.storage = {"storage_key": "storage entry value"} + context.feature_storage = {} + dataset.store_key_in_storage(context, "[FEATURE:storage_key]", "feature storage entry value") + dataset.behave_context = context + + result_st = map_param("[CONTEXT:storage_key]") + expected_st = "feature storage entry value" + assert expected_st == result_st + + +def test_store_key_in_run_storage(): + """ + Verification of method store_key_in_storage with a mapped parameter as RUN saved in run storage + """ + class Context(object): + pass + context = Context() + context.attribute = "attribute value" + context.storage = {"storage_key": "storage entry value"} + context.run_storage = {} + context.feature_storage = {} + dataset.store_key_in_storage(context, "[RUN:storage_key]", "run storage entry value") + dataset.behave_context = context + + result_st = map_param("[CONTEXT:storage_key]") + expected_st = "run storage entry value" + assert expected_st == result_st + + +def test_a_context_param_using_store_key_in_storage(): + """ + Verification of a mapped parameter as CONTEXT saved in storage and run storage + """ + class Context(object): + pass + context = Context() + context.attribute = "attribute value" + context.feature_storage = {} + context.storage = {"storage_key": "previous storage entry value"} + dataset.store_key_in_storage(context, "[FEATURE:storage_key]", "feature storage entry value") + dataset.store_key_in_storage(context, "[storage_key]", "storage entry value") + dataset.behave_context = context + + result_st = map_param("[CONTEXT:storage_key]") + expected_st = "storage entry value" + assert expected_st == result_st + + def test_a_context_param_without_storage_and_feature_storage(): """ Verification of a mapped parameter as CONTEXT when before_feature and before_scenario have not been executed, so diff --git a/toolium/utils/dataset.py b/toolium/utils/dataset.py index 2750f81c..42774160 100644 --- a/toolium/utils/dataset.py +++ b/toolium/utils/dataset.py @@ -682,19 +682,24 @@ def _get_initial_value_from_context(initial_key, context): :param context: behave context :return: mapped value """ - context_storage = context.storage if hasattr(context, 'storage') else {} - if hasattr(context, 'feature_storage'): - # context.feature_storage is initialized only when before_feature method is called - context_storage = collections.ChainMap(context.storage, context.feature_storage) + # If dynamic env is not initialized, the storages are initialized if needed + + context_storage = getattr(context, 'storage', {}) + run_storage = getattr(context, 'run_storage', {}) + feature_storage = getattr(context, 'feature_storage', {}) + + if not isinstance(context_storage, collections.ChainMap): + context_storage = collections.ChainMap(context_storage, run_storage, feature_storage) + if initial_key in context_storage: - value = context_storage[initial_key] - elif hasattr(context, initial_key): - value = getattr(context, initial_key) - else: - msg = f"'{initial_key}' key not found in context" - logger.error(msg) - raise Exception(msg) - return value + return context_storage[initial_key] + + if hasattr(context, initial_key): + return getattr(context, initial_key) + + msg = f"'{initial_key}' key not found in context" + logger.error(msg) + raise Exception(msg) def get_message_property(param, language_terms, language_key): @@ -813,3 +818,33 @@ def convert_file_to_base64(file_path): except Exception as e: raise Exception(f' ERROR - converting the "{file_path}" file to Base64...: {e}') return file_content + + +def store_key_in_storage(context, key, value): + """ + Store values in context.storage, context.feature_storage or context.run_storage, + using [key], [FEATURE:key] OR [RUN:key] from steps. + context.storage is also updated with given key,value + By default, values are stored in context.storage. + + :param key: key to store the value in proper storage + :param value: value to store in key + :param context: behave context + :return: + """ + clean_key = re.sub(r'[\[\]]', '', key) + if ":" in clean_key: + context_type = clean_key.split(":")[0] + context_key = clean_key.split(":")[1] + acccepted_context_types = ["FEATURE", "RUN"] + assert context_type in acccepted_context_types, (f"Invalid key: {context_key}. " + f"Accepted keys: {acccepted_context_types}") + if context_type == "RUN": + context.run_storage[context_key] = value + elif context_type == "FEATURE": + context.feature_storage[context_key] = value + # If dynamic env is not initialized linked or key exists in context.storage, the value is updated in it + if hasattr(context.storage, context_key) or not isinstance(context.storage, collections.ChainMap): + context.storage[context_key] = value + else: + context.storage[clean_key] = value From 1fa042a4a784e440329156be3cbb5a7708cf113a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= Date: Fri, 13 Sep 2024 12:19:27 +0200 Subject: [PATCH 2/5] Release Toolium 3.2.0 (#401) --- CHANGELOG.rst | 10 +++++----- VERSION | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2208938a..56e0b993 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,14 +1,14 @@ Toolium Changelog ================= -v3.1.6 +v3.2.0 ------ -*Release date: In development* +*Release date: 2024-09-13* -- Added `run_storage` to store information during the whole test execution -- ChainMap storages (context.storage, context.feature_storage and context.run_storage) -- In steps, be able to store values into desire storage by using [key], [FEATURE:key] and [RUN:key] +- Add `run_storage` dictionary to context to store information during the whole test execution +- Update current ChainMap context storages (context.storage, context.feature_storage and context.run_storage) +- Allow to store values from steps into desire storage by using [key], [FEATURE:key] and [RUN:key] v3.1.5 ------ diff --git a/VERSION b/VERSION index 488d53f0..944880fa 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1 @@ -3.1.6.dev1 - +3.2.0 From 7d868a128108a737266a39969b201730282fbc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= Date: Fri, 13 Sep 2024 12:32:45 +0200 Subject: [PATCH 3/5] Initialize 3.2.1.dev0 version (#402) --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 56e0b993..62f102ed 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Toolium Changelog ================= +v3.2.1 +------ + +*Release date: In development* + v3.2.0 ------ From 5699865a73497abda3ec5b78e0026f40dd2447ce Mon Sep 17 00:00:00 2001 From: Patricia Roman Sanchez <58507180+promans718@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:03:31 +0200 Subject: [PATCH 4/5] feat: improve and add new replacements to the dataset module (#403) * Update utils datasets functions * Fix linter issues * Fix linter issue C901 * Fix linter issues * Fix linter issues * Fix linter issues * Restore some changes * Restore some changes * Remove test * Remove test * Remove requirement * Fix linter issues * Fix tests issue * Update CHANGELOG.rst Co-authored-by: Pablo Guijarro * Update toolium/utils/dataset.py Co-authored-by: Pablo Guijarro * Fix pull request issues * Fix pull request issues * Restore incorrect pr requested change * Remove unrequired tests * Fix pull request issue * Fix typo * Fix linter issues * Fix test issue * Fix linter issues * Fix tests issue * Update CHANGELOG.rst Co-authored-by: Pablo Guijarro * Update toolium/test/utils/test_dataset_replace_param.py Co-authored-by: Pablo Guijarro * Update toolium/utils/dataset.py Co-authored-by: Pablo Guijarro * Fix pull request issue * Fix pull request issue * Fix linter issues * Fix linter issues * Fix pull request issues * Fix issue * Fix linter issues * Fix fuction issue * Fix issue * Fix pull request issues * Fix pull request issues * Update toolium/test/utils/test_dataset_map_param_context.py Co-authored-by: Pablo Guijarro * Update toolium/test/utils/test_dataset_replace_param.py Co-authored-by: Pablo Guijarro * Reorder tests * Restore missing tests * Fix pull request issue * Fix pull request issue * Include json option to list element * Fix tests issue * Fix pull request issue --------- Co-authored-by: Pablo Guijarro --- .gitignore | 1 + CHANGELOG.rst | 5 ++ .../utils/test_dataset_map_param_context.py | 59 ++++++++++++- .../test/utils/test_dataset_replace_param.py | 40 +++++++++ toolium/utils/dataset.py | 82 +++++++++++++++---- 5 files changed, 168 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index bdc60f50..c4c0c5af 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ target .vscode # virtualenv +.venv venv/ VENV/ ENV/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62f102ed..b00f317c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,11 @@ v3.2.1 ------ *Release date: In development* +- Allow negative indexes for list elements in context searches. Example: [CONTEXT:element.-1] +- Add support for JSON strings to the `DICT` and `LIST`` replacement. Example: [DICT:{"key": true}], [LIST:[null]] +- Add `REPLACE` replacement, to replace a substring with another. Example: [REPLACE:[CONTEXT:some_url]::https::http] +- Add `TITLE` replacement, to apply Python's title() function. Example: [TITLE:the title] +- Add `ROUND` replacement, float number to a string with the indicated number of decimals. Example: [ROUND:3.3333::2] v3.2.0 ------ diff --git a/toolium/test/utils/test_dataset_map_param_context.py b/toolium/test/utils/test_dataset_map_param_context.py index 222d91ff..8b8aeca6 100644 --- a/toolium/test/utils/test_dataset_map_param_context.py +++ b/toolium/test/utils/test_dataset_map_param_context.py @@ -429,6 +429,64 @@ class Context(object): assert map_param("[CONTEXT:list.cmsScrollableActions.1.id]") == 'ask-for-qa' +def test_a_context_param_list_correct_negative_index(): + """ + Verification of a list with a correct negative index (In bounds) as CONTEXT + """ + class Context(object): + pass + context = Context() + + context.list = { + 'cmsScrollableActions': [ + { + 'id': 'ask-for-duplicate', + 'text': 'QA duplica' + }, + { + 'id': 'ask-for-qa', + 'text': 'QA no duplica' + }, + { + 'id': 'ask-for-negative', + 'text': 'QA negative index' + } + ] + } + dataset.behave_context = context + assert map_param("[CONTEXT:list.cmsScrollableActions.-1.id]") == 'ask-for-negative' + assert map_param("[CONTEXT:list.cmsScrollableActions.-3.id]") == 'ask-for-duplicate' + + +def test_a_context_param_list_incorrect_negative_index(): + """ + Verification of a list with a incorrect negative index (In bounds) as CONTEXT + """ + class Context(object): + pass + context = Context() + + context.list = { + 'cmsScrollableActions': [ + { + 'id': 'ask-for-duplicate', + 'text': 'QA duplica' + }, + { + 'id': 'ask-for-qa', + 'text': 'QA no duplica' + }, + { + 'id': 'ask-for-negative', + 'text': 'QA negative index' + } + ] + } + with pytest.raises(Exception) as excinfo: + map_param("[CONTEXT:list.cmsScrollableActions.-5.id]") + assert "the expression '-5' was not able to select an element in the list" == str(excinfo.value) + + def test_a_context_param_list_oob_index(): """ Verification of a list with an incorrect index (Out of bounds) as CONTEXT @@ -511,7 +569,6 @@ def __init__(self): context.list = ExampleClass() dataset.behave_context = context - print(context) with pytest.raises(Exception) as excinfo: map_param("[CONTEXT:list.cmsScrollableActions.prueba.id]") assert "the expression 'prueba' was not able to select an element in the list" == str(excinfo.value) diff --git a/toolium/test/utils/test_dataset_replace_param.py b/toolium/test/utils/test_dataset_replace_param.py index ce5361f8..716caa08 100644 --- a/toolium/test/utils/test_dataset_replace_param.py +++ b/toolium/test/utils/test_dataset_replace_param.py @@ -333,6 +333,20 @@ def test_replace_param_now_offsets_with_and_without_format_and_more(): assert param == f'The date {offset_date} was yesterday and I have an appointment at {offset_datetime}' +def test_replace_param_round_with_type_inference(): + param = replace_param('[ROUND:7.5::2]') + assert param == 7.5 + param = replace_param('[ROUND:3.33333333::3]') + assert param == 3.333 + + +def test_replace_param_round_without_type_inference(): + param = replace_param('[ROUND:7.500::2]', infer_param_type=False) + assert param == '7.50' + param = replace_param('[ROUND:3.33333333::3]', infer_param_type=False) + assert param == '3.333' + + def test_replace_param_str_int(): param = replace_param('[STR:28]') assert isinstance(param, str) @@ -369,12 +383,22 @@ def test_replace_param_list_strings(): assert param == ['1', '2', '3'] +def test_replace_param_list_json_format(): + param = replace_param('[LIST:["value", true, null]]') + assert param == ["value", True, None] + + def test_replace_param_dict(): param = replace_param("[DICT:{'a':'test1','b':'test2','c':'test3'}]") assert isinstance(param, dict) assert param == {'a': 'test1', 'b': 'test2', 'c': 'test3'} +def test_replace_param_dict_json_format(): + param = replace_param('[DICT:{"key": "value", "key_2": true, "key_3": null}]') + assert param == {"key": "value", "key_2": True, "key_3": None} + + def test_replace_param_upper(): param = replace_param('[UPPER:test]') assert param == 'TEST' @@ -438,3 +462,19 @@ def test_replace_param_partial_string_with_length(): assert param == 'aaaaa is string' param = replace_param('parameter [STRING_WITH_LENGTH_5] is string') assert param == 'parameter aaaaa is string' + + +def test_replace_param_replace(): + param = replace_param('[REPLACE:https://url.com::https::http]') + assert param == "http://url.com" + param = replace_param('[REPLACE:https://url.com::https://]') + assert param == "url.com" + + +def test_replace_param_title(): + param = replace_param('[TITLE:hola hola]') + assert param == "Hola Hola" + param = replace_param('[TITLE:holahola]') + assert param == "Holahola" + param = replace_param('[TITLE:hOlA]') + assert param == "HOlA" diff --git a/toolium/utils/dataset.py b/toolium/utils/dataset.py index 42774160..58ab089d 100644 --- a/toolium/utils/dataset.py +++ b/toolium/utils/dataset.py @@ -81,6 +81,7 @@ def replace_param(param, language='es', infer_param_type=True): [NOW(%Y-%m-%dT%H:%M:%SZ) - 7 DAYS] Similar to NOW but seven days before and with the indicated format [TODAY] Similar to NOW without time; the format depends on the language [TODAY + 2 DAYS] Similar to NOW, but two days later + [ROUND:xxxx::y] Generates a string from a float number (xxxx) with the indicated number of decimals (y) [STR:xxxx] Cast xxxx to a string [INT:xxxx] Cast xxxx to an int [FLOAT:xxxx] Cast xxxx to a float @@ -88,6 +89,8 @@ def replace_param(param, language='es', infer_param_type=True): [DICT:xxxx] Cast xxxx to a dict [UPPER:xxxx] Converts xxxx to upper case [LOWER:xxxx] Converts xxxx to lower case + [REPLACE:xxxxx::yy::zz] Replace elements in string. Example: [REPLACE:[CONTEXT:some_url]::https::http] + [TITLE:xxxxx] Apply .title() to string value. Example: [TITLE:the title] If infer_param_type is True and the result of the replacement process is a string, this function also tries to infer and cast the result to the most appropriate data type, attempting first the direct conversion to a Python built-in data type and then, @@ -181,7 +184,7 @@ def _replace_param_replacement(param, language): """ Replace param with a new param value. Available replacements: [EMPTY], [B], [UUID], [RANDOM], [RANDOM_PHONE_NUMBER], - [TIMESTAMP], [DATETIME], [NOW], [TODAY] + [TIMESTAMP], [DATETIME], [NOW], [TODAY], [ROUND:xxxxx::d] :param param: parameter value :param language: language to configure date format for NOW and TODAY @@ -200,7 +203,8 @@ def _replace_param_replacement(param, language): '[TIMESTAMP]': str(int(datetime.datetime.timestamp(datetime.datetime.utcnow()))), '[DATETIME]': str(datetime.datetime.utcnow()), '[NOW]': str(datetime.datetime.utcnow().strftime(date_format)), - '[TODAY]': str(datetime.datetime.utcnow().strftime(date_day_format)) + '[TODAY]': str(datetime.datetime.utcnow().strftime(date_day_format)), + r'\[ROUND:(.*?)::(\d*)\]': _get_rounded_float_number } # append date expressions found in param to the replacement dict @@ -210,14 +214,28 @@ def _replace_param_replacement(param, language): new_param = param param_replaced = False - for key in replacements.keys(): - if key in new_param: - new_value = replacements[key]() if isfunction(replacements[key]) else replacements[key] - new_param = new_param.replace(key, new_value) + for key, value in replacements.items(): + if key.startswith('['): # tags without placeholders + if key in new_param: + new_value = value() if isfunction(value) else value + new_param = new_param.replace(key, new_value) + param_replaced = True + elif match := re.search(key, new_param): # tags with placeholders + new_value = value(match) # a function to parse the values is always required + new_param = new_param.replace(match.group(), new_value) param_replaced = True return new_param, param_replaced +def _get_rounded_float_number(match): + """ + Round float number with the expected decimals + :param match: match object of the regex for this transformation: [ROUND:(.*?)::(d*)] + :return: float as string with the expected decimals + """ + return f"{round(float(match.group(1)), int(match.group(2))):.{int(match.group(2))}f}" + + def _get_random_phone_number(): # Method to avoid executing data generator when it is not needed return DataGenerator().phone_number @@ -226,31 +244,58 @@ def _get_random_phone_number(): def _replace_param_transform_string(param): """ Transform param value according to the specified prefix. - Available transformations: DICT, LIST, INT, FLOAT, STR, UPPER, LOWER + Available transformations: DICT, LIST, INT, FLOAT, STR, UPPER, LOWER, REPLACE, TITLE :param param: parameter value :return: tuple with replaced value and boolean to know if replacement has been done """ - type_mapping_regex = r'\[(DICT|LIST|INT|FLOAT|STR|UPPER|LOWER):(.*)\]' + type_mapping_regex = r'\[(DICT|LIST|INT|FLOAT|STR|UPPER|LOWER|REPLACE|TITLE):([\w\W]*)\]' type_mapping_match_group = re.match(type_mapping_regex, param) new_param = param param_transformed = False if type_mapping_match_group: param_transformed = True - if type_mapping_match_group.group(1) == 'STR': - new_param = type_mapping_match_group.group(2) - elif type_mapping_match_group.group(1) in ['LIST', 'DICT', 'INT', 'FLOAT']: - exec('exec_param = {type}({value})'.format(type=type_mapping_match_group.group(1).lower(), - value=type_mapping_match_group.group(2))) + if type_mapping_match_group.group(1) in ['DICT', 'LIST']: + try: + new_param = json.loads(type_mapping_match_group.group(2).strip()) + except json.decoder.JSONDecodeError: + new_param = eval(type_mapping_match_group.group(2)) + elif type_mapping_match_group.group(1) in ['INT', 'FLOAT']: + exec(f'exec_param = {type_mapping_match_group.group(1).lower()}({type_mapping_match_group.group(2)})') new_param = locals()['exec_param'] - elif type_mapping_match_group.group(1) == 'UPPER': - new_param = type_mapping_match_group.group(2).upper() - elif type_mapping_match_group.group(1) == 'LOWER': - new_param = type_mapping_match_group.group(2).lower() + else: + replace_param = _get_substring_replacement(type_mapping_match_group) + new_param = new_param.replace(type_mapping_match_group.group(), replace_param) return new_param, param_transformed +def _get_substring_replacement(type_mapping_match_group): + """ + Transform param value according to the specified prefix. + Available transformations: STR, UPPER, LOWER, REPLACE, TITLE + + :param type_mapping_match_group: match group + :return: return the string with the replaced param + """ + if type_mapping_match_group.group(1) == 'STR': + replace_param = type_mapping_match_group.group(2) + elif type_mapping_match_group.group(1) == 'UPPER': + replace_param = type_mapping_match_group.group(2).upper() + elif type_mapping_match_group.group(1) == 'LOWER': + replace_param = type_mapping_match_group.group(2).lower() + elif type_mapping_match_group.group(1) == 'REPLACE': + params_to_replace = type_mapping_match_group.group(2).split('::') + replace_param = params_to_replace[2] if len(params_to_replace) > 2 else '' + param_to_replace = params_to_replace[1] if params_to_replace[1] != '\\n' else '\n' + param_to_replace = params_to_replace[1] if params_to_replace[1] != '\\r' else '\r' + replace_param = params_to_replace[0].replace(param_to_replace, replace_param) + elif type_mapping_match_group.group(1) == 'TITLE': + replace_param = "".join(map(min, zip(type_mapping_match_group.group(2), + type_mapping_match_group.group(2).title()))) + return replace_param + + def _replace_param_date(param, language): """ Transform param value in a date after applying the specified delta. @@ -611,7 +656,8 @@ def get_value_from_context(param, context): if isinstance(value, dict) and part in value: value = value[part] # evaluate if in an array, access is requested by index - elif isinstance(value, list) and part.isdigit() and int(part) < len(value): + elif isinstance(value, list) and part.lstrip('-+').isdigit() \ + and abs(int(part)) < (len(value) + 1 if part.startswith("-") else len(value)): value = value[int(part)] # or by a key=value expression elif isinstance(value, list) and (element := _select_element_in_list(value, part)): From d9312f28ab67e8e0d62d5fa699c48f918fcab1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= Date: Mon, 25 Nov 2024 16:27:15 +0100 Subject: [PATCH 5/5] fix: remove accents from file names (#406) --- CHANGELOG.rst | 2 ++ toolium/test/utils/test_path_utils.py | 1 + toolium/utils/path_utils.py | 6 +++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b00f317c..7a3cb9bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,11 +5,13 @@ v3.2.1 ------ *Release date: In development* + - Allow negative indexes for list elements in context searches. Example: [CONTEXT:element.-1] - Add support for JSON strings to the `DICT` and `LIST`` replacement. Example: [DICT:{"key": true}], [LIST:[null]] - Add `REPLACE` replacement, to replace a substring with another. Example: [REPLACE:[CONTEXT:some_url]::https::http] - Add `TITLE` replacement, to apply Python's title() function. Example: [TITLE:the title] - Add `ROUND` replacement, float number to a string with the indicated number of decimals. Example: [ROUND:3.3333::2] +- Remove accents from generated file names to avoid errors in some filesystems v3.2.0 ------ diff --git a/toolium/test/utils/test_path_utils.py b/toolium/test/utils/test_path_utils.py index b3a8c820..b8149fa5 100644 --- a/toolium/test/utils/test_path_utils.py +++ b/toolium/test/utils/test_path_utils.py @@ -30,6 +30,7 @@ ('successful login -- @1.1 john.doe', 'successful_login_1_1_john_doe'), ('successful login -- @1.2 Mark options: {Length=10 Mark=mark File=file_name.jpg}', 'successful_login_1_2_Mark_options___Length_10_Mark_mark_File_file_name_jpg'), + ('successful login -- @1.3 acción', 'successful_login_1_3_accion'), ) diff --git a/toolium/utils/path_utils.py b/toolium/utils/path_utils.py index a6af05bb..010f3015 100644 --- a/toolium/utils/path_utils.py +++ b/toolium/utils/path_utils.py @@ -16,9 +16,10 @@ limitations under the License. """ -from os import makedirs import errno import re +import unicodedata +from os import makedirs FILENAME_MAX_LENGTH = 100 @@ -34,6 +35,9 @@ def get_valid_filename(s, max_length=FILENAME_MAX_LENGTH): """ s = str(s).strip().replace(' -- @', '_') s = re.sub(r'(?u)[^-\w]', '_', s).strip('_') + # Remove accents to avoid errors in some filesystems + nfkd_form = unicodedata.normalize('NFKD', s) + s = ''.join([c for c in nfkd_form if not unicodedata.combining(c)]) return s[:max_length]