-
Notifications
You must be signed in to change notification settings - Fork 18
Extend step decorator #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ | |
| after, | ||
| around, | ||
| before, | ||
| extended_step, | ||
| register_placeholder, | ||
| step, | ||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,19 +214,24 @@ 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 | ||
| pass | ||
|
|
||
| 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still looking through the implementation, but I'd really prefer to do what Behave does and register a new sentence parser instead of throwing everything into regular expressions. |
||
| """ | ||
| 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"'[^']*'")), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are these two for? |
||
| 'NON_CAPTURING_NUMBER': r'-?\d+(?:\.\d*)?', | ||
| } | ||
| self.multi_expression_placeholders = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are wrapping everything with regexes, there's no need for this |
||
| '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(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hope this is not needed after single/multi stuff is merged. |
||
| 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a comprehension.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, use a comprehension every time you do: foo = []
for ... in ...:
foo.append(...) |
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Functions docstrings should be imperative ("Fire the missiles", not "This fires the missiles").
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, this is a decorator, which the docs fail to mention. |
||
| 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Common" can be interpreted as "most often used - there are more, this is a sample". |
||
| {STRING} | ||
| {NUMBER} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't find the documentation of what does this do. (I suspect I know, but would be nice to see anyway.) |
||
| {NON_CAPTURING_STRING} | ||
| {NON_CAPTURING_NUMBER} | ||
| """ | ||
|
|
||
| def decorator(func): | ||
| """Register a function as a step using the parsed sentence.""" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sentences, perhaps? |
||
|
|
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes API stink. (What if you add another way of parsing steps, A better way would be to do |
||
| # pylint:enable=invalid-name | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.""" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does not test anything useful. How can I actually use those |
||
|
|
||
| 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.""" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not test private methods. This does not show me how to use the custom placeholders, and does not guarantee that they will actually work (no one says |
||
|
|
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not the same way as all the other tests? |
||
|
|
||
| # Load | ||
| self.extended_step.extended_step(r'My step {NUMBER}')(step) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how the custom placeholders should be tested. |
||
|
|
||
| assert_matches(steps, "My step 1", (step, ('1',), {})) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, so it doesn't even convert the number to a number? |
||
|
|
||
| # Unload | ||
| step.unregister() # pylint:disable=no-member | ||
|
|
||
| # Load | ||
| self.extended_step.extended_step(r'My step {STRING}')(step) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just register two different steps and don't bother with unregistering. |
||
|
|
||
| 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"') | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good! Is there a test for this? Without the extended steps, just registering a function twice and calling
unregistershould unregister both.However, where are
patternsused?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with the
extended_stepit can register the function to two different sentences:Will register the
loginfunction with the following sentences (one for single quotes the other for double quotes):There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand what this is doing. But where is the
patternsattribute used, other than right here, where it can be replaced by a local variable?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I see it's accumulated on the function. Ignore.