Skip to content

Commit c379fb9

Browse files
Naaremanclaude
andcommitted
Push to 10/10: shell injection, testing depth, FAQ
Closing the gap from 9.4 to 10: 1. Shell command injection: /check now runs check-structure.py automatically via ${CLAUDE_SKILL_DIR}, giving Claude real audit data instead of improvising 2. Testing depth: added 12-testing-mocking.md (pytest-mock, monkeypatch, HTTP mocking, anti-patterns) and 13-testing-snapshots.md (syrupy, CLI output) 3. FAQ: added 14-faq.md answering 12 pushback questions (why hatchling, why not poetry, why src layout, why rich, why ruff not black, etc.) 4. No-args assessment now runs automated audit first, then manual review 14 reference docs total. All within token budget (200/500 lines, ~1900 tokens). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ec8d050 commit c379fb9

File tree

4 files changed

+357
-8
lines changed

4 files changed

+357
-8
lines changed

skills/python-package-development/SKILL.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ relevant reference file from `references/`:
3636
| Adding a CLI to your package | [references/09-cli-entry-points.md](references/09-cli-entry-points.md) |
3737
| Managing a monorepo / namespace packages | [references/10-monorepo.md](references/10-monorepo.md) |
3838
| Automating releases (bump, changelog, CI) | [references/11-automated-release.md](references/11-automated-release.md) |
39+
| Mocking in tests (APIs, filesystem, time) | [references/12-testing-mocking.md](references/12-testing-mocking.md) |
40+
| Snapshot testing | [references/13-testing-snapshots.md](references/13-testing-snapshots.md) |
41+
| FAQ (why these opinions?) | [references/14-faq.md](references/14-faq.md) |
3942

4043
Read only what's relevant to the current task. Don't load everything at once.
4144

@@ -113,20 +116,25 @@ When invoked with `/python-package-development <subcommand>`, route based on the
113116
| `/python-package-development docs` | Read [references/04-docs.md](references/04-docs.md) and set up or improve documentation |
114117
| `/python-package-development lifecycle` | Read [references/05-lifecycle.md](references/05-lifecycle.md) and manage deprecations |
115118
| `/python-package-development release` | Read [references/06-release.md](references/06-release.md) and walk through the release ritual |
116-
| `/python-package-development check` | Read [references/07-common-mistakes.md](references/07-common-mistakes.md) and audit current project for anti-patterns |
119+
| `/python-package-development check` | Run `python ${CLAUDE_SKILL_DIR}/../../scripts/check-structure.py .` then read [references/07-common-mistakes.md](references/07-common-mistakes.md) to fix any failures |
117120
| `/python-package-development pre-commit` | Read [references/08-pre-commit.md](references/08-pre-commit.md) and set up pre-commit hooks |
118121
| `/python-package-development cli` | Read [references/09-cli-entry-points.md](references/09-cli-entry-points.md) and add a CLI to the package |
119122
| `/python-package-development` (no args) | Assess the current project against all five principles (see checklist below) |
120123

121-
When invoked without a subcommand (auto-triggered or plain `/python-package-development`), run this assessment:
124+
When invoked without a subcommand (auto-triggered or plain `/python-package-development`):
122125

123-
1. **Structure** — Is there a `src/` layout? Does `__init__.py` have `__all__`?
124-
2. **Communication** — Is there an `errors.py` with a base exception? A `_messages.py` with `rich`?
125-
3. **Naming** — Do public functions follow `verb_noun()`? Are families consistent?
126-
4. **Documentation** — Do all public functions have Google-style docstrings?
127-
5. **Lifecycle** — Is `__version__` from `importlib.metadata`? Is there a CHANGELOG?
126+
**Step 1 — Automated audit.** Run the convention checker if available:
127+
```
128+
python ${CLAUDE_SKILL_DIR}/../../scripts/check-structure.py .
129+
```
130+
131+
**Step 2 — Manual review** of things the script can't check:
132+
1. **Naming** — Do public functions follow `verb_noun()`? Are families consistent?
133+
2. **Documentation** — Do all public functions have Google-style docstrings with Args/Returns/Raises?
134+
3. **Messages** — Is `_messages.py` actually used? Any bare `print()` calls?
135+
4. **Lifecycle** — Is `__version__` from `importlib.metadata`? Any undocumented breaking changes?
128136

