diff --git a/aloe/__init__.py b/aloe/__init__.py index cbf3a8a6..fa28826e 100755 --- a/aloe/__init__.py +++ b/aloe/__init__.py @@ -16,6 +16,8 @@ after, around, before, + extended_step, + register_placeholder, step, ) diff --git a/aloe/registry.py b/aloe/registry.py index 08e97d55..6296c1f8 100644 --- a/aloe/registry.py +++ b/aloe/registry.py @@ -214,8 +214,12 @@ def load(self, sentence, func): self.steps[step_re.pattern] = (step_re, func) try: + if not getattr(func, 'patterns', None): + func.patterns = set() + + func.patterns.add(step_re.pattern) func.sentence = sentence - func.unregister = partial(self.unload, step_re.pattern) + func.unregister = partial(self.unload, *func.patterns) except AttributeError: # func might have been a bound method, no way to set attributes # on that @@ -223,10 +227,11 @@ def load(self, sentence, func): return func - def unload(self, sentence): + def unload(self, *sentences): """Remove a mapping for a given step sentence, if it exists.""" try: - del self.steps[sentence] + for sentence in sentences: + del self.steps[sentence] except KeyError: pass @@ -449,3 +454,111 @@ def decorator(self, function, **kwargs): around = CallbackDecorator(CALLBACK_REGISTRY, 'around') before = CallbackDecorator(CALLBACK_REGISTRY, 'before') # pylint:enable=invalid-name + + +class ExtendedStep(object): + """ + An extended step decorator that defines placeholders and allows to easily + assign multiple sentences to a single function. + Useful for sentences that capture a string which is enclosed either in + single or double quotes + """ + CLOSURE = '{%s}' + + def __init__(self): + self.single_expression_placeholders = { + 'NUMBER': r'(-?\d+(?:\.\d*)?)', + 'NON_CAPTURING_STRING': r'|'.join((r'"[^"]*"', r"'[^']*'")), + 'NON_CAPTURING_NUMBER': r'-?\d+(?:\.\d*)?', + } + self.multi_expression_placeholders = { + 'STRING': (r'"([^"]*)"', r"'([^']*)'"), + } + + def register_placeholder(self, placeholder, *args): + """Register a placeholder to be used on the extended_step.""" + + if len(args) > 1: + self.multi_expression_placeholders[placeholder] = args + else: + self.single_expression_placeholders[placeholder] = args[0] + + def _replace_placeholders(self, sentence): + """ + Replace placeholders with their associated expressions. + Returns a list with at least one sentences to the product of all + the expressions when one or more multiple-expressions are used. + `replace` is use instead of `format` as the latter interferes with + defining complex custom regexes that contains {}. + """ + + for placeholder, expression in \ + self.single_expression_placeholders.iteritems(): + sentence = sentence.replace(self.CLOSURE % placeholder, expression) + + sentences = [sentence] + + for placeholder in self.multi_expression_placeholders.keys(): + sentences = self._replace_multi_expression_placeholder( + sentences, placeholder + ) + + return sentences + + # pylint:disable=invalid-name + def _replace_multi_expression_placeholder(self, sentences, placeholder): + """ + Replace a placeholder that is associated with multiple expressions. + """ + + placeholder_str = self.CLOSURE % placeholder + + if placeholder_str not in sentences[0]: + return sentences + + expressions = self.multi_expression_placeholders[placeholder] + new_sentences = [] + + for expression in expressions: + for sentence in sentences: + new_sentences.append( + sentence.replace(placeholder_str, expression) + ) + + return new_sentences + # pylint:enable=invalid-name + + def extended_step(self, sentence): + """ + Creates one or multiple step definitions and associate them to the same + function. + The sentence can contain placeholders that are replaced by common + expressions/regexes. + A placeholder can be associated with multiple regexes creating in that + way multiple step definitions. + Placeholders are case sensitive and are enclosed in {}. + Common placeholders: + {STRING} + {NUMBER} + {NON_CAPTURING_STRING} + {NON_CAPTURING_NUMBER} + """ + + def decorator(func): + """Register a function as a step using the parsed sentence.""" + + sentences = self._replace_placeholders(sentence) + + for parsed_sentence in sentences: + step(parsed_sentence)(func) + + return decorator + + +EXTENDED_STEP = ExtendedStep() + +# These are functions, not constants +# pylint:disable=invalid-name +register_placeholder = EXTENDED_STEP.register_placeholder +extended_step = EXTENDED_STEP.extended_step +# pylint:enable=invalid-name diff --git a/tests/unit/test_registry.py b/tests/unit/test_registry.py index 24ffb5a6..ec5fb5d6 100644 --- a/tests/unit/test_registry.py +++ b/tests/unit/test_registry.py @@ -18,8 +18,10 @@ from aloe.registry import ( CallbackDecorator, CallbackDict, + ExtendedStep, PriorityClass, StepDict, + STEP_REGISTRY, ) from aloe.exceptions import ( StepLoadingError, @@ -267,6 +269,7 @@ def step(): # pylint:disable=missing-docstring steps.step(step.sentence)(step) assert_matches(steps, "My step 1", (step, ('1',), {})) + # pylint:enable=no-member class CallbackDictTest(unittest.TestCase): @@ -561,3 +564,102 @@ def prepare_hooks(): self.assertEqual([item for (item,) in sequence], [ 'wrapped', ]) + + +class ExtendedStepTest(unittest.TestCase): + """ + Test extended step. + """ + + def setUp(self): + self.extended_step = ExtendedStep() + self.single_expression_placeholders = \ + self.extended_step.single_expression_placeholders + self.multi_expression_placeholders = \ + self.extended_step.multi_expression_placeholders + + self.extended_step.register_placeholder('TEST1', 't1') + self.extended_step.register_placeholder('TEST2', 't2_1', 't2_2') + + def test_register_placeholder(self): + """Test registering placeholders.""" + + self.single_expression_placeholders.update({'TEST': 't1'}) + self.multi_expression_placeholders.update({'TEST2': ('t2_1', 't2_2')}) + + self.assertDictEqual( + self.extended_step.single_expression_placeholders, + self.single_expression_placeholders + ) + + self.assertDictEqual( + self.extended_step.multi_expression_placeholders, + self.multi_expression_placeholders + ) + + def test_replace_placeholders(self): + """Test replacing placeholders in string.""" + + sentences = [ + '{TEST1}', + '{TEST2}', + '{TEST1}-{TEST2}', + '{TEST1}-TEST2', + '{test1}-{test2}', + '{NUMBER}', + '{STRING}', + '{NON_CAPTURING_STRING}', + '{NON_CAPTURING_NUMBER}', + '{TEST2}-{STRING}', + ] + + results = [ + ('t1', ), + ('t2_1', 't2_2'), + ('t1-t2_1', 't1-t2_2'), + ('t1-TEST2', ), + ('{test1}-{test2}', ), + (r'(-?\d+(?:\.\d*)?)', ), + (r'"([^"]*)"', r"'([^']*)'"), + (r'|'.join((r'"[^"]*"', r"'[^']*'")), ), + (r'-?\d+(?:\.\d*)?', ), + (r't2_1-"([^"]*)"', + r't2_2-"([^"]*)"', + r"t2_1-'([^']*)'", + r"t2_2-'([^']*)'", + ) + ] + + for index, sentence in enumerate(sentences): + self.assertItemsEqual( + self.extended_step._replace_placeholders(sentence), # pylint:disable=protected-access + results[index] + ) + + def test_extended_step_func(self): + """Test extended_step function.""" + + def step(): # pylint:disable=missing-docstring + pass + + steps = STEP_REGISTRY + + # Load + self.extended_step.extended_step(r'My step {NUMBER}')(step) + + assert_matches(steps, "My step 1", (step, ('1',), {})) + + # Unload + step.unregister() # pylint:disable=no-member + + # Load + self.extended_step.extended_step(r'My step {STRING}')(step) + + assert_matches(steps, "My step 'one'", (step, ('one',), {})) + assert_matches(steps, 'My step "two"', (step, ('two',), {})) + + # Unload + step.unregister() # pylint:disable=no-member + + assert_no_match(steps, "My step 'one'") + assert_no_match(steps, 'My step "two"')