python-skills 1.0.0__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 (105) hide show
  1. python_skills/__init__.py +10 -0
  2. python_skills/__main__.py +6 -0
  3. python_skills/adapters/__init__.py +48 -0
  4. python_skills/adapters/agent_skills.py +415 -0
  5. python_skills/adapters/aider_adapter.py +226 -0
  6. python_skills/adapters/base.py +153 -0
  7. python_skills/adapters/claude.py +474 -0
  8. python_skills/adapters/cline.py +332 -0
  9. python_skills/adapters/codex.py +24 -0
  10. python_skills/adapters/continue_adapter.py +198 -0
  11. python_skills/adapters/cursor.py +327 -0
  12. python_skills/adapters/gemini.py +26 -0
  13. python_skills/adapters/goose.py +26 -0
  14. python_skills/adapters/junie.py +25 -0
  15. python_skills/adapters/kiro.py +382 -0
  16. python_skills/adapters/opencode.py +27 -0
  17. python_skills/adapters/roo.py +25 -0
  18. python_skills/adapters/universal.py +203 -0
  19. python_skills/adapters/vscode.py +27 -0
  20. python_skills/adapters/windsurf.py +26 -0
  21. python_skills/adapters/zed.py +27 -0
  22. python_skills/cli.py +326 -0
  23. python_skills/config.py +160 -0
  24. python_skills/detector.py +152 -0
  25. python_skills/installer.py +163 -0
  26. python_skills/markers.py +115 -0
  27. python_skills/skills/__init__.py +14 -0
  28. python_skills/skills/loader.py +171 -0
  29. python_skills/skills/metadata.py +152 -0
  30. python_skills/skills/registry.py +101 -0
  31. python_skills/state.py +204 -0
  32. python_skills-1.0.0.dist-info/METADATA +99 -0
  33. python_skills-1.0.0.dist-info/RECORD +105 -0
  34. python_skills-1.0.0.dist-info/WHEEL +4 -0
  35. python_skills-1.0.0.dist-info/entry_points.txt +2 -0
  36. python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
  37. skills/advanced_python.md +239 -0
  38. skills/anti_patterns/index.md +406 -0
  39. skills/comprehensions.md +167 -0
  40. skills/control_flow.md +175 -0
  41. skills/data_structures.md +243 -0
  42. skills/debugging/common_bugs.md +222 -0
  43. skills/debugging/inspection_techniques.md +249 -0
  44. skills/debugging/root_cause.md +203 -0
  45. skills/engineering/application_logging.md +195 -0
  46. skills/engineering/cli_apps.md +207 -0
  47. skills/engineering/configuration.md +218 -0
  48. skills/engineering/database.md +240 -0
  49. skills/engineering/dependency_management.md +205 -0
  50. skills/engineering/http_clients.md +267 -0
  51. skills/engineering/modules_packages.md +211 -0
  52. skills/engineering/packaging.md +197 -0
  53. skills/engineering/project_structure.md +155 -0
  54. skills/engineering/pyproject_toml.md +302 -0
  55. skills/engineering/virtual_environments.md +206 -0
  56. skills/functions.md +244 -0
  57. skills/generation/async_concurrency.md +291 -0
  58. skills/generation/error_handling.md +276 -0
  59. skills/generation/protocols_generics.md +243 -0
  60. skills/generation/type_hints.md +290 -0
  61. skills/generation/validation_pipeline.md +274 -0
  62. skills/generation/workflow.md +190 -0
  63. skills/oop.md +228 -0
  64. skills/quality/abstractions.md +154 -0
  65. skills/quality/comments.md +177 -0
  66. skills/quality/documentation.md +176 -0
  67. skills/quality/duplication.md +137 -0
  68. skills/quality/maintainability.md +142 -0
  69. skills/quality/naming.md +171 -0
  70. skills/quality/quality_functions.md +245 -0
  71. skills/quality/readability.md +239 -0
  72. skills/quality/type_annotations.md +192 -0
  73. skills/refactoring/behavior_preservation.md +157 -0
  74. skills/refactoring/incremental.md +187 -0
  75. skills/refactoring/interface_stability.md +199 -0
  76. skills/refactoring/safe_refactoring.md +206 -0
  77. skills/security/auth_boundaries.md +200 -0
  78. skills/security/command_injection.md +207 -0
  79. skills/security/dependency_risks.md +282 -0
  80. skills/security/file_handling.md +156 -0
  81. skills/security/input_validation.md +190 -0
  82. skills/security/path_traversal.md +172 -0
  83. skills/security/secrets.md +171 -0
  84. skills/security/sql_injection.md +188 -0
  85. skills/security/unsafe_deserialization.md +164 -0
  86. skills/stdlib/argparse.md +178 -0
  87. skills/stdlib/collections.md +212 -0
  88. skills/stdlib/datetime.md +187 -0
  89. skills/stdlib/functools.md +238 -0
  90. skills/stdlib/itertools.md +183 -0
  91. skills/stdlib/json.md +162 -0
  92. skills/stdlib/logging.md +185 -0
  93. skills/stdlib/os_sys.md +184 -0
  94. skills/stdlib/pathlib.md +218 -0
  95. skills/stdlib/re.md +171 -0
  96. skills/stdlib/statistics.md +112 -0
  97. skills/stdlib/subprocess.md +211 -0
  98. skills/testing/async_tests.md +249 -0
  99. skills/testing/coverage.md +168 -0
  100. skills/testing/edge_cases.md +197 -0
  101. skills/testing/fixtures_mocks.md +203 -0
  102. skills/testing/organization.md +205 -0
  103. skills/testing/parameterized.md +174 -0
  104. skills/testing/regression_tests.md +165 -0
  105. skills/variables_types.md +107 -0
