Skip to content

Commit abdb16a

Browse files
committed
First commit: add named groups for python
1 parent d779966 commit abdb16a

21 files changed

+369
-314
lines changed

python/cucumber_expressions/argument.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
11
from __future__ import annotations
22

3-
from typing import Optional, List
3+
from typing import Optional
44

5-
from cucumber_expressions.group import Group
65
from cucumber_expressions.parameter_type import ParameterType
7-
from cucumber_expressions.tree_regexp import TreeRegexp
6+
from cucumber_expressions.tree_regexp import TreeRegexp, Group
87
from cucumber_expressions.errors import CucumberExpressionError
98

109

1110
class Argument:
12-
def __init__(self, group, parameter_type):
13-
self._group: Group = group
14-
self.parameter_type: ParameterType = parameter_type
11+
def __init__(self, group: Group, parameter_type: ParameterType, name: Optional[str]):
12+
self.group = group
13+
self.parameter_type = parameter_type
14+
self.name = name
1515

1616
@staticmethod
1717
def build(
18-
tree_regexp: TreeRegexp, text: str, parameter_types: List
19-
) -> Optional[List[Argument]]:
18+
tree_regexp: TreeRegexp, text: str, parameter_types_and_names: list[tuple[ParameterType, Optional[str]]]
19+
) -> Optional[list[Argument]]:
20+
# Check if all elements in parameter_types_and_names are tuples
21+
for item in parameter_types_and_names:
22+
if not isinstance(item, tuple) or len(item) != 2:
23+
raise CucumberExpressionError(
24+
f"Expected a tuple of (ParameterType, Optional[str]), but got {type(item)}: {item}"
25+
)
26+
2027
match_group = tree_regexp.match(text)
2128
if not match_group:
2229
return None
2330

2431
arg_groups = match_group.children
2532

26-
if len(arg_groups) != len(parameter_types):
33+
if len(arg_groups) != len(parameter_types_and_names):
2734
raise CucumberExpressionError(
28-
f"Group has {len(arg_groups)} capture groups, but there were {len(parameter_types)} parameter types"
35+
f"Group has {len(arg_groups)} capture groups, but there were {len(parameter_types_and_names)} parameter types/names"
2936
)
3037

3138
return [
32-
Argument(arg_group, parameter_type)
33-
for parameter_type, arg_group in zip(parameter_types, arg_groups)
39+
Argument(arg_group, parameter_type, parameter_name)
40+
for (parameter_type, parameter_name), arg_group in zip(parameter_types_and_names, arg_groups)
3441
]
3542

3643
@property
3744
def value(self):
3845
return self.parameter_type.transform(self.group.values if self.group else None)
39-
40-
@property
41-
def group(self):
42-
return self._group

python/cucumber_expressions/ast.py

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from enum import Enum
4-
from typing import Optional, List
4+
from typing import Optional, Any
55

66

77
class NodeType(Enum):
@@ -41,44 +41,24 @@ class Node:
4141
def __init__(
4242
self,
4343
ast_type: NodeType,
44-
nodes: Optional[List[Node]],
44+
nodes: Optional[list[Node]],
4545
token: Optional[str],
4646
start: int,
4747
end: int,
4848
):
4949
if nodes is None and token is None:
5050
raise Exception("Either nodes or token must be defined")
51-
self._ast_type = ast_type
52-
self._nodes = nodes
53-
self._token = token
54-
self._start = start
55-
self._end = end
56-
57-
@property
58-
def ast_type(self) -> NodeType:
59-
return self._ast_type
60-
61-
@property
62-
def nodes(self) -> List[Node]:
63-
return self._nodes
64-
65-
@property
66-
def token(self) -> str:
67-
return self._token
68-
69-
@property
70-
def start(self) -> int:
71-
return self._start
72-
73-
@property
74-
def end(self) -> int:
75-
return self._end
51+
self.ast_type = ast_type
52+
self.nodes = nodes
53+
self.token = token
54+
self.start = start
55+
self.end = end
7656

7757
@property
7858
def text(self) -> str:
7959
return self.token or "".join([node_value.text for node_value in self.nodes])
8060

81-
def to_json(self):
61+
def to_json(self) -> dict[str, Any]:
8262
json_obj = {"type": self.ast_type.value}
8363
if self.nodes is not None:
8464
json_obj["nodes"] = [node_value.to_json() for node_value in self.nodes]
@@ -91,26 +71,10 @@ def to_json(self):
9171

9272
class Token:
9373
def __init__(self, ast_type: TokenType, text: str, start: int, end: int):
94-
self._ast_type = ast_type
95-
self._text = text
96-
self._start = start
97-
self._end = end
98-
99-
@property
100-
def ast_type(self):
101-
return self._ast_type
102-
103-
@property
104-
def text(self):
105-
return self._text
106-
107-
@property
108-
def start(self):
109-
return self._start
110-
111-
@property
112-
def end(self):
113-
return self._end
74+
self.ast_type = ast_type
75+
self.text = text
76+
self.start = start
77+
self.end = end
11478

