behave-data 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {behave_data-0.1.2 → behave_data-0.1.3}/CHANGELOG.md +19 -0
  2. {behave_data-0.1.2 → behave_data-0.1.3}/PKG-INFO +1 -1
  3. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/__init__.py +1 -1
  4. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/tags.py +11 -6
  5. {behave_data-0.1.2 → behave_data-0.1.3}/pyproject.toml +1 -1
  6. behave_data-0.1.3/tests/integration/environment.py +48 -0
  7. behave_data-0.1.3/tests/integration/features/data/products.json +5 -0
  8. behave_data-0.1.3/tests/integration/features/data/users.csv +4 -0
  9. behave_data-0.1.3/tests/integration/features/dynamic_examples.feature +19 -0
  10. behave_data-0.1.3/tests/integration/features/fixtures_builders.feature +34 -0
  11. behave_data-0.1.3/tests/integration/features/secrets.feature +19 -0
  12. behave_data-0.1.3/tests/integration/features/typed_table_edge.feature +37 -0
  13. behave_data-0.1.3/tests/integration/steps/e2e_steps.py +247 -0
  14. behave_data-0.1.3/tests/unit/test_edge_cases.py +613 -0
  15. behave_data-0.1.2/tests/integration/environment.py +0 -14
  16. {behave_data-0.1.2 → behave_data-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  17. {behave_data-0.1.2 → behave_data-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {behave_data-0.1.2 → behave_data-0.1.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {behave_data-0.1.2 → behave_data-0.1.3}/.github/workflows/ci.yml +0 -0
  20. {behave_data-0.1.2 → behave_data-0.1.3}/.github/workflows/release.yml +0 -0
  21. {behave_data-0.1.2 → behave_data-0.1.3}/.gitignore +0 -0
  22. {behave_data-0.1.2 → behave_data-0.1.3}/.pre-commit-config.yaml +0 -0
  23. {behave_data-0.1.2 → behave_data-0.1.3}/CODE_OF_CONDUCT.md +0 -0
  24. {behave_data-0.1.2 → behave_data-0.1.3}/CONTRIBUTING.md +0 -0
  25. {behave_data-0.1.2 → behave_data-0.1.3}/LICENSE +0 -0
  26. {behave_data-0.1.2 → behave_data-0.1.3}/Makefile +0 -0
  27. {behave_data-0.1.2 → behave_data-0.1.3}/README.md +0 -0
  28. {behave_data-0.1.2 → behave_data-0.1.3}/SECURITY.md +0 -0
  29. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/builders.py +0 -0
  30. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/config.py +0 -0
  31. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/diff.py +0 -0
  32. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/errors.py +0 -0
  33. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/examples.py +0 -0
  34. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/fixtures.py +0 -0
  35. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/hooks.py +0 -0
  36. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/__init__.py +0 -0
  37. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/csv.py +0 -0
  38. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/excel.py +0 -0
  39. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/http.py +0 -0
  40. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/json.py +0 -0
  41. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/sql.py +0 -0
  42. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/loaders/yaml.py +0 -0
  43. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/manager.py +0 -0
  44. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/null.py +0 -0
  45. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/patch.py +0 -0
  46. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/py.typed +0 -0
  47. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/raw_table.py +0 -0
  48. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/secrets.py +0 -0
  49. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/typed_table.py +0 -0
  50. {behave_data-0.1.2 → behave_data-0.1.3}/behave_data/types.py +0 -0
  51. {behave_data-0.1.2 → behave_data-0.1.3}/tests/conftest.py +0 -0
  52. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/features/data_tables.feature +0 -0
  53. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/features/diff.feature +0 -0
  54. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/features/null_handling.feature +0 -0
  55. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/features/nullable_types.feature +0 -0
  56. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/features/raw_table.feature +0 -0
  57. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/features/typing.feature +0 -0
  58. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/steps/data_tables_steps.py +0 -0
  59. {behave_data-0.1.2 → behave_data-0.1.3}/tests/integration/test_integration.py +0 -0
  60. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_builders.py +0 -0
  61. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_config.py +0 -0
  62. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_diff.py +0 -0
  63. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_errors.py +0 -0
  64. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_examples.py +0 -0
  65. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_fixtures.py +0 -0
  66. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_hooks.py +0 -0
  67. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_loaders.py +0 -0
  68. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_manager.py +0 -0
  69. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_null.py +0 -0
  70. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_patch.py +0 -0
  71. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_raw_table.py +0 -0
  72. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_secrets.py +0 -0
  73. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_tags.py +0 -0
  74. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_typed_table.py +0 -0
  75. {behave_data-0.1.2 → behave_data-0.1.3}/tests/unit/test_types.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2026-07-17
9
+
10
+ ### Added
11
+
12
+ - 75 edge case unit tests covering hooks, secrets, examples, raw_table, yaml, types, diff, fixtures, loaders, config, and manager
13
+ - 4 new E2E Behave integration features: fixtures_builders, secrets, typed_table_edge, dynamic_examples
14
+ - 27 E2E scenarios with 64 steps testing all behave-data features in real Behave
15
+
16
+ ### Fixed
17
+
18
+ - `tags.py` now strips `@` prefix from Behave tags (Behave strips `@` from scenario tags)
19
+ - `tags.py` `@needs_data` now sets attribute on context in addition to `_behave_data_loaded`
20
+ - `tags.py` cleanup functions now support both `func(context)` and `func()` signatures
21
+
22
+ ### Changed
23
+
24
+ - Coverage increased from 94% to 98%
25
+ - Total tests: 411 passed, 1 skipped (pandas optional)
26
+
8
27
  ## [0.1.2] - 2026-07-17
9
28
 
10
29
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: behave-data
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Data management for Behave — typed tables, diff, dynamic examples, fixtures, secrets
5
5
  Project-URL: Homepage, https://github.com/MathiasPaulenko/behave-data
6
6
  Project-URL: Repository, https://github.com/MathiasPaulenko/behave-data
@@ -38,7 +38,7 @@ from behave_data.tags import process_tags_after_scenario, process_tags_before_sc
38
38
  from behave_data.typed_table import TypedTableWrapper, typed_wrap
39
39
  from behave_data.types import TYPE_CONVERTERS, convert_cell, parse_column_header, register_type
40
40
 
41
- __version__ = "0.1.2"
41
+ __version__ = "0.1.3"
42
42
 
43
43
  __all__ = [
44
44
  "BehaveDataError",
@@ -29,15 +29,17 @@ def process_tags_before_scenario(context: Any, scenario: Any) -> None:
29
29
  context._behave_data_loaded = {}
30
30
 
31
31
  for tag in tags:
32
- if tag.startswith("@needs_data:"):
33
- name = tag[len("@needs_data:") :]
32
+ tag = tag.lstrip("@")
33
+ if tag.startswith("needs_data:"):
34
+ name = tag[len("needs_data:") :]
34
35
  data = context.data.fixture(name)
35
36
  context._behave_data_loaded[name] = data
36
- elif tag.startswith("@with_fixture:"):
37
- name = tag[len("@with_fixture:") :]
37
+ setattr(context, name, data)
38
+ elif tag.startswith("with_fixture:"):
39
+ name = tag[len("with_fixture:") :]
38
40
  data = context.data.fixture(name)
39
41
  setattr(context, name, data)
40
- elif tag == "@cleanup_after":
42
+ elif tag == "cleanup_after":
41
43
  context._behave_data_cleanup = True
42
44
  if not hasattr(context, "_behave_data_cleanup_funcs"):
43
45
  context._behave_data_cleanup_funcs = []
@@ -58,6 +60,9 @@ def process_tags_after_scenario(context: Any, scenario: Any) -> None:
58
60
 
59
61
  funcs = getattr(context, "_behave_data_cleanup_funcs", [])
60
62
  for func in funcs:
61
- func(context)
63
+ try:
64
+ func(context)
65
+ except TypeError:
66
+ func()
62
67
 
63
68
  context._behave_data_cleanup = False
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "behave-data"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Data management for Behave — typed tables, diff, dynamic examples, fixtures, secrets"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,48 @@
1
+ """Behave environment for integration tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from behave_data import (
6
+ after_scenario_hook,
7
+ before_feature_hook,
8
+ before_scenario_hook,
9
+ before_step_hook,
10
+ setup_data,
11
+ )
12
+ from behave_data.builders import data_builder
13
+ from behave_data.fixtures import data_fixture
14
+
15
+
16
+ @data_fixture("admin_user")
17
+ def admin_user():
18
+ return {"name": "Admin", "role": "admin", "email": "admin@test.com"}
19
+
20
+
21
+ @data_fixture("regular_user", params=["alice", "bob"])
22
+ def regular_user(param):
23
+ return {"name": param, "role": "user"}
24
+
25
+
26
+ @data_builder("product")
27
+ def product(overrides):
28
+ return {"name": "Widget", "price": 9.99, **overrides}
29
+
30
+
31
+ def before_all(context):
32
+ setup_data(context)
33
+
34
+
35
+ def before_feature(context, feature):
36
+ before_feature_hook(context, feature)
37
+
38
+
39
+ def before_scenario(context, scenario):
40
+ before_scenario_hook(context, scenario)
41
+
42
+
43
+ def before_step(context, step):
44
+ before_step_hook(context, step)
45
+
46
+
47
+ def after_scenario(context, scenario):
48
+ after_scenario_hook(context, scenario)
@@ -0,0 +1,5 @@
1
+ [
2
+ {"name": "Laptop", "price": "999.99"},
3
+ {"name": "Mouse", "price": "29.99"},
4
+ {"name": "Keyboard", "price": "79.99"}
5
+ ]
@@ -0,0 +1,4 @@
1
+ name,email
2
+ Alice,alice@example.com
3
+ Bob,bob@example.com
4
+ Carol,carol@example.com
@@ -0,0 +1,19 @@
1
+ Feature: Dynamic examples E2E
2
+
3
+ @load_examples:csv:features/data/users.csv
4
+ Scenario Outline: Load users from CSV
5
+ Given a user with name "<name>" and email "<email>"
6
+ Then the user "<name>" should have email "<email>"
7
+
8
+ Examples:
9
+ | name | email |
10
+ | old | old |
11
+
12
+ @load_examples:json:features/data/products.json
13
+ Scenario Outline: Load products from JSON
14
+ Given a product with name "<name>" and price "<price>"
15
+ Then the product "<name>" should cost "<price>"
16
+
17
+ Examples:
18
+ | name | price |
19
+ | old | old |
@@ -0,0 +1,34 @@
1
+ Feature: Fixtures and builders E2E
2
+
3
+ @needs_data:admin_user
4
+ Scenario: Admin user fixture is available via needs_data tag
5
+ Given I need an admin user
6
+ Then the admin user should have role "admin"
7
+ And the admin user should have email "admin@test.com"
8
+
9
+ @needs_data:regular_user:alice
10
+ Scenario: Parametrized fixture is available via needs_data tag
11
+ Given I need a regular user
12
+ Then the regular user should have name "alice"
13
+ And the regular user should have role "user"
14
+
15
+ Scenario: Build a single product via DataManager
16
+ Given I build a product
17
+ Then the product should have name "Widget"
18
+ And the product should have price 9.99
19
+
20
+ Scenario: Build a product with overrides
21
+ Given I build a product with name "Gadget" and price 19.99
22
+ Then the product should have name "Gadget"
23
+ And the product should have price 19.99
24
+
25
+ Scenario: Build multiple products
26
+ Given I build 3 products
27
+ Then I should have 3 products
28
+ And each product should have name "Widget"
29
+
30
+ @cleanup_after
31
+ Scenario: Cleanup after scenario
32
+ Given I register a cleanup function
33
+ Then the cleanup flag should be True
34
+ # After scenario, cleanup runs automatically via after_scenario_hook
@@ -0,0 +1,19 @@
1
+ Feature: Secrets and placeholder resolution E2E
2
+
3
+ Scenario: Resolve env placeholder in step
4
+ Given I set environment variable "TEST_SECRET" to "my_secret_value"
5
+ When I resolve the placeholder "env:TEST_SECRET"
6
+ Then the resolved value should be "my_secret_value"
7
+
8
+ Scenario: Resolve plain text passthrough
9
+ When I resolve the placeholder "just_plain_text"
10
+ Then the resolved value should be "just_plain_text"
11
+
12
+ Scenario: Mask a resolved env value
13
+ Given I set environment variable "API_TOKEN" to "secret_token_123"
14
+ When I resolve and mask the placeholder "env:API_TOKEN"
15
+ Then the masked value should be "secret_token_123"
16
+
17
+ Scenario: Resolve integer passthrough
18
+ When I resolve the value 42
19
+ Then the resolved value should be "42"
@@ -0,0 +1,37 @@
1
+ Feature: Typed table edge cases E2E
2
+
3
+ Scenario: All supported types in one table
4
+ Given a table with all types
5
+ | name:str | age:int | price:float | active:bool | created:date |
6
+ | Alice | 30 | 99.99 | true | 2024-01-15 |
7
+ | Bob | 25 | 49.50 | false | 2024-06-30 |
8
+ Then all types should be correctly converted
9
+ | name:str | age:int | price:float | active:bool | created:date |
10
+ | Alice | 30 | 99.99 | true | 2024-01-15 |
11
+ | Bob | 25 | 49.50 | false | 2024-06-30 |
12
+
13
+ Scenario: Nullable columns with null markers
14
+ Given a table with nullable typed columns
15
+ | name:str | age:int? | city:str? |
16
+ | Alice | 30 | NYC |
17
+ | Bob | null | N/A |
18
+ | Carol | | |
19
+ Then nullable columns should resolve nulls to None
20
+
21
+ Scenario: Empty table with typed headers
22
+ Given an empty typed table
23
+ | name:str | age:int |
24
+ Then the typed table should have 0 data rows
25
+
26
+ Scenario: Single column table
27
+ Given a single column table
28
+ | name:str |
29
+ | Alice |
30
+ Then the single column should have one entry "Alice"
31
+
32
+ Scenario: Table with special characters
33
+ Given a table with special characters
34
+ | name:str | description:str |
35
+ | Alice | A "quoted" value |
36
+ | Bob | A 'single' value |
37
+ Then special characters should be preserved
@@ -0,0 +1,247 @@
1
+ """E2E steps for fixtures, builders, secrets, typed tables, and dynamic examples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from datetime import date
7
+
8
+ from behave import given, then, when
9
+
10
+ from behave_data import typed_wrap
11
+
12
+ # --------------------------------------------------------------------------- #
13
+ # Fixtures & Builders
14
+ # --------------------------------------------------------------------------- #
15
+
16
+
17
+ @given("I need an admin user")
18
+ def step_need_admin(context):
19
+ assert hasattr(context, "admin_user")
20
+ context.current_user = context.admin_user
21
+
22
+
23
+ @then('the admin user should have role "{role}"')
24
+ def step_admin_role(context, role):
25
+ assert context.current_user["role"] == role
26
+
27
+
28
+ @then('the admin user should have email "{email}"')
29
+ def step_admin_email(context, email):
30
+ assert context.current_user["email"] == email
31
+
32
+
33
+ @given("I need a regular user")
34
+ def step_need_regular(context):
35
+ loaded = getattr(context, "_behave_data_loaded", {})
36
+ # Find any regular_user:* fixture that was loaded
37
+ for key, val in loaded.items():
38
+ if key.startswith("regular_user:"):
39
+ context.current_user = val
40
+ return
41
+ raise AssertionError("No regular_user fixture loaded")
42
+
43
+
44
+ @then('the regular user should have name "{name}"')
45
+ def step_regular_name(context, name):
46
+ assert context.current_user["name"] == name
47
+
48
+
49
+ @then('the regular user should have role "{role}"')
50
+ def step_regular_role(context, role):
51
+ assert context.current_user["role"] == role
52
+
53
+
54
+ @given("I build a product")
55
+ def step_build_product(context):
56
+ context.product = context.data.build("product")
57
+
58
+
59
+ @then('the product should have name "{name}"')
60
+ def step_product_name(context, name):
61
+ assert context.product["name"] == name
62
+
63
+
64
+ @then("the product should have price {price:f}")
65
+ def step_product_price(context, price):
66
+ assert abs(context.product["price"] - price) < 0.01
67
+
68
+
69
+ @given('I build a product with name "{name}" and price {price:f}')
70
+ def step_build_product_overrides(context, name, price):
71
+ context.product = context.data.build("product", overrides={"name": name, "price": price})
72
+
73
+
74
+ @given("I build {n:d} products")
75
+ def step_build_multiple(context, n):
76
+ context.products = context.data.build("product", count=n)
77
+
78
+
79
+ @then("I should have {n:d} products")
80
+ def step_count_products(context, n):
81
+ assert len(context.products) == n
82
+
83
+
84
+ @then('each product should have name "{name}"')
85
+ def step_each_product_name(context, name):
86
+ for p in context.products:
87
+ assert p["name"] == name
88
+
89
+
90
+ @given("I register a cleanup function")
91
+ def step_register_cleanup(context):
92
+ context.cleanup_flag = False
93
+
94
+ def _cleanup():
95
+ context.cleanup_flag = False
96
+
97
+ if not hasattr(context, "_behave_data_cleanup_funcs"):
98
+ context._behave_data_cleanup_funcs = []
99
+ context._behave_data_cleanup_funcs.append(_cleanup)
100
+ context.cleanup_flag = True
101
+
102
+
103
+ @then("the cleanup flag should be True")
104
+ def step_cleanup_flag(context):
105
+ assert context.cleanup_flag is True
106
+
107
+
108
+ # --------------------------------------------------------------------------- #
109
+ # Secrets
110
+ # --------------------------------------------------------------------------- #
111
+
112
+
113
+ @given('I set environment variable "{var}" to "{value}"')
114
+ def step_set_env(context, var, value):
115
+ os.environ[var] = value
116
+ context._env_var = var
117
+
118
+
119
+ @when('I resolve the placeholder "{placeholder}"')
120
+ def step_resolve(context, placeholder):
121
+ context.resolved = context.data.resolve(placeholder)
122
+
123
+
124
+ @then('the resolved value should be "{value}"')
125
+ def step_resolved_value(context, value):
126
+ assert str(context.resolved) == value
127
+
128
+
129
+ @when('I resolve and mask the placeholder "{placeholder}"')
130
+ def step_resolve_and_mask(context, placeholder):
131
+ raw = context.data.resolve(placeholder)
132
+ context.resolved = raw
133
+ context.masked = context.data.mask(raw)
134
+
135
+
136
+ @then('the masked value should be "{value}"')
137
+ def step_masked_value(context, value):
138
+ assert str(context.masked) == value
139
+
140
+
141
+ @when("I resolve the value {val:d}")
142
+ def step_resolve_int(context, val):
143
+ context.resolved = context.data.resolve(val)
144
+
145
+
146
+ # --------------------------------------------------------------------------- #
147
+ # Typed table edge cases
148
+ # --------------------------------------------------------------------------- #
149
+
150
+
151
+ @given("a table with all types")
152
+ def step_all_types(context):
153
+ context.typed = typed_wrap(context.table).typed_dicts()
154
+
155
+
156
+ @then("all types should be correctly converted")
157
+ def step_check_all_types(context):
158
+ expected = typed_wrap(context.table).typed_dicts()
159
+ assert len(expected) == 2
160
+ assert isinstance(expected[0]["name"], str)
161
+ assert isinstance(expected[0]["age"], int)
162
+ assert isinstance(expected[0]["price"], float)
163
+ assert isinstance(expected[0]["active"], bool)
164
+ assert isinstance(expected[0]["created"], date)
165
+ assert expected[0]["active"] is True
166
+ assert expected[1]["active"] is False
167
+ assert expected[0]["created"] == date(2024, 1, 15)
168
+ assert expected[1]["created"] == date(2024, 6, 30)
169
+
170
+
171
+ @given("a table with nullable typed columns")
172
+ def step_nullable_typed(context):
173
+ context.typed = typed_wrap(context.table).typed_dicts()
174
+
175
+
176
+ @then("nullable columns should resolve nulls to None")
177
+ def step_check_nullable(context):
178
+ rows = context.typed
179
+ assert rows[0]["age"] == 30
180
+ assert rows[0]["city"] == "NYC"
181
+ assert rows[1]["age"] is None
182
+ assert rows[1]["city"] is None
183
+ assert rows[2]["age"] is None
184
+ assert rows[2]["city"] is None
185
+
186
+
187
+ @given("an empty typed table")
188
+ def step_empty_typed(context):
189
+ context.typed = typed_wrap(context.table)
190
+
191
+
192
+ @then("the typed table should have 0 data rows")
193
+ def step_zero_rows(context):
194
+ assert len(context.typed.typed_dicts()) == 0
195
+
196
+
197
+ @given("a single column table")
198
+ def step_single_col(context):
199
+ context.typed = typed_wrap(context.table).typed_dicts()
200
+
201
+
202
+ @then('the single column should have one entry "{value}"')
203
+ def step_single_entry(context, value):
204
+ rows = context.typed
205
+ assert len(rows) == 1
206
+ assert rows[0]["name"] == value
207
+
208
+
209
+ @given("a table with special characters")
210
+ def step_special_chars(context):
211
+ context.typed = typed_wrap(context.table).typed_dicts()
212
+
213
+
214
+ @then("special characters should be preserved")
215
+ def step_check_special(context):
216
+ rows = context.typed
217
+ assert "quoted" in rows[0]["description"]
218
+ assert "single" in rows[1]["description"]
219
+
220
+
221
+ # --------------------------------------------------------------------------- #
222
+ # Dynamic examples
223
+ # --------------------------------------------------------------------------- #
224
+
225
+
226
+ @given('a user with name "{name}" and email "{email}"')
227
+ def step_user_with(context, name, email):
228
+ context.current_name = name
229
+ context.current_email = email
230
+
231
+
232
+ @then('the user "{name}" should have email "{email}"')
233
+ def step_user_email(context, name, email):
234
+ assert context.current_name == name
235
+ assert context.current_email == email
236
+
237
+
238
+ @given('a product with name "{name}" and price "{price}"')
239
+ def step_product_with(context, name, price):
240
+ context.current_product_name = name
241
+ context.current_product_price = price
242
+
243
+
244
+ @then('the product "{name}" should cost "{price}"')
245
+ def step_product_cost(context, name, price):
246
+ assert context.current_product_name == name
247
+ assert context.current_product_price == price