0
# Python Implementation
1
2
Pythonic implementation of Cucumber Expressions with clean APIs, built-in parameter types, and full feature compatibility for Python BDD testing frameworks.
3
4
## Package Information
5
6
- **Package Name**: `cucumber-expressions`
7
- **Language**: Python 3.7+
8
- **Installation**: `pip install cucumber-expressions`
9
- **Module**: `cucumber_expressions`
10
11
## Core Imports
12
13
```python
14
from cucumber_expressions.expression import CucumberExpression
15
from cucumber_expressions.parameter_type import ParameterType
16
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
17
from cucumber_expressions.expression_generator import CucumberExpressionGenerator
18
from cucumber_expressions.argument import Argument
19
```
20
21
## Capabilities
22
23
### Expression Creation and Matching
24
25
Create and match Cucumber expressions with Pythonic API patterns and type conversion.
26
27
```python { .api }
28
class CucumberExpression:
29
"""Main class for parsing and matching Cucumber expressions"""
30
31
def __init__(self, expression: str, parameter_type_registry: ParameterTypeRegistry):
32
"""
33
Create a new Cucumber expression
34
35
Args:
36
expression: The Cucumber expression string
37
parameter_type_registry: Registry containing parameter types
38
"""
39
...
40
41
def match(self, text: str) -> Optional[List[Argument]]:
42
"""
43
Match text against this expression and extract arguments
44
45
Args:
46
text: Text to match against the expression
47
48
Returns:
49
List of matched arguments or None if no match
50
"""
51
...
52
53
@property
54
def source(self) -> str:
55
"""The original expression string"""
56
...
57
58
@property
59
def regexp(self) -> str:
60
"""The compiled regular expression pattern"""
61
...
62
```
63
64
**Usage Examples:**
65
66
```python
67
from cucumber_expressions.expression import CucumberExpression
68
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
69
70
# Create registry with built-in parameter types
71
registry = ParameterTypeRegistry()
72
73
# Simple integer parameter
74
expr1 = CucumberExpression('I have {int} cucumbers', registry)
75
args1 = expr1.match('I have 42 cucumbers')
76
if args1:
77
print(args1[0].value) # 42 (int)
78
79
# Multiple parameters
80
expr2 = CucumberExpression('I have {int} {word} and {float} {word}', registry)
81
args2 = expr2.match('I have 42 cucumbers and 3.5 apples')
82
if args2:
83
print(args2[0].value) # 42 (int)
84
print(args2[1].value) # "cucumbers" (str)
85
print(args2[2].value) # 3.5 (float)
86
print(args2[3].value) # "apples" (str)
87
88
# Optional text
89
expr3 = CucumberExpression('I have {int} cucumber(s)', registry)
90
print(expr3.match('I have 1 cucumber')) # matches
91
print(expr3.match('I have 5 cucumbers')) # matches
92
93
# Alternative text
94
expr4 = CucumberExpression('I put it in my belly/stomach', registry)
95
print(expr4.match('I put it in my belly')) # matches
96
print(expr4.match('I put it in my stomach')) # matches
97
98
# String parameters (removes quotes)
99
expr5 = CucumberExpression('I say {string}', registry)
100
args5 = expr5.match('I say "hello world"')
101
if args5:
102
print(args5[0].value) # "hello world" (without quotes)
103
```
104
105
### Parameter Type Definition
106
107
Define custom parameter types with regex patterns and transformation functions using Pythonic patterns.
108
109
```python { .api }
110
class ParameterType:
111
"""Defines a parameter type with name, patterns, and transformer"""
112
113
def __init__(
114
self,
115
name: Optional[str],
116
regexp: Union[List[str], str, List[Pattern], Pattern],
117
type: type,
118
transformer: Optional[Callable] = None,
119
use_for_snippets: bool = True,
120
prefer_for_regexp_match: bool = False
121
):
122
"""
123
Create a new parameter type
124
125
Args:
126
name: Name used in expressions (e.g., 'color' for {color})
127
regexp: Regular expression patterns to match
128
type: Python type for the result
129
transformer: Function to transform matched strings to result type
130
use_for_snippets: Whether to use this type for snippet generation
131
prefer_for_regexp_match: Whether to prefer this type in regexp matches
132
"""
133
...
134
135
@staticmethod
136
def compare(pt1: 'ParameterType', pt2: 'ParameterType') -> int:
137
"""Compare two parameter types for sorting"""
138
...
139
140
@staticmethod
141
def _is_valid_parameter_type_name(type_name: str) -> bool:
142
"""Test if a parameter type name is valid"""
143
...
144
145
def transform(self, group_values: List[str]) -> Any:
146
"""
147
Transform matched groups to the target type
148
149
Args:
150
group_values: Matched string groups
151
152
Returns:
153
Transformed value
154
"""
155
...
156
157
@property
158
def prefer_for_regexp_match(self) -> bool:
159
"""Whether to prefer for regexp matching"""
160
...
161
162
@property
163
def use_for_snippets(self) -> bool:
164
"""Whether to use for snippet generation"""
165
...
166
167
# Public attributes
168
name: Optional[str] # Parameter type name
169
type: type # Python type
170
regexps: List[str] # Regular expression patterns
171
```
172
173
**Usage Examples:**
174
175
```python
176
from cucumber_expressions.parameter_type import ParameterType
177
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
178
from cucumber_expressions.expression import CucumberExpression
179
import re
180
from enum import Enum
181
from dataclasses import dataclass
182
from decimal import Decimal
183
from datetime import datetime
184
185
# Simple enum-like parameter type
186
class Color(Enum):
187
RED = "red"
188
GREEN = "green"
189
BLUE = "blue"
190
YELLOW = "yellow"
191
192
color_type = ParameterType(
193
name="color",
194
regexp=r"red|green|blue|yellow",
195
type=Color,
196
transformer=lambda s: Color(s.lower())
197
)
198
199
# Custom object parameter type
200
@dataclass
201
class Point:
202
x: int
203
y: int
204
205
point_type = ParameterType(
206
name="point",
207
regexp=r"\((-?\d+),\s*(-?\d+)\)",
208
type=Point,
209
transformer=lambda x, y: Point(int(x), int(y))
210
)
211
212
# Date parameter type with multiple capture groups
213
date_type = ParameterType(
214
name="date",
215
regexp=r"(\d{4})-(\d{2})-(\d{2})",
216
type=datetime,
217
transformer=lambda year, month, day: datetime(int(year), int(month), int(day))
218
)
219
220
# High-precision decimal type
221
decimal_type = ParameterType(
222
name="precise_decimal",
223
regexp=r"\d+\.\d{4,}",
224
type=Decimal,
225
transformer=lambda s: Decimal(s)
226
)
227
228
# Email with validation
229
def validate_email(email: str) -> str:
230
if '@' not in email:
231
raise ValueError(f"Invalid email: {email}")
232
return email.lower()
233
234
email_type = ParameterType(
235
name="email",
236
regexp=r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
237
type=str,
238
transformer=validate_email
239
)
240
241
# Register and use custom types
242
registry = ParameterTypeRegistry()
243
registry.define_parameter_type(color_type)
244
registry.define_parameter_type(point_type)
245
registry.define_parameter_type(date_type)
246
registry.define_parameter_type(decimal_type)
247
registry.define_parameter_type(email_type)
248
249
# Use in expressions
250
color_expr = CucumberExpression("I have a {color} car", registry)
251
result = color_expr.match("I have a red car")
252
print(result[0].value) # Color.RED
253
254
point_expr = CucumberExpression("Move to {point}", registry)
255
result = point_expr.match("Move to (10, -5)")
256
print(result[0].value) # Point(x=10, y=-5)
257
258
date_expr = CucumberExpression("Meeting on {date}", registry)
259
result = date_expr.match("Meeting on 2023-12-25")
260
print(result[0].value) # datetime(2023, 12, 25, 0, 0)
261
262
decimal_expr = CucumberExpression("Price is {precise_decimal}", registry)
263
result = decimal_expr.match("Price is 123.4567")
264
print(result[0].value) # Decimal('123.4567')
265
266
email_expr = CucumberExpression("Send to {email}", registry)
267
result = email_expr.match("Send to User@Example.COM")
268
print(result[0].value) # "user@example.com"
269
```
270
271
### Parameter Type Registry
272
273
Central registry for managing parameter types with Pythonic property access and method naming.
274
275
```python { .api }
276
class ParameterTypeRegistry:
277
"""Registry for managing parameter types"""
278
279
def __init__(self):
280
"""Create a new parameter type registry with built-in types"""
281
...
282
283
@property
284
def parameter_types(self) -> List[ParameterType]:
285
"""List of all registered parameter types"""
286
...
287
288
def lookup_by_type_name(self, name: str) -> Optional[ParameterType]:
289
"""
290
Look up a parameter type by name
291
292
Args:
293
name: Name of the parameter type
294
295
Returns:
296
Parameter type or None if not found
297
"""
298
...
299
300
def lookup_by_regexp(
301
self,
302
parameter_type_regexp: str,
303
expression_regexp,
304
text: str
305
) -> Optional[ParameterType]:
306
"""
307
Look up parameter type by regular expression pattern
308
309
Args:
310
parameter_type_regexp: Regular expression to match
311
expression_regexp: Full expression regexp
312
text: Text being matched
313
314
Returns:
315
Matching parameter type or None
316
"""
317
...
318
319
def define_parameter_type(self, parameter_type: ParameterType) -> None:
320
"""
321
Register a new parameter type
322
323
Args:
324
parameter_type: Parameter type to register
325
"""
326
...
327
```
328
329
**Usage Examples:**
330
331
```python
332
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
333
from cucumber_expressions.parameter_type import ParameterType
334
335
registry = ParameterTypeRegistry()
336
337
# Check built-in types
338
int_type = registry.lookup_by_type_name('int')
339
print(int_type.name) # "int"
340
print(int_type.type) # <class 'int'>
341
342
# List all parameter types
343
for param_type in registry.parameter_types:
344
print(f"{param_type.name}: {param_type.regexps}")
345
346
# Define and retrieve custom type
347
custom_type = ParameterType('uuid', r'[0-9a-f-]{36}', str)
348
registry.define_parameter_type(custom_type)
349
350
retrieved = registry.lookup_by_type_name('uuid')
351
print(retrieved is custom_type) # True
352
```
353
354
### Expression Generation
355
356
Generate Cucumber expressions from example text with Pythonic API patterns.
357
358
```python { .api }
359
class CucumberExpressionGenerator:
360
"""Generates Cucumber expressions from example text"""
361
362
def __init__(self, parameter_type_registry: ParameterTypeRegistry):
363
"""
364
Create generator with parameter type registry
365
366
Args:
367
parameter_type_registry: Registry containing available parameter types
368
"""
369
...
370
371
def generate_expressions(self, text: str) -> List[GeneratedExpression]:
372
"""
373
Generate expressions from example text
374
375
Args:
376
text: Example text to generate expressions from
377
378
Returns:
379
List of generated expressions ranked by specificity
380
"""
381
...
382
383
@staticmethod
384
def escape(string: str) -> str:
385
"""
386
Escape special regex characters in string
387
388
Args:
389
string: String to escape
390
391
Returns:
392
Escaped string safe for regex
393
"""
394
...
395
396
class GeneratedExpression:
397
"""Generated expression with metadata"""
398
399
def __init__(self, expression_template: str, parameter_types: List[ParameterType]):
400
"""
401
Create generated expression
402
403
Args:
404
expression_template: Template string with parameter placeholders
405
parameter_types: List of parameter types in order
406
"""
407
self.source = expression_template
408
self.parameter_types = parameter_types
409
self.parameter_names = [pt.name for pt in parameter_types if pt.name]
410
411
@property
412
def source(self) -> str:
413
"""The generated expression source"""
414
...
415
416
@property
417
def parameter_names(self) -> List[str]:
418
"""Parameter names for code generation"""
419
...
420
421
@property
422
def parameter_types(self) -> List[ParameterType]:
423
"""Parameter types in declaration order"""
424
...
425
```
426
427
**Usage Examples:**
428
429
```python
430
from cucumber_expressions.expression_generator import CucumberExpressionGenerator
431
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
432
433
registry = ParameterTypeRegistry()
434
generator = CucumberExpressionGenerator(registry)
435
436
# Generate from text with numbers
437
expressions = generator.generate_expressions('I have 42 cucumbers')
438
print(expressions[0].source) # "I have {int} cucumbers"
439
print(expressions[0].parameter_names) # ["int"]
440
441
# Generate from text with floats
442
expressions = generator.generate_expressions('Price is 29.99 dollars')
443
print(expressions[0].source) # "Price is {float} dollars"
444
445
# Generate from text with strings
446
expressions = generator.generate_expressions('I select "Premium Plan" option')
447
print(expressions[0].source) # "I select {string} option"
448
449
# Generate from complex text
450
expressions = generator.generate_expressions('User alice with age 25 has balance 123.45')
451
for i, expr in enumerate(expressions):
452
print(f"Option {i+1}: {expr.source}")
453
print(f"Parameters: {[pt.name for pt in expr.parameter_types]}")
454
455
# Generate method signatures for step definitions
456
expressions = generator.generate_expressions('Order contains 5 items')
457
best = expressions[0]
458
print(f"@given('{best.source}')")
459
param_list = ', '.join(f"{name}: {get_python_type(pt)}"
460
for name, pt in zip(best.parameter_names, best.parameter_types))
461
print(f"def order_contains_items({param_list}):")
462
print(" pass")
463
464
def get_python_type(param_type: ParameterType) -> str:
465
"""Get Python type hint for parameter type"""
466
if param_type.type == int:
467
return "int"
468
elif param_type.type == float:
469
return "float"
470
elif param_type.type == str:
471
return "str"
472
else:
473
return "Any"
474
```
475
476
### Argument Extraction
477
478
Extract and access matched arguments with Pythonic property access.
479
480
```python { .api }
481
class Argument:
482
"""Represents a matched argument from an expression"""
483
484
def __init__(self, group: Group, parameter_type: ParameterType):
485
"""
486
Create argument with group and parameter type
487
488
Args:
489
group: Matched group from regex
490
parameter_type: Parameter type for transformation
491
"""
492
...
493
494
@staticmethod
495
def build(
496
tree_regexp: TreeRegexp,
497
text: str,
498
parameter_types: List[ParameterType]
499
) -> Optional[List[Argument]]:
500
"""
501
Build arguments from matched groups
502
503
Args:
504
tree_regexp: Tree regular expression matcher
505
text: Text that was matched
506
parameter_types: Parameter types for transformation
507
508
Returns:
509
List of arguments or None if no match
510
"""
511
...
512
513
@property
514
def value(self) -> Any:
515
"""The transformed value"""
516
...
517
518
@property
519
def group(self) -> Group:
520
"""The matched group"""
521
...
522
523
# Public attribute
524
parameter_type: ParameterType # The parameter type used for transformation
525
526
class Group:
527
"""Represents a matched group from regex matching"""
528
529
def __init__(self, value: Optional[str], start: int, end: int, children: List['Group']):
530
self.value = value
531
self.start = start
532
self.end = end
533
self.children = children
534
535
@property
536
def value(self) -> Optional[str]:
537
"""Matched text value"""
538
...
539
540
@property
541
def start(self) -> int:
542
"""Start position in source text"""
543
...
544
545
@property
546
def end(self) -> int:
547
"""End position in source text"""
548
...
549
550
@property
551
def children(self) -> List['Group']:
552
"""Child groups"""
553
...
554
```
555
556
**Usage Examples:**
557
558
```python
559
from cucumber_expressions.expression import CucumberExpression
560
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
561
562
registry = ParameterTypeRegistry()
563
expr = CucumberExpression('User {word} has {int} items', registry)
564
565
args = expr.match('User alice has 42 items')
566
if args:
567
# Access values directly
568
username = args[0].value # "alice" (str)
569
item_count = args[1].value # 42 (int)
570
571
# Access parameter types
572
print(args[0].parameter_type.name) # "word"
573
print(args[1].parameter_type.name) # "int"
574
575
# Access groups
576
print(args[0].group.value) # "alice"
577
print(args[0].group.start) # position in original text
578
print(args[0].group.end) # end position in original text
579
```
580
581
### Built-in Parameter Types
582
583
Python implementation includes comprehensive built-in parameter types with appropriate Python type conversion.
584
585
```python { .api }
586
# Built-in parameter types in Python implementation:
587
588
{int} # Converts to Python int
589
{float} # Converts to Python float
590
{word} # Returns as Python str (single word)
591
{string} # Returns as Python str (removes quotes)
592
{bigdecimal} # Converts to decimal.Decimal for high precision
593
{double} # Converts to Python float (64-bit)
594
{biginteger} # Converts to Python int (arbitrary precision)
595
{byte} # Converts to Python int (8-bit range)
596
{short} # Converts to Python int (16-bit range)
597
{long} # Converts to Python int (64-bit range)
598
{} # Anonymous - returns as str
599
```
600
601
**Usage Examples:**
602
603
```python
604
from cucumber_expressions.expression import CucumberExpression
605
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
606
from decimal import Decimal
607
608
registry = ParameterTypeRegistry()
609
610
# Test all built-in types
611
expr = CucumberExpression(
612
'Values: {int} {float} {word} {string} {bigdecimal}',
613
registry
614
)
615
616
args = expr.match('Values: 42 3.14 hello "world test" 123.456789')
617
if args:
618
print(f"int: {args[0].value} ({type(args[0].value)})") # 42 (<class 'int'>)
619
print(f"float: {args[1].value} ({type(args[1].value)})") # 3.14 (<class 'float'>)
620
print(f"word: {args[2].value} ({type(args[2].value)})") # hello (<class 'str'>)
621
print(f"string: {args[3].value} ({type(args[3].value)})") # world test (<class 'str'>)
622
print(f"bigdecimal: {args[4].value} ({type(args[4].value)})") # 123.456789 (<class 'decimal.Decimal'>)
623
```
624
625
### Error Handling
626
627
Pythonic error handling with appropriate exception types.
628
629
```python { .api }
630
# Exception handling in parameter transformation
631
def safe_transformer(value: str) -> int:
632
try:
633
return int(value)
634
except ValueError as e:
635
raise ValueError(f"Cannot convert '{value}' to integer") from e
636
637
safe_int_type = ParameterType(
638
name="safe_int",
639
regexp=r"\d+",
640
type=int,
641
transformer=safe_transformer
642
)
643
644
# Usage with try/catch
645
try:
646
registry.define_parameter_type(safe_int_type)
647
expr = CucumberExpression('Count: {safe_int}', registry)
648
args = expr.match('Count: invalid') # Will raise during transformation
649
except ValueError as e:
650
print(f"Transformation error: {e}")
651
```
652
653
### Integration with Python Testing Frameworks
654
655
Common patterns for integrating with pytest, behave, and other Python BDD frameworks.
656
657
```python
658
# pytest-bdd integration example
659
from pytest_bdd import given, when, then, parsers
660
from cucumber_expressions.expression import CucumberExpression
661
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
662
663
# Set up registry once
664
registry = ParameterTypeRegistry()
665
666
# Custom parameter types for domain objects
667
user_type = ParameterType('user', r'user_(\w+)', dict, lambda name: {'username': name})
668
registry.define_parameter_type(user_type)
669
670
# Use in step definitions
671
@given(parsers.cucumber('I have {int} items in my cart'))
672
def have_items_in_cart(int_value):
673
# int_value is already converted to Python int
674
assert isinstance(int_value, int)
675
676
@when(parsers.cucumber('I add {string} to my cart'))
677
def add_item_to_cart(string_value):
678
# string_value has quotes removed
679
assert isinstance(string_value, str)
680
681
# behave integration example
682
from behave import given, when, then
683
from cucumber_expressions.expression import CucumberExpression
684
from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry
685
686
# In environment.py or step files
687
def setup_parameter_types(context):
688
context.registry = ParameterTypeRegistry()
689
# Add custom types...
690
691
@given('User {word} has {int} credits')
692
def user_has_credits(context, username, credit_count):
693
# Parameters automatically converted based on built-in types
694
context.users[username] = {'credits': credit_count}
695
```
696
697
The Python implementation provides idiomatic Python APIs while maintaining full compatibility with the Cucumber Expressions specification, enabling seamless integration with Python BDD testing frameworks and tools.