@@ -0,0 +1,218 @@
1
+ # Stdlib: pathlib
2
+
3
+ **Purpose**: Modern, object-oriented filesystem paths.
4
+
5
+ **When to use**: All filesystem operations. Replaces `os.path`, `os.walk`, `glob`.
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Path Objects
12
+ ```python
13
+ from pathlib import Path
14
+
15
+ # Creation
16
+ Path("relative/path")
17
+ Path("/absolute/path")
18
+ Path.home() / "documents"
19
+ Path.cwd()
20
+
21
+ # From parts
22
+ Path("a", "b", "c") # a/b/c
23
+ ```
24
+
25
+ ### Properties
26
+ ```python
27
+ p = Path("a/b/c.txt")
28
+
29
+ p.name # "c.txt"
30
+ p.stem # "c"
31
+ p.suffix # ".txt"
32
+ p.suffixes # [".txt"]
33
+ p.parent # Path("a/b")
34
+ p.parents # [Path("a/b"), Path("a"), Path(".")]
35
+ p.anchor # "" (or "/" on Unix, "C:\\" on Windows)
36
+ p.root # "/" or "C:\\"
37
+ p.drive # "" or "C:"
38
+ ```
39
+
40
+ ### Navigation
41
+ ```python
42
+ p / "subdir" / "file.txt" # Join
43
+ p.joinpath("sub", "file.txt") # Join (multiple args)
44
+ p.resolve() # Absolute, resolve symlinks
45
+ p.absolute() # Absolute (no symlink resolve)
46
+ p.relative_to(Path("a")) # Relative path (raises if not subpath)
47
+ p.is_relative_to(Path("a")) # Boolean check (Python 3.9+)
48
+ ```
49
+
50
+ ### Filesystem Operations
51
+ ```python
52
+ p.exists() # Exists (file, dir, symlink)
53
+ p.is_file() # Regular file
54
+ p.is_dir() # Directory
55
+ p.is_symlink() # Symlink
56
+ p.is_absolute() # Absolute path
57
+ p.is_relative() # Relative path (Python 3.13+)
58
+
59
+ p.stat() # os.stat_result
60
+ p.lstat() # Don't follow symlinks
61
+ p.owner() # Owner name
62
+ p.group() # Group name
63
+
64
+ # Times (float seconds since epoch)
65
+ p.stat().st_mtime # Modified
66
+ p.stat().st_atime # Accessed
67
+ p.stat().st_ctime # Created (Unix: metadata change)
68
+ ```
69
+
70
+ ### Reading/Writing
71
+ ```python
72
+ # Text
73
+ text = p.read_text(encoding="utf-8")
74
+ p.write_text("content", encoding="utf-8")
75
+
76
+ # Binary
77
+ data = p.read_bytes()
78
+ p.write_bytes(b"content")
79
+
80
+ # Open (returns file object)
81
+ with p.open("r", encoding="utf-8") as f:
82
+ ...
83
+
84
+ # Append
85
+ p.write_text("more", encoding="utf-8", mode="a")
86
+ ```
87
+
88
+ ### Directory Operations
89
+ ```python
90
+ p.mkdir(parents=True, exist_ok=True) # Create dir (mkdir -p)
91
+ p.rmdir() # Remove empty dir
92
+ p.unlink(missing_ok=True) # Remove file (missing_ok Python 3.8+)
93
+ p.replace(target) # Atomic replace
94
+ p.rename(target) # Rename (not atomic across fs)
95
+ p.symlink_to(target) # Create symlink
96
+ p.hardlink_to(target) # Create hardlink
97
+ p.touch(exist_ok=True) # Create empty file / update mtime
98
+ ```
99
+
100
+ ### Iteration and Globbing
101
+ ```python
102
+ # Iterate directory
103
+ for child in p.iterdir():
104
+ ...
105
+
106
+ # Glob patterns
107
+ for match in p.glob("*.py"): # Non-recursive
108
+ ...
109
+ for match in p.rglob("*.py"): # Recursive
110
+ ...
111
+
112
+ # Walk (like os.walk)
113
+ for root, dirs, files in p.walk():
114
+ ...
115
+ ```
116
+
117
+ ### Path Matching
118
+ ```python
119
+ p.match("*.py") # Match final component
120
+ p.match("*/*.py") # Match relative pattern
121
+ ```
122
+
123
+ ### Temporary Files
124
+ ```python
125
+ import tempfile
126
+
127
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
128
+ path = Path(f.name)
129
+ # Use path
130
+ # Cleanup manual or use context manager
131
+
132
+ # Better: tempfile.TemporaryDirectory
133
+ with tempfile.TemporaryDirectory() as tmp:
134
+ path = Path(tmp) / "file.txt"
135
+ ...
136
+ # Auto cleanup
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Decision Rules
142
+
143
+ | Operation | pathlib Method |
144
+ |-----------|----------------|
145
+ | Join paths | `p / "name"` or `p.joinpath()` |
146
+ | Get parent | `p.parent` |
147
+ | Get filename | `p.name` |
148
+ | Get extension | `p.suffix` / `p.suffixes` |
149
+ | Read text file | `p.read_text(encoding="utf-8")` |
150
+ | Write text file | `p.write_text(content, encoding="utf-8")` |
151
+ | Read binary | `p.read_bytes()` |
152
+ | Write binary | `p.write_bytes(data)` |
153
+ | Create dirs | `p.mkdir(parents=True, exist_ok=True)` |
154
+ | Delete file | `p.unlink(missing_ok=True)` |
155
+ | Delete empty dir | `p.rmdir()` |
156
+ | List directory | `p.iterdir()` |
157
+ | Find files | `p.glob()` / `p.rglob()` |
158
+ | Walk tree | `p.walk()` |
159
+ | Atomic write | Write to temp, `p.replace(target)` |
160
+
161
+ ---
162
+
163
+ ## Preferred Patterns
164
+
165
+ ```python
166
+ # Atomic write (prevents partial reads)
167
+ def atomic_write(path: Path, content: str, encoding: str = "utf-8") -> None:
168
+ tmp = path.with_suffix(path.suffix + ".tmp")
169
+ tmp.write_text(content, encoding=encoding)
170
+ tmp.replace(path)
171
+
172
+ # Safe config loading
173
+ def load_config(path: Path) -> dict:
174
+ if not path.is_file():
175
+ return {}
176
+ return json.loads(path.read_text(encoding="utf-8"))
177
+
178
+ # Recursive find with filter
179
+ py_files = list(project_root.rglob("*.py"))
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Avoid
185
+
186
+ - `os.path` functions (use pathlib equivalents)
187
+ - String manipulation for paths (`+` or `os.path.join`)
188
+ - `glob.glob()` (use `Path.glob()` / `rglob()`)
189
+ - `os.walk()` (use `Path.walk()`)
190
+ - Not specifying `encoding` in text operations
191
+ - `missing_ok=False` (default) — raises on missing file
192
+
193
+ ---
194
+
195
+ ## Windows Notes
196
+
197
+ - `Path` handles `\` and `/` correctly
198
+ - `p.drive` returns `"C:"` on Windows
199
+ - `p.anchor` returns `"C:\\"` on Windows
200
+ - Symlinks require admin or Developer Mode
201
+ - Case-insensitive filesystem
202
+
203
+ ---
204
+
205
+ ## Validation Considerations
206
+
207
+ - Type checkers understand `Path` methods
208
+ - `Path` implements `os.PathLike` — works with stdlib APIs
209
+ - `resolve()` may raise on broken symlinks
210
+
211
+ ---
212
+
213
+ ## Related Skills
214
+
215
+ - `stdlib/os_sys.md`
216
+ - `engineering/configuration.md`
217
+ - `security/path_traversal.md`
218
+ - `security/file_handling.md`
skills/stdlib/re.md ADDED
@@ -0,0 +1,171 @@
1
+ # Stdlib: re (Regular Expressions)
2
+
3
+ **Purpose**: Pattern matching and text processing.
4
+
5
+ **When to use**: String validation, extraction, transformation. Not for parsing structured formats (HTML, JSON, etc.).
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Basic Usage
12
+ ```python
13
+ import re
14
+
15
+ # Compile once, use many (performance)
16
+ PATTERN = re.compile(r"\d{4}-\d{2}-\d{2}")
17
+
18
+ # Match at start
19
+ PATTERN.match("2024-01-15") # Match object or None
20
+ PATTERN.match("2024-01-15 text") # Matches!
21
+
22
+ # Search anywhere
23
+ PATTERN.search("Date: 2024-01-15") # Match object or None
24
+
25
+ # Full match (entire string)
26
+ PATTERN.fullmatch("2024-01-15") # Match object or None
27
+
28
+ # Find all
29
+ PATTERN.findall("2024-01-15 and 2024-02-20") # List of strings/groups
30
+ PATTERN.finditer("...") # Iterator of Match objects
31
+
32
+ # Substitute
33
+ PATTERN.sub("REDACTED", text) # Replace all
34
+ PATTERN.subn("REDACTED", text) # (new_string, count)
35
+
36
+ # Split
37
+ PATTERN.split("a2024-01-15b") # ["a", "b"]
38
+ ```
39
+
40
+ ### Match Object
41
+ ```python
42
+ m = PATTERN.search("Date: 2024-01-15")
43
+ if m:
44
+ m.group() # Entire match
45
+ m.group(1) # Group 1
46
+ m.groups() # All groups tuple
47
+ m.groupdict() # Named groups dict
48
+ m.start(), m.end() # Span
49
+ m.span(1) # Span of group 1
50
+ ```
51
+
52
+ ### Common Patterns
53
+ ```python
54
+ # Email (simplified)
55
+ r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
56
+
57
+ # URL
58
+ r"https?://[^\s/$.?#].[^\s]*"
59
+
60
+ # IP address
61
+ r"\b(?:\d{1,3}\.){3}\d{1,3}\b"
62
+
63
+ # UUID
64
+ r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
65
+
66
+ # ISO datetime
67
+ r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?"
68
+ ```
69
+
70
+ ### Flags
71
+ ```python
72
+ re.IGNORECASE / re.I # Case insensitive
73
+ re.MULTILINE / re.M # ^ $ match line start/end
74
+ re.DOTALL / re.S # . matches newline
75
+ re.VERBOSE / re.X # Verbose mode (whitespace ignored, comments)
76
+ re.ASCII / re.A # \w, \d, \s ASCII only
77
+ ```
78
+
79
+ ### Verbose Mode (Readable)
80
+ ```python
81
+ EMAIL_PATTERN = re.compile(r"""
82
+ [a-zA-Z0-9._%+-]+ # Local part
83
+ @ # Separator
84
+ [a-zA-Z0-9.-]+ # Domain
85
+ \.[a-zA-Z]{2,} # TLD
86
+ """, re.VERBOSE)
87
+ ```
88
+
89
+ ### Raw Strings (Critical)
90
+ ```python
91
+ # ALWAYS use raw strings for regex
92
+ r"\d+" # Correct: backslash preserved
93
+ "\d+" # Wrong: "\d" = "d" in string literal
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Decision Rules
99
+
100
+ | Need | Method |
101
+ |------|--------|
102
+ | Validate entire string | `fullmatch` |
103
+ | Find first occurrence | `search` |
104
+ | Check prefix | `match` |
105
+ | Extract all occurrences | `findall` / `finditer` |
106
+ | Replace all | `sub` |
107
+ | Replace with function | `sub(lambda m: ...)` |
108
+ | Split on pattern | `split` |
109
+
110
+ ---
111
+
112
+ ## Preferred Patterns
113
+
114
+ ```python
115
+ # Compile at module level (not in function)
116
+ ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
117
+
118
+ def is_valid_date(s: str) -> bool:
119
+ return ISO_DATE.fullmatch(s) is not None
120
+
121
+ # Named groups for clarity
122
+ LOG_PATTERN = re.compile(
123
+ r"(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})"
124
+ r"\s+"
125
+ r"(?P<level>\w+)"
126
+ r"\s+"
127
+ r"(?P<message>.*)"
128
+ )
129
+
130
+ for match in LOG_PATTERN.finditer(log_text):
131
+ print(match.groupdict())
132
+
133
+ # Substitution with function
134
+ def redact_emails(text: str) -> str:
135
+ return EMAIL_PATTERN.sub(lambda m: f"{m.group(1)}@[REDACTED]", text)
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Avoid
141
+
142
+ - Regex for parsing HTML/XML/JSON (use proper parsers)
143
+ - Complex regex without `re.VERBOSE` and comments
144
+ - `.*` greedy matching when `.*?` non-greedy needed
145
+ - Not using raw strings (`r"..."`)
146
+ - Compiling regex inside hot loops
147
+ - Catastrophic backtracking (nested quantifiers: `(a+)+`)
148
+
149
+ ---
150
+
151
+ ## Performance
152
+
153
+ - Compile once, reuse
154
+ - `fullmatch` > `match` + `$` > `search` + `^...$`
155
+ - `finditer` > `findall` for large data (memory)
156
+ - Specific patterns faster than generic
157
+
158
+ ---
159
+
160
+ ## Validation Considerations
161
+
162
+ - `re.compile` validates pattern syntax at compile time
163
+ - Type checkers don't validate regex patterns
164
+ - Test edge cases: empty string, no match, multiple matches
165
+
166
+ ---
167
+
168
+ ## Related Skills
169
+
170
+ - `security/input_validation.md`
171
+ - `generation/type_hints.md` (Pattern type)
@@ -0,0 +1,112 @@
1
+ # Stdlib: statistics
2
+
3
+ **Purpose**: Basic statistical operations on numeric data.
4
+
5
+ **When to use**: Simple statistics without NumPy/pandas dependency.
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Measures of Central Tendency
12
+ ```python
13
+ import statistics
14
+
15
+ data = [1, 2, 3, 4, 5, 6, 7, 8, 9]
16
+
17
+ statistics.mean(data) # Arithmetic mean: 5
18
+ statistics.fmean(data) # Fast float mean (3.8+): 5.0
19
+ statistics.median(data) # Median: 5
20
+ statistics.median_low(data) # Low median: 5
21
+ statistics.median_high(data) # High median: 5
22
+ statistics.median_grouped(data, interval=1) # Grouped median
23
+ statistics.mode(data) # Single mode (raises if multimodal)
24
+ statistics.multimode(data) # List of modes (3.8+)
25
+ statistics.harmonic_mean(data) # Harmonic mean
26
+ statistics.geometric_mean(data) # Geometric mean (3.8+)
27
+ ```
28
+
29
+ ### Measures of Spread
30
+ ```python
31
+ statistics.stdev(data) # Sample standard deviation
32
+ statistics.pstdev(data) # Population standard deviation
33
+ statistics.variance(data) # Sample variance
34
+ statistics.pvariance(data) # Population variance
35
+ ```
36
+
37
+ ### Requirements
38
+ - Input: iterable of numeric (int, float, Decimal, Fraction)
39
+ - At least 2 data points for stdev/variance
40
+ - At least 1 for mean/median
41
+ - Raises `StatisticsError` for insufficient data
42
+
43
+ ---
44
+
45
+ ## Decision Rules
46
+
47
+ | Need | Function |
48
+ |------|----------|
49
+ | Average | `mean` / `fmean` |
50
+ | Middle value | `median` |
51
+ | Most common | `mode` / `multimode` |
52
+ | Spread (sample) | `stdev` / `variance` |
53
+ | Spread (population) | `pstdev` / `pvariance` |
54
+ | Weighted average | Manual or NumPy |
55
+
56
+ ---
57
+
58
+ ## Preferred Patterns
59
+
60
+ ```python
61
+ def summarize(values: list[float]) -> dict:
62
+ if not values:
63
+ return {"count": 0}
64
+ if len(values) == 1:
65
+ return {"count": 1, "mean": values[0], "median": values[0]}
66
+
67
+ return {
68
+ "count": len(values),
69
+ "mean": statistics.fmean(values),
70
+ "median": statistics.median(values),
71
+ "stdev": statistics.stdev(values),
72
+ "min": min(values),
73
+ "max": max(values),
74
+ }
75
+
76
+ # Streaming (large data) — use running calculation
77
+ def running_stats():
78
+ n = 0
79
+ mean = 0.0
80
+ m2 = 0.0 # Sum of squares of differences
81
+ for x in data_stream:
82
+ n += 1
83
+ delta = x - mean
84
+ mean += delta / n
85
+ m2 += delta * (x - mean)
86
+ if n > 1:
87
+ yield {"mean": mean, "stdev": (m2 / (n - 1)) ** 0.5}
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Avoid
93
+
94
+ - Using for large datasets (use NumPy/pandas)
95
+ - Calling on empty iterables (raises)
96
+ - Expecting weighted statistics (not supported)
97
+ - Using `mode` on multimodal data (raises; use `multimode`)
98
+
99
+ ---
100
+
101
+ ## Validation Considerations
102
+
103
+ - Test with edge cases: empty, single element, two elements
104
+ - Verify numeric precision for large values
105
+ - Compare with NumPy for correctness if available
106
+
107
+ ---
108
+
109
+ ## Related Skills
110
+
111
+ - `generation/type_hints.md`
112
+ - `engineering/dependency_management.md` (when to add NumPy)
@@ -0,0 +1,211 @@
1
+ # Stdlib: subprocess
2
+
3
+ **Purpose**: Safe subprocess execution and process management.
4
+
5
+ **When to use**: Running external commands. Prefer stdlib APIs when possible.
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Basic Usage (Python 3.5+)
12
+ ```python
13
+ import subprocess
14
+ from pathlib import Path
15
+
16
+ # Simple run (recommended)
17
+ result = subprocess.run(
18
+ ["cmd", "arg1", "arg2"],
19
+ capture_output=True, # stdout, stderr as bytes
20
+ text=True, # Decode as str (encoding=locale)
21
+ encoding="utf-8", # Explicit encoding
22
+ timeout=30, # Seconds
23
+ check=False, # Don't raise on non-zero
24
+ cwd=Path("/work"),
25
+ env={"VAR": "value"}, # Or None to inherit
26
+ )
27
+
28
+ result.returncode
29
+ result.stdout
30
+ result.stderr
31
+ ```
32
+
33
+ ### Safe Patterns
34
+ ```python
35
+ # ALWAYS use list form (not shell=True)
36
+ subprocess.run(["git", "status"]) # Safe
37
+ subprocess.run("git status", shell=True) # UNSAFE - shell injection!
38
+
39
+ # With input
40
+ result = subprocess.run(
41
+ ["grep", "pattern"],
42
+ input="data\nto\nsearch",
43
+ capture_output=True,
44
+ text=True,
45
+ )
46
+
47
+ # Streaming (large output)
48
+ proc = subprocess.Popen(
49
+ ["cmd", "arg"],
50
+ stdout=subprocess.PIPE,
51
+ stderr=subprocess.PIPE,
52
+ text=True,
53
+ )
54
+ for line in proc.stdout:
55
+ process(line)
56
+ proc.wait()
57
+ ```
58
+
59
+ ### Error Handling
60
+ ```python
61
+ try:
62
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
63
+ except subprocess.CalledProcessError as e:
64
+ # e.returncode, e.stdout, e.stderr, e.cmd
65
+ logger.error("Command failed", extra={"cmd": e.cmd, "stderr": e.stderr})
66
+ raise
67
+ except subprocess.TimeoutExpired as e:
68
+ # e.stdout, e.stderr (partial)
69
+ logger.error("Command timed out")
70
+ raise
71
+ except FileNotFoundError:
72
+ logger.error("Command not found", extra={"cmd": cmd[0]})
73
+ raise
74
+ ```
75
+
76
+ ### Pipes and Chaining
77
+ ```python
78
+ # Safe pipeline without shell
79
+ p1 = subprocess.Popen(["cmd1"], stdout=subprocess.PIPE)
80
+ p2 = subprocess.Popen(["cmd2"], stdin=p1.stdout, stdout=subprocess.PIPE)
81
+ p1.stdout.close() # Allow p1 to receive SIGPIPE
82
+ output = p2.communicate()[0]
83
+ ```
84
+
85
+ ### Environment
86
+ ```python
87
+ # Inherit + modify
88
+ env = os.environ.copy()
89
+ env["CUSTOM_VAR"] = "value"
90
+ subprocess.run(cmd, env=env)
91
+
92
+ # Clean environment
93
+ subprocess.run(cmd, env={"PATH": "/usr/bin", "HOME": "/tmp"})
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Security Rules (Critical)
99
+
100
+ ### NEVER use `shell=True` with untrusted input
101
+ ```python
102
+ # DANGEROUS
103
+ subprocess.run(f"echo {user_input}", shell=True)
104
+ # User input: "; rm -rf /"
105
+
106
+ # SAFE
107
+ subprocess.run(["echo", user_input])
108
+ ```
109
+
110
+ ### Validate/Allowlist Commands
111
+ ```python
112
+ ALLOWED_COMMANDS = {"git", "docker", "kubectl"}
113
+
114
+ def safe_run(cmd: list[str], **kwargs):
115
+ if cmd[0] not in ALLOWED_COMMANDS:
116
+ raise ValueError(f"Command not allowed: {cmd[0]}")
117
+ return subprocess.run(cmd, **kwargs)
118
+ ```
119
+
120
+ ### Path Safety
121
+ ```python
122
+ # Resolve paths, prevent traversal
123
+ safe_path = Path(user_input).resolve()
124
+ if not safe_path.is_relative_to(ALLOWED_ROOT):
125
+ raise ValueError("Path traversal attempt")
126
+ subprocess.run(["process", str(safe_path)])
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Decision Rules
132
+
133
+ | Situation | Approach |
134
+ |-----------|----------|
135
+ | Simple command, wait for result | `subprocess.run()` |
136
+ | Need streaming/large output | `subprocess.Popen` |
137
+ | Pipeline | `Popen` chain (no shell) |
138
+ | Fire and forget | `Popen` + `detach()` or `start_new_session=True` |
139
+ | Need shell features (glob, vars) | Avoid; reimplement in Python |
140
+ | Untrusted input in command | List form, validate/allowlist |
141
+
142
+ ---
143
+
144
+ ## Preferred Patterns
145
+
146
+ ```python
147
+ def run_command(
148
+ cmd: list[str],
149
+ *,
150
+ cwd: Path | None = None,
151
+ timeout: float = 30.0,
152
+ env: dict[str, str] | None = None,
153
+ input_data: str | None = None,
154
+ ) -> subprocess.CompletedProcess:
155
+ """Safe subprocess wrapper."""
156
+ # Validate command
157
+ if not cmd or not isinstance(cmd, list):
158
+ raise ValueError("Command must be non-empty list")
159
+
160
+ # Allowlist check (optional but recommended)
161
+ # if cmd[0] not in ALLOWED_COMMANDS: ...
162
+
163
+ return subprocess.run(
164
+ cmd,
165
+ capture_output=True,
166
+ text=True,
167
+ encoding="utf-8",
168
+ errors="replace", # Handle encoding issues
169
+ timeout=timeout,
170
+ check=False, # Handle returncode manually
171
+ cwd=cwd,
172
+ env=env,
173
+ input=input_data,
174
+ )
175
+
176
+ # Usage
177
+ result = run_command(["git", "diff", "--name-only"])
178
+ if result.returncode != 0:
179
+ logger.warning("Git diff failed", stderr=result.stderr)
180
+ files = result.stdout.strip().splitlines()
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Avoid
186
+
187
+ - `shell=True` (except trusted, static commands)
188
+ - String commands (always use list)
189
+ - Unvalidated user input in command args
190
+ - `os.system()`, `os.popen()` (legacy, unsafe)
191
+ - Ignoring `returncode`
192
+ - No timeout (hangs indefinitely)
193
+ - Inheriting env without consideration (leaks secrets)
194
+
195
+ ---
196
+
197
+ ## Validation Considerations
198
+
199
+ - Test with malicious input (injection attempts)
200
+ - Verify timeout behavior
201
+ - Check encoding handling
202
+ - Audit command allowlists
203
+
204
+ ---
205
+
206
+ ## Related Skills
207
+
208
+ - `security/command_injection.md`
209
+ - `security/path_traversal.md`
210
+ - `stdlib/os_sys.md`
211
+ - `engineering/configuration.md`