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.
- package/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- 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.
|