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.
Files changed (331) hide show
  1. erk/__init__.py +12 -0
  2. erk/__main__.py +6 -0
  3. erk/agent_docs/__init__.py +5 -0
  4. erk/agent_docs/models.py +123 -0
  5. erk/agent_docs/operations.py +666 -0
  6. erk/artifacts/__init__.py +5 -0
  7. erk/artifacts/artifact_health.py +623 -0
  8. erk/artifacts/detection.py +16 -0
  9. erk/artifacts/discovery.py +343 -0
  10. erk/artifacts/models.py +63 -0
  11. erk/artifacts/staleness.py +56 -0
  12. erk/artifacts/state.py +100 -0
  13. erk/artifacts/sync.py +624 -0
  14. erk/cli/__init__.py +0 -0
  15. erk/cli/activation.py +132 -0
  16. erk/cli/alias.py +53 -0
  17. erk/cli/cli.py +221 -0
  18. erk/cli/commands/__init__.py +0 -0
  19. erk/cli/commands/admin.py +153 -0
  20. erk/cli/commands/artifact/__init__.py +1 -0
  21. erk/cli/commands/artifact/check.py +260 -0
  22. erk/cli/commands/artifact/group.py +31 -0
  23. erk/cli/commands/artifact/list_cmd.py +89 -0
  24. erk/cli/commands/artifact/show.py +62 -0
  25. erk/cli/commands/artifact/sync_cmd.py +39 -0
  26. erk/cli/commands/branch/__init__.py +26 -0
  27. erk/cli/commands/branch/assign_cmd.py +152 -0
  28. erk/cli/commands/branch/checkout_cmd.py +357 -0
  29. erk/cli/commands/branch/create_cmd.py +161 -0
  30. erk/cli/commands/branch/list_cmd.py +82 -0
  31. erk/cli/commands/branch/unassign_cmd.py +197 -0
  32. erk/cli/commands/cc/__init__.py +15 -0
  33. erk/cli/commands/cc/jsonl_cmd.py +20 -0
  34. erk/cli/commands/cc/session/AGENTS.md +30 -0
  35. erk/cli/commands/cc/session/CLAUDE.md +1 -0
  36. erk/cli/commands/cc/session/__init__.py +15 -0
  37. erk/cli/commands/cc/session/list_cmd.py +167 -0
  38. erk/cli/commands/cc/session/show_cmd.py +175 -0
  39. erk/cli/commands/completion.py +89 -0
  40. erk/cli/commands/completions.py +165 -0
  41. erk/cli/commands/config.py +327 -0
  42. erk/cli/commands/docs/__init__.py +1 -0
  43. erk/cli/commands/docs/group.py +16 -0
  44. erk/cli/commands/docs/sync.py +121 -0
  45. erk/cli/commands/docs/validate.py +102 -0
  46. erk/cli/commands/doctor.py +243 -0
  47. erk/cli/commands/down.py +171 -0
  48. erk/cli/commands/exec/__init__.py +1 -0
  49. erk/cli/commands/exec/group.py +164 -0
  50. erk/cli/commands/exec/scripts/AGENTS.md +79 -0
  51. erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
  52. erk/cli/commands/exec/scripts/__init__.py +5 -0
  53. erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
  54. erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
  55. erk/cli/commands/exec/scripts/check_impl.py +152 -0
  56. erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
  57. erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
  58. erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
  59. erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
  60. erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
  61. erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
  62. erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
  63. erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
  64. erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
  65. erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
  66. erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
  67. erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
  68. erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
  69. erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
  70. erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
  71. erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
  72. erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
  73. erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
  74. erk/cli/commands/exec/scripts/impl_init.py +158 -0
  75. erk/cli/commands/exec/scripts/impl_signal.py +375 -0
  76. erk/cli/commands/exec/scripts/impl_verify.py +49 -0
  77. erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
  78. erk/cli/commands/exec/scripts/list_sessions.py +296 -0
  79. erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
  80. erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
  81. erk/cli/commands/exec/scripts/marker.py +163 -0
  82. erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
  83. erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
  84. erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
  85. erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
  86. erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
  87. erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
  88. erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
  89. erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
  90. erk/cli/commands/exec/scripts/quick_submit.py +32 -0
  91. erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
  92. erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
  93. erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
  94. erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
  95. erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
  96. erk/cli/commands/exec/scripts/slot_objective.py +102 -0
  97. erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
  98. erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
  99. erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
  100. erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
  101. erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
  102. erk/cli/commands/implement.py +695 -0
  103. erk/cli/commands/implement_shared.py +649 -0
  104. erk/cli/commands/info/__init__.py +14 -0
  105. erk/cli/commands/info/release_notes_cmd.py +128 -0
  106. erk/cli/commands/init.py +801 -0
  107. erk/cli/commands/land_cmd.py +690 -0
  108. erk/cli/commands/log_cmd.py +137 -0
  109. erk/cli/commands/md/__init__.py +5 -0
  110. erk/cli/commands/md/check.py +118 -0
  111. erk/cli/commands/md/group.py +14 -0
  112. erk/cli/commands/navigation_helpers.py +430 -0
  113. erk/cli/commands/objective/__init__.py +16 -0
  114. erk/cli/commands/objective/list_cmd.py +47 -0
  115. erk/cli/commands/objective_helpers.py +132 -0
  116. erk/cli/commands/plan/__init__.py +32 -0
  117. erk/cli/commands/plan/check_cmd.py +174 -0
  118. erk/cli/commands/plan/close_cmd.py +69 -0
  119. erk/cli/commands/plan/create_cmd.py +120 -0
  120. erk/cli/commands/plan/docs/__init__.py +18 -0
  121. erk/cli/commands/plan/docs/extract_cmd.py +53 -0
  122. erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
  123. erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
  124. erk/cli/commands/plan/extraction/__init__.py +16 -0
  125. erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
  126. erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
  127. erk/cli/commands/plan/get.py +71 -0
  128. erk/cli/commands/plan/list_cmd.py +754 -0
  129. erk/cli/commands/plan/log_cmd.py +440 -0
  130. erk/cli/commands/plan/start_cmd.py +459 -0
  131. erk/cli/commands/planner/__init__.py +40 -0
  132. erk/cli/commands/planner/configure_cmd.py +73 -0
  133. erk/cli/commands/planner/connect_cmd.py +96 -0
  134. erk/cli/commands/planner/create_cmd.py +148 -0
  135. erk/cli/commands/planner/list_cmd.py +51 -0
  136. erk/cli/commands/planner/register_cmd.py +105 -0
  137. erk/cli/commands/planner/set_default_cmd.py +23 -0
  138. erk/cli/commands/planner/unregister_cmd.py +43 -0
  139. erk/cli/commands/pr/__init__.py +23 -0
  140. erk/cli/commands/pr/check_cmd.py +112 -0
  141. erk/cli/commands/pr/checkout_cmd.py +165 -0
  142. erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
  143. erk/cli/commands/pr/parse_pr_reference.py +10 -0
  144. erk/cli/commands/pr/submit_cmd.py +360 -0
  145. erk/cli/commands/pr/sync_cmd.py +181 -0
  146. erk/cli/commands/prepare_cwd_recovery.py +60 -0
  147. erk/cli/commands/project/__init__.py +16 -0
  148. erk/cli/commands/project/init_cmd.py +91 -0
  149. erk/cli/commands/run/__init__.py +17 -0
  150. erk/cli/commands/run/list_cmd.py +189 -0
  151. erk/cli/commands/run/logs_cmd.py +54 -0
  152. erk/cli/commands/run/shared.py +19 -0
  153. erk/cli/commands/shell_integration.py +29 -0
  154. erk/cli/commands/slot/__init__.py +23 -0
  155. erk/cli/commands/slot/check_cmd.py +277 -0
  156. erk/cli/commands/slot/common.py +314 -0
  157. erk/cli/commands/slot/init_pool_cmd.py +157 -0
  158. erk/cli/commands/slot/list_cmd.py +228 -0
  159. erk/cli/commands/slot/repair_cmd.py +190 -0
  160. erk/cli/commands/stack/__init__.py +23 -0
  161. erk/cli/commands/stack/consolidate_cmd.py +470 -0
  162. erk/cli/commands/stack/list_cmd.py +79 -0
  163. erk/cli/commands/stack/move_cmd.py +309 -0
  164. erk/cli/commands/stack/split_old/README.md +64 -0
  165. erk/cli/commands/stack/split_old/__init__.py +5 -0
  166. erk/cli/commands/stack/split_old/command.py +233 -0
  167. erk/cli/commands/stack/split_old/display.py +116 -0
  168. erk/cli/commands/stack/split_old/plan.py +216 -0
  169. erk/cli/commands/status.py +58 -0
  170. erk/cli/commands/submit.py +768 -0
  171. erk/cli/commands/up.py +154 -0
  172. erk/cli/commands/upgrade.py +82 -0
  173. erk/cli/commands/wt/__init__.py +29 -0
  174. erk/cli/commands/wt/checkout_cmd.py +110 -0
  175. erk/cli/commands/wt/create_cmd.py +998 -0
  176. erk/cli/commands/wt/current_cmd.py +35 -0
  177. erk/cli/commands/wt/delete_cmd.py +573 -0
  178. erk/cli/commands/wt/list_cmd.py +332 -0
  179. erk/cli/commands/wt/rename_cmd.py +66 -0
  180. erk/cli/config.py +242 -0
  181. erk/cli/constants.py +29 -0
  182. erk/cli/core.py +65 -0
  183. erk/cli/debug.py +9 -0
  184. erk/cli/ensure-conversion-tasks.md +288 -0
  185. erk/cli/ensure.py +628 -0
  186. erk/cli/github_parsing.py +96 -0
  187. erk/cli/graphite.py +81 -0
  188. erk/cli/graphite_command.py +80 -0
  189. erk/cli/help_formatter.py +345 -0
  190. erk/cli/output.py +361 -0
  191. erk/cli/presets/dagster.toml +12 -0
  192. erk/cli/presets/generic.toml +12 -0
  193. erk/cli/prompt_hooks_templates/README.md +68 -0
  194. erk/cli/script_output.py +32 -0
  195. erk/cli/shell_integration/bash_wrapper.sh +32 -0
  196. erk/cli/shell_integration/fish_wrapper.fish +39 -0
  197. erk/cli/shell_integration/handler.py +338 -0
  198. erk/cli/shell_integration/zsh_wrapper.sh +32 -0
  199. erk/cli/shell_utils.py +171 -0
  200. erk/cli/subprocess_utils.py +92 -0
  201. erk/cli/uvx_detection.py +59 -0
  202. erk/core/__init__.py +0 -0
  203. erk/core/claude_executor.py +511 -0
  204. erk/core/claude_settings.py +317 -0
  205. erk/core/command_log.py +406 -0
  206. erk/core/commit_message_generator.py +234 -0
  207. erk/core/completion.py +10 -0
  208. erk/core/consolidation_utils.py +177 -0
  209. erk/core/context.py +570 -0
  210. erk/core/display/__init__.py +4 -0
  211. erk/core/display/abc.py +24 -0
  212. erk/core/display/real.py +30 -0
  213. erk/core/display_utils.py +526 -0
  214. erk/core/file_utils.py +87 -0
  215. erk/core/health_checks.py +1315 -0
  216. erk/core/health_checks_dogfooder/__init__.py +85 -0
  217. erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
  218. erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
  219. erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
  220. erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
  221. erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
  222. erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
  223. erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
  224. erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
  225. erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
  226. erk/core/implementation_queue/__init__.py +1 -0
  227. erk/core/implementation_queue/github/__init__.py +8 -0
  228. erk/core/implementation_queue/github/abc.py +7 -0
  229. erk/core/implementation_queue/github/noop.py +38 -0
  230. erk/core/implementation_queue/github/printing.py +43 -0
  231. erk/core/implementation_queue/github/real.py +119 -0
  232. erk/core/init_utils.py +227 -0
  233. erk/core/output_filter.py +338 -0
  234. erk/core/plan_store/__init__.py +6 -0
  235. erk/core/planner/__init__.py +1 -0
  236. erk/core/planner/registry_abc.py +8 -0
  237. erk/core/planner/registry_fake.py +129 -0
  238. erk/core/planner/registry_real.py +195 -0
  239. erk/core/planner/types.py +7 -0
  240. erk/core/pr_utils.py +30 -0
  241. erk/core/release_notes.py +263 -0
  242. erk/core/repo_discovery.py +126 -0
  243. erk/core/script_writer.py +41 -0
  244. erk/core/services/__init__.py +1 -0
  245. erk/core/services/plan_list_service.py +94 -0
  246. erk/core/shell.py +51 -0
  247. erk/core/user_feedback.py +11 -0
  248. erk/core/version_check.py +55 -0
  249. erk/core/workflow_display.py +75 -0
  250. erk/core/worktree_pool.py +190 -0
  251. erk/core/worktree_utils.py +300 -0
  252. erk/data/CHANGELOG.md +438 -0
  253. erk/data/__init__.py +1 -0
  254. erk/data/claude/agents/devrun.md +180 -0
  255. erk/data/claude/commands/erk/__init__.py +0 -0
  256. erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
  257. erk/data/claude/commands/erk/fix-conflicts.md +25 -0
  258. erk/data/claude/commands/erk/git-pr-push.md +345 -0
  259. erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
  260. erk/data/claude/commands/erk/land.md +193 -0
  261. erk/data/claude/commands/erk/objective-create.md +370 -0
  262. erk/data/claude/commands/erk/objective-list.md +34 -0
  263. erk/data/claude/commands/erk/objective-next-plan.md +220 -0
  264. erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
  265. erk/data/claude/commands/erk/plan-implement.md +202 -0
  266. erk/data/claude/commands/erk/plan-save.md +45 -0
  267. erk/data/claude/commands/erk/plan-submit.md +39 -0
  268. erk/data/claude/commands/erk/pr-address.md +367 -0
  269. erk/data/claude/commands/erk/pr-submit.md +58 -0
  270. erk/data/claude/skills/dignified-python/SKILL.md +48 -0
  271. erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
  272. erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
  273. erk/data/claude/skills/dignified-python/subprocess.md +99 -0
  274. erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
  275. erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
  276. erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
  277. erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
  278. erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
  279. erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
  280. erk/data/claude/skills/learned-docs/SKILL.md +362 -0
  281. erk/data/github/actions/setup-claude-erk/action.yml +11 -0
  282. erk/data/github/prompts/dignified-python-review.md +125 -0
  283. erk/data/github/workflows/dignified-python-review.yml +61 -0
  284. erk/data/github/workflows/erk-impl.yml +251 -0
  285. erk/hooks/__init__.py +1 -0
  286. erk/hooks/decorators.py +319 -0
  287. erk/status/__init__.py +8 -0
  288. erk/status/collectors/__init__.py +9 -0
  289. erk/status/collectors/base.py +52 -0
  290. erk/status/collectors/git.py +76 -0
  291. erk/status/collectors/github.py +81 -0
  292. erk/status/collectors/graphite.py +80 -0
  293. erk/status/collectors/impl.py +145 -0
  294. erk/status/models/__init__.py +4 -0
  295. erk/status/models/status_data.py +404 -0
  296. erk/status/orchestrator.py +169 -0
  297. erk/status/renderers/__init__.py +5 -0
  298. erk/status/renderers/simple.py +322 -0
  299. erk/tui/AGENTS.md +193 -0
  300. erk/tui/CLAUDE.md +1 -0
  301. erk/tui/__init__.py +1 -0
  302. erk/tui/app.py +1404 -0
  303. erk/tui/commands/__init__.py +1 -0
  304. erk/tui/commands/executor.py +66 -0
  305. erk/tui/commands/provider.py +165 -0
  306. erk/tui/commands/real_executor.py +63 -0
  307. erk/tui/commands/registry.py +121 -0
  308. erk/tui/commands/types.py +36 -0
  309. erk/tui/data/__init__.py +1 -0
  310. erk/tui/data/provider.py +492 -0
  311. erk/tui/data/types.py +104 -0
  312. erk/tui/filtering/__init__.py +1 -0
  313. erk/tui/filtering/logic.py +43 -0
  314. erk/tui/filtering/types.py +55 -0
  315. erk/tui/jsonl_viewer/__init__.py +1 -0
  316. erk/tui/jsonl_viewer/app.py +61 -0
  317. erk/tui/jsonl_viewer/models.py +208 -0
  318. erk/tui/jsonl_viewer/widgets.py +204 -0
  319. erk/tui/sorting/__init__.py +6 -0
  320. erk/tui/sorting/logic.py +55 -0
  321. erk/tui/sorting/types.py +68 -0
  322. erk/tui/styles/dash.tcss +95 -0
  323. erk/tui/widgets/__init__.py +1 -0
  324. erk/tui/widgets/command_output.py +112 -0
  325. erk/tui/widgets/plan_table.py +276 -0
  326. erk/tui/widgets/status_bar.py +116 -0
  327. erk-0.4.5.dist-info/METADATA +376 -0
  328. erk-0.4.5.dist-info/RECORD +331 -0
  329. erk-0.4.5.dist-info/WHEEL +4 -0
  330. erk-0.4.5.dist-info/entry_points.txt +2 -0
  331. 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)