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,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Subprocess Handling - Safe Execution
|
|
5
|
+
|
|
6
|
+
## Core Rule
|
|
7
|
+
|
|
8
|
+
**ALWAYS use `check=True` with `subprocess.run()`**
|
|
9
|
+
|
|
10
|
+
## Basic Subprocess Pattern
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# ✅ CORRECT: check=True to raise on error
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
["git", "status"],
|
|
19
|
+
check=True,
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True
|
|
22
|
+
)
|
|
23
|
+
print(result.stdout)
|
|
24
|
+
|
|
25
|
+
# ❌ WRONG: No check - silently ignores errors
|
|
26
|
+
result = subprocess.run(["git", "status"])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Complete Subprocess Example
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
def run_git_command(args: list[str], cwd: Path | None = None) -> str:
|
|
33
|
+
"""Run a git command and return output."""
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["git"] + args,
|
|
37
|
+
check=True, # Raise on non-zero exit
|
|
38
|
+
capture_output=True, # Capture stdout/stderr
|
|
39
|
+
text=True, # Return strings, not bytes
|
|
40
|
+
cwd=cwd # Working directory
|
|
41
|
+
)
|
|
42
|
+
return result.stdout.strip()
|
|
43
|
+
except subprocess.CalledProcessError as e:
|
|
44
|
+
# Error boundary - add context
|
|
45
|
+
raise RuntimeError(f"Git command failed: {e.stderr}") from e
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Error Handling
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
["make", "test"],
|
|
54
|
+
check=True,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True
|
|
57
|
+
)
|
|
58
|
+
except subprocess.CalledProcessError as e:
|
|
59
|
+
# Access error details
|
|
60
|
+
print(f"Command: {e.cmd}")
|
|
61
|
+
print(f"Exit code: {e.returncode}")
|
|
62
|
+
print(f"Stdout: {e.stdout}")
|
|
63
|
+
print(f"Stderr: {e.stderr}")
|
|
64
|
+
raise
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Common Patterns
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Silent execution (no output)
|
|
71
|
+
subprocess.run(["git", "fetch"], check=True, capture_output=True)
|
|
72
|
+
|
|
73
|
+
# Stream output in real-time
|
|
74
|
+
process = subprocess.Popen(
|
|
75
|
+
["pytest", "-v"],
|
|
76
|
+
stdout=subprocess.PIPE,
|
|
77
|
+
stderr=subprocess.STDOUT,
|
|
78
|
+
text=True
|
|
79
|
+
)
|
|
80
|
+
for line in process.stdout:
|
|
81
|
+
print(line, end="")
|
|
82
|
+
process.wait()
|
|
83
|
+
if process.returncode != 0:
|
|
84
|
+
raise subprocess.CalledProcessError(process.returncode, process.args)
|
|
85
|
+
|
|
86
|
+
# With timeout
|
|
87
|
+
try:
|
|
88
|
+
subprocess.run(["long-command"], check=True, timeout=30)
|
|
89
|
+
except subprocess.TimeoutExpired:
|
|
90
|
+
print("Command timed out")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Key Takeaways
|
|
94
|
+
|
|
95
|
+
1. **Always check=True**: Ensure errors are not silently ignored
|
|
96
|
+
2. **Capture output**: Use `capture_output=True` for stdout/stderr
|
|
97
|
+
3. **Text mode**: Use `text=True` for string output
|
|
98
|
+
4. **Error context**: Wrap in try/except at boundaries
|
|
99
|
+
5. **Timeout safety**: Set timeout for long-running commands
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
# Type Annotations - Python 3.10
|
|
5
|
+
|
|
6
|
+
This document provides complete, canonical type annotation guidance for Python 3.10. This is the baseline for modern Python type syntax.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Python 3.10 introduced major improvements to type annotation syntax through PEP 604 (union types via `|`) and PEP 585 (generic types in standard collections). These features eliminated the need for most `typing` module imports and made type annotations more concise and readable.
|
|
11
|
+
|
|
12
|
+
**What's new in 3.10:**
|
|
13
|
+
|
|
14
|
+
- Union types with `|` operator (PEP 604)
|
|
15
|
+
- Built-in generic types: `list[T]`, `dict[K, V]`, etc. (PEP 585)
|
|
16
|
+
- No more need for `List`, `Dict`, `Union`, `Optional` from typing
|
|
17
|
+
|
|
18
|
+
**What you need from typing module:**
|
|
19
|
+
|
|
20
|
+
- `TypeVar` for generic functions/classes
|
|
21
|
+
- `Protocol` for structural typing (rare - prefer ABC)
|
|
22
|
+
- `TYPE_CHECKING` for conditional imports
|
|
23
|
+
- `Any` (use sparingly)
|
|
24
|
+
|
|
25
|
+
## Complete Type Annotation Syntax for Python 3.10
|
|
26
|
+
|
|
27
|
+
### Basic Collection Types
|
|
28
|
+
|
|
29
|
+
✅ **PREFERRED** - Use built-in generic types:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
names: list[str] = []
|
|
33
|
+
mapping: dict[str, int] = {}
|
|
34
|
+
unique_ids: set[str] = set()
|
|
35
|
+
coordinates: tuple[int, int] = (0, 0)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
❌ **WRONG** - Don't use typing module equivalents:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from typing import List, Dict, Set, Tuple # Don't do this
|
|
42
|
+
names: List[str] = []
|
|
43
|
+
mapping: Dict[str, int] = {}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Why**: Built-in types are more concise, don't require imports, and are the modern Python standard.
|
|
47
|
+
|
|
48
|
+
### Union Types
|
|
49
|
+
|
|
50
|
+
✅ **PREFERRED** - Use `|` operator:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
def process(value: str | int) -> str:
|
|
54
|
+
return str(value)
|
|
55
|
+
|
|
56
|
+
def find_config(name: str) -> dict[str, str] | dict[str, int]:
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
# Multiple unions
|
|
60
|
+
def parse(input: str | int | float) -> str:
|
|
61
|
+
return str(input)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
❌ **WRONG** - Don't use `typing.Union`:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from typing import Union
|
|
68
|
+
def process(value: Union[str, int]) -> str: # Don't do this
|
|
69
|
+
...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Optional Types
|
|
73
|
+
|
|
74
|
+
✅ **PREFERRED** - Use `X | None`:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
def find_user(id: str) -> User | None:
|
|
78
|
+
"""Returns user or None if not found."""
|
|
79
|
+
if id in users:
|
|
80
|
+
return users[id]
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def get_config(key: str) -> str | None:
|
|
84
|
+
return config.get(key)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
❌ **WRONG** - Don't use `typing.Optional`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from typing import Optional
|
|
91
|
+
def find_user(id: str) -> Optional[User]: # Don't do this
|
|
92
|
+
...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Generic Functions with TypeVar
|
|
96
|
+
|
|
97
|
+
✅ **PREFERRED** - Use TypeVar for generic functions:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from typing import TypeVar
|
|
101
|
+
|
|
102
|
+
T = TypeVar("T")
|
|
103
|
+
|
|
104
|
+
def first(items: list[T]) -> T | None:
|
|
105
|
+
"""Return first item or None if empty."""
|
|
106
|
+
if not items:
|
|
107
|
+
return None
|
|
108
|
+
return items[0]
|
|
109
|
+
|
|
110
|
+
def identity(value: T) -> T:
|
|
111
|
+
"""Return the value unchanged."""
|
|
112
|
+
return value
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Note**: This is the standard way in Python 3.10. Python 3.12 introduces better syntax (PEP 695).
|
|
116
|
+
|
|
117
|
+
### Generic Classes
|
|
118
|
+
|
|
119
|
+
✅ **PREFERRED** - Use Generic with TypeVar:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from typing import Generic, TypeVar
|
|
123
|
+
|
|
124
|
+
T = TypeVar("T")
|
|
125
|
+
|
|
126
|
+
class Stack(Generic[T]):
|
|
127
|
+
"""A generic stack data structure."""
|
|
128
|
+
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
self._items: list[T] = []
|
|
131
|
+
|
|
132
|
+
def push(self, item: T) -> None:
|
|
133
|
+
self._items.append(item)
|
|
134
|
+
|
|
135
|
+
def pop(self) -> T | None:
|
|
136
|
+
if not self._items:
|
|
137
|
+
return None
|
|
138
|
+
return self._items.pop()
|
|
139
|
+
|
|
140
|
+
# Usage
|
|
141
|
+
int_stack = Stack[int]()
|
|
142
|
+
int_stack.push(42)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Note**: Python 3.12 introduces cleaner syntax for this pattern.
|
|
146
|
+
|
|
147
|
+
### Constrained and Bounded TypeVars
|
|
148
|
+
|
|
149
|
+
✅ **Use TypeVar constraints when needed**:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from typing import TypeVar
|
|
153
|
+
|
|
154
|
+
# Constrained to specific types
|
|
155
|
+
Numeric = TypeVar("Numeric", int, float)
|
|
156
|
+
|
|
157
|
+
def add(a: Numeric, b: Numeric) -> Numeric:
|
|
158
|
+
return a + b
|
|
159
|
+
|
|
160
|
+
# Bounded to base class
|
|
161
|
+
T = TypeVar("T", bound=BaseClass)
|
|
162
|
+
|
|
163
|
+
def process(obj: T) -> T:
|
|
164
|
+
return obj
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Callable Types
|
|
168
|
+
|
|
169
|
+
✅ **PREFERRED** - Use `collections.abc.Callable`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from collections.abc import Callable
|
|
173
|
+
|
|
174
|
+
# Function that takes int, returns str
|
|
175
|
+
processor: Callable[[int], str] = str
|
|
176
|
+
|
|
177
|
+
# Function with no args, returns None
|
|
178
|
+
callback: Callable[[], None] = lambda: None
|
|
179
|
+
|
|
180
|
+
# Function with multiple args
|
|
181
|
+
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Type Aliases
|
|
185
|
+
|
|
186
|
+
✅ **Use simple assignment for type aliases**:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Simple alias
|
|
190
|
+
UserId = str
|
|
191
|
+
Config = dict[str, str | int | bool]
|
|
192
|
+
|
|
193
|
+
# Complex nested type
|
|
194
|
+
JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float | bool | None
|
|
195
|
+
|
|
196
|
+
def load_config() -> Config:
|
|
197
|
+
return {"host": "localhost", "port": 8080}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Note**: Python 3.12 introduces `type` statement for better alias support.
|
|
201
|
+
|
|
202
|
+
### when from **future** import annotations is Needed
|
|
203
|
+
|
|
204
|
+
Use `from __future__ import annotations` when you encounter:
|
|
205
|
+
|
|
206
|
+
**Forward references** (class referencing itself):
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from __future__ import annotations
|
|
210
|
+
|
|
211
|
+
class Node:
|
|
212
|
+
def __init__(self, value: int, parent: Node | None = None):
|
|
213
|
+
self.value = value
|
|
214
|
+
self.parent = parent
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Circular type imports**:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# a.py
|
|
221
|
+
from __future__ import annotations
|
|
222
|
+
from typing import TYPE_CHECKING
|
|
223
|
+
|
|
224
|
+
if TYPE_CHECKING:
|
|
225
|
+
from b import B
|
|
226
|
+
|
|
227
|
+
class A:
|
|
228
|
+
def method(self) -> B:
|
|
229
|
+
...
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Complex recursive types**:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from __future__ import annotations
|
|
236
|
+
|
|
237
|
+
JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Interfaces: ABC vs Protocol
|
|
241
|
+
|
|
242
|
+
✅ **PREFERRED** - Use ABC for interfaces:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from abc import ABC, abstractmethod
|
|
246
|
+
|
|
247
|
+
class Repository(ABC):
|
|
248
|
+
@abstractmethod
|
|
249
|
+
def get(self, id: str) -> User | None:
|
|
250
|
+
"""Get user by ID."""
|
|
251
|
+
|
|
252
|
+
@abstractmethod
|
|
253
|
+
def save(self, user: User) -> None:
|
|
254
|
+
"""Save user."""
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
🟡 **VALID** - Use Protocol only for structural typing:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from typing import Protocol
|
|
261
|
+
|
|
262
|
+
class Drawable(Protocol):
|
|
263
|
+
def draw(self) -> None: ...
|
|
264
|
+
|
|
265
|
+
# Any object with draw() method matches
|
|
266
|
+
def render(obj: Drawable) -> None:
|
|
267
|
+
obj.draw()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Dignified Python prefers ABC** because it makes inheritance and intent explicit.
|
|
271
|
+
|
|
272
|
+
## Complete Examples
|
|
273
|
+
|
|
274
|
+
### Repository Pattern
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from abc import ABC, abstractmethod
|
|
278
|
+
|
|
279
|
+
class Repository(ABC):
|
|
280
|
+
"""Abstract base class for data repositories."""
|
|
281
|
+
|
|
282
|
+
@abstractmethod
|
|
283
|
+
def get(self, id: str) -> dict[str, str] | None:
|
|
284
|
+
"""Get entity by ID."""
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def save(self, entity: dict[str, str]) -> None:
|
|
288
|
+
"""Save entity."""
|
|
289
|
+
|
|
290
|
+
@abstractmethod
|
|
291
|
+
def delete(self, id: str) -> bool:
|
|
292
|
+
"""Delete entity, return success."""
|
|
293
|
+
|
|
294
|
+
class UserRepository(Repository):
|
|
295
|
+
def __init__(self) -> None:
|
|
296
|
+
self._users: dict[str, dict[str, str]] = {}
|
|
297
|
+
|
|
298
|
+
def get(self, id: str) -> dict[str, str] | None:
|
|
299
|
+
return self._users.get(id)
|
|
300
|
+
|
|
301
|
+
def save(self, entity: dict[str, str]) -> None:
|
|
302
|
+
if "id" not in entity:
|
|
303
|
+
raise ValueError("Entity must have id")
|
|
304
|
+
self._users[entity["id"]] = entity
|
|
305
|
+
|
|
306
|
+
def delete(self, id: str) -> bool:
|
|
307
|
+
if id in self._users:
|
|
308
|
+
del self._users[id]
|
|
309
|
+
return True
|
|
310
|
+
return False
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Generic Data Structures
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from typing import Generic, TypeVar
|
|
317
|
+
|
|
318
|
+
T = TypeVar("T")
|
|
319
|
+
|
|
320
|
+
class Node(Generic[T]):
|
|
321
|
+
"""A node in a tree structure."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, value: T, children: list[Node[T]] | None = None) -> None:
|
|
324
|
+
self.value = value
|
|
325
|
+
self.children = children or []
|
|
326
|
+
|
|
327
|
+
def add_child(self, child: Node[T]) -> None:
|
|
328
|
+
self.children.append(child)
|
|
329
|
+
|
|
330
|
+
def find(self, predicate: Callable[[T], bool]) -> Node[T] | None:
|
|
331
|
+
"""Find first node matching predicate."""
|
|
332
|
+
if predicate(self.value):
|
|
333
|
+
return self
|
|
334
|
+
for child in self.children:
|
|
335
|
+
result = child.find(predicate)
|
|
336
|
+
if result:
|
|
337
|
+
return result
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
# Usage
|
|
341
|
+
from collections.abc import Callable
|
|
342
|
+
|
|
343
|
+
root = Node[int](1)
|
|
344
|
+
root.add_child(Node[int](2))
|
|
345
|
+
root.add_child(Node[int](3))
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Configuration Management
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
from dataclasses import dataclass
|
|
352
|
+
|
|
353
|
+
@dataclass(frozen=True)
|
|
354
|
+
class DatabaseConfig:
|
|
355
|
+
host: str
|
|
356
|
+
port: int
|
|
357
|
+
username: str
|
|
358
|
+
password: str | None = None
|
|
359
|
+
ssl_enabled: bool = False
|
|
360
|
+
|
|
361
|
+
@dataclass(frozen=True)
|
|
362
|
+
class AppConfig:
|
|
363
|
+
app_name: str
|
|
364
|
+
debug_mode: bool
|
|
365
|
+
database: DatabaseConfig
|
|
366
|
+
feature_flags: dict[str, bool]
|
|
367
|
+
|
|
368
|
+
def load_config(path: str) -> AppConfig:
|
|
369
|
+
"""Load application configuration from file."""
|
|
370
|
+
import json
|
|
371
|
+
from pathlib import Path
|
|
372
|
+
|
|
373
|
+
config_path = Path(path)
|
|
374
|
+
if not config_path.exists():
|
|
375
|
+
raise FileNotFoundError(f"Config not found: {path}")
|
|
376
|
+
|
|
377
|
+
data: dict[str, str | int | bool | dict[str, str | int | bool]] = json.loads(
|
|
378
|
+
config_path.read_text(encoding="utf-8")
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Parse and validate...
|
|
382
|
+
return AppConfig(...)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### API Client with Error Handling
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
from collections.abc import Callable
|
|
389
|
+
from typing import TypeVar
|
|
390
|
+
|
|
391
|
+
T = TypeVar("T")
|
|
392
|
+
|
|
393
|
+
class ApiResponse(Generic[T]):
|
|
394
|
+
"""Container for API response with data or error."""
|
|
395
|
+
|
|
396
|
+
def __init__(self, data: T | None = None, error: str | None = None) -> None:
|
|
397
|
+
self.data = data
|
|
398
|
+
self.error = error
|
|
399
|
+
|
|
400
|
+
def is_success(self) -> bool:
|
|
401
|
+
return self.error is None
|
|
402
|
+
|
|
403
|
+
def map(self, func: Callable[[T], U]) -> ApiResponse[U]:
|
|
404
|
+
"""Transform successful response data."""
|
|
405
|
+
if self.is_success() and self.data is not None:
|
|
406
|
+
return ApiResponse(data=func(self.data))
|
|
407
|
+
return ApiResponse(error=self.error)
|
|
408
|
+
|
|
409
|
+
U = TypeVar("U")
|
|
410
|
+
|
|
411
|
+
def fetch_user(id: str) -> ApiResponse[dict[str, str]]:
|
|
412
|
+
"""Fetch user from API."""
|
|
413
|
+
# Implementation...
|
|
414
|
+
return ApiResponse(data={"id": id, "name": "Alice"})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Type Checking Rules
|
|
418
|
+
|
|
419
|
+
### What to Type
|
|
420
|
+
|
|
421
|
+
✅ **MUST type**:
|
|
422
|
+
|
|
423
|
+
- All public function parameters (except `self`, `cls`)
|
|
424
|
+
- All public function return values
|
|
425
|
+
- All class attributes (public and private)
|
|
426
|
+
- Module-level constants
|
|
427
|
+
|
|
428
|
+
🟡 **SHOULD type**:
|
|
429
|
+
|
|
430
|
+
- Internal function signatures
|
|
431
|
+
- Complex local variables
|
|
432
|
+
|
|
433
|
+
🟢 **MAY skip**:
|
|
434
|
+
|
|
435
|
+
- Simple local variables where type is obvious (`count = 0`)
|
|
436
|
+
- Lambda parameters in short inline lambdas
|
|
437
|
+
- Loop variables in short comprehensions
|
|
438
|
+
|
|
439
|
+
### Running Type Checker
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
uv run ty check
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
All code should pass type checking without errors.
|
|
446
|
+
|
|
447
|
+
### Type Checking Configuration
|
|
448
|
+
|
|
449
|
+
Configure ty in `pyproject.toml`:
|
|
450
|
+
|
|
451
|
+
```toml
|
|
452
|
+
[tool.ty.environment]
|
|
453
|
+
python-version = "3.10"
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Common Patterns
|
|
457
|
+
|
|
458
|
+
### Checking for None
|
|
459
|
+
|
|
460
|
+
✅ **CORRECT** - Check before use:
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
def process_user(user: User | None) -> str:
|
|
464
|
+
if user is None:
|
|
465
|
+
return "No user"
|
|
466
|
+
return user.name
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Dict.get() with Type Safety
|
|
470
|
+
|
|
471
|
+
✅ **CORRECT** - Handle None case:
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
def get_port(config: dict[str, int]) -> int:
|
|
475
|
+
port = config.get("port")
|
|
476
|
+
if port is None:
|
|
477
|
+
return 8080
|
|
478
|
+
return port
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### List Operations
|
|
482
|
+
|
|
483
|
+
✅ **CORRECT** - Check before accessing:
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
def first_or_default(items: list[str], default: str) -> str:
|
|
487
|
+
if not items:
|
|
488
|
+
return default
|
|
489
|
+
return items[0]
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Migration from Python 3.9
|
|
493
|
+
|
|
494
|
+
If upgrading from Python 3.9, apply these changes:
|
|
495
|
+
|
|
496
|
+
1. **Replace typing module types**:
|
|
497
|
+
- `List[X]` → `list[X]`
|
|
498
|
+
- `Dict[K, V]` → `dict[K, V]`
|
|
499
|
+
- `Set[X]` → `set[X]`
|
|
500
|
+
- `Tuple[X, Y]` → `tuple[X, Y]`
|
|
501
|
+
- `Union[X, Y]` → `X | Y`
|
|
502
|
+
- `Optional[X]` → `X | None`
|
|
503
|
+
|
|
504
|
+
2. **Add future annotations if needed**:
|
|
505
|
+
- Add `from __future__ import annotations` for forward references
|
|
506
|
+
- Add for circular imports with `TYPE_CHECKING`
|
|
507
|
+
|
|
508
|
+
3. **Remove unnecessary imports**:
|
|
509
|
+
- Remove `from typing import List, Dict, Optional, Union`
|
|
510
|
+
- Keep only `TypeVar`, `Generic`, `Protocol`, `TYPE_CHECKING`, `Any`
|
|
511
|
+
|
|
512
|
+
## References
|
|
513
|
+
|
|
514
|
+
- [PEP 604: Union Types](https://peps.python.org/pep-0604/)
|
|
515
|
+
- [PEP 585: Type Hinting Generics In Standard Collections](https://peps.python.org/pep-0585/)
|
|
516
|
+
- [PEP 563: Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/)
|
|
517
|
+
- [Python 3.10 What's New - Type Hints](https://docs.python.org/3.10/whatsnew/3.10.html)
|