129-
Then use the Quick Decision Guide below for any specific improvements.
137+
**Step 3 — Suggest improvements** using the Quick Decision Guide below.
130138

131139
---
132140

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# 12 — Testing: Mocking
2+
3+
R equivalent: `testthat::local_mocked_bindings()`, `httptest2`, `webfakes`. The goal is
4+
the same: isolate your code from things you don't control.
5+
6+
---
7+
8+
## When to Mock
9+
10+
Mock **external boundaries** — things that are slow, flaky, or have side effects:
11+
12+
- HTTP APIs and web services
13+
- File systems (when testing logic, not I/O)
14+
- Time (`datetime.now()`, `time.time()`)
15+
- Databases and caches
16+
- Environment variables
17+
18+
Do **not** mock:
19+
20+
- Your own package code (test the real thing)
21+
- Standard library basics (don't mock `len()` or `dict.get()`)
22+
- Simple data transformations (just assert the output)
23+
24+
---
25+
26+
## pytest-mock — The Preferred Approach
27+
28+
Use `pytest-mock` over raw `unittest.mock`. It gives you a `mocker` fixture that
29+
auto-cleans up after each test.
30+
31+
```bash
32+
uv add --dev pytest-mock
33+
```
34+
35+
```python
36+
def test_fetch_data_calls_api(mocker):
37+
mock_get = mocker.patch("my_package.client.httpx.get")
38+
mock_get.return_value.json.return_value = {"status": "ok"}
39+
40+
result = fetch_data("https://api.example.com/data")
41+
42+
mock_get.assert_called_once_with("https://api.example.com/data")
43+
assert result == {"status": "ok"}
44+
```
45+
46+
### Common mocker methods
47+
48+
```python
49+
mocker.patch("module.path.function") # replace a function
50+
mocker.patch.object(obj, "method") # replace a method on an object
51+
mocker.patch.dict("os.environ", {"KEY": "v"}) # patch a dict
52+
mocker.spy(obj, "method") # wrap real method, track calls
53+
```
54+
55+
---
56+
57+
## MagicMock Basics
58+
59+
When you need a stand-in object that accepts any attribute or call:
60+
61+
```python
62+
def test_processor_calls_writer(mocker):
63+
writer = mocker.MagicMock()
64+
processor = DataProcessor(writer=writer)
65+
66+
processor.run(records=[{"id": 1}])
67+
68+
writer.write.assert_called_once()
69+
assert writer.write.call_args[0][0] == [{"id": 1}]
70+
```
71+
72+
---
73+
74+
## Mocking HTTP Calls
75+
76+
For packages that hit APIs, use `responses` (for `requests`) or `respx` (for `httpx`).
77+
78+
```bash
79+
uv add --dev responses # if using requests
80+
uv add --dev respx # if using httpx
81+
```
82+
83+
### With responses
84+
85+
```python
86+
import responses
87+
88+
@responses.activate
89+
def test_fetch_users():
90+
responses.add(
91+
responses.GET,
92+
"https://api.example.com/users",
93+
json=[{"id": 1, "name": "Alice"}],
94+
status=200,
95+
)
96+
result = fetch_users()
97+
assert len(result) == 1
98+
assert result[0]["name"] == "Alice"
99+
```
100+
101+
### With respx
102+
103+
```python
104+
import respx
105+
import httpx
106+
107+
def test_fetch_users(respx_mock):
108+
respx_mock.get("https://api.example.com/users").mock(
109+
return_value=httpx.Response(200, json=[{"id": 1, "name": "Alice"}])
110+
)
111+
result = fetch_users()
112+
assert result[0]["name"] == "Alice"
113+
```
114+
115+
---
116+
117+
## monkeypatch — Environment and Attributes
118+
119+
Built into pytest. Best for environment variables and simple attribute swaps.
120+
121+
```python
122+
def test_reads_api_key_from_env(monkeypatch):
123+
monkeypatch.setenv("API_KEY", "test-key-123")
124+
config = load_config()
125+
assert config.api_key == "test-key-123"
126+
127+
128+
def test_handles_missing_api_key(monkeypatch):
129+
monkeypatch.delenv("API_KEY", raising=False)
130+
with pytest.raises(ConfigError, match="API_KEY"):
131+
load_config()
132+
```
133+
134+
Use `monkeypatch` for env vars. Use `mocker.patch` for replacing functions and methods.
135+
136+
---
137+
138+
## Anti-Patterns
139+
140+
1. **Mocking everything** — if a test mocks five things, it tests nothing. Mock only
141+
the boundary, then assert real behavior.
142+
2. **Mocking implementation details** — don't mock internal helpers. If you refactor
143+
internals, tests shouldn't break.
144+
3. **Brittle return chains**`mock.return_value.foo.return_value.bar` is a sign
145+
you're testing the mock, not your code. Simplify the interface.
146+
4. **Forgetting to assert calls** — a mock that's never checked is just dead code.
147+
Always verify the mock was actually used.
148+
5. **Patching the wrong path** — patch where the name is *looked up*, not where it's
149+
*defined*. Patch `my_package.client.httpx.get`, not `httpx.get`.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# 13 — Testing: Snapshots
2+
3+
R equivalent: `testthat::expect_snapshot()`. Snapshot tests capture complex output once,
4+
then verify it doesn't change unexpectedly.
5+
6+
---
7+
8+
## When to Use Snapshots
9+
10+
Use them when the expected output is **complex, verbose, or tedious to write by hand**:
11+
12+
- Serialized data structures (JSON, YAML, dictionaries)
13+
- Error messages and exception formatting
14+
- CLI output and help text
15+
- Rendered templates or reports
16+
- Complex string representations (`__repr__`, `__str__`)
17+
18+
Do **not** use snapshots for:
19+
20+
- Simple equality checks (`assert x == 42` is clearer)
21+
- Output that changes frequently (timestamps, random IDs)
22+
- Performance-sensitive values (thresholds belong in explicit assertions)
23+
24+
---
25+
26+
## syrupy — Recommended Library
27+
28+
`syrupy` is the most actively maintained snapshot library for pytest.
29+
30+
```bash
31+
uv add --dev syrupy
32+
```
33+
34+
### Basic usage
35+
36+
```python
37+
def test_report_output(snapshot):
38+
result = generate_report(year=2025, quarter=1)
39+
assert result == snapshot
40+
```
41+
42+
On first run, syrupy creates a snapshot file in `__snapshots__/` next to your test file.
43+
On subsequent runs, it compares the output against the stored snapshot.
44+
45+
### Snapshot of a specific attribute
46+
47+
```python
48+
def test_error_message(snapshot):
49+
with pytest.raises(ValidationError) as exc_info:
50+
validate(bad_data)
51+
assert str(exc_info.value) == snapshot
52+
```
53+
54+
---
55+
56+
## Updating Snapshots
57+
58+
When output changes intentionally, update the stored snapshots:
59+
60+
```bash
61+
uv run pytest --snapshot-update
62+
```
63+
64+
Always **review the diff** before committing updated snapshots. Treat snapshot updates
65+
like code review — the diff should make sense.
66+
67+
---
68+
69+
## CLI Output Snapshots
70+
71+
Particularly useful for packages with a CLI interface:
72+
73+
```python
74+
from click.testing import CliRunner
75+
76+
def test_help_output(snapshot):
77+
runner = CliRunner()
78+
result = runner.invoke(cli, ["--help"])
79+
assert result.output == snapshot
80+
81+
82+
def test_error_output(snapshot):
83+
runner = CliRunner()
84+
result = runner.invoke(cli, ["bad-command"])
85+
assert result.output == snapshot
86+
assert result.exit_code == 2
87+
```
88+
89+
---
90+
91+
## Snapshot File Hygiene
92+
93+
- Commit `__snapshots__/` directories to version control
94+
- Run `uv run pytest --snapshot-update` to remove orphaned snapshots
95+
- Keep snapshots small — if a snapshot is 500 lines, consider testing individual
96+
components instead
97+
- Add `__snapshots__/` to your `.gitattributes` as `linguist-generated` so they
98+
don't clutter pull request diffs
99+
100+
```gitattributes
101+
**/__snapshots__/** linguist-generated=true
102+
```
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# 14 — FAQ
2+
3+
Pushback you'll get on these conventions, and why we made the choices we did.
4+
5+
---
6+
7+
## Build System
8+
9+
### Why hatchling and not setuptools?
10+
11+
Simpler config, sane defaults, faster builds. Setuptools requires more boilerplate and has
12+
legacy baggage (`setup.py`, `setup.cfg`, `MANIFEST.in`). Hatchling reads `pyproject.toml`
13+
natively with zero extra files.
14+
15+
### Why not poetry?
16+
17+
Poetry uses its own lock format and dependency resolver that conflicts with PEP standards.
18+
uv is faster, PEP-compliant, and gaining rapid adoption. Poetry is fine — but we pick one,
19+
and we pick the one aligned with standards.
20+
21+
---
22+
23+
## Tooling
24+
25+
### Why uv and not pip?
26+
27+
uv is 10-100x faster than pip, handles venvs automatically, supports workspaces, and is the
28+
direction the ecosystem is heading. pip still works but uv is strictly better for development.
29+
30+
### Why no black/isort/flake8?
31+
32+
Ruff replaces all three. It's faster, has fewer config files, and maintains compatibility.
33+
There's no reason to use the originals anymore.
34+
35+
### Why PEP 735 dependency-groups instead of `[tool.uv.dev-dependencies]`?
36+
37+
PEP 735 is the standard. `[tool.uv.dev-dependencies]` is a uv-specific legacy convention.
38+
Standards last longer than tool conventions.
39+
40+
---
41+
42+
## Code Style
43+
44+
### Why Google docstrings and not NumPy or Sphinx style?
45+
46+
Readable without rendering. Google style is the most compact and works well with mkdocstrings.
47+
NumPy style is verbose. Sphinx style uses RST which is dying.
48+
49+
### Why rich and not just `print()`?
50+
51+
Structured output, consistent styling, stderr separation, progress bars. When your package
52+
talks to users, it should speak clearly. R's `cli` package proved this matters.
53+
54+
---
55+
56+
## Project Structure
57+
58+
### Why src layout?
59+
60+
Prevents importing from the source directory instead of the installed package. Without it,
61+
tests can pass locally but the installed package is broken. This is a solved problem — use
62+
`src/`.
63+
64+
### Why `errors.py` without underscore but `_messages.py` with underscore?
65+
66+
`errors.py` is public API — users import and catch your exceptions directly. `_messages.py`
67+
is internal — users never interact with it. The underscore convention signals this.
68+
69+
---
70+
71+
## Scope
72+
73+
### Why not add pandas/numpy as default dependencies?
74+
75+
Not every package is a data science package. The scaffold includes only `rich` (for user
76+
communication). Add domain-specific deps yourself.
77+
78+
### What if I need a CLI?
79+
80+
See reference `09-cli-entry-points.md`. Use `click` for multi-command CLIs, `argparse` for
81+
simple ones.
82+
83+
---
84+
85+
## Adoption
86+
87+
### Can I use this with an existing project?
88+
89+
Yes. Run `/python-package-development check` to audit your project, then address the gaps
90+
one by one. You don't need to start from scratch.

0 commit comments

Comments
 (0)