11579
@staticmethod
11680
def is_escape_character(char: str) -> bool:
@@ -140,7 +104,7 @@ def type_of(char: str) -> TokenType:
140104
return TokenType.TEXT
141105

142106
@staticmethod
143-
def symbol_of(token: TokenType):
107+
def symbol_of(token: TokenType) -> str:
144108
possible_token_character_key = token.name + "_CHARACTER"
145109
if any(
146110
e.name
@@ -151,7 +115,7 @@ def symbol_of(token: TokenType):
151115
return ""
152116

153117
@staticmethod
154-
def purpose_of(token: TokenType):
118+
def purpose_of(token: TokenType) -> str:
155119
if token in [TokenType.BEGIN_OPTIONAL, TokenType.END_OPTIONAL]:
156120
return "optional text"
157121
if token in [TokenType.BEGIN_PARAMETER, TokenType.END_PARAMETER]:
@@ -160,7 +124,7 @@ def purpose_of(token: TokenType):
160124
return "alternation"
161125
return ""
162126

163-
def to_json(self):
127+
def to_json(self) -> dict[str, Any]:
164128
return {
165129
"type": self.ast_type.value,
166130
"text": self.text,

python/cucumber_expressions/combinatorial_generated_expression_factory.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from typing import List
2-
31
from cucumber_expressions.generated_expression import GeneratedExpression
42
from cucumber_expressions.parameter_type import ParameterType
53

@@ -12,16 +10,16 @@ def __init__(self, expression_template, parameter_type_combinations):
1210
self.expression_template = expression_template
1311
self.parameter_type_combinations = parameter_type_combinations
1412

15-
def generate_expressions(self) -> List[GeneratedExpression]:
13+
def generate_expressions(self) -> list[GeneratedExpression]:
1614
generated_expressions = []
1715
self.generate_permutations(generated_expressions, 0, [])
1816
return generated_expressions
1917

2018
def generate_permutations(
2119
self,
22-
generated_expressions: List[GeneratedExpression],
20+
generated_expressions: list[GeneratedExpression],
2321
depth: int,
24-
current_parameter_types: List[ParameterType],
22+
current_parameter_types: list[ParameterType],
2523
):
2624
if len(generated_expressions) >= MAX_EXPRESSIONS:
2725
return

python/cucumber_expressions/expression.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
1-
from typing import Optional, List
1+
from typing import Optional
22

33
from cucumber_expressions.argument import Argument
44
from cucumber_expressions.ast import Node, NodeType
55
from cucumber_expressions.expression_parser import CucumberExpressionParser
66
from cucumber_expressions.parameter_type import ParameterType
7+
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
78
from cucumber_expressions.tree_regexp import TreeRegexp
89
from cucumber_expressions.errors import (
9-
UndefinedParameterTypeError,
1010
ParameterIsNotAllowedInOptional,
1111
OptionalIsNotAllowedInOptional,
1212
OptionalMayNotBeEmpty,
1313
AlternativeMayNotBeEmpty,
14-
AlternativeMayNotExclusivelyContainOptionals,
14+
AlternativeMayNotExclusivelyContainOptionals, UndefinedParameterTypeError,
1515
)
1616

1717
ESCAPE_PATTERN = rb"([\\^\[({$.|?*+})\]])"
1818

1919

2020
class CucumberExpression:
21-
def __init__(self, expression, parameter_type_registry):
21+
def __init__(self, expression: str, parameter_type_registry: ParameterTypeRegistry):
2222
self.expression = expression
2323
self.parameter_type_registry = parameter_type_registry
24-
self.parameter_types: List[ParameterType] = []
24+
self.parameter_types_and_names: list[tuple[ParameterType, Optional[str]]] = []
2525
self.tree_regexp = TreeRegexp(
2626
self.rewrite_to_regex(CucumberExpressionParser().parse(self.expression))
2727
)
2828

29-
def match(self, text: str) -> Optional[List[Argument]]:
30-
return Argument.build(self.tree_regexp, text, self.parameter_types)
29+
def match(self, text: str) -> Optional[list[Argument]]:
30+
return Argument.build(self.tree_regexp, text, self.parameter_types_and_names)
3131

3232
@property
3333
def source(self):
@@ -57,23 +57,17 @@ def rewrite_to_regex(self, node: Node):
5757
def escape_regex(expression) -> str:
5858
return expression.translate({i: "\\" + chr(i) for i in ESCAPE_PATTERN})
5959

60-
def rewrite_optional(self, node: Node):
61-
_possible_node_with_params = self.get_possible_node_with_parameters(node)
62-
if _possible_node_with_params:
63-
raise ParameterIsNotAllowedInOptional(
64-
_possible_node_with_params, self.expression
65-
)
66-
_possible_node_with_optionals = self.get_possible_node_with_optionals(node)
67-
if _possible_node_with_optionals:
68-
raise OptionalIsNotAllowedInOptional(
69-
_possible_node_with_optionals, self.expression
70-
)
60+
def rewrite_optional(self, node: Node) -> str:
61+
if self.get_possible_node_with_parameters(node):
62+
raise ParameterIsNotAllowedInOptional(self.get_possible_node_with_parameters(node), self.expression)
63+
if self.get_possible_node_with_optionals(node):
64+
raise OptionalIsNotAllowedInOptional(self.get_possible_node_with_optionals(node), self.expression)
7165
if self.are_nodes_empty(node):
7266
raise OptionalMayNotBeEmpty(node, self.expression)
7367
regex = "".join([self.rewrite_to_regex(_node) for _node in node.nodes])
7468
return rf"(?:{regex})?"
7569

76-
def rewrite_alternation(self, node: Node):
70+
def rewrite_alternation(self, node: Node) -> str:
7771
for alternative in node.nodes:
7872
if not alternative.nodes:
7973
raise AlternativeMayNotBeEmpty(alternative, self.expression)
@@ -87,20 +81,30 @@ def rewrite_alternation(self, node: Node):
8781
def rewrite_alternative(self, node: Node):
8882
return "".join([self.rewrite_to_regex(_node) for _node in node.nodes])
8983

90-
def rewrite_parameter(self, node: Node):
84+
def rewrite_parameter(self, node: Node) -> str:
9185
name = node.text
92-
parameter_type = self.parameter_type_registry.lookup_by_type_name(name)
86+
group_name, parameter_type = self.parse_parameter_name(name)
9387

9488
if not parameter_type:
9589
raise UndefinedParameterTypeError(node, self.expression, name)
9690

97-
self.parameter_types.append(parameter_type)
91+
self.parameter_types_and_names.append((parameter_type, group_name))
9892

9993
regexps = parameter_type.regexps
10094
if len(regexps) == 1:
10195
return rf"({regexps[0]})"
10296
return rf"((?:{')|(?:'.join(regexps)}))"
10397

98+
def parse_parameter_name(self, name: str) -> tuple[Optional[str], Optional[ParameterType]]:
99+
"""Helper function to parse the parameter name and return group_name and parameter_type."""
100+
if ":" in name:
101+
group_name, parameter_type_name = name.split(":")
102+
parameter_type = self.parameter_type_registry.lookup_by_type_name(parameter_type_name)
103+
else:
104+
group_name = None
105+
parameter_type = self.parameter_type_registry.lookup_by_type_name(name)
106+
return group_name, parameter_type
107+
104108
def rewrite_expression(self, node: Node):
105109
regex = "".join([self.rewrite_to_regex(_node) for _node in node.nodes])
106110
return rf"^{regex}$"
@@ -117,5 +121,5 @@ def get_possible_node_with_optionals(self, node: Node) -> Optional[Node]:
117121
return results[0] if results else None
118122

119123
@staticmethod
120-
def get_nodes_with_ast_type(node: Node, ast_type: NodeType) -> List[Node]:
124+
def get_nodes_with_ast_type(node: Node, ast_type: NodeType) -> list[Node]:
121125
return [ast_node for ast_node in node.nodes if ast_node.ast_type == ast_type]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import re
2+
3+
from cucumber_expressions.expression import CucumberExpression
4+
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
5+
from cucumber_expressions.regular_expression import RegularExpression
6+
7+
CURLY_BRACKET_PATTERN = re.compile(r"{(.*?)}")
8+
INVALID_CURLY_PATTERN = re.compile(r"^\d+(?:,\d+)?$")
9+
10+
11+
class ExpressionFactory:
12+
def __init__(self, parameter_type_registry: ParameterTypeRegistry = ParameterTypeRegistry()):
13+
self.parameter_type_registry = parameter_type_registry
14+
15+
@staticmethod
16+
def _has_curly_brackets(string: str) -> bool:
17+
return "{" in string and "}" in string
18+
19+
@staticmethod
20+
def _extract_text_in_curly_brackets(string: str) -> list:
21+
return CURLY_BRACKET_PATTERN.findall(string)
22+
23+
def is_cucumber_expression(self, expression_string: str):
24+
if not self._has_curly_brackets(expression_string):
25+
return False
26+
bracket_texts = self._extract_text_in_curly_brackets(expression_string)
27+
# Check if any match does not contain an integer or an integer and a comma
28+
for text in bracket_texts:
29+
# Check if the match is a regex pattern (matches integer or integer-comma pattern)
30+
if INVALID_CURLY_PATTERN.match(text):
31+
return False # Found a form of curly bracket
32+
return True # All curly brackets are valid
33+
34+
35+
def create_expression(self, expression_string: str):
36+
if self.is_cucumber_expression(expression_string):
37+
return CucumberExpression(expression_string, self.parameter_type_registry)
38+
return RegularExpression(expression_string, self.parameter_type_registry)

0 commit comments

Comments
 (0)