@coralai/sps-cli 0.41.2 → 0.43.0
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.
- package/README.md +34 -3
- package/dist/commands/cardAdd.d.ts +1 -1
- package/dist/commands/cardAdd.d.ts.map +1 -1
- package/dist/commands/cardAdd.js +16 -6
- package/dist/commands/cardAdd.js.map +1 -1
- package/dist/commands/cardDashboard.js +1 -1
- package/dist/commands/cardDashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -314
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hookCommand.d.ts.map +1 -1
- package/dist/commands/hookCommand.js +6 -7
- package/dist/commands/hookCommand.js.map +1 -1
- package/dist/commands/pmCommand.js +1 -1
- package/dist/commands/pmCommand.js.map +1 -1
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +60 -37
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -30
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/commands/tick.js +1 -1
- package/dist/commands/tick.js.map +1 -1
- package/dist/core/checklist.d.ts +22 -0
- package/dist/core/checklist.d.ts.map +1 -0
- package/dist/core/checklist.js +38 -0
- package/dist/core/checklist.js.map +1 -0
- package/dist/core/checklist.test.d.ts +2 -0
- package/dist/core/checklist.test.d.ts.map +1 -0
- package/dist/core/checklist.test.js +74 -0
- package/dist/core/checklist.test.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +7 -4
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/context.d.ts +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/engines/EventHandler.test.js +3 -3
- package/dist/engines/EventHandler.test.js.map +1 -1
- package/dist/engines/MonitorEngine.js +2 -2
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/engines/SchedulerEngine.js +1 -1
- package/dist/engines/SchedulerEngine.js.map +1 -1
- package/dist/engines/StageEngine.js +3 -3
- package/dist/engines/StageEngine.js.map +1 -1
- package/dist/engines/engine-pipeline-adapter.test.js +2 -2
- package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
- package/dist/interfaces/TaskBackend.d.ts +3 -1
- package/dist/interfaces/TaskBackend.d.ts.map +1 -1
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/dist/models/types.d.ts +16 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.js +28 -5
- package/dist/providers/MarkdownTaskBackend.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +5 -7
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
- package/project-template/.claude/hooks/start.sh +44 -0
- package/project-template/.claude/settings.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Python Idioms
|
|
2
|
+
|
|
3
|
+
Core Pythonic patterns. Language-focused (no architecture here).
|
|
4
|
+
|
|
5
|
+
## EAFP — Easier to Ask Forgiveness Than Permission
|
|
6
|
+
|
|
7
|
+
Python prefers exception handling over condition checking.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# Good: EAFP
|
|
11
|
+
def get_value(d: dict, key: str, default=None):
|
|
12
|
+
try:
|
|
13
|
+
return d[key]
|
|
14
|
+
except KeyError:
|
|
15
|
+
return default
|
|
16
|
+
|
|
17
|
+
# Bad: LBYL (Look Before You Leap) — race condition risk, slower, less Pythonic
|
|
18
|
+
def get_value(d: dict, key: str, default=None):
|
|
19
|
+
if key in d:
|
|
20
|
+
return d[key]
|
|
21
|
+
return default
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
When to use which: EAFP is Pythonic for dict / attr / file access. LBYL is fine for user-facing validation where the failure message matters.
|
|
25
|
+
|
|
26
|
+
## Comprehensions
|
|
27
|
+
|
|
28
|
+
Prefer comprehensions over `map`/`filter` + lambda. Readability wins.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# List / set / dict comprehensions
|
|
32
|
+
active_names = [u.name for u in users if u.is_active]
|
|
33
|
+
unique_tags = {t.strip() for tag_group in raw for t in tag_group.split(',')}
|
|
34
|
+
user_ages = {u.id: u.age for u in users}
|
|
35
|
+
|
|
36
|
+
# Generator expression (lazy, memory-efficient)
|
|
37
|
+
total = sum(order.total for order in orders if order.paid)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Rule**: If the comprehension doesn't fit on 2 lines, rewrite as a `for` loop. Nested comprehensions (`[x for x in y for z in w]`) are almost always unreadable.
|
|
41
|
+
|
|
42
|
+
## Generator Functions
|
|
43
|
+
|
|
44
|
+
Lazy iteration for large datasets. Use `yield` to avoid loading everything into memory.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
def read_large_file(path: str) -> Iterator[str]:
|
|
48
|
+
with open(path) as f:
|
|
49
|
+
for line in f:
|
|
50
|
+
yield line.strip()
|
|
51
|
+
|
|
52
|
+
# Caller: streams one line at a time
|
|
53
|
+
for line in read_large_file('huge.log'):
|
|
54
|
+
process(line)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Generator expressions (parentheses not brackets):
|
|
58
|
+
```python
|
|
59
|
+
squared = (x * x for x in range(10_000_000)) # 0 memory allocated
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Context Managers (`with`)
|
|
63
|
+
|
|
64
|
+
Use `with` for any resource (file, lock, DB conn, etc.). Never manual `open()` / `close()`.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# File
|
|
68
|
+
with open('data.json') as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
|
|
71
|
+
# Multiple resources
|
|
72
|
+
with open('in.txt') as src, open('out.txt', 'w') as dst:
|
|
73
|
+
dst.write(src.read().upper())
|
|
74
|
+
|
|
75
|
+
# Custom context manager (contextlib)
|
|
76
|
+
from contextlib import contextmanager
|
|
77
|
+
|
|
78
|
+
@contextmanager
|
|
79
|
+
def temp_directory():
|
|
80
|
+
d = tempfile.mkdtemp()
|
|
81
|
+
try:
|
|
82
|
+
yield d
|
|
83
|
+
finally:
|
|
84
|
+
shutil.rmtree(d)
|
|
85
|
+
|
|
86
|
+
with temp_directory() as d:
|
|
87
|
+
# use d, auto-cleaned
|
|
88
|
+
pass
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Decorators
|
|
92
|
+
|
|
93
|
+
Use for cross-cutting concerns (logging, caching, timing, auth). Don't nest more than 2 deep.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Simple decorator
|
|
97
|
+
from functools import wraps
|
|
98
|
+
|
|
99
|
+
def timed(func):
|
|
100
|
+
@wraps(func) # preserve name, docstring, signature
|
|
101
|
+
def wrapper(*args, **kwargs):
|
|
102
|
+
start = time.perf_counter()
|
|
103
|
+
result = func(*args, **kwargs)
|
|
104
|
+
duration = time.perf_counter() - start
|
|
105
|
+
logger.info(f"{func.__name__} took {duration:.3f}s")
|
|
106
|
+
return result
|
|
107
|
+
return wrapper
|
|
108
|
+
|
|
109
|
+
@timed
|
|
110
|
+
def expensive_operation(x: int) -> int:
|
|
111
|
+
time.sleep(1)
|
|
112
|
+
return x * 2
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Parameterized decorators (3 levels deep — use sparingly):
|
|
116
|
+
```python
|
|
117
|
+
def retry(max_attempts: int = 3):
|
|
118
|
+
def decorator(func):
|
|
119
|
+
@wraps(func)
|
|
120
|
+
def wrapper(*args, **kwargs):
|
|
121
|
+
for attempt in range(max_attempts):
|
|
122
|
+
try:
|
|
123
|
+
return func(*args, **kwargs)
|
|
124
|
+
except Exception:
|
|
125
|
+
if attempt == max_attempts - 1:
|
|
126
|
+
raise
|
|
127
|
+
return wrapper
|
|
128
|
+
return decorator
|
|
129
|
+
|
|
130
|
+
@retry(max_attempts=5)
|
|
131
|
+
def flaky_api_call(): ...
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Data Classes
|
|
135
|
+
|
|
136
|
+
Prefer `dataclass` over manual `__init__` for value objects. Use `frozen=True` for immutability.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from dataclasses import dataclass, field
|
|
140
|
+
from typing import ClassVar
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True, slots=True)
|
|
143
|
+
class User:
|
|
144
|
+
id: str
|
|
145
|
+
name: str
|
|
146
|
+
email: str
|
|
147
|
+
roles: list[str] = field(default_factory=list) # never mutable default!
|
|
148
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
149
|
+
|
|
150
|
+
# Class-level constant
|
|
151
|
+
MAX_ROLES: ClassVar[int] = 10
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Rules**:
|
|
155
|
+
- `slots=True` for performance (Python 3.10+)
|
|
156
|
+
- `frozen=True` if the object shouldn't change after creation
|
|
157
|
+
- Never use mutable default (`roles: list[str] = []`) — use `field(default_factory=list)`
|
|
158
|
+
- `ClassVar` for constants that shouldn't become instance attrs
|
|
159
|
+
|
|
160
|
+
## `pathlib` over `os.path`
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pathlib import Path
|
|
164
|
+
|
|
165
|
+
# Good
|
|
166
|
+
config = Path.home() / '.myapp' / 'config.json'
|
|
167
|
+
if config.exists():
|
|
168
|
+
data = json.loads(config.read_text())
|
|
169
|
+
for log in Path('logs').glob('*.log'):
|
|
170
|
+
process(log)
|
|
171
|
+
|
|
172
|
+
# Bad
|
|
173
|
+
import os
|
|
174
|
+
config = os.path.join(os.path.expanduser('~'), '.myapp', 'config.json')
|
|
175
|
+
if os.path.exists(config):
|
|
176
|
+
with open(config) as f:
|
|
177
|
+
data = json.load(f)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## `match`/`case` — structural pattern matching (Python 3.10+)
|
|
181
|
+
|
|
182
|
+
Use for dispatch on shape, not just value. For simple value checks, `if/elif` is still cleaner.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
def describe(point):
|
|
186
|
+
match point:
|
|
187
|
+
case (0, 0):
|
|
188
|
+
return "origin"
|
|
189
|
+
case (0, y):
|
|
190
|
+
return f"y-axis at {y}"
|
|
191
|
+
case (x, 0):
|
|
192
|
+
return f"x-axis at {x}"
|
|
193
|
+
case (x, y) if x == y:
|
|
194
|
+
return f"diagonal at {x}"
|
|
195
|
+
case (x, y):
|
|
196
|
+
return f"point ({x}, {y})"
|
|
197
|
+
case _:
|
|
198
|
+
return "not a point"
|
|
199
|
+
|
|
200
|
+
# Class patterns — great for ADTs / sum types
|
|
201
|
+
match event:
|
|
202
|
+
case Click(x=x, y=y):
|
|
203
|
+
handle_click(x, y)
|
|
204
|
+
case KeyPress(key="q"):
|
|
205
|
+
quit()
|
|
206
|
+
case KeyPress(key=k):
|
|
207
|
+
handle_key(k)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**When NOT to use**: straightforward value checks (`if x == 1: ... elif x == 2: ...`). Pattern matching shines for destructuring nested shapes.
|
|
211
|
+
|
|
212
|
+
## Walrus operator `:=` (Python 3.8+)
|
|
213
|
+
|
|
214
|
+
Assignment expression — assign and use in the same statement. Use sparingly.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# Good: avoid double-reading / double-computing
|
|
218
|
+
while chunk := f.read(8192):
|
|
219
|
+
process(chunk)
|
|
220
|
+
|
|
221
|
+
if (n := len(data)) > 10:
|
|
222
|
+
print(f"Too many items ({n})")
|
|
223
|
+
|
|
224
|
+
# Good in comprehensions: avoid recomputing
|
|
225
|
+
results = [y for x in items if (y := expensive(x)) is not None]
|
|
226
|
+
|
|
227
|
+
# Bad: over-use where a regular assignment is clearer
|
|
228
|
+
if (user := db.find(uid)) and user.active and (perms := user.get_perms()) and "admin" in perms:
|
|
229
|
+
...
|
|
230
|
+
# Prefer:
|
|
231
|
+
user = db.find(uid)
|
|
232
|
+
if user and user.active:
|
|
233
|
+
perms = user.get_perms()
|
|
234
|
+
if "admin" in perms: ...
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## f-strings (Python 3.6+)
|
|
238
|
+
|
|
239
|
+
Only use `.format()` or `%` for legacy code.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# Good
|
|
243
|
+
msg = f"User {user.name} (id={user.id}) logged in at {when:%Y-%m-%d %H:%M}"
|
|
244
|
+
|
|
245
|
+
# Also good: f-string expressions
|
|
246
|
+
msg = f"Total: ${sum(o.amount for o in orders):,.2f}"
|
|
247
|
+
|
|
248
|
+
# Python 3.8+: self-documenting expressions with =
|
|
249
|
+
msg = f"{x=}, {y=}" # => "x=5, y=10"
|
|
250
|
+
|
|
251
|
+
# Python 3.12+: f-strings accept any quotes, backslashes, and multiline
|
|
252
|
+
msg = f"path: {Path('a') / 'b'}, lines:\n{"\n".join(items)}"
|
|
253
|
+
|
|
254
|
+
# Bad
|
|
255
|
+
msg = "User {} (id={}) logged in at {}".format(user.name, user.id, when)
|
|
256
|
+
msg = "User %s (id=%d) logged in at %s" % (user.name, user.id, when)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Forbidden patterns (auto-reject in code review)
|
|
260
|
+
|
|
261
|
+
| Anti-pattern | Why bad | Fix |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `def f(x=[])` | Mutable default shared across calls | `def f(x=None): x = x or []` |
|
|
264
|
+
| `except:` (bare) | Catches SystemExit, KeyboardInterrupt | `except SomeError:` |
|
|
265
|
+
| `from m import *` | Pollutes namespace, hides origin | explicit imports |
|
|
266
|
+
| `global x` in functions | Hidden state | pass explicitly or use a class |
|
|
267
|
+
| `type(x) == Foo` | Doesn't handle subclasses | `isinstance(x, Foo)` |
|
|
268
|
+
| `if x == None` | Identity check, not equality | `if x is None` |
|
|
269
|
+
|
|
270
|
+
## Naming conventions (PEP 8)
|
|
271
|
+
|
|
272
|
+
| Kind | Style | Example |
|
|
273
|
+
|---|---|---|
|
|
274
|
+
| Variables, functions | `snake_case` | `user_count`, `get_user()` |
|
|
275
|
+
| Constants | `UPPER_SNAKE` | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
|
|
276
|
+
| Classes | `PascalCase` | `UserService`, `HTTPClient` |
|
|
277
|
+
| Private (intent) | `_leading_underscore` | `_internal_cache` |
|
|
278
|
+
| Name mangling | `__double_leading` | `__private_attr` (use sparingly) |
|
|
279
|
+
| Module, package | `snake_case` | `user_service.py`, `payment_gateway/` |
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Python Packaging & Environments
|
|
2
|
+
|
|
3
|
+
Modern Python packaging. Defaults: `pyproject.toml` + `uv` (or `pip` + `venv`). No `setup.py`, no `requirements.txt` pinning in libraries.
|
|
4
|
+
|
|
5
|
+
## Tool landscape (2024–2026)
|
|
6
|
+
|
|
7
|
+
| Need | Tool | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Env + lockfile + installer | **`uv`** | Fastest; Rust-based; drop-in for pip/venv/pip-tools/poetry |
|
|
10
|
+
| Traditional installer | `pip` | Still the universal fallback |
|
|
11
|
+
| Env isolation only | `venv` (stdlib) | Zero-dependency; fine for simple cases |
|
|
12
|
+
| Project manager (alt) | `poetry`, `hatch`, `pdm` | Mature; pick one per project, don't mix |
|
|
13
|
+
| Build backend | `hatchling`, `setuptools`, `flit-core`, `poetry-core` | Declared in `pyproject.toml` |
|
|
14
|
+
|
|
15
|
+
Default recommendation: `uv` for new work. Its `pyproject.toml` is standard so projects stay portable.
|
|
16
|
+
|
|
17
|
+
## `pyproject.toml` — the one config file
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[project]
|
|
21
|
+
name = "myapp"
|
|
22
|
+
version = "0.1.0"
|
|
23
|
+
description = "Short one-liner"
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
requires-python = ">=3.11"
|
|
26
|
+
license = { text = "MIT" }
|
|
27
|
+
authors = [{ name = "Coral", email = "coral@example.com" }]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27",
|
|
30
|
+
"pydantic>=2.5",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0",
|
|
36
|
+
"pytest-cov",
|
|
37
|
+
"mypy",
|
|
38
|
+
"ruff",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
myapp = "myapp.cli:main" # creates a `myapp` executable on install
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["hatchling"]
|
|
46
|
+
build-backend = "hatchling.build"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/myapp"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
strict = true
|
|
57
|
+
python_version = "3.11"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Project layout — `src/` over flat
|
|
61
|
+
|
|
62
|
+
Keep source under `src/` so `import myapp` during tests uses the installed package, not the working directory. Catches "works locally, broken when installed" bugs early.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
myapp/
|
|
66
|
+
├── pyproject.toml
|
|
67
|
+
├── README.md
|
|
68
|
+
├── src/
|
|
69
|
+
│ └── myapp/
|
|
70
|
+
│ ├── __init__.py
|
|
71
|
+
│ ├── cli.py
|
|
72
|
+
│ └── services/
|
|
73
|
+
│ └── user.py
|
|
74
|
+
└── tests/
|
|
75
|
+
├── conftest.py
|
|
76
|
+
└── test_user.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Avoid: top-level `myapp/` mixed with `tests/` — `sys.path` tricks mask import bugs.
|
|
80
|
+
|
|
81
|
+
## `uv` — day-to-day commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Create project
|
|
85
|
+
uv init myapp --package
|
|
86
|
+
|
|
87
|
+
# Add / remove dependencies (updates pyproject.toml + uv.lock)
|
|
88
|
+
uv add httpx pydantic
|
|
89
|
+
uv add --dev pytest pytest-cov mypy ruff
|
|
90
|
+
uv remove requests
|
|
91
|
+
|
|
92
|
+
# Install everything (reproducible from lockfile)
|
|
93
|
+
uv sync
|
|
94
|
+
|
|
95
|
+
# Run anything inside the env
|
|
96
|
+
uv run pytest
|
|
97
|
+
uv run python -m myapp
|
|
98
|
+
uv run ruff check src/
|
|
99
|
+
|
|
100
|
+
# Pin Python version
|
|
101
|
+
uv python pin 3.12
|
|
102
|
+
|
|
103
|
+
# Build a wheel / sdist
|
|
104
|
+
uv build
|
|
105
|
+
|
|
106
|
+
# Upgrade dependencies
|
|
107
|
+
uv lock --upgrade
|
|
108
|
+
uv lock --upgrade-package httpx
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Commit `uv.lock` for **applications**. For **libraries**, don't commit it — let downstreams resolve against their own constraints.
|
|
112
|
+
|
|
113
|
+
## `pip` + `venv` — classic workflow
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python -m venv .venv
|
|
117
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
118
|
+
|
|
119
|
+
pip install -e '.[dev]' # editable install with dev extras
|
|
120
|
+
pip install --upgrade pip
|
|
121
|
+
|
|
122
|
+
# Freeze what's installed (for apps, not libs)
|
|
123
|
+
pip freeze > requirements.lock.txt
|
|
124
|
+
pip install -r requirements.lock.txt
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Never `pip install` into the system Python. Use a venv per project.
|
|
128
|
+
|
|
129
|
+
## Dependency specification
|
|
130
|
+
|
|
131
|
+
```toml
|
|
132
|
+
[project]
|
|
133
|
+
dependencies = [
|
|
134
|
+
"httpx>=0.27,<1.0", # library: allow a compatible range
|
|
135
|
+
"pydantic~=2.5", # ~= means >=2.5,<3.0
|
|
136
|
+
"requests; python_version<'3.11'", # conditional
|
|
137
|
+
"rich[jupyter]>=13.0", # with extras
|
|
138
|
+
]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Specifier | Meaning | Use for |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `pkg>=X.Y` | at least this version | libraries |
|
|
144
|
+
| `pkg~=X.Y` | compatible release (=X.*, <X+1) | common default |
|
|
145
|
+
| `pkg==X.Y.Z` | exact pin | applications with lockfile |
|
|
146
|
+
| `pkg>=A,<B` | explicit range | upper bound on known-breaking releases |
|
|
147
|
+
|
|
148
|
+
**Libraries**: loose constraints (`>=X`). **Applications**: tight lockfile (`uv.lock` or `requirements.lock.txt`).
|
|
149
|
+
|
|
150
|
+
## Version management
|
|
151
|
+
|
|
152
|
+
Single source of truth for the version. Don't duplicate in `__init__.py`.
|
|
153
|
+
|
|
154
|
+
```toml
|
|
155
|
+
# pyproject.toml
|
|
156
|
+
[project]
|
|
157
|
+
dynamic = ["version"]
|
|
158
|
+
|
|
159
|
+
[tool.hatch.version]
|
|
160
|
+
path = "src/myapp/__about__.py"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# src/myapp/__about__.py
|
|
165
|
+
__version__ = "0.1.0"
|
|
166
|
+
|
|
167
|
+
# src/myapp/__init__.py
|
|
168
|
+
from myapp.__about__ import __version__
|
|
169
|
+
__all__ = ["__version__"]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Python version management
|
|
173
|
+
|
|
174
|
+
`pyenv` for installing Python versions; `uv python` integrates this. Project-level pin:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv python pin 3.12 # writes .python-version
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
CI: install the exact version from `.python-version` so prod and dev match.
|
|
181
|
+
|
|
182
|
+
## Publishing to PyPI
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
uv build
|
|
186
|
+
uv publish # uses PyPI API token from UV_PUBLISH_TOKEN
|
|
187
|
+
|
|
188
|
+
# Or with twine
|
|
189
|
+
python -m build
|
|
190
|
+
twine upload dist/*
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Pre-flight checks:
|
|
194
|
+
- `README.md` renders on PyPI (test with `twine check dist/*`)
|
|
195
|
+
- Version bumped
|
|
196
|
+
- `CHANGELOG.md` updated
|
|
197
|
+
- Tag release in git: `git tag v0.1.0 && git push --tags`
|
|
198
|
+
|
|
199
|
+
## Private packages
|
|
200
|
+
|
|
201
|
+
```toml
|
|
202
|
+
[tool.uv.sources]
|
|
203
|
+
mycompany-lib = { git = "ssh://git@github.com/mycompany/lib.git", tag = "v1.2.0" }
|
|
204
|
+
internal = { path = "../internal", editable = true }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Or via `--extra-index-url` for private PyPI indexes.
|
|
208
|
+
|
|
209
|
+
## `__init__.py` — keep it minimal
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# src/myapp/__init__.py
|
|
213
|
+
"""Public API for myapp."""
|
|
214
|
+
from myapp.services import UserService
|
|
215
|
+
from myapp.models import User
|
|
216
|
+
|
|
217
|
+
__all__ = ["UserService", "User"]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Don't run heavy init logic at import time — it slows down `import myapp` for every caller.
|
|
221
|
+
|
|
222
|
+
## Common mistakes
|
|
223
|
+
|
|
224
|
+
| Mistake | Fix |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `setup.py` in a new project | Use `pyproject.toml` only |
|
|
227
|
+
| Committing `venv/` or `.venv/` | Add to `.gitignore` |
|
|
228
|
+
| `pip install` without a venv | Always use `uv` or `venv` |
|
|
229
|
+
| `requirements.txt` in a library | Declare in `pyproject.toml`; let consumers pin |
|
|
230
|
+
| Version duplicated in 3 files | Use `dynamic = ["version"]` |
|
|
231
|
+
| `from myapp import *` in `__init__.py` | Explicit exports via `__all__` |
|
|
232
|
+
| Flat layout causing import surprises | Use `src/` layout |
|
|
233
|
+
| Running tests with local `myapp/` shadowing the installed one | `pip install -e '.[dev]'`, then `pytest` |
|