ima-claude 2.9.0

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -0
  3. package/dist/cli.js +1064 -0
  4. package/package.json +49 -0
  5. package/platforms/claude/adapter.ts +115 -0
  6. package/platforms/junie/adapter.ts +254 -0
  7. package/platforms/junie/agents-template.md +113 -0
  8. package/platforms/junie/hook-translations.md +84 -0
  9. package/platforms/shared/detector.ts +27 -0
  10. package/platforms/shared/installer.ts +202 -0
  11. package/platforms/shared/types.ts +78 -0
  12. package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
  13. package/plugins/ima-claude/agents/explorer.md +30 -0
  14. package/plugins/ima-claude/agents/implementer.md +30 -0
  15. package/plugins/ima-claude/agents/memory.md +42 -0
  16. package/plugins/ima-claude/agents/reviewer.md +53 -0
  17. package/plugins/ima-claude/agents/tester.md +33 -0
  18. package/plugins/ima-claude/agents/wp-developer.md +46 -0
  19. package/plugins/ima-claude/hooks/README.md +145 -0
  20. package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
  21. package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
  22. package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
  23. package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
  24. package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
  25. package/plugins/ima-claude/hooks/docs_organization.py +104 -0
  26. package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
  27. package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
  28. package/plugins/ima-claude/hooks/hook_logger.py +69 -0
  29. package/plugins/ima-claude/hooks/hooks.json +239 -0
  30. package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
  31. package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
  32. package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
  33. package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
  34. package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
  35. package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
  36. package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
  37. package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
  38. package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
  39. package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
  40. package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
  41. package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
  42. package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
  43. package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
  44. package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
  45. package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
  46. package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
  47. package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
  48. package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
  49. package/plugins/ima-claude/personalities/README.md +45 -0
  50. package/plugins/ima-claude/personalities/enable-40k.md +69 -0
  51. package/plugins/ima-claude/personalities/enable-templars.md +69 -0
  52. package/plugins/ima-claude/skills/.research-summary.md +340 -0
  53. package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
  54. package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
  55. package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
  56. package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
  57. package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
  58. package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
  59. package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
  60. package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
  61. package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
  62. package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
  63. package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
  64. package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
  65. package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
  66. package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
  67. package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
  68. package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
  69. package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
  70. package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
  71. package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
  72. package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
  73. package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
  74. package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
  75. package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
  76. package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
  77. package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
  78. package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
  79. package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
  80. package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
  81. package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
  82. package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
  83. package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
  84. package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
  85. package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
  86. package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
  87. package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
  88. package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
  89. package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
  90. package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
  91. package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
  92. package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
  93. package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
  94. package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
  95. package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
  96. package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
  97. package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
  98. package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
  99. package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
  100. package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
  101. package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
  102. package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
  103. package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
  104. package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
  105. package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
  106. package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
  107. package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
  108. package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
  109. package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
  110. package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
  111. package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
  112. package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
  113. package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
  114. package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
  115. package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
  116. package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
  117. package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
  118. package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
  119. package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
  120. package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
  121. package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
  122. package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
  123. package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
  124. package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
  125. package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
  126. package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
  127. package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
  128. package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
  129. package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
  130. package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
  131. package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
  132. package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
  133. package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
  134. package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
  135. package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
  136. package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
  137. package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
  138. package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
  139. package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
  140. package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
  141. package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
  142. package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
  143. package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
  144. package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
  145. package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
  146. package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
  147. package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
  148. package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
  149. package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
  150. package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
  151. package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
  152. package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
  153. package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
  154. package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
  155. package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
  156. package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
  157. package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
  158. package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
  159. package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
  160. package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
  161. package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
  162. package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
  163. package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
  164. package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
  165. package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
  166. package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
  167. package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
  168. package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
  169. package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
  170. package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
  171. package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
  172. package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
  173. package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
  174. package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
  175. package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
  176. package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
  177. package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
  178. package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
  179. package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
  180. package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
  181. package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
  182. package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
