From 34aa9c117ab102a08edbb5f711e3d050e2a59817 Mon Sep 17 00:00:00 2001 From: Joshua Monteiro Date: Tue, 28 Oct 2025 00:08:20 +1300 Subject: [PATCH 1/6] add new validator --- openhtf/util/validators.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openhtf/util/validators.py b/openhtf/util/validators.py index 7c1d83648..62d319936 100644 --- a/openhtf/util/validators.py +++ b/openhtf/util/validators.py @@ -440,6 +440,36 @@ def __ne__(self, other) -> bool: def matches_regex(regex): return RegexMatcher(regex, re.compile(regex)) +class MultiRegexMatcher(ValidatorBase): + def __init__(self, regex_list: list[str], compiled_list: list[re.Pattern]) -> None: + self.regex_list = regex_list + self._compiled_list = compiled_list + + def __call__(self, value: str) -> bool: + str_value = str(value) + for compiled_pattern in self._compiled_list: + if compiled_pattern.match(str_value) is not None: + return True + return False + + def __deepcopy__(self, dummy_memo): + return type(self)(self.regex_list[:], self._compiled_list[:]) + + def __str__(self): + patterns_str = " | ".join(self.regex_list) + return "'x' matches any of: /%s/" % patterns_str + + def __eq__(self, other): + return isinstance(other, type(self)) and self.regex_list == other.regex_list + + def __ne__(self, other) -> bool: + return not self == other + +@register +def matches_any_regex(regex_list: list[str]): + compiled_list = [re.compile(regex) for regex in regex_list] + return MultiRegexMatcher(regex_list, compiled_list) + class WithinPercent(RangeValidatorBase): """Validates that a number is within percent of a value.""" From 7acd832c684e8b959d64ce4c8b3609baba68ab42 Mon Sep 17 00:00:00 2001 From: Joshua Monteiro Date: Wed, 5 Nov 2025 23:02:30 +1300 Subject: [PATCH 2/6] use args as input --- openhtf/util/validators.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openhtf/util/validators.py b/openhtf/util/validators.py index 62d319936..520781fcf 100644 --- a/openhtf/util/validators.py +++ b/openhtf/util/validators.py @@ -440,6 +440,7 @@ def __ne__(self, other) -> bool: def matches_regex(regex): return RegexMatcher(regex, re.compile(regex)) +class MultiRegexMatcher(ValidatorBase): class MultiRegexMatcher(ValidatorBase): def __init__(self, regex_list: list[str], compiled_list: list[re.Pattern]) -> None: self.regex_list = regex_list @@ -466,10 +467,23 @@ def __ne__(self, other) -> bool: return not self == other @register -def matches_any_regex(regex_list: list[str]): - compiled_list = [re.compile(regex) for regex in regex_list] - return MultiRegexMatcher(regex_list, compiled_list) - +def matches_any_regex(*regex_lists: list[list[str]]): + """ + Creates a validator that checks if a value matches ANY of the provided regex patterns. + + Accepts one or more lists of regex strings. + Example: matches_any_regex(['a.*'], ['b.*', 'c.*']) + """ + # 1. Amalgamate all lists into a single flat list of regex strings + flat_regex_list = [] + for regex_list in regex_lists: + flat_regex_list.extend(regex_list) + + # 2. Compile the flat list of regex strings + compiled_list = [re.compile(regex) for regex in flat_regex_list] + + # 3. Pass both the original flat list and the compiled list to the matcher + return MultiRegexMatcher(flat_regex_list, compiled_list) class WithinPercent(RangeValidatorBase): """Validates that a number is within percent of a value.""" From c07764f2b69c61b79b66bf9b1b0476cae5f4348e Mon Sep 17 00:00:00 2001 From: Joshua Monteiro Date: Wed, 5 Nov 2025 23:04:46 +1300 Subject: [PATCH 3/6] fix typo --- openhtf/util/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhtf/util/validators.py b/openhtf/util/validators.py index 520781fcf..b519c016f 100644 --- a/openhtf/util/validators.py +++ b/openhtf/util/validators.py @@ -440,7 +440,7 @@ def __ne__(self, other) -> bool: def matches_regex(regex): return RegexMatcher(regex, re.compile(regex)) -class MultiRegexMatcher(ValidatorBase): + class MultiRegexMatcher(ValidatorBase): def __init__(self, regex_list: list[str], compiled_list: list[re.Pattern]) -> None: self.regex_list = regex_list From 464e2992d52f3f44bf994d7f7275acdb9a39dc21 Mon Sep 17 00:00:00 2001 From: Joshua Monteiro Date: Fri, 7 Nov 2025 23:04:27 +1300 Subject: [PATCH 4/6] add unit tests --- openhtf/util/validators.py | 16 ++++++--------- test/util/validators_test.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/openhtf/util/validators.py b/openhtf/util/validators.py index b519c016f..fe756bc00 100644 --- a/openhtf/util/validators.py +++ b/openhtf/util/validators.py @@ -446,10 +446,9 @@ def __init__(self, regex_list: list[str], compiled_list: list[re.Pattern]) -> No self.regex_list = regex_list self._compiled_list = compiled_list - def __call__(self, value: str) -> bool: - str_value = str(value) + def __call__(self, candidate_str: str) -> bool: for compiled_pattern in self._compiled_list: - if compiled_pattern.match(str_value) is not None: + if compiled_pattern.match(candidate_str): return True return False @@ -467,22 +466,19 @@ def __ne__(self, other) -> bool: return not self == other @register -def matches_any_regex(*regex_lists: list[list[str]]): +def matches_any_regex(*regex_lists: tuple[list[str]]): """ Creates a validator that checks if a value matches ANY of the provided regex patterns. - - Accepts one or more lists of regex strings. - Example: matches_any_regex(['a.*'], ['b.*', 'c.*']) """ - # 1. Amalgamate all lists into a single flat list of regex strings + flat_regex_list = [] for regex_list in regex_lists: + if not isinstance(regex_list, list) or not regex_list: + raise ValueError("Each argument must be a list of regex patterns.") flat_regex_list.extend(regex_list) - # 2. Compile the flat list of regex strings compiled_list = [re.compile(regex) for regex in flat_regex_list] - # 3. Pass both the original flat list and the compiled list to the matcher return MultiRegexMatcher(flat_regex_list, compiled_list) class WithinPercent(RangeValidatorBase): diff --git a/test/util/validators_test.py b/test/util/validators_test.py index f3060e9ca..539ac6b6f 100644 --- a/test/util/validators_test.py +++ b/test/util/validators_test.py @@ -78,6 +78,42 @@ def test_with_custom_type(self): self.assertEqual(test_validator.maximum, 0x12) +class TestSingleRegex(unittest.TestCase): + def test_single_regex(self): + pattern = r'^[A-Z]{3}\d{3}$' + validator = validators.matches_regex(pattern) + self.assertTrue(validator('ABC123')) + self.assertFalse(validator('abc123')) + self.assertFalse(validator('AB1234')) + self.assertFalse(validator('ABCD12')) + + +class TestMultipleRegex(unittest.TestCase): + patterns_1 = [r'^[A-Z]{1}\d{1}$', r'^[A-Z]{2}\d{2}$', r'^[A-Z]{3}\d{3}$'] + patterns_2 = [r'^\d{1}[A-Z]{1}$'] + def test_multiple_regex_lists(self): + + validator_1 = validators.matches_any_regex(TestMultipleRegex.patterns_1, TestMultipleRegex.patterns_2) + self.assertTrue(validator_1('ABC123')) + self.assertTrue(validator_1('A1')) + self.assertTrue(validator_1('1A')) + self.assertFalse(validator_1('123-ABCD')) + + def test_single_regex_list(self): + validator_2 = validators.matches_any_regex(TestMultipleRegex.patterns_2) + self.assertTrue(validator_2('1A')) + self.assertFalse(validator_2('A1')) + + def test_invalid_arguments(self): + with self.assertRaisesRegex(ValueError, "Each argument must be a list of regex patterns."): + validators.matches_any_regex(r'^[A-Z]{3}\d{3}$') + + with self.assertRaisesRegex(ValueError, "Each argument must be a list of regex patterns."): + validators.matches_any_regex([]) + + with self.assertRaisesRegex(ValueError, "Each argument must be a list of regex patterns."): + validators.matches_any_regex(r'd{3}', r'd{4}') + class TestAllInRange(unittest.TestCase): def setUp(self): @@ -395,3 +431,7 @@ def phase(test): phase_record = yield phase self.assertMeasurementFail(phase_record, 'pivot') + +if __name__ == '__main__': + from unittest import main + main() \ No newline at end of file From dc20406f9841585084f32accff72fda77fad34d3 Mon Sep 17 00:00:00 2001 From: Joshua Monteiro Date: Fri, 7 Nov 2025 23:07:25 +1300 Subject: [PATCH 5/6] remove main method --- test/util/validators_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/util/validators_test.py b/test/util/validators_test.py index 539ac6b6f..5c4e65dce 100644 --- a/test/util/validators_test.py +++ b/test/util/validators_test.py @@ -431,7 +431,3 @@ def phase(test): phase_record = yield phase self.assertMeasurementFail(phase_record, 'pivot') - -if __name__ == '__main__': - from unittest import main - main() \ No newline at end of file From 6eff82753eae346a899ebed9c9f6f33f73057755 Mon Sep 17 00:00:00 2001 From: Joshua Monteiro Date: Fri, 7 Nov 2025 23:13:15 +1300 Subject: [PATCH 6/6] fix type hint --- openhtf/util/validators.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openhtf/util/validators.py b/openhtf/util/validators.py index fe756bc00..d37cd7bad 100644 --- a/openhtf/util/validators.py +++ b/openhtf/util/validators.py @@ -466,19 +466,14 @@ def __ne__(self, other) -> bool: return not self == other @register -def matches_any_regex(*regex_lists: tuple[list[str]]): - """ - Creates a validator that checks if a value matches ANY of the provided regex patterns. - """ - +def matches_any_regex(*regex_collections: list[str]): flat_regex_list = [] - for regex_list in regex_lists: - if not isinstance(regex_list, list) or not regex_list: + for collection in regex_collections: + if not isinstance(collection, list) or not collection: raise ValueError("Each argument must be a list of regex patterns.") - flat_regex_list.extend(regex_list) - + flat_regex_list.extend(collection) + compiled_list = [re.compile(regex) for regex in flat_regex_list] - return MultiRegexMatcher(flat_regex_list, compiled_list) class WithinPercent(RangeValidatorBase):