-
-
Notifications
You must be signed in to change notification settings - Fork 53
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
Add named groups for python #316
base: main
Are you sure you want to change the base?
Changes from all commits
abdb16a
f9a7afb
ea7cef3
0efab69
51a3c34
8272b12
a51f9de
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 |
---|---|---|
@@ -1,42 +1,52 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Optional, List | ||
from typing import Optional, List, Tuple | ||
|
||
from cucumber_expressions.group import Group | ||
from cucumber_expressions.parameter_type import ParameterType | ||
from cucumber_expressions.tree_regexp import TreeRegexp | ||
from cucumber_expressions.tree_regexp import TreeRegexp, Group | ||
from cucumber_expressions.errors import CucumberExpressionError | ||
|
||
|
||
class Argument: | ||
def __init__(self, group, parameter_type): | ||
self._group: Group = group | ||
self.parameter_type: ParameterType = parameter_type | ||
def __init__( | ||
self, group: Group, parameter_type: ParameterType, name: Optional[str] | ||
): | ||
self.group = group | ||
self.parameter_type = parameter_type | ||
self.name = name | ||
|
||
@staticmethod | ||
def build( | ||
tree_regexp: TreeRegexp, text: str, parameter_types: List | ||
tree_regexp: TreeRegexp, | ||
text: str, | ||
parameter_types_and_names: List[Tuple[ParameterType, Optional[str]]], | ||
) -> Optional[List[Argument]]: | ||
# Check if all elements in parameter_types_and_names are tuples | ||
for item in parameter_types_and_names: | ||
if not isinstance(item, tuple) or len(item) != 2: | ||
raise CucumberExpressionError( | ||
f"Expected a tuple of (ParameterType, Optional[str]), but got {type(item)}: {item}" | ||
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 error is very technical. What should a user do if/when they encounter this error? 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 do most users know what a tuple is? 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. Good point - I'll have a think - this was mainly done for my benefit when debugging! 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. Tuple is a common type used in Python - the type itself should make sense, but I'll review for clarity 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. Syntax: |
||
) | ||
|
||
match_group = tree_regexp.match(text) | ||
if not match_group: | ||
return None | ||
|
||
arg_groups = match_group.children | ||
|
||
if len(arg_groups) != len(parameter_types): | ||
if len(arg_groups) != len(parameter_types_and_names): | ||
param_count = len(parameter_types_and_names) | ||
raise CucumberExpressionError( | ||
f"Group has {len(arg_groups)} capture groups, but there were {len(parameter_types)} parameter types" | ||
f"Group has {len(arg_groups)} capture groups, but there were {param_count} parameter types/names" | ||
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. think the ending of this error shouldn't be amended - the issue is still that there were an incorrect number of parameter types (The names being present / not is irrelevant for the length issue) |
||
) | ||
|
||
return [ | ||
Argument(arg_group, parameter_type) | ||
for parameter_type, arg_group in zip(parameter_types, arg_groups) | ||
Argument(arg_group, parameter_type, parameter_name) | ||
for (parameter_type, parameter_name), arg_group in zip( | ||
parameter_types_and_names, arg_groups | ||
) | ||
] | ||
|
||
@property | ||
def value(self): | ||
return self.parameter_type.transform(self.group.values if self.group else None) | ||
|
||
@property | ||
def group(self): | ||
return self._group |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,34 @@ | ||
from typing import Optional, List | ||
from typing import Optional, List, Tuple | ||
|
||
from cucumber_expressions.argument import Argument | ||
from cucumber_expressions.ast import Node, NodeType | ||
from cucumber_expressions.expression_parser import CucumberExpressionParser | ||
from cucumber_expressions.parameter_type import ParameterType | ||
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry | ||
from cucumber_expressions.tree_regexp import TreeRegexp | ||
from cucumber_expressions.errors import ( | ||
UndefinedParameterTypeError, | ||
ParameterIsNotAllowedInOptional, | ||
OptionalIsNotAllowedInOptional, | ||
OptionalMayNotBeEmpty, | ||
AlternativeMayNotBeEmpty, | ||
AlternativeMayNotExclusivelyContainOptionals, | ||
UndefinedParameterTypeError, | ||
) | ||
|
||
ESCAPE_PATTERN = rb"([\\^\[({$.|?*+})\]])" | ||
|
||
|
||
class CucumberExpression: | ||
def __init__(self, expression, parameter_type_registry): | ||
def __init__(self, expression: str, parameter_type_registry: ParameterTypeRegistry): | ||
self.expression = expression | ||
self.parameter_type_registry = parameter_type_registry | ||
self.parameter_types: List[ParameterType] = [] | ||
self.parameter_types_and_names: List[Tuple[ParameterType, Optional[str]]] = [] | ||
self.tree_regexp = TreeRegexp( | ||
self.rewrite_to_regex(CucumberExpressionParser().parse(self.expression)) | ||
) | ||
|
||
def match(self, text: str) -> Optional[List[Argument]]: | ||
return Argument.build(self.tree_regexp, text, self.parameter_types) | ||
return Argument.build(self.tree_regexp, text, self.parameter_types_and_names) | ||
|
||
@property | ||
def source(self): | ||
|
@@ -57,23 +58,21 @@ def rewrite_to_regex(self, node: Node): | |
def escape_regex(expression) -> str: | ||
return expression.translate({i: "\\" + chr(i) for i in ESCAPE_PATTERN}) | ||
|
||
def rewrite_optional(self, node: Node): | ||
_possible_node_with_params = self.get_possible_node_with_parameters(node) | ||
if _possible_node_with_params: | ||
def rewrite_optional(self, node: Node) -> str: | ||
if self.get_possible_node_with_parameters(node): | ||
raise ParameterIsNotAllowedInOptional( | ||
_possible_node_with_params, self.expression | ||
self.get_possible_node_with_parameters(node), self.expression | ||
) | ||
_possible_node_with_optionals = self.get_possible_node_with_optionals(node) | ||
if _possible_node_with_optionals: | ||
if self.get_possible_node_with_optionals(node): | ||
raise OptionalIsNotAllowedInOptional( | ||
_possible_node_with_optionals, self.expression | ||
self.get_possible_node_with_optionals(node), self.expression | ||
) | ||
if self.are_nodes_empty(node): | ||
raise OptionalMayNotBeEmpty(node, self.expression) | ||
regex = "".join([self.rewrite_to_regex(_node) for _node in node.nodes]) | ||
return rf"(?:{regex})?" | ||
|
||
def rewrite_alternation(self, node: Node): | ||
def rewrite_alternation(self, node: Node) -> str: | ||
for alternative in node.nodes: | ||
if not alternative.nodes: | ||
raise AlternativeMayNotBeEmpty(alternative, self.expression) | ||
|
@@ -87,20 +86,34 @@ def rewrite_alternation(self, node: Node): | |
def rewrite_alternative(self, node: Node): | ||
return "".join([self.rewrite_to_regex(_node) for _node in node.nodes]) | ||
|
||
def rewrite_parameter(self, node: Node): | ||
def rewrite_parameter(self, node: Node) -> str: | ||
name = node.text | ||
parameter_type = self.parameter_type_registry.lookup_by_type_name(name) | ||
group_name, parameter_type = self.parse_parameter_name(name) | ||
|
||
if not parameter_type: | ||
raise UndefinedParameterTypeError(node, self.expression, name) | ||
|
||
self.parameter_types.append(parameter_type) | ||
self.parameter_types_and_names.append((parameter_type, group_name)) | ||
|
||
regexps = parameter_type.regexps | ||
if len(regexps) == 1: | ||
return rf"({regexps[0]})" | ||
return rf"((?:{')|(?:'.join(regexps)}))" | ||
|
||
def parse_parameter_name( | ||
self, name: str | ||
) -> Tuple[Optional[str], Optional[ParameterType]]: | ||
"""Helper function to parse the parameter name and return group_name and parameter_type.""" | ||
if ":" in name: | ||
group_name, parameter_type_name = [part.strip() for part in name.split(":")] | ||
parameter_type = self.parameter_type_registry.lookup_by_type_name( | ||
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 allows for the empty group name, which is distinct from the 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. It might also be worth while to push this into the parser. |
||
parameter_type_name | ||
) | ||
else: | ||
group_name = None | ||
parameter_type = self.parameter_type_registry.lookup_by_type_name(name) | ||
return group_name, parameter_type | ||
|
||
def rewrite_expression(self, node: Node): | ||
regex = "".join([self.rewrite_to_regex(_node) for _node in node.nodes]) | ||
return rf"^{regex}$" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import re | ||
|
||
from cucumber_expressions.expression import CucumberExpression | ||
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry | ||
from cucumber_expressions.regular_expression import RegularExpression | ||
|
||
CURLY_BRACKET_PATTERN = re.compile(r"{(.*?)}") | ||
INVALID_CURLY_PATTERN = re.compile(r"^\d+(?:,\d+)?$") | ||
|
||
|
||
class ExpressionFactory: | ||
def __init__( | ||
self, parameter_type_registry: ParameterTypeRegistry = ParameterTypeRegistry() | ||
): | ||
self.parameter_type_registry = parameter_type_registry | ||
|
||
@staticmethod | ||
def _has_curly_brackets(string: str) -> bool: | ||
return "{" in string and "}" in string | ||
|
||
@staticmethod | ||
def _extract_text_in_curly_brackets(string: str) -> list: | ||
return CURLY_BRACKET_PATTERN.findall(string) | ||
|
||
def is_cucumber_expression(self, expression_string: str): | ||
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 don't think this check is simple enough. The primary constraint is explaining to people what is and is not a cucumber expression. For Java I eventually settled on requiring that all regular expressions start with This helps avoid a situation where a user makes a mistake in a Cucumber expression, causing Cucumber to think it is a regular expressions and then fail because the regular expression also isn't valid and results in a very cryptic error message. 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. Is it worth standardising this check then across all flavours? I have no idea what we do in ruby as I've not dug into this stuff since the initial release some 4/5 years ago 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. The problem is that I cannot think of an easy way to distinguish a regex from a normal string in Python - the ^$ syntax isn't used, and are generally just strings. Hence, I thought to try and identify the other way around - seeing if it's a Cucumber Expression. I realise now looking at it that just looking for curly bracket pairs as a discriminator for Cucumber Expressions doesn't fly, so this will need to be fixed. I was a bit stuck here as I couldn't think of a reliable deterministic manner to distinguish between the two types, so input very welcome! 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 think that you can still look for the
Pytest-BDD and other python BDD frameworks, when no specific parser is specified (default), they should check if |
||
if not self._has_curly_brackets(expression_string): | ||
return False | ||
bracket_texts = self._extract_text_in_curly_brackets(expression_string) | ||
# Check if any match does not contain an integer or an integer and a comma | ||
for text in bracket_texts: | ||
# Check if the match is a regex pattern (matches integer or integer-comma pattern) | ||
if INVALID_CURLY_PATTERN.match(text): | ||
return False # Found a form of curly bracket | ||
return True # All curly brackets are valid | ||
|
||
def create_expression(self, expression_string: str): | ||
if self.is_cucumber_expression(expression_string): | ||
return CucumberExpression(expression_string, self.parameter_type_registry) | ||
return RegularExpression(expression_string, self.parameter_type_registry) |
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.
should this maybe be
parameter_types_with_names
which then would make sense because the name could often be nil (Which feels "right")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 point!