erk 0.4.5__py3-none-any.whl
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.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Dignified Python - Core Standards
|
|
5
|
+
|
|
6
|
+
This document contains the core Python coding standards that apply to 80%+ of Python code. These principles are loaded with every skill invocation.
|
|
7
|
+
|
|
8
|
+
For conditional loading of specialized patterns:
|
|
9
|
+
|
|
10
|
+
- CLI development → Load `cli-patterns.md`
|
|
11
|
+
- Subprocess operations → Load `subprocess.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The Cornerstone: LBYL Over EAFP
|
|
16
|
+
|
|
17
|
+
**Look Before You Leap: Check conditions proactively, NEVER use exceptions for control flow.**
|
|
18
|
+
|
|
19
|
+
This is the single most important rule in dignified Python. Every pattern below flows from this principle.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# ✅ CORRECT: Check first
|
|
23
|
+
if key in mapping:
|
|
24
|
+
value = mapping[key]
|
|
25
|
+
process(value)
|
|
26
|
+
|
|
27
|
+
# ❌ WRONG: Exception as control flow
|
|
28
|
+
try:
|
|
29
|
+
value = mapping[key]
|
|
30
|
+
process(value)
|
|
31
|
+
except KeyError:
|
|
32
|
+
pass
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Exception Handling
|
|
38
|
+
|
|
39
|
+
### Core Principle
|
|
40
|
+
|
|
41
|
+
**ALWAYS use LBYL, NEVER EAFP for control flow**
|
|
42
|
+
|
|
43
|
+
LBYL means checking conditions before acting. EAFP (Easier to Ask for Forgiveness than Permission) means trying operations and catching exceptions. In dignified Python, we strongly prefer LBYL.
|
|
44
|
+
|
|
45
|
+
### Dictionary Access Patterns
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# ✅ CORRECT: Membership testing
|
|
49
|
+
if key in mapping:
|
|
50
|
+
value = mapping[key]
|
|
51
|
+
process(value)
|
|
52
|
+
else:
|
|
53
|
+
handle_missing()
|
|
54
|
+
|
|
55
|
+
# ✅ ALSO CORRECT: .get() with default
|
|
56
|
+
value = mapping.get(key, default_value)
|
|
57
|
+
process(value)
|
|
58
|
+
|
|
59
|
+
# ✅ CORRECT: Check before nested access
|
|
60
|
+
if "config" in data and "timeout" in data["config"]:
|
|
61
|
+
timeout = data["config"]["timeout"]
|
|
62
|
+
|
|
63
|
+
# ❌ WRONG: KeyError as control flow
|
|
64
|
+
try:
|
|
65
|
+
value = mapping[key]
|
|
66
|
+
except KeyError:
|
|
67
|
+
handle_missing()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### When Exceptions ARE Acceptable
|
|
71
|
+
|
|
72
|
+
Exceptions are ONLY acceptable at:
|
|
73
|
+
|
|
74
|
+
1. **Error boundaries** (CLI/API level)
|
|
75
|
+
2. **Third-party API compatibility** (when no alternative exists)
|
|
76
|
+
3. **Adding context before re-raising**
|
|
77
|
+
|
|
78
|
+
#### 1. Error Boundaries
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# ✅ ACCEPTABLE: CLI command error boundary
|
|
82
|
+
@click.command("create")
|
|
83
|
+
@click.pass_obj
|
|
84
|
+
def create(ctx: ErkContext, name: str) -> None:
|
|
85
|
+
"""Create a worktree."""
|
|
86
|
+
try:
|
|
87
|
+
create_worktree(ctx, name)
|
|
88
|
+
except subprocess.CalledProcessError as e:
|
|
89
|
+
click.echo(f"Error: Git command failed: {e.stderr}", err=True)
|
|
90
|
+
raise SystemExit(1)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 2. Third-Party API Compatibility
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# ✅ ACCEPTABLE: Third-party API forces exception handling
|
|
97
|
+
def _get_bigquery_sample(sql_client, table_name):
|
|
98
|
+
"""
|
|
99
|
+
BigQuery's TABLESAMPLE doesn't work on views.
|
|
100
|
+
There's no reliable way to determine a priori whether
|
|
101
|
+
a table supports TABLESAMPLE.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
return sql_client.run_query(f"SELECT * FROM {table_name} TABLESAMPLE...")
|
|
105
|
+
except Exception:
|
|
106
|
+
return sql_client.run_query(f"SELECT * FROM {table_name} ORDER BY RAND()...")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> **The test for "no alternative exists"**: Can you validate or check the condition BEFORE calling the API? If yes (even using a different function/method), use LBYL. The exception only applies when the API provides NO way to determine success a priori—you literally must attempt the operation to know if it will work.
|
|
110
|
+
|
|
111
|
+
#### What Does NOT Qualify as Third-Party API Compatibility
|
|
112
|
+
|
|
113
|
+
Standard library functions with known LBYL alternatives do NOT qualify:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# ❌ WRONG: int() has LBYL alternative (str.isdigit)
|
|
117
|
+
try:
|
|
118
|
+
port = int(user_input)
|
|
119
|
+
except ValueError:
|
|
120
|
+
port = 80
|
|
121
|
+
|
|
122
|
+
# ✅ CORRECT: Check before calling
|
|
123
|
+
if user_input.lstrip('-+').isdigit():
|
|
124
|
+
port = int(user_input)
|
|
125
|
+
else:
|
|
126
|
+
port = 80
|
|
127
|
+
|
|
128
|
+
# ❌ WRONG: datetime.fromisoformat() can be validated first
|
|
129
|
+
try:
|
|
130
|
+
dt = datetime.fromisoformat(timestamp_str)
|
|
131
|
+
except ValueError:
|
|
132
|
+
dt = None
|
|
133
|
+
|
|
134
|
+
# ✅ CORRECT: Validate format before parsing
|
|
135
|
+
def _is_iso_format(s: str) -> bool:
|
|
136
|
+
return len(s) >= 10 and s[4] == "-" and s[7] == "-"
|
|
137
|
+
|
|
138
|
+
if _is_iso_format(timestamp_str):
|
|
139
|
+
dt = datetime.fromisoformat(timestamp_str)
|
|
140
|
+
else:
|
|
141
|
+
dt = None
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### 3. Adding Context Before Re-raising
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# ✅ ACCEPTABLE: Adding context before re-raising
|
|
148
|
+
try:
|
|
149
|
+
process_file(config_file)
|
|
150
|
+
except yaml.YAMLError as e:
|
|
151
|
+
raise ValueError(f"Failed to parse config file {config_file}: {e}") from e
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Exception Chaining (B904 Lint Compliance)
|
|
155
|
+
|
|
156
|
+
**Ruff rule B904** requires explicit exception chaining when raising inside an `except` block. This prevents losing the original traceback.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# ✅ CORRECT: Chain to preserve context
|
|
160
|
+
try:
|
|
161
|
+
parse_config(path)
|
|
162
|
+
except ValueError as e:
|
|
163
|
+
click.echo(json.dumps({"success": False, "error": str(e)}))
|
|
164
|
+
raise SystemExit(1) from e # Preserves traceback
|
|
165
|
+
|
|
166
|
+
# ✅ CORRECT: Explicitly break chain when intentional
|
|
167
|
+
try:
|
|
168
|
+
fetch_from_cache(key)
|
|
169
|
+
except KeyError:
|
|
170
|
+
# Original exception is not relevant to caller
|
|
171
|
+
raise ValueError(f"Unknown key: {key}") from None
|
|
172
|
+
|
|
173
|
+
# ❌ WRONG: Missing exception chain (B904 violation)
|
|
174
|
+
try:
|
|
175
|
+
parse_config(path)
|
|
176
|
+
except ValueError:
|
|
177
|
+
raise SystemExit(1) # Lint error: missing 'from e' or 'from None'
|
|
178
|
+
|
|
179
|
+
# ✅ CORRECT: CLI error boundary with JSON output
|
|
180
|
+
try:
|
|
181
|
+
result = some_operation()
|
|
182
|
+
except RuntimeError as e:
|
|
183
|
+
click.echo(json.dumps({"success": False, "error": str(e)}))
|
|
184
|
+
raise SystemExit(0) from None # Exception is in JSON, traceback irrelevant to CLI user
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**When to use each:**
|
|
188
|
+
|
|
189
|
+
- `from e` - Preserve original exception for debugging
|
|
190
|
+
- `from None` - Intentionally suppress original (e.g., transforming exception type, CLI JSON output)
|
|
191
|
+
|
|
192
|
+
### Exception Anti-Patterns
|
|
193
|
+
|
|
194
|
+
**❌ Never swallow exceptions silently**
|
|
195
|
+
|
|
196
|
+
Even at error boundaries, you must at least log/warn so issues can be diagnosed:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
# ❌ WRONG: Silent exception swallowing
|
|
200
|
+
try:
|
|
201
|
+
risky_operation()
|
|
202
|
+
except:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# ❌ WRONG: Silent swallowing even at error boundary
|
|
206
|
+
try:
|
|
207
|
+
optional_feature()
|
|
208
|
+
except Exception:
|
|
209
|
+
pass # Silent - impossible to diagnose issues
|
|
210
|
+
|
|
211
|
+
# ✅ CORRECT: Let exceptions bubble up (default)
|
|
212
|
+
risky_operation()
|
|
213
|
+
|
|
214
|
+
# ✅ CORRECT: At error boundaries, log the exception
|
|
215
|
+
try:
|
|
216
|
+
optional_feature()
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logging.warning("Optional feature failed: %s", e) # Diagnosable
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**❌ Never use silent fallback behavior**
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# ❌ WRONG: Silent fallback masks failure
|
|
225
|
+
def process_text(text: str) -> dict:
|
|
226
|
+
try:
|
|
227
|
+
return llm_client.process(text)
|
|
228
|
+
except Exception:
|
|
229
|
+
return regex_parse_fallback(text)
|
|
230
|
+
|
|
231
|
+
# ✅ CORRECT: Let error bubble to boundary
|
|
232
|
+
def process_text(text: str) -> dict:
|
|
233
|
+
return llm_client.process(text)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Path Operations
|
|
239
|
+
|
|
240
|
+
### The Golden Rule
|
|
241
|
+
|
|
242
|
+
**ALWAYS check `.exists()` BEFORE `.resolve()` or `.is_relative_to()`**
|
|
243
|
+
|
|
244
|
+
### Why This Matters
|
|
245
|
+
|
|
246
|
+
- `.resolve()` raises `OSError` for non-existent paths
|
|
247
|
+
- `.is_relative_to()` raises `ValueError` for invalid comparisons
|
|
248
|
+
- Checking `.exists()` first avoids exceptions entirely (LBYL!)
|
|
249
|
+
|
|
250
|
+
### Correct Patterns
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from pathlib import Path
|
|
254
|
+
|
|
255
|
+
# ✅ CORRECT: Check exists first
|
|
256
|
+
for wt_path in worktree_paths:
|
|
257
|
+
if wt_path.exists():
|
|
258
|
+
wt_path_resolved = wt_path.resolve()
|
|
259
|
+
if current_dir.is_relative_to(wt_path_resolved):
|
|
260
|
+
current_worktree = wt_path_resolved
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
# ❌ WRONG: Using exceptions for path validation
|
|
264
|
+
try:
|
|
265
|
+
wt_path_resolved = wt_path.resolve()
|
|
266
|
+
if current_dir.is_relative_to(wt_path_resolved):
|
|
267
|
+
current_worktree = wt_path_resolved
|
|
268
|
+
except (OSError, ValueError):
|
|
269
|
+
continue
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Pathlib Best Practices
|
|
273
|
+
|
|
274
|
+
**Always Use Pathlib (Never os.path)**
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# ✅ CORRECT: Use pathlib.Path
|
|
278
|
+
from pathlib import Path
|
|
279
|
+
|
|
280
|
+
config_file = Path.home() / ".config" / "app.yml"
|
|
281
|
+
if config_file.exists():
|
|
282
|
+
content = config_file.read_text(encoding="utf-8")
|
|
283
|
+
|
|
284
|
+
# ❌ WRONG: Use os.path
|
|
285
|
+
import os.path
|
|
286
|
+
config_file = os.path.join(os.path.expanduser("~"), ".config", "app.yml")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Always Specify Encoding**
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
# ✅ CORRECT: Always specify encoding
|
|
293
|
+
content = path.read_text(encoding="utf-8")
|
|
294
|
+
path.write_text(data, encoding="utf-8")
|
|
295
|
+
|
|
296
|
+
# ❌ WRONG: Default encoding
|
|
297
|
+
content = path.read_text() # Platform-dependent!
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Import Organization
|
|
303
|
+
|
|
304
|
+
### Core Rules
|
|
305
|
+
|
|
306
|
+
1. **Default: ALWAYS place imports at module level**
|
|
307
|
+
2. **Use absolute imports only** (no relative imports)
|
|
308
|
+
3. **Inline imports only for specific exceptions** (see below)
|
|
309
|
+
|
|
310
|
+
### Correct Import Patterns
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
# ✅ CORRECT: Module-level imports
|
|
314
|
+
import json
|
|
315
|
+
import click
|
|
316
|
+
from pathlib import Path
|
|
317
|
+
from erk.config import load_config
|
|
318
|
+
|
|
319
|
+
def my_function() -> None:
|
|
320
|
+
data = json.loads(content)
|
|
321
|
+
click.echo("Processing")
|
|
322
|
+
config = load_config()
|
|
323
|
+
|
|
324
|
+
# ❌ WRONG: Inline imports without justification
|
|
325
|
+
def my_function() -> None:
|
|
326
|
+
import json # NEVER do this
|
|
327
|
+
import click # NEVER do this
|
|
328
|
+
data = json.loads(content)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Legitimate Inline Import Patterns
|
|
332
|
+
|
|
333
|
+
#### 1. Circular Import Prevention
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
# commands/sync.py
|
|
337
|
+
def register_commands(cli_group):
|
|
338
|
+
"""Register commands with CLI group (avoids circular import)."""
|
|
339
|
+
from myapp.cli import sync_command # Breaks circular dependency
|
|
340
|
+
cli_group.add_command(sync_command)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**When to use:**
|
|
344
|
+
|
|
345
|
+
- CLI command registration
|
|
346
|
+
- Plugin systems with bidirectional dependencies
|
|
347
|
+
- Lazy loading to break import cycles
|
|
348
|
+
|
|
349
|
+
#### 2. Conditional Feature Imports
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
def process_data(data: dict, dry_run: bool = False) -> None:
|
|
353
|
+
if dry_run:
|
|
354
|
+
# Inline import: Only needed for dry-run mode
|
|
355
|
+
from myapp.dry_run import NoopProcessor
|
|
356
|
+
processor = NoopProcessor()
|
|
357
|
+
else:
|
|
358
|
+
processor = RealProcessor()
|
|
359
|
+
processor.execute(data)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**When to use:**
|
|
363
|
+
|
|
364
|
+
- Debug/verbose mode utilities
|
|
365
|
+
- Dry-run mode wrappers
|
|
366
|
+
- Optional feature modules
|
|
367
|
+
- Platform-specific implementations
|
|
368
|
+
|
|
369
|
+
#### 3. TYPE_CHECKING Imports
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from typing import TYPE_CHECKING
|
|
373
|
+
|
|
374
|
+
if TYPE_CHECKING:
|
|
375
|
+
from myapp.models import User # Only for type hints
|
|
376
|
+
|
|
377
|
+
def process_user(user: "User") -> None:
|
|
378
|
+
...
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**When to use:**
|
|
382
|
+
|
|
383
|
+
- Avoiding circular dependencies in type hints
|
|
384
|
+
- Forward declarations
|
|
385
|
+
|
|
386
|
+
#### 4. Startup Time Optimization (Rare)
|
|
387
|
+
|
|
388
|
+
Some packages have genuinely heavy import costs (pyspark, jupyter ecosystem, large ML frameworks). Deferring these imports can improve CLI startup time.
|
|
389
|
+
|
|
390
|
+
**However, apply "innocent until proven guilty":**
|
|
391
|
+
|
|
392
|
+
- Default to module-level imports
|
|
393
|
+
- Only defer imports when you have MEASURED evidence of startup impact
|
|
394
|
+
- Document the measured cost in a comment
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
# ✅ ACCEPTABLE: Measured heavy import (adds 800ms to startup)
|
|
398
|
+
def run_spark_job(config: SparkConfig) -> None:
|
|
399
|
+
from pyspark.sql import SparkSession # Heavy: 800ms import time
|
|
400
|
+
session = SparkSession.builder.getOrCreate()
|
|
401
|
+
...
|
|
402
|
+
|
|
403
|
+
# ❌ WRONG: Speculative deferral without measurement
|
|
404
|
+
def check_staleness(project_dir: Path) -> None:
|
|
405
|
+
# Inline imports to avoid import-time side effects <- WRONG: no evidence
|
|
406
|
+
from myapp.staleness import get_version
|
|
407
|
+
...
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**When NOT to defer:**
|
|
411
|
+
|
|
412
|
+
- Standard library modules
|
|
413
|
+
- Lightweight internal modules
|
|
414
|
+
- Modules you haven't measured
|
|
415
|
+
- "Just in case" optimization
|
|
416
|
+
|
|
417
|
+
### Absolute vs Relative Imports
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
# ✅ CORRECT: Absolute import
|
|
421
|
+
from erk.config import load_config
|
|
422
|
+
|
|
423
|
+
# ❌ WRONG: Relative import
|
|
424
|
+
from .config import load_config
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Import-Time Side Effects
|
|
430
|
+
|
|
431
|
+
### Core Rule
|
|
432
|
+
|
|
433
|
+
**Avoid computation and side effects at import time. Defer to function calls.**
|
|
434
|
+
|
|
435
|
+
Module-level code runs when the module is imported. Side effects at import time cause:
|
|
436
|
+
|
|
437
|
+
1. **Slower startup** - Every import triggers computation
|
|
438
|
+
2. **Test brittleness** - Hard to mock/control behavior
|
|
439
|
+
3. **Circular import issues** - Dependencies evaluated too early
|
|
440
|
+
4. **Unpredictable order** - Import order affects behavior
|
|
441
|
+
|
|
442
|
+
### Common Anti-Patterns
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
# ❌ WRONG: Path computed at import time
|
|
446
|
+
SESSION_ID_FILE = Path(".erk/scratch/current-session-id")
|
|
447
|
+
|
|
448
|
+
def get_session_id() -> str | None:
|
|
449
|
+
if SESSION_ID_FILE.exists():
|
|
450
|
+
return SESSION_ID_FILE.read_text(encoding="utf-8")
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
# ❌ WRONG: Config loaded at import time
|
|
454
|
+
CONFIG = load_config() # I/O at import!
|
|
455
|
+
|
|
456
|
+
# ❌ WRONG: Connection established at import time
|
|
457
|
+
DB_CLIENT = DatabaseClient(os.environ["DB_URL"]) # Side effect at import!
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Correct Patterns
|
|
461
|
+
|
|
462
|
+
**Use `@cache` for deferred computation:**
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from functools import cache
|
|
466
|
+
|
|
467
|
+
# ✅ CORRECT: Defer computation until first call
|
|
468
|
+
@cache
|
|
469
|
+
def _session_id_file_path() -> Path:
|
|
470
|
+
"""Return path to session ID file (cached after first call)."""
|
|
471
|
+
return Path(".erk/scratch/current-session-id")
|
|
472
|
+
|
|
473
|
+
def get_session_id() -> str | None:
|
|
474
|
+
session_file = _session_id_file_path()
|
|
475
|
+
if session_file.exists():
|
|
476
|
+
return session_file.read_text(encoding="utf-8")
|
|
477
|
+
return None
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Use functions for resources:**
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
# ✅ CORRECT: Defer resource creation to function call
|
|
484
|
+
@cache
|
|
485
|
+
def get_config() -> Config:
|
|
486
|
+
"""Load config on first call, cache result."""
|
|
487
|
+
return load_config()
|
|
488
|
+
|
|
489
|
+
@cache
|
|
490
|
+
def get_db_client() -> DatabaseClient:
|
|
491
|
+
"""Create database client on first call."""
|
|
492
|
+
return DatabaseClient(os.environ["DB_URL"])
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### When Module-Level Constants ARE Acceptable
|
|
496
|
+
|
|
497
|
+
Simple, static values that don't involve computation or I/O:
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
# ✅ ACCEPTABLE: Static constants
|
|
501
|
+
DEFAULT_TIMEOUT = 30
|
|
502
|
+
MAX_RETRIES = 3
|
|
503
|
+
SUPPORTED_FORMATS = frozenset({"json", "yaml", "toml"})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Decision Checklist
|
|
507
|
+
|
|
508
|
+
Before writing module-level code:
|
|
509
|
+
|
|
510
|
+
- [ ] Does this involve any computation (even `Path()` construction)?
|
|
511
|
+
- [ ] Does this involve I/O (file, network, environment)?
|
|
512
|
+
- [ ] Could this fail or raise exceptions?
|
|
513
|
+
- [ ] Would tests need to mock this value?
|
|
514
|
+
|
|
515
|
+
If any answer is "yes", wrap in a `@cache`-decorated function instead.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Dependency Injection
|
|
520
|
+
|
|
521
|
+
### Core Rule
|
|
522
|
+
|
|
523
|
+
**Use ABC for interfaces, NEVER Protocol**
|
|
524
|
+
|
|
525
|
+
### ABC Interface Pattern
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
# ✅ CORRECT: Use ABC for interfaces
|
|
529
|
+
from abc import ABC, abstractmethod
|
|
530
|
+
|
|
531
|
+
class Repository(ABC):
|
|
532
|
+
@abstractmethod
|
|
533
|
+
def save(self, entity: Entity) -> None:
|
|
534
|
+
"""Save entity to storage."""
|
|
535
|
+
...
|
|
536
|
+
|
|
537
|
+
@abstractmethod
|
|
538
|
+
def load(self, id: str) -> Entity:
|
|
539
|
+
"""Load entity by ID."""
|
|
540
|
+
...
|
|
541
|
+
|
|
542
|
+
class PostgresRepository(Repository):
|
|
543
|
+
def save(self, entity: Entity) -> None:
|
|
544
|
+
# Implementation
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
def load(self, id: str) -> Entity:
|
|
548
|
+
# Implementation
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
# ❌ WRONG: Using Protocol
|
|
552
|
+
from typing import Protocol
|
|
553
|
+
|
|
554
|
+
class Repository(Protocol):
|
|
555
|
+
def save(self, entity: Entity) -> None: ...
|
|
556
|
+
def load(self, id: str) -> Entity: ...
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Benefits of ABC
|
|
560
|
+
|
|
561
|
+
1. **Explicit inheritance** - Clear class hierarchy
|
|
562
|
+
2. **Runtime validation** - Errors if abstract methods not implemented
|
|
563
|
+
3. **Better IDE support** - Autocomplete and refactoring work better
|
|
564
|
+
4. **Documentation** - Clear contract definition
|
|
565
|
+
|
|
566
|
+
### Complete DI Example
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
from abc import ABC, abstractmethod
|
|
570
|
+
from dataclasses import dataclass
|
|
571
|
+
|
|
572
|
+
# Define the interface
|
|
573
|
+
class DataStore(ABC):
|
|
574
|
+
@abstractmethod
|
|
575
|
+
def get(self, key: str) -> str | None:
|
|
576
|
+
"""Retrieve value by key."""
|
|
577
|
+
...
|
|
578
|
+
|
|
579
|
+
@abstractmethod
|
|
580
|
+
def set(self, key: str, value: str) -> None:
|
|
581
|
+
"""Store value with key."""
|
|
582
|
+
...
|
|
583
|
+
|
|
584
|
+
# Real implementation
|
|
585
|
+
class RedisStore(DataStore):
|
|
586
|
+
def get(self, key: str) -> str | None:
|
|
587
|
+
return self.client.get(key)
|
|
588
|
+
|
|
589
|
+
def set(self, key: str, value: str) -> None:
|
|
590
|
+
self.client.set(key, value)
|
|
591
|
+
|
|
592
|
+
# Fake for testing
|
|
593
|
+
class FakeStore(DataStore):
|
|
594
|
+
def __init__(self) -> None:
|
|
595
|
+
self._data: dict[str, str] = {}
|
|
596
|
+
|
|
597
|
+
def get(self, key: str) -> str | None:
|
|
598
|
+
if key not in self._data:
|
|
599
|
+
return None
|
|
600
|
+
return self._data[key]
|
|
601
|
+
|
|
602
|
+
def set(self, key: str, value: str) -> None:
|
|
603
|
+
self._data[key] = value
|
|
604
|
+
|
|
605
|
+
# Business logic accepts interface
|
|
606
|
+
@dataclass
|
|
607
|
+
class Service:
|
|
608
|
+
store: DataStore # Depends on abstraction
|
|
609
|
+
|
|
610
|
+
def process(self, item: str) -> None:
|
|
611
|
+
cached = self.store.get(item)
|
|
612
|
+
if cached is None:
|
|
613
|
+
result = expensive_computation(item)
|
|
614
|
+
self.store.set(item, result)
|
|
615
|
+
else:
|
|
616
|
+
result = cached
|
|
617
|
+
use_result(result)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Performance Guidelines
|
|
623
|
+
|
|
624
|
+
### Properties Must Be O(1)
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
# ❌ WRONG: Property doing I/O
|
|
628
|
+
@property
|
|
629
|
+
def size(self) -> int:
|
|
630
|
+
return self._fetch_from_db()
|
|
631
|
+
|
|
632
|
+
# ✅ CORRECT: Explicit method name
|
|
633
|
+
def fetch_size_from_db(self) -> int:
|
|
634
|
+
return self._fetch_from_db()
|
|
635
|
+
|
|
636
|
+
# ✅ CORRECT: O(1) property
|
|
637
|
+
@property
|
|
638
|
+
def size(self) -> int:
|
|
639
|
+
return self._cached_size
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Magic Methods Must Be O(1)
|
|
643
|
+
|
|
644
|
+
```python
|
|
645
|
+
# ❌ WRONG: __len__ doing iteration
|
|
646
|
+
def __len__(self) -> int:
|
|
647
|
+
return sum(1 for _ in self._items)
|
|
648
|
+
|
|
649
|
+
# ✅ CORRECT: O(1) __len__
|
|
650
|
+
def __len__(self) -> int:
|
|
651
|
+
return self._count
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Using `typing.cast()`
|
|
657
|
+
|
|
658
|
+
### Core Rule
|
|
659
|
+
|
|
660
|
+
**ALWAYS verify `cast()` with a runtime assertion, unless there's a documented reason not to.**
|
|
661
|
+
|
|
662
|
+
`typing.cast()` is a compile-time only construct—it tells the type checker to trust you but performs no runtime verification. If your assumption is wrong, you'll get silent misbehavior instead of a clear error.
|
|
663
|
+
|
|
664
|
+
### Required Pattern
|
|
665
|
+
|
|
666
|
+
```python
|
|
667
|
+
from collections.abc import MutableMapping
|
|
668
|
+
from typing import Any, cast
|
|
669
|
+
|
|
670
|
+
# ✅ CORRECT: Runtime assertion before cast
|
|
671
|
+
assert isinstance(doc, MutableMapping), f"Expected MutableMapping, got {type(doc)}"
|
|
672
|
+
cast(dict[str, Any], doc)["key"] = value
|
|
673
|
+
|
|
674
|
+
# ✅ CORRECT: Alternative with hasattr for duck typing
|
|
675
|
+
assert hasattr(obj, '__setitem__'), f"Expected subscriptable, got {type(obj)}"
|
|
676
|
+
cast(dict[str, Any], obj)["key"] = value
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Anti-Pattern
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
# ❌ WRONG: Cast without runtime verification
|
|
683
|
+
cast(dict[str, Any], doc)["key"] = value # If doc isn't a dict-like, silent failure
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### When to Skip Runtime Verification
|
|
687
|
+
|
|
688
|
+
**Default: Always add the assertion when cost is trivial (O(1) checks like `in`, `isinstance`).**
|
|
689
|
+
|
|
690
|
+
Skip the assertion only in these narrow cases:
|
|
691
|
+
|
|
692
|
+
1. **Immediately after a type guard**: The check was just performed and would be redundant
|
|
693
|
+
|
|
694
|
+
```python
|
|
695
|
+
if isinstance(value, str):
|
|
696
|
+
# No assertion needed - we just checked
|
|
697
|
+
result = cast(str, value).upper()
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
2. **Performance-critical hot path**: Add a comment explaining the measured overhead
|
|
701
|
+
```python
|
|
702
|
+
# Skip assertion: called 10M times/sec, isinstance adds 15% overhead
|
|
703
|
+
# Type invariant maintained by _validate_input() at entry point
|
|
704
|
+
cast(int, cached_value)
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**What is NOT a valid reason to skip:**
|
|
708
|
+
|
|
709
|
+
- "Click validates the choice set" - Add assertion anyway; cost is trivial
|
|
710
|
+
- "The library guarantees the type" - Add assertion anyway; defense in depth
|
|
711
|
+
- "It's obvious from context" - Add assertion anyway; future readers benefit
|
|
712
|
+
|
|
713
|
+
### Why This Matters
|
|
714
|
+
|
|
715
|
+
- **Silent bugs are worse than loud bugs**: An assertion failure gives you a stack trace and clear error message
|
|
716
|
+
- **Documentation**: The assertion documents your assumption for future readers
|
|
717
|
+
- **Defense in depth**: Third-party libraries can change behavior between versions
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Programmatically Significant Strings
|
|
722
|
+
|
|
723
|
+
**Use `Literal` types for strings that have programmatic meaning.**
|
|
724
|
+
|
|
725
|
+
When strings represent a fixed set of valid values (error codes, status values, command types), model them in the type system using `Literal`.
|
|
726
|
+
|
|
727
|
+
### Why This Matters
|
|
728
|
+
|
|
729
|
+
1. **Type safety** - Typos caught at type-check time, not runtime
|
|
730
|
+
2. **IDE support** - Autocomplete shows valid options
|
|
731
|
+
3. **Documentation** - Valid values are explicit in the code
|
|
732
|
+
4. **Refactoring** - Rename operations work correctly
|
|
733
|
+
|
|
734
|
+
### Naming Convention
|
|
735
|
+
|
|
736
|
+
**Use kebab-case for all internal Literal string values:**
|
|
737
|
+
|
|
738
|
+
```python
|
|
739
|
+
# ✅ CORRECT: kebab-case for internal values
|
|
740
|
+
IssueCode = Literal["orphan-state", "orphan-dir", "missing-branch"]
|
|
741
|
+
ErrorType = Literal["not-found", "invalid-format", "timeout-exceeded"]
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**Exception: When modeling external systems, match the external API's convention:**
|
|
745
|
+
|
|
746
|
+
```python
|
|
747
|
+
# ✅ CORRECT: Match GitHub API's UPPER_CASE
|
|
748
|
+
PRState = Literal["OPEN", "MERGED", "CLOSED"]
|
|
749
|
+
|
|
750
|
+
# ✅ CORRECT: Match GitHub Actions API's lowercase
|
|
751
|
+
WorkflowStatus = Literal["completed", "in_progress", "queued"]
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
The rule is: kebab-case by default, external convention when modeling external APIs.
|
|
755
|
+
|
|
756
|
+
### Pattern
|
|
757
|
+
|
|
758
|
+
```python
|
|
759
|
+
from dataclasses import dataclass
|
|
760
|
+
from typing import Literal
|
|
761
|
+
|
|
762
|
+
# ✅ CORRECT: Define a type alias for the valid values
|
|
763
|
+
IssueCode = Literal["orphan-state", "orphan-dir", "missing-branch"]
|
|
764
|
+
|
|
765
|
+
@dataclass(frozen=True)
|
|
766
|
+
class Issue:
|
|
767
|
+
code: IssueCode
|
|
768
|
+
message: str
|
|
769
|
+
|
|
770
|
+
def check_state() -> list[Issue]:
|
|
771
|
+
issues: list[Issue] = []
|
|
772
|
+
if problem_detected:
|
|
773
|
+
issues.append(Issue(code="orphan-state", message="description")) # Type-checked!
|
|
774
|
+
return issues
|
|
775
|
+
|
|
776
|
+
# ❌ WRONG: Bare strings without type constraint
|
|
777
|
+
def check_state() -> list[tuple[str, str]]:
|
|
778
|
+
issues: list[tuple[str, str]] = []
|
|
779
|
+
issues.append(("orphen-state", "desc")) # Typo goes unnoticed!
|
|
780
|
+
return issues
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### When to Use Literal
|
|
784
|
+
|
|
785
|
+
- Error/issue codes
|
|
786
|
+
- Status values (pending, complete, failed)
|
|
787
|
+
- Command types or action names
|
|
788
|
+
- Configuration keys with fixed valid values
|
|
789
|
+
- Any string that is compared programmatically
|
|
790
|
+
|
|
791
|
+
### Decision Checklist
|
|
792
|
+
|
|
793
|
+
Before using a bare `str` type, ask:
|
|
794
|
+
|
|
795
|
+
- Is this string compared with `==` or `in` anywhere?
|
|
796
|
+
- Is there a fixed set of valid values?
|
|
797
|
+
- Would a typo in this string cause a bug?
|
|
798
|
+
|
|
799
|
+
If any answer is "yes", use `Literal` instead.
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## Anti-Patterns
|
|
804
|
+
|
|
805
|
+
### Preserving Unnecessary Backwards Compatibility
|
|
806
|
+
|
|
807
|
+
```python
|
|
808
|
+
# ❌ WRONG: Keeping old API unnecessarily
|
|
809
|
+
def process_data(data: dict, legacy_format: bool = False) -> Result:
|
|
810
|
+
if legacy_format:
|
|
811
|
+
return legacy_process(data)
|
|
812
|
+
return new_process(data)
|
|
813
|
+
|
|
814
|
+
# ✅ CORRECT: Break and migrate immediately
|
|
815
|
+
def process_data(data: dict) -> Result:
|
|
816
|
+
return new_process(data)
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### No Re-Exports: One Canonical Import Path
|
|
820
|
+
|
|
821
|
+
**Core Principle:** Every symbol has exactly one import path. Never re-export.
|
|
822
|
+
|
|
823
|
+
This rule applies to:
|
|
824
|
+
|
|
825
|
+
- `__all__` exports in `__init__.py`
|
|
826
|
+
- Re-exporting symbols from other modules
|
|
827
|
+
- Shim modules that import and expose symbols from elsewhere
|
|
828
|
+
|
|
829
|
+
```python
|
|
830
|
+
# ❌ WRONG: __all__ exports create duplicate import paths
|
|
831
|
+
# myapp/__init__.py
|
|
832
|
+
from myapp.core import Process
|
|
833
|
+
__all__ = ["Process"]
|
|
834
|
+
|
|
835
|
+
# Now Process can be imported two ways - breaks grepability
|
|
836
|
+
|
|
837
|
+
# ❌ WRONG: Re-exporting symbols in a shim module
|
|
838
|
+
# myapp/compat.py
|
|
839
|
+
from myapp.core import Process, Config, execute
|
|
840
|
+
# These can now be imported from myapp.compat OR myapp.core
|
|
841
|
+
|
|
842
|
+
# ✅ CORRECT: Empty __init__.py, import from canonical location
|
|
843
|
+
# from myapp.core import Process
|
|
844
|
+
|
|
845
|
+
# ✅ CORRECT: Shim imports only what it needs for its own use
|
|
846
|
+
# myapp/cli_entry.py (needs the click command for CLI registration)
|
|
847
|
+
from myapp.core import main_command # Only import what this module uses
|
|
848
|
+
# Other code imports Process, Config from myapp.core directly
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**Why prohibited:**
|
|
852
|
+
|
|
853
|
+
1. Breaks grepability - hard to find all usages
|
|
854
|
+
2. Confuses static analysis tools
|
|
855
|
+
3. Impairs refactoring safety
|
|
856
|
+
4. Violates explicit > implicit
|
|
857
|
+
5. Creates confusion about canonical import location
|
|
858
|
+
|
|
859
|
+
**Shim modules:** When a module must exist as an entry point (e.g., for plugin systems or CLI registration), import only the minimum symbols needed for that purpose. Document that other symbols should be imported from the canonical location.
|
|
860
|
+
|
|
861
|
+
**CI Review Behavior:**
|
|
862
|
+
|
|
863
|
+
- New `__all__` usage → Always flagged
|
|
864
|
+
- Modifications to existing `__all__` (adding exports) → Flagged
|
|
865
|
+
- Pre-existing `__all__` in file only moved/refactored (unchanged) → Skipped
|
|
866
|
+
|
|
867
|
+
The principle: If you're actively modifying a file, fix its violations. If you're just moving it, don't force unrelated cleanup.
|
|
868
|
+
|
|
869
|
+
**When re-exports ARE required:** Some systems (like plugin entry points) require a module to exist at a specific path and expose a specific symbol. In these cases, use the explicit `import X as X` syntax to signal intentional re-export:
|
|
870
|
+
|
|
871
|
+
```python
|
|
872
|
+
# ✅ CORRECT: Explicit re-export syntax for required entry points
|
|
873
|
+
# This shim exists because the plugin system expects a module at this path
|
|
874
|
+
from myapp.core.feature import my_function as my_function
|
|
875
|
+
|
|
876
|
+
# ❌ WRONG: Plain import looks like unused import to linters
|
|
877
|
+
from myapp.core.feature import my_function # ruff will flag as F401
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
The `as X` syntax is the PEP 484 standard for indicating intentional re-exports. It tells both linters and readers that this import is meant to be consumed from this module.
|
|
881
|
+
|
|
882
|
+
### Default Parameter Values Are Dangerous
|
|
883
|
+
|
|
884
|
+
**Avoid default parameter values unless absolutely necessary.** They are a significant source of bugs.
|
|
885
|
+
|
|
886
|
+
**Why defaults are dangerous:**
|
|
887
|
+
|
|
888
|
+
1. **Silent incorrect behavior** - Callers forget to pass a parameter and get unexpected results
|
|
889
|
+
2. **Hidden coupling** - The default encodes an assumption that may not hold for all callers
|
|
890
|
+
3. **Audit difficulty** - Hard to verify all call sites are using the right value
|
|
891
|
+
4. **Refactoring hazard** - Adding a new parameter with a default doesn't trigger errors at existing call sites
|
|
892
|
+
|
|
893
|
+
```python
|
|
894
|
+
# ❌ DANGEROUS: Default that might be wrong for some callers
|
|
895
|
+
def process_file(path: Path, encoding: str = "utf-8") -> str:
|
|
896
|
+
return path.read_text(encoding=encoding)
|
|
897
|
+
|
|
898
|
+
# Caller forgets encoding, silently gets wrong behavior for legacy file
|
|
899
|
+
content = process_file(legacy_latin1_file) # Bug: should be encoding="latin-1"
|
|
900
|
+
|
|
901
|
+
# ✅ SAFER: Require explicit choice
|
|
902
|
+
def process_file(path: Path, encoding: str) -> str:
|
|
903
|
+
return path.read_text(encoding=encoding)
|
|
904
|
+
|
|
905
|
+
# Caller must think about encoding
|
|
906
|
+
content = process_file(legacy_latin1_file, encoding="latin-1")
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
**When you discover a default is never overridden, eliminate it:**
|
|
910
|
+
|
|
911
|
+
```python
|
|
912
|
+
# If every call site uses the default...
|
|
913
|
+
activate_worktree(ctx, repo, path, script, "up", preserve_relative_path=True) # Always True
|
|
914
|
+
activate_worktree(ctx, repo, path, script, "down", preserve_relative_path=True) # Always True
|
|
915
|
+
|
|
916
|
+
# ✅ CORRECT: Remove the parameter entirely
|
|
917
|
+
def activate_worktree(ctx, repo, path, script, command_name) -> None:
|
|
918
|
+
# Always preserve relative path - it's just the behavior
|
|
919
|
+
...
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Acceptable uses of defaults:**
|
|
923
|
+
|
|
924
|
+
1. **Truly optional behavior** - Where the default is correct for 95%+ of callers
|
|
925
|
+
2. **Backwards compatibility** - When adding a parameter to existing API (temporary)
|
|
926
|
+
3. **Test conveniences** - Defaults that simplify test setup
|
|
927
|
+
|
|
928
|
+
**When reviewing code with defaults, ask:**
|
|
929
|
+
|
|
930
|
+
- Do all call sites actually want this default?
|
|
931
|
+
- Would a caller forgetting this parameter cause a bug?
|
|
932
|
+
- Is there a safer design that makes the choice explicit?
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
### Speculative Tests
|
|
937
|
+
|
|
938
|
+
```python
|
|
939
|
+
# ❌ FORBIDDEN: Tests for future features
|
|
940
|
+
# def test_feature_we_might_add():
|
|
941
|
+
# pass
|
|
942
|
+
|
|
943
|
+
# ✅ CORRECT: TDD for current implementation
|
|
944
|
+
def test_feature_being_built_now():
|
|
945
|
+
result = new_feature()
|
|
946
|
+
assert result == expected
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
### Speculative Test Infrastructure
|
|
952
|
+
|
|
953
|
+
**Don't add parameters to fakes "just in case" they might be useful for testing.**
|
|
954
|
+
|
|
955
|
+
Fakes should mirror production interfaces. Adding test-only configuration knobs that never get used creates dead code and false complexity.
|
|
956
|
+
|
|
957
|
+
```python
|
|
958
|
+
# ❌ WRONG: Test-only parameter that's never used in production
|
|
959
|
+
class FakeGitHub:
|
|
960
|
+
def __init__(
|
|
961
|
+
self,
|
|
962
|
+
prs: dict[str, PullRequestInfo] | None = None,
|
|
963
|
+
rate_limited: bool = False, # "Might test this later"
|
|
964
|
+
) -> None:
|
|
965
|
+
self._rate_limited = rate_limited # Never set to True anywhere
|
|
966
|
+
|
|
967
|
+
# ✅ CORRECT: Only add infrastructure when you need it
|
|
968
|
+
class FakeGitHub:
|
|
969
|
+
def __init__(
|
|
970
|
+
self,
|
|
971
|
+
prs: dict[str, PullRequestInfo] | None = None,
|
|
972
|
+
) -> None:
|
|
973
|
+
...
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
**The test for this:** If grep shows a parameter is only ever passed in test files, and those tests are testing hypothetical scenarios rather than actual production behavior, delete both the parameter and the tests.
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
## Code Organization
|
|
981
|
+
|
|
982
|
+
### Declare Variables Close to Use
|
|
983
|
+
|
|
984
|
+
**Variables should be declared as close as possible to where they are used.** Avoid early declarations that pollute scope and obscure data flow.
|
|
985
|
+
|
|
986
|
+
```python
|
|
987
|
+
# ❌ WRONG: Variable declared far from use
|
|
988
|
+
def process_data(ctx, items):
|
|
989
|
+
# Declared here...
|
|
990
|
+
result_path = compute_result_path(ctx)
|
|
991
|
+
|
|
992
|
+
# 20+ lines of other logic...
|
|
993
|
+
validate_items(items)
|
|
994
|
+
transformed = transform_items(items)
|
|
995
|
+
check_permissions(ctx)
|
|
996
|
+
|
|
997
|
+
# ...used here, far below
|
|
998
|
+
save_to_path(transformed, result_path)
|
|
999
|
+
|
|
1000
|
+
# ✅ CORRECT: Inline at use site
|
|
1001
|
+
def process_data(ctx, items):
|
|
1002
|
+
validate_items(items)
|
|
1003
|
+
transformed = transform_items(items)
|
|
1004
|
+
check_permissions(ctx)
|
|
1005
|
+
|
|
1006
|
+
# Computed right where it's needed
|
|
1007
|
+
save_to_path(transformed, compute_result_path(ctx))
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**When passing to functions, prefer inline computation:**
|
|
1011
|
+
|
|
1012
|
+
```python
|
|
1013
|
+
# ❌ WRONG: Unnecessary intermediate variable
|
|
1014
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
1015
|
+
relative_path = compute_relative_path(worktrees, ctx.cwd) # Only used once below
|
|
1016
|
+
|
|
1017
|
+
activation_script = render_activation_script(
|
|
1018
|
+
worktree_path=target_path,
|
|
1019
|
+
target_subpath=relative_path,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# ✅ CORRECT: Inline the computation
|
|
1023
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
1024
|
+
|
|
1025
|
+
activation_script = render_activation_script(
|
|
1026
|
+
worktree_path=target_path,
|
|
1027
|
+
target_subpath=compute_relative_path(worktrees, ctx.cwd),
|
|
1028
|
+
)
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
**Exception:** If a variable is used multiple times or if inline computation hurts readability, a local variable is appropriate.
|
|
1032
|
+
|
|
1033
|
+
### Don't Destructure Objects Into Single-Use Locals
|
|
1034
|
+
|
|
1035
|
+
**Prefer direct attribute access over intermediate variables.** When you have an object, access its attributes at the point of use rather than extracting them into local variables that are only used once.
|
|
1036
|
+
|
|
1037
|
+
```python
|
|
1038
|
+
# ❌ WRONG: Unnecessary field extraction
|
|
1039
|
+
result = fetch_user(user_id)
|
|
1040
|
+
name = result.name # only used once below
|
|
1041
|
+
email = result.email # only used once below
|
|
1042
|
+
role = result.role # only used once below
|
|
1043
|
+
|
|
1044
|
+
send_notification(name, email, role)
|
|
1045
|
+
|
|
1046
|
+
# ✅ CORRECT: Access fields directly
|
|
1047
|
+
user = fetch_user(user_id)
|
|
1048
|
+
send_notification(user.name, user.email, user.role)
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
**Why this matters:**
|
|
1052
|
+
|
|
1053
|
+
- Reduces cognitive load - no need to track extra variable names
|
|
1054
|
+
- Makes data flow clearer - you can see where values come from
|
|
1055
|
+
- Avoids stale variable bugs when object is mutated
|
|
1056
|
+
- The object name (`user`) provides context; `name` alone is ambiguous
|
|
1057
|
+
|
|
1058
|
+
**Exception:** Extract to a local when:
|
|
1059
|
+
|
|
1060
|
+
- The value is used multiple times
|
|
1061
|
+
- The expression is complex and a name improves readability
|
|
1062
|
+
- You need to modify the value before use
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
### Indentation Depth Limit
|
|
1067
|
+
|
|
1068
|
+
**Maximum indentation: 4 levels**
|
|
1069
|
+
|
|
1070
|
+
```python
|
|
1071
|
+
# ❌ WRONG: Too deeply nested
|
|
1072
|
+
def process_items(items):
|
|
1073
|
+
for item in items:
|
|
1074
|
+
if item.valid:
|
|
1075
|
+
for child in item.children:
|
|
1076
|
+
if child.enabled:
|
|
1077
|
+
for grandchild in child.descendants:
|
|
1078
|
+
# 5 levels deep!
|
|
1079
|
+
pass
|
|
1080
|
+
|
|
1081
|
+
# ✅ CORRECT: Extract helper functions
|
|
1082
|
+
def process_items(items):
|
|
1083
|
+
for item in items:
|
|
1084
|
+
if item.valid:
|
|
1085
|
+
process_children(item.children)
|
|
1086
|
+
|
|
1087
|
+
def process_children(children):
|
|
1088
|
+
for child in children:
|
|
1089
|
+
if child.enabled:
|
|
1090
|
+
process_descendants(child.descendants)
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
1095
|
+
## Backwards Compatibility Philosophy
|
|
1096
|
+
|
|
1097
|
+
**Default stance: NO backwards compatibility preservation**
|
|
1098
|
+
|
|
1099
|
+
Only preserve backwards compatibility when:
|
|
1100
|
+
|
|
1101
|
+
- Code is clearly part of public API
|
|
1102
|
+
- User explicitly requests it
|
|
1103
|
+
- Migration cost is prohibitively high (rare)
|
|
1104
|
+
|
|
1105
|
+
Benefits:
|
|
1106
|
+
|
|
1107
|
+
- Cleaner, maintainable codebase
|
|
1108
|
+
- Faster iteration
|
|
1109
|
+
- No legacy code accumulation
|
|
1110
|
+
- Simpler mental models
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## Decision Checklist
|
|
1115
|
+
|
|
1116
|
+
### Before writing `try/except`:
|
|
1117
|
+
|
|
1118
|
+
- [ ] Is this at an error boundary? (CLI/API level)
|
|
1119
|
+
- [ ] Can I check the condition proactively? (LBYL)
|
|
1120
|
+
- [ ] Am I adding meaningful context, or just hiding?
|
|
1121
|
+
- [ ] Is third-party API forcing me to use exceptions? (No LBYL check exists—not even format validation)
|
|
1122
|
+
- [ ] Have I encapsulated the violation?
|
|
1123
|
+
- [ ] Am I catching specific exceptions, not broad?
|
|
1124
|
+
- [ ] If catching at error boundary, am I logging/warning? (Never silently swallow)
|
|
1125
|
+
|
|
1126
|
+
**Default: Let exceptions bubble up**
|
|
1127
|
+
|
|
1128
|
+
### Before path operations:
|
|
1129
|
+
|
|
1130
|
+
- [ ] Did I check `.exists()` before `.resolve()`?
|
|
1131
|
+
- [ ] Did I check `.exists()` before `.is_relative_to()`?
|
|
1132
|
+
- [ ] Am I using `pathlib.Path`, not `os.path`?
|
|
1133
|
+
- [ ] Did I specify `encoding="utf-8"`?
|
|
1134
|
+
|
|
1135
|
+
### Before using `typing.cast()`:
|
|
1136
|
+
|
|
1137
|
+
- [ ] Have I added a runtime assertion to verify the cast?
|
|
1138
|
+
- [ ] Is the assertion cost trivial (O(1))? If yes, always add it.
|
|
1139
|
+
- [ ] If skipping, is it because I just performed an isinstance check (redundant)?
|
|
1140
|
+
- [ ] If skipping for performance, have I documented the measured overhead?
|
|
1141
|
+
|
|
1142
|
+
**Default: Always add runtime assertion before cast when cost is trivial**
|
|
1143
|
+
|
|
1144
|
+
### Before preserving backwards compatibility:
|
|
1145
|
+
|
|
1146
|
+
- [ ] Did the user explicitly request it?
|
|
1147
|
+
- [ ] Is this a public API with external consumers?
|
|
1148
|
+
- [ ] Have I documented why it's needed?
|
|
1149
|
+
- [ ] Is migration cost prohibitively high?
|
|
1150
|
+
|
|
1151
|
+
**Default: Break the API and migrate callsites immediately**
|
|
1152
|
+
|
|
1153
|
+
### Before inline imports:
|
|
1154
|
+
|
|
1155
|
+
- [ ] Is this to break a circular dependency?
|
|
1156
|
+
- [ ] Is this for TYPE_CHECKING?
|
|
1157
|
+
- [ ] Is this for conditional features?
|
|
1158
|
+
- [ ] If for startup time: Have I MEASURED the import cost?
|
|
1159
|
+
- [ ] If for startup time: Is the cost significant (>100ms)?
|
|
1160
|
+
- [ ] If for startup time: Have I documented the measured cost in a comment?
|
|
1161
|
+
- [ ] Have I documented why the inline import is needed?
|
|
1162
|
+
|
|
1163
|
+
**Default: Module-level imports**
|
|
1164
|
+
|
|
1165
|
+
### Before importing/re-exporting symbols:
|
|
1166
|
+
|
|
1167
|
+
- [ ] Is there already a canonical location for this symbol?
|
|
1168
|
+
- [ ] Am I creating a second import path for the same symbol?
|
|
1169
|
+
- [ ] If this is a shim module, am I importing only what's needed for this module's purpose?
|
|
1170
|
+
- [ ] Have I avoided `__all__` exports?
|
|
1171
|
+
|
|
1172
|
+
**Default: Import from canonical location, never re-export**
|
|
1173
|
+
|
|
1174
|
+
### Before declaring a local variable:
|
|
1175
|
+
|
|
1176
|
+
- [ ] Is this variable used more than once?
|
|
1177
|
+
- [ ] Is this variable used close to where it's declared?
|
|
1178
|
+
- [ ] Would inlining the computation hurt readability?
|
|
1179
|
+
- [ ] Am I extracting object fields into locals that are only used once?
|
|
1180
|
+
|
|
1181
|
+
**Default: Inline single-use computations at the call site; access object attributes directly**
|
|
1182
|
+
|
|
1183
|
+
### Before adding a default parameter value:
|
|
1184
|
+
|
|
1185
|
+
- [ ] Do 95%+ of callers actually want this default?
|
|
1186
|
+
- [ ] Would forgetting to pass this parameter cause a subtle bug?
|
|
1187
|
+
- [ ] Is there a safer design that makes the choice explicit?
|
|
1188
|
+
- [ ] If the default is never overridden anywhere, should this parameter exist at all?
|
|
1189
|
+
|
|
1190
|
+
**Default: Require explicit values; eliminate unused defaults**
|