@@ -0,0 +1,381 @@
1
+ # Python FP Core Principles (Deep Dive)
2
+
3
+ Comprehensive FP philosophy and patterns for Python. Load this when explaining WHY, making architectural decisions, or need detailed pattern guidance.
4
+
5
+ ## The Pythonic FP Philosophy
6
+
7
+ Python's creator Guido van Rossum has been explicit: Python is not a functional language. It borrows FP concepts selectively. The key insight is working WITH this design, not against it.
8
+
9
+ **Pythonic FP means**: Use FP concepts (purity, immutability, composition, higher-order functions) but express them in Pythonic syntax. Don't import Haskell wholesale.
10
+
11
+ ### What to Adopt
12
+
13
+ | Pattern | Python Expression |
14
+ |---------|-------------------|
15
+ | Pure functions | Regular functions with no side effects |
16
+ | Immutable records | `@dataclass(frozen=True)` or `NamedTuple` |
17
+ | Composition | Direct function calls, `pipe()` for pandas |
18
+ | Higher-order functions | Comprehensions, `functools`, `itertools` |
19
+ | Lazy evaluation | Generators and generator expressions |
20
+ | Partial application | `functools.partial` |
21
+ | Memoization | `@lru_cache` / `@cache` |
22
+ | Pattern matching | `match`/`case` (3.10+) |
23
+
24
+ ### What to Avoid
25
+
26
+ | Pattern | Why Not in Python |
27
+ |---------|-------------------|
28
+ | Custom pipe/compose | Adds indirection, not Pythonic |
29
+ | Custom curry | `partial` covers the need |
30
+ | Custom monads | Fight the language, hard to read |
31
+ | Point-free style | Python doesn't support it well |
32
+ | Deep recursion | No TCO, 1000-call limit |
33
+ | Overloading `__or__` for pipes | Clever but confusing |
34
+
35
+ ## Functional Core, Imperative Shell
36
+
37
+ The single highest-leverage FP technique for Python. Originated from Gary Bernhardt's "Boundaries" talk.
38
+
39
+ ### Architecture
40
+
41
+ ```
42
+ ┌─────────────────────────────────────┐
43
+ │ Imperative Shell │
44
+ │ (I/O, side effects, orchestration) │
45
+ │ │
46
+ │ ┌─────────────────────────────┐ │
47
+ │ │ Functional Core │ │
48
+ │ │ (pure business logic) │ │
49
+ │ │ - calculations │ │
50
+ │ │ - validations │ │
51
+ │ │ - transformations │ │
52
+ │ │ - business rules │ │
53
+ │ └─────────────────────────────┘ │
54
+ │ │
55
+ │ Database, APIs, files, logging │
56
+ └─────────────────────────────────────┘
57
+ ```
58
+
59
+ ### Example
60
+
61
+ ```python
62
+ # === PURE CORE ===
63
+
64
+ def calculate_order_total(items: list[dict], tax_rate: float) -> float:
65
+ subtotal = sum(item["price"] * item["quantity"] for item in items)
66
+ return round(subtotal * (1 + tax_rate), 2)
67
+
68
+ def validate_order(order: dict) -> dict:
69
+ if not order.get("items"):
70
+ return {"success": False, "error": "Order must have items"}
71
+ if any(item["quantity"] <= 0 for item in order["items"]):
72
+ return {"success": False, "error": "Quantities must be positive"}
73
+ return {"success": True, "data": order}
74
+
75
+ def apply_discount(total: float, discount_code: str, discount_table: dict) -> float:
76
+ rate = discount_table.get(discount_code, 0.0)
77
+ return round(total * (1 - rate), 2)
78
+
79
+
80
+ # === IMPERATIVE SHELL ===
81
+
82
+ def process_order(order: dict, db, payment_gateway, logger):
83
+ """Shell: orchestrates I/O around pure core."""
84
+ validated = validate_order(order)
85
+ if not validated["success"]:
86
+ return validated
87
+
88
+ # Pure calculations
89
+ tax_rate = db.get_tax_rate(order["region"])
90
+ discount_table = db.get_discount_table()
91
+ total = calculate_order_total(order["items"], tax_rate)
92
+ final = apply_discount(total, order.get("discount_code", ""), discount_table)
93
+
94
+ # Side effects
95
+ charge_result = payment_gateway.charge(order["customer_id"], final)
96
+ if not charge_result["success"]:
97
+ return charge_result
98
+
99
+ db.save_order({**order, "total": final, "status": "paid"})
100
+ logger.info(f"Order processed: {final}")
101
+ return {"success": True, "data": {"total": final}}
102
+ ```
103
+
104
+ **The ratio to aim for**: 80% pure core, 20% impure shell.
105
+
106
+ ## Immutability Deep Dive
107
+
108
+ ### The Immutability Spectrum in Python
109
+
110
+ From most to least immutable:
111
+
112
+ 1. **Truly immutable**: `int`, `float`, `str`, `bytes`, `tuple`, `frozenset`
113
+ 2. **Shallow frozen**: `@dataclass(frozen=True)`, `NamedTuple`
114
+ 3. **Read-only views**: `types.MappingProxyType`
115
+ 4. **Convention-only**: Regular objects you choose not to mutate
116
+ 5. **Deeply immutable (library)**: `pyrsistent.PMap`, `pyrsistent.PVector`
117
+
118
+ ### Frozen Dataclasses — The Default Choice
119
+
120
+ ```python
121
+ from dataclasses import dataclass, field, replace
122
+
123
+ @dataclass(frozen=True)
124
+ class Order:
125
+ customer_id: int
126
+ items: tuple[dict, ...] # Use tuple, not list, for deep immutability
127
+ status: str = "pending"
128
+ metadata: dict = field(default_factory=dict)
129
+
130
+ # Create
131
+ order = Order(customer_id=1, items=({"sku": "A", "qty": 2},))
132
+
133
+ # "Update" — returns new instance
134
+ paid_order = replace(order, status="paid")
135
+
136
+ # Original is untouched
137
+ assert order.status == "pending"
138
+ assert paid_order.status == "paid"
139
+ ```
140
+
141
+ ### When to Use pyrsistent
142
+
143
+ Only when you need efficient structural sharing for large nested data:
144
+
145
+ ```python
146
+ from pyrsistent import pmap, pvector, freeze, thaw
147
+
148
+ # Convert mutable to immutable
149
+ config = freeze({"db": {"host": "localhost", "port": 5432}, "debug": True})
150
+
151
+ # Efficient "updates" via structural sharing
152
+ new_config = config.set("debug", False)
153
+ # config is unchanged, new_config shares the "db" subtree
154
+
155
+ # Convert back when needed for I/O
156
+ mutable = thaw(new_config)
157
+ ```
158
+
159
+ **Don't reach for pyrsistent** for simple cases — `frozen=True` dataclasses with `replace()` cover 90% of needs.
160
+
161
+ ## Result Type Patterns
162
+
163
+ ### The Standard Shape
164
+
165
+ ```python
166
+ from typing import TypeVar, Any
167
+
168
+ T = TypeVar("T")
169
+
170
+ def success(data: Any = None) -> dict:
171
+ return {"success": True, "data": data}
172
+
173
+ def failure(error: str) -> dict:
174
+ return {"success": False, "error": error}
175
+ ```
176
+
177
+ ### Chaining Results
178
+
179
+ ```python
180
+ def chain_results(*steps):
181
+ """Apply steps sequentially, short-circuiting on failure."""
182
+ def run(data):
183
+ current = data
184
+ for step in steps:
185
+ result = step(current)
186
+ if not result["success"]:
187
+ return result
188
+ current = result["data"]
189
+ return success(current)
190
+ return run
191
+
192
+ # Usage
193
+ process = chain_results(validate, transform, enrich)
194
+ result = process(raw_data)
195
+ ```
196
+
197
+ ### Typed Results (For Larger Codebases)
198
+
199
+ ```python
200
+ from dataclasses import dataclass
201
+ from typing import TypeVar, Generic, Union
202
+
203
+ T = TypeVar("T")
204
+
205
+ @dataclass(frozen=True)
206
+ class Success(Generic[T]):
207
+ data: T
208
+
209
+ @dataclass(frozen=True)
210
+ class Failure:
211
+ error: str
212
+
213
+ Result = Union[Success[T], Failure]
214
+
215
+ def divide(a: float, b: float) -> Result[float]:
216
+ if b == 0:
217
+ return Failure("Division by zero")
218
+ return Success(a / b)
219
+
220
+ # Pattern matching with results (3.10+)
221
+ match divide(10, 3):
222
+ case Success(data=value):
223
+ print(f"Result: {value}")
224
+ case Failure(error=msg):
225
+ print(f"Error: {msg}")
226
+ ```
227
+
228
+ ## Higher-Order Functions Done Right
229
+
230
+ ### Comprehensions Are Your map/filter
231
+
232
+ ```python
233
+ # These are equivalent, but comprehensions are Pythonic:
234
+ list(map(str.upper, names)) # Haskell-ish
235
+ [name.upper() for name in names] # Pythonic
236
+
237
+ list(filter(lambda x: x > 0, nums)) # Haskell-ish
238
+ [x for x in nums if x > 0] # Pythonic
239
+
240
+ # Nested transformations
241
+ {
242
+ dept: [e["name"] for e in employees if e["active"]]
243
+ for dept, employees in grouped.items()
244
+ }
245
+ ```
246
+
247
+ ### When map/filter ARE Appropriate
248
+
249
+ ```python
250
+ # When you have a named function already — no lambda needed
251
+ clean_lines = list(map(str.strip, lines))
252
+ valid_emails = list(filter(is_valid_email, emails))
253
+
254
+ # With operator module — cleaner than lambda
255
+ from operator import itemgetter
256
+ sorted_users = sorted(users, key=itemgetter("last_name", "first_name"))
257
+ ```
258
+
259
+ ### functools.reduce — Use Sparingly
260
+
261
+ ```python
262
+ from functools import reduce
263
+ from operator import mul
264
+
265
+ # Good: simple accumulation with named operator
266
+ factorial = reduce(mul, range(1, n + 1), 1)
267
+
268
+ # Bad: complex reduce is hard to read
269
+ # Use a loop or comprehension instead
270
+ result = reduce(
271
+ lambda acc, x: {**acc, x["key"]: x["value"]}, # Unreadable
272
+ items,
273
+ {}
274
+ )
275
+
276
+ # Better: dict comprehension
277
+ result = {item["key"]: item["value"] for item in items}
278
+ ```
279
+
280
+ ## Error Handling Philosophy
281
+
282
+ ### Exceptions vs Result Types
283
+
284
+ **Use exceptions for**:
285
+ - Truly exceptional conditions (out of memory, disk full)
286
+ - Programming errors (wrong type, missing key)
287
+ - Framework integration (Django, FastAPI expect exceptions)
288
+
289
+ **Use result types for**:
290
+ - Expected failures (validation errors, not found, business rule violations)
291
+ - Operations that callers should handle explicitly
292
+ - Pipeline/chain processing where failures are data
293
+
294
+ ```python
295
+ # Exceptions for programming errors
296
+ def process(data: list[dict]) -> list[dict]:
297
+ if not isinstance(data, list):
298
+ raise TypeError(f"Expected list, got {type(data)}")
299
+ return [transform(item) for item in data]
300
+
301
+ # Result types for business logic
302
+ def withdraw(account: dict, amount: float) -> dict:
303
+ if amount <= 0:
304
+ return failure("Amount must be positive")
305
+ if amount > account["balance"]:
306
+ return failure("Insufficient funds")
307
+ return success({**account, "balance": account["balance"] - amount})
308
+ ```
309
+
310
+ ## Established Libraries Reference
311
+
312
+ ### When to Pull In a Library
313
+
314
+ | Need | Library | Justification |
315
+ |------|---------|---------------|
316
+ | Heavy function composition | `toolz` / `cytoolz` | Battle-tested, Cython performance |
317
+ | Persistent data structures | `pyrsistent` | Structural sharing for large data |
318
+ | Extended iterators | `more-itertools` | Hundreds of useful utilities |
319
+ | Typed error handling | `returns` (dry-python) | Only if team commits to it |
320
+ | F#-style pipelines | `expression` | Lighter than `returns` |
321
+
322
+ ### toolz Quick Reference
323
+
324
+ ```python
325
+ from toolz import pipe, groupby, valmap, merge
326
+
327
+ # pipe is acceptable FROM toolz — don't build your own
328
+ result = pipe(
329
+ data,
330
+ clean,
331
+ validate,
332
+ transform,
333
+ )
334
+
335
+ # Useful dict operations
336
+ grouped = groupby("category", items)
337
+ totals = valmap(lambda items: sum(i["price"] for i in items), grouped)
338
+ merged = merge(defaults, overrides)
339
+ ```
340
+
341
+ **Rule**: Using `toolz.pipe` is fine. Building your own `pipe` is not. The distinction is between using established, tested tools and reinventing them.
342
+
343
+ ## Concurrency and Parallelism
344
+
345
+ ### Pure Functions Enable Easy Parallelism
346
+
347
+ ```python
348
+ from multiprocessing import Pool
349
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
350
+
351
+ # CPU-bound: multiprocessing (pure functions serialize safely)
352
+ def compute_score(item: dict) -> dict:
353
+ return {**item, "score": heavy_computation(item["data"])}
354
+
355
+ with ProcessPoolExecutor(max_workers=4) as executor:
356
+ results = list(executor.map(compute_score, items))
357
+
358
+ # I/O-bound: threading (even with GIL, I/O releases it)
359
+ def fetch_data(url: str) -> dict:
360
+ response = requests.get(url)
361
+ return {"url": url, "data": response.json()}
362
+
363
+ with ThreadPoolExecutor(max_workers=10) as executor:
364
+ results = list(executor.map(fetch_data, urls))
365
+ ```
366
+
367
+ ### async/await — The Shell Layer
368
+
369
+ ```python
370
+ import asyncio
371
+
372
+ # Pure core — same as synchronous
373
+ def process_response(data: dict) -> dict:
374
+ return {**data, "processed": True, "score": compute_score(data)}
375
+
376
+ # Async shell — handles I/O
377
+ async def fetch_and_process(session, url: str) -> dict:
378
+ async with session.get(url) as response:
379
+ data = await response.json()
380
+ return process_response(data) # Pure function, no await needed
381
+ ```
@@ -0,0 +1,283 @@
1
+ # Python FP Testing Patterns (Deep Dive)
2
+
3
+ Comprehensive testing strategies for functional Python code using pytest. Load this when building test suites, improving coverage, or need testing guidance.
4
+
5
+ ## The FP Testing Advantage
6
+
7
+ Pure functions are trivially testable:
8
+ - No setup/teardown ceremony
9
+ - No mocking infrastructure
10
+ - All edge cases enumerable
11
+ - Parametrized testing is natural
12
+
13
+ ## pytest Fundamentals for FP
14
+
15
+ ### Parametrized Tests — The Core Pattern
16
+
17
+ ```python
18
+ import pytest
19
+
20
+ @pytest.mark.parametrize("input_val,expected", [
21
+ ("hello@example.com", True),
22
+ ("invalid", False),
23
+ ("", False),
24
+ ("a@b.c", True),
25
+ ("@no-local.com", False),
26
+ ("no-domain@", False),
27
+ ])
28
+ def test_validate_email(input_val, expected):
29
+ assert validate_email(input_val) == expected
30
+ ```
31
+
32
+ ### Grouped Parametrize for Complex Cases
33
+
34
+ ```python
35
+ @pytest.mark.parametrize("items,tax_rate,expected", [
36
+ # Happy path
37
+ ([{"price": 10, "qty": 2}, {"price": 5, "qty": 1}], 0.1, 27.50),
38
+ # Empty order
39
+ ([], 0.1, 0.0),
40
+ # Zero tax
41
+ ([{"price": 100, "qty": 1}], 0.0, 100.0),
42
+ # High precision
43
+ ([{"price": 9.99, "qty": 3}], 0.0825, 32.44),
44
+ ])
45
+ def test_calculate_order_total(items, tax_rate, expected):
46
+ assert calculate_order_total(items, tax_rate) == expected
47
+ ```
48
+
49
+ ### Testing Result Types
50
+
51
+ ```python
52
+ def test_withdraw_success():
53
+ account = {"id": 1, "balance": 100.0}
54
+ result = withdraw(account, 30.0)
55
+ assert result["success"] is True
56
+ assert result["data"]["balance"] == 70.0
57
+
58
+ def test_withdraw_insufficient_funds():
59
+ account = {"id": 1, "balance": 10.0}
60
+ result = withdraw(account, 50.0)
61
+ assert result["success"] is False
62
+ assert "insufficient" in result["error"].lower()
63
+
64
+ @pytest.mark.parametrize("amount,should_succeed", [
65
+ (50.0, True),
66
+ (100.0, True), # Exact balance
67
+ (100.01, False), # Just over
68
+ (0.0, False), # Zero
69
+ (-10.0, False), # Negative
70
+ ])
71
+ def test_withdraw_boundary_cases(amount, should_succeed):
72
+ account = {"id": 1, "balance": 100.0}
73
+ result = withdraw(account, amount)
74
+ assert result["success"] is should_succeed
75
+ ```
76
+
77
+ ## Testing Immutability
78
+
79
+ ```python
80
+ from dataclasses import FrozenInstanceError
81
+
82
+ def test_frozen_dataclass_prevents_mutation():
83
+ user = User(name="Alice", email="alice@test.com")
84
+ with pytest.raises(FrozenInstanceError):
85
+ user.name = "Bob"
86
+
87
+ def test_replace_returns_new_instance():
88
+ original = User(name="Alice", email="alice@test.com")
89
+ updated = replace(original, email="new@test.com")
90
+
91
+ assert updated.email == "new@test.com"
92
+ assert original.email == "alice@test.com" # Unchanged
93
+ assert original is not updated # Different objects
94
+
95
+ def test_dict_immutability_pattern():
96
+ original = {"name": "Alice", "age": 30}
97
+ updated = {**original, "age": 31}
98
+
99
+ assert updated["age"] == 31
100
+ assert original["age"] == 30 # Unchanged
101
+ ```
102
+
103
+ ## Testing Generators and Lazy Pipelines
104
+
105
+ ```python
106
+ def test_generator_pipeline():
107
+ data = [" Alice ", "", " Bob ", " ", " Carol "]
108
+
109
+ def clean(lines):
110
+ for line in lines:
111
+ stripped = line.strip()
112
+ if stripped:
113
+ yield stripped
114
+
115
+ def upper(lines):
116
+ for line in lines:
117
+ yield line.upper()
118
+
119
+ result = list(upper(clean(data)))
120
+ assert result == ["ALICE", "BOB", "CAROL"]
121
+
122
+ def test_generator_is_lazy():
123
+ call_count = 0
124
+
125
+ def counting_transform(items):
126
+ nonlocal call_count
127
+ for item in items:
128
+ call_count += 1
129
+ yield item * 2
130
+
131
+ gen = counting_transform([1, 2, 3, 4, 5])
132
+ assert call_count == 0 # Nothing executed yet
133
+
134
+ first = next(gen)
135
+ assert first == 2
136
+ assert call_count == 1 # Only one item processed
137
+ ```
138
+
139
+ ## Testing Data Pipelines (pandas)
140
+
141
+ ```python
142
+ import pandas as pd
143
+ import pytest
144
+
145
+ @pytest.fixture
146
+ def sample_df():
147
+ return pd.DataFrame({
148
+ "name": [" Alice ", "Bob", None, "Carol"],
149
+ "age": [25, 30, 35, 40],
150
+ "email": ["a@test.com", None, "c@test.com", "d@test.com"],
151
+ })
152
+
153
+ def test_clean_nulls(sample_df):
154
+ result = clean_nulls(sample_df)
155
+ assert len(result) == 2 # Only rows with both name and email
156
+ assert result["name"].isna().sum() == 0
157
+ assert result["email"].isna().sum() == 0
158
+
159
+ def test_normalize_names(sample_df):
160
+ result = normalize_names(sample_df.dropna(subset=["name"]))
161
+ assert list(result["name"]) == ["Alice", "Bob", "Carol"]
162
+
163
+ def test_pipeline_composition(sample_df):
164
+ """Test the full pipeline produces expected shape."""
165
+ result = (
166
+ sample_df
167
+ .pipe(clean_nulls)
168
+ .pipe(normalize_names)
169
+ )
170
+ assert "name" in result.columns
171
+ assert result["name"].str.strip().equals(result["name"]) # No whitespace
172
+ ```
173
+
174
+ ## Property-Based Testing with Hypothesis
175
+
176
+ ```python
177
+ from hypothesis import given, strategies as st
178
+
179
+ # Pure functions enable property-based testing naturally
180
+ @given(st.lists(st.floats(min_value=0, max_value=10000, allow_nan=False)))
181
+ def test_calculate_total_non_negative(prices):
182
+ items = [{"price": p} for p in prices]
183
+ total = calculate_total(items)
184
+ assert total >= 0
185
+
186
+ @given(st.text(), st.text())
187
+ def test_concat_length(a, b):
188
+ result = concat(a, b)
189
+ assert len(result) == len(a) + len(b)
190
+
191
+ @given(
192
+ st.dictionaries(st.text(min_size=1), st.integers()),
193
+ st.dictionaries(st.text(min_size=1), st.integers()),
194
+ )
195
+ def test_merge_dicts_contains_all_keys(d1, d2):
196
+ result = merge_dicts(d1, d2)
197
+ assert all(k in result for k in d1)
198
+ assert all(k in result for k in d2)
199
+
200
+ # Roundtrip properties
201
+ @given(st.builds(User, name=st.text(min_size=1), email=st.emails()))
202
+ def test_user_serialization_roundtrip(user):
203
+ serialized = user_to_dict(user)
204
+ deserialized = user_from_dict(serialized)
205
+ assert deserialized == user
206
+ ```
207
+
208
+ ## Testing Side Effect Isolation
209
+
210
+ ```python
211
+ def test_pure_core_without_mocking():
212
+ """Pure functions need no mocks — just pass data in, check data out."""
213
+ order = {"items": [{"price": 10, "qty": 2}], "discount_code": "SAVE10"}
214
+ discount_table = {"SAVE10": 0.10}
215
+
216
+ total = calculate_order_total(order["items"], tax_rate=0.0)
217
+ final = apply_discount(total, order["discount_code"], discount_table)
218
+
219
+ assert final == 18.0 # 20 - 10%
220
+
221
+ def test_shell_with_minimal_mocking():
222
+ """Shell tests need mocks, but they're thin."""
223
+ mock_db = Mock()
224
+ mock_db.get_tax_rate.return_value = 0.0
225
+ mock_db.get_discount_table.return_value = {}
226
+ mock_payment = Mock()
227
+ mock_payment.charge.return_value = {"success": True}
228
+ mock_logger = Mock()
229
+
230
+ result = process_order(
231
+ {"items": [{"price": 10, "qty": 1}], "region": "US", "customer_id": 1},
232
+ db=mock_db,
233
+ payment_gateway=mock_payment,
234
+ logger=mock_logger,
235
+ )
236
+
237
+ assert result["success"]
238
+ mock_db.save_order.assert_called_once()
239
+ ```
240
+
241
+ ## Fixtures for FP Testing
242
+
243
+ ```python
244
+ @pytest.fixture
245
+ def make_user():
246
+ """Factory fixture — returns a function for creating test users."""
247
+ def _make(name="Test User", email="test@example.com", **overrides):
248
+ return User(name=name, email=email, **overrides)
249
+ return _make
250
+
251
+ def test_update_email(make_user):
252
+ user = make_user(email="old@test.com")
253
+ updated = update_email(user, "new@test.com")
254
+ assert updated.email == "new@test.com"
255
+
256
+ @pytest.fixture
257
+ def sample_items():
258
+ """Reusable test data — immutable tuple."""
259
+ return (
260
+ {"id": 1, "name": "Widget", "price": 10.0, "category": "A"},
261
+ {"id": 2, "name": "Gadget", "price": 25.0, "category": "B"},
262
+ {"id": 3, "name": "Doohickey", "price": 5.0, "category": "A"},
263
+ )
264
+ ```
265
+
266
+ ## Test Organization
267
+
268
+ ```
269
+ tests/
270
+ test_core/ # Pure function tests (no mocks)
271
+ test_calculations.py
272
+ test_validations.py
273
+ test_transformations.py
274
+ test_shell/ # Integration tests (minimal mocks)
275
+ test_order_service.py
276
+ test_api_handlers.py
277
+ test_pipelines/ # Data pipeline tests
278
+ test_etl.py
279
+ test_features.py
280
+ conftest.py # Shared fixtures
281
+ ```
282
+
283
+ **Key insight**: The pure core tests should be the majority (80%+) and run fast with no I/O. Shell tests are fewer and may need mocks or test databases.