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,474 @@
1
+ """Claude Code adapter for python-skills."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from ..adapters.base import (
7
+ AgentAdapter,
8
+ DetectionResult,
9
+ InstallResult,
10
+ StatusResult,
11
+ SyncResult,
12
+ UninstallResult,
13
+ )
14
+ from ..config import Target
15
+ from ..markers import BEGIN_MARKER, END_MARKER
16
+ from ..skills.registry import SkillRegistry
17
+ from ..state import LockManager
18
+
19
+
20
+ class ClaudeAdapter(AgentAdapter):
21
+ """Adapter for Claude Code."""
22
+
23
+ name = "Claude Code"
24
+ target = Target.CLAUDE.value
25
+
26
+ def __init__(self, project_root: Path, skills_registry: SkillRegistry, lock_manager: LockManager, config):
27
+ super().__init__(project_root, skills_registry, lock_manager, config)
28
+
29
+ def _get_global_path(self) -> Path:
30
+ """Get the global Claude config path."""
31
+ return Path.home() / ".claude"
32
+
33
+ def detect(self) -> DetectionResult:
34
+ """Detect Claude Code installation and configuration."""
35
+ # Check if claude is in PATH
36
+ app_detected = shutil.which("claude") is not None
37
+
38
+ # Check for project config
39
+ project_config = (self.project_root / ".claude").exists()
40
+
41
+ # Check for global config
42
+ global_config = self._get_global_path().exists()
43
+
44
+ return DetectionResult(
45
+ application_detected=app_detected,
46
+ project_config_detected=project_config,
47
+ adapter_available=True,
48
+ details=f"App: {'yes' if app_detected else 'no'}, Project config: {'yes' if project_config else 'no'}, Global config: {'yes' if global_config else 'no'}"
49
+ )
50
+
51
+ def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
52
+ """Install python-skills for Claude Code."""
53
+ result = InstallResult(success=True, target=self.target, scope=scope)
54
+ target_path = self._get_scope_path(scope)
55
+
56
+ try:
57
+ # 1. Install native skills to .claude/skills/
58
+ skills_dir = target_path / ".claude" / "skills"
59
+ skills_created = self._install_skills(skills_dir, dry_run)
60
+ result.files_created.extend(skills_created)
61
+
62
+ # 2. Generate path-scoped rules
63
+ rules_created = self._install_rules(target_path, dry_run)
64
+ result.files_created.extend(rules_created)
65
+
66
+ # 3. Generate/update CLAUDE.md bootstrap
67
+ claude_md = self._generate_claude_md(target_path, dry_run)
68
+ if claude_md:
69
+ result.files_created.append(claude_md)
70
+
71
+ # 4. Update lock state
72
+ if not dry_run:
73
+ self._update_lock_state(scope)
74
+
75
+ except Exception as e:
76
+ result.success = False
77
+ result.errors.append(str(e))
78
+
79
+ return result
80
+
81
+ def _install_skills(self, skills_dir: Path, dry_run: bool) -> list[str]:
82
+ """Install canonical skills to .claude/skills/."""
83
+ created = []
84
+ skills_root = self.skills_registry.skills_root
85
+
86
+ if not skills_root.exists():
87
+ return created
88
+
89
+ skills_dir.mkdir(parents=True, exist_ok=True)
90
+
91
+ # Copy each skill directory
92
+ for category_dir in skills_root.iterdir():
93
+ if not category_dir.is_dir():
94
+ continue
95
+ for skill_dir in category_dir.iterdir():
96
+ if not skill_dir.is_dir():
97
+ continue
98
+ skill_file = skill_dir / "SKILL.md"
99
+ if not skill_file.exists():
100
+ continue
101
+
102
+ target_skill_dir = skills_dir / skill_dir.name
103
+ if dry_run:
104
+ created.append(str(target_skill_dir.relative_to(self.project_root)))
105
+ continue
106
+
107
+ # Copy entire skill directory
108
+ if target_skill_dir.exists():
109
+ shutil.rmtree(target_skill_dir)
110
+ shutil.copytree(skill_dir, target_skill_dir)
111
+
112
+ created.append(str(target_skill_dir.relative_to(self.project_root)))
113
+
114
+ # Record in lock state
115
+ self._record_file(self.target, "project", target_skill_dir / "SKILL.md")
116
+
117
+ return created
118
+
119
+ def _install_rules(self, target_path: Path, dry_run: bool) -> list[str]:
120
+ """Install path-scoped rules for Python engineering."""
121
+ created = []
122
+ rules_dir = target_path / ".claude" / "rules"
123
+ rules_dir.mkdir(parents=True, exist_ok=True)
124
+
125
+ # Generate core Python rules
126
+ rules = [
127
+ ("python-typing.md", self._generate_typing_rule()),
128
+ ("python-testing.md", self._generate_testing_rule()),
129
+ ("python-security.md", self._generate_security_rule()),
130
+ ("python-async.md", self._generate_async_rule()),
131
+ ("python-engineering.md", self._generate_engineering_rule()),
132
+ ]
133
+
134
+ for filename, content in rules:
135
+ rule_path = rules_dir / filename
136
+ if dry_run:
137
+ created.append(str(rule_path.relative_to(self.project_root)))
138
+ continue
139
+
140
+ # Add ownership markers
141
+ marked_content = self._wrap_managed(content, rule_path)
142
+ self._safe_write_file(rule_path, marked_content, dry_run)
143
+ created.append(str(rule_path.relative_to(self.project_root)))
144
+
145
+ if not dry_run:
146
+ self._record_file(self.target, "project", rule_path)
147
+
148
+ return created
149
+
150
+ def _generate_claude_md(self, target_path: Path, dry_run: bool) -> str | None:
151
+ """Generate/update CLAUDE.md bootstrap."""
152
+ claude_md = target_path / "CLAUDE.md"
153
+
154
+ if dry_run:
155
+ return str(claude_md.relative_to(self.project_root))
156
+
157
+ # Read existing or create new
158
+ existing = ""
159
+ if claude_md.exists():
160
+ existing = claude_md.read_text(encoding="utf-8")
161
+
162
+ # Generate bootstrap content
163
+ bootstrap = self._generate_claude_md_content()
164
+
165
+ if existing:
166
+ # Replace or append managed section
167
+ new_content, replaced = self._replace_managed_region(existing, bootstrap, claude_md)
168
+ if replaced:
169
+ self._safe_write_file(claude_md, new_content, dry_run)
170
+ self._record_file(self.target, "project", claude_md, (BEGIN_MARKER, END_MARKER))
171
+ return str(claude_md.relative_to(self.project_root))
172
+ # If no managed region, append
173
+ new_content = existing + "\n\n" + self._wrap_managed(bootstrap, claude_md)
174
+ self._safe_write_file(claude_md, new_content, dry_run)
175
+ self._record_file(self.target, "project", claude_md, (BEGIN_MARKER, END_MARKER))
176
+ else:
177
+ # Create new with bootstrap
178
+ full_content = f"# Project Instructions\n\n{bootstrap}"
179
+ self._safe_write_file(claude_md, self._wrap_managed(full_content, claude_md), dry_run)
180
+ self._record_file(self.target, "project", claude_md, (BEGIN_MARKER, END_MARKER))
181
+
182
+ return str(claude_md.relative_to(self.project_root))
183
+
184
+ def _generate_claude_md_content(self) -> str:
185
+ """Generate CLAUDE.md bootstrap content."""
186
+ return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
187
+ # Python Skills Integration
188
+
189
+ This project uses [python-skills](https://github.com/FoxPink-dev/python-skills) for Python engineering standards.
190
+
191
+ ## Available Skills
192
+
193
+ The following skill categories are available in `.claude/skills/`:
194
+
195
+ - **core/** - Core Python language patterns
196
+ - **stdlib/** - Standard library usage
197
+ - **generation/** - Code generation workflows
198
+ - **engineering/** - Project engineering practices
199
+ - **quality/** - Code quality standards
200
+ - **security/** - Security engineering
201
+ - **testing/** - Testing methodologies
202
+ - **refactoring/** - Safe refactoring patterns
203
+ - **debugging/** - Debugging techniques
204
+ - **anti_patterns/** - Anti-pattern prevention
205
+
206
+ ## Usage
207
+
208
+ Skills are loaded automatically when relevant. You can also invoke them directly:
209
+
210
+ ```
211
+ /skill-name
212
+ ```
213
+
214
+ See `.claude/skills/<skill-name>/SKILL.md` for each skill's description and usage.
215
+ <!-- END PYTHON-SKILLS MANAGED -->"""
216
+
217
+ def _generate_typing_rule(self) -> str:
218
+ return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
219
+ ---
220
+ description: "Python typing engineering guidance"
221
+ globs: ["**/*.py"]
222
+ alwaysApply: false
223
+ ---
224
+
225
+ # Python Typing Standards
226
+
227
+ ## Type Hints
228
+ - Use built-in generics (Python 3.9+): `list[str]`, `dict[str, int]`
229
+ - Union types: `int | str` (3.10+) or `Union[int, str]`
230
+ - Optional: `str | None` not `Optional[str]`
231
+
232
+ ## Protocols & Generics
233
+ - Use `Protocol` for structural typing
234
+ - `TypeVar` with constraints for generics
235
+ - `@dataclass` with type hints
236
+
237
+ ## Validation
238
+ - Use Pydantic for runtime validation
239
+ - `TypedDict` for structured dicts
240
+ <!-- END PYTHON-SKILLS MANAGED -->"""
241
+
242
+ def _generate_testing_rule(self) -> str:
243
+ return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
244
+ ---
245
+ description: "Python testing best practices"
246
+ globs: ["**/*test*.py", "**/test_*.py", "**/*_test.py"]
247
+ alwaysApply: false
248
+ ---
249
+
250
+ # Testing Standards
251
+
252
+ ## pytest
253
+ - Use `pytest` with `pytest-asyncio` for async
254
+ - Fixtures in `conftest.py`
255
+ - `pytest.mark.parametrize` for parameterized tests
256
+
257
+ ## Coverage
258
+ - Target 90%+ branch coverage
259
+ - Exclude: `__repr__`, `raise AssertionError`, `if __name__ == "__main__"`
260
+
261
+ ## Async Testing
262
+ - Use `pytest-asyncio` mode="auto"
263
+ - Test async functions with `await`
264
+ <!-- END PYTHON-SKILLS MANAGED -->"""
265
+
266
+ def _generate_security_rule(self) -> str:
267
+ return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
268
+ ---
269
+ description: "Python security engineering"
270
+ globs: ["**/*.py"]
271
+ alwaysApply: false
272
+ ---
273
+
274
+ # Security Standards
275
+
276
+ ## Input Validation
277
+ - Validate all external input at boundaries
278
+ - Use allowlists, not blocklists
279
+ - Pydantic for API validation
280
+
281
+ ## Injection Prevention
282
+ - SQL: parameterized queries only
283
+ - Command: list form, no shell=True
284
+ - Path: resolve + is_relative_to()
285
+ - Deserialization: json/yaml.safe_load only
286
+ <!-- END PYTHON-SKILLS MANAGED -->"""
287
+
288
+ def _generate_async_rule(self) -> str:
289
+ return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
290
+ ---
291
+ description: "Python async/concurrency patterns"
292
+ globs: ["**/*.py"]
293
+ alwaysApply: false
294
+ ---
295
+
296
+ # Async Patterns
297
+
298
+ ## asyncio
299
+ - Use `asyncio.gather()` for concurrency
300
+ - `asyncio.Semaphore` for limiting
301
+ - `asyncio.TaskGroup` (3.11+) for structured concurrency
302
+
303
+ ## Timeouts
304
+ - Always set timeouts: `asyncio.wait_for(coro, timeout=5.0)`
305
+ - Handle `asyncio.TimeoutError`
306
+
307
+ ## Cancellation
308
+ - Check `task.cancelled()` in long operations
309
+ - Use `asyncio.shield()` for critical sections
310
+ <!-- END PYTHON-SKILLS MANAGED -->"""
311
+
312
+ def _generate_engineering_rule(self) -> str:
313
+ return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
314
+ ---
315
+ description: "Python engineering practices"
316
+ globs: ["**/*.py"]
317
+ alwaysApply: false
318
+ ---
319
+
320
+ # Engineering Practices
321
+
322
+ ## Project Structure
323
+ - Use src-layout: `src/package/`
324
+ - `pyproject.toml` with hatchling
325
+ - `CLAUDE.md` for project instructions
326
+
327
+ ## Configuration
328
+ - Layered: defaults → file → env → CLI
329
+ - Pydantic Settings for validation
330
+ - Environment variables for secrets
331
+
332
+ ## HTTP Clients
333
+ - Use `httpx` (sync + async)
334
+ - Connection pooling with limits
335
+ - Retry with tenacity/httpx retries
336
+
337
+ ## Packaging
338
+ - Use `build` (PEP 517)
339
+ - Version from `__version__` in package
340
+ - Publish with twine
341
+ <!-- END PYTHON-SKILLS MANAGED -->"""
342
+
343
+ def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
344
+ result = SyncResult(success=True, target=self.target, scope=scope)
345
+ target_path = self._get_scope_path(scope)
346
+
347
+ try:
348
+ # Reload canonical skills
349
+ skills_root = self.skills_registry.skills_root
350
+
351
+ # Update skills
352
+ skills_dir = target_path / ".claude" / "skills"
353
+ if skills_dir.exists():
354
+ for skill_dir in skills_dir.iterdir():
355
+ if skill_dir.is_dir():
356
+ # Check if skill still exists in canonical
357
+ canonical = skills_root / skill_dir.name
358
+ if canonical.exists():
359
+ # Update if changed
360
+ pass
361
+ else:
362
+ # Skill removed
363
+ if not dry_run:
364
+ shutil.rmtree(skill_dir)
365
+ result.removed.append(str(skill_dir.relative_to(self.project_root)))
366
+
367
+ # Add new skills
368
+ for category_dir in skills_root.iterdir():
369
+ if not category_dir.is_dir():
370
+ continue
371
+ for skill_dir in category_dir.iterdir():
372
+ if not skill_dir.is_dir():
373
+ continue
374
+ target_skill_dir = skills_dir / skill_dir.name
375
+ if not target_skill_dir.exists():
376
+ if not dry_run:
377
+ shutil.copytree(skill_dir, target_skill_dir)
378
+ result.added.append(str(target_skill_dir.relative_to(self.project_root)))
379
+
380
+ # Update lock state
381
+ if not dry_run:
382
+ self._update_lock_state(scope)
383
+
384
+ except Exception as e:
385
+ result.success = False
386
+ result.errors.append(str(e))
387
+
388
+ return result
389
+
390
+ def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
391
+ result = UninstallResult(success=True, target=self.target, scope=scope)
392
+ target_path = self._get_scope_path(scope)
393
+
394
+ try:
395
+ # Remove .claude/skills/
396
+ skills_dir = target_path / ".claude" / "skills"
397
+ if skills_dir.exists():
398
+ if not dry_run:
399
+ shutil.rmtree(skills_dir)
400
+ result.files_removed.append(str(skills_dir.relative_to(self.project_root)))
401
+
402
+ # Remove rules
403
+ rules_dir = target_path / ".claude" / "rules"
404
+ if rules_dir.exists():
405
+ for rule_file in rules_dir.glob("python-*.md"):
406
+ if self._has_ownership_marker(rule_file):
407
+ if not dry_run:
408
+ rule_file.unlink()
409
+ result.files_removed.append(str(rule_file.relative_to(self.project_root)))
410
+
411
+ # Remove CLAUDE.md managed section
412
+ claude_md = target_path / "CLAUDE.md"
413
+ if claude_md.exists():
414
+ content = claude_md.read_text(encoding="utf-8")
415
+ new_content, removed = self._remove_managed_region(content, claude_md)
416
+ if removed and not dry_run:
417
+ self._safe_write_file(claude_md, new_content, dry_run)
418
+ result.regions_removed.append(str(claude_md.relative_to(self.project_root)))
419
+
420
+ if not dry_run:
421
+ self._update_lock_state(scope)
422
+
423
+ except Exception as e:
424
+ result.success = False
425
+ result.errors.append(str(e))
426
+
427
+ return result
428
+
429
+ def _has_ownership_marker(self, path: Path) -> bool:
430
+ """Check if file has python-skills ownership marker."""
431
+ if not path.exists():
432
+ return False
433
+ content = path.read_text(encoding="utf-8")
434
+ begin, end = self._get_markers(path)
435
+ return begin in content and end in content
436
+
437
+ def status(self, scope: str = "project") -> StatusResult:
438
+ target_path = self._get_scope_path(scope)
439
+ installed = False
440
+ files = []
441
+ version = ""
442
+
443
+ # Check skills dir
444
+ skills_dir = target_path / ".claude" / "skills"
445
+ if skills_dir.exists():
446
+ installed = True
447
+ for f in skills_dir.rglob("SKILL.md"):
448
+ files.append(str(f.relative_to(self.project_root)))
449
+
450
+ # Check rules
451
+ rules_dir = target_path / ".claude" / "rules"
452
+ if rules_dir.exists():
453
+ for f in rules_dir.glob("python-*.md"):
454
+ if self._has_ownership_marker(f):
455
+ installed = True
456
+ files.append(str(f.relative_to(self.project_root)))
457
+
458
+ # Check CLAUDE.md
459
+ claude_md = target_path / "CLAUDE.md"
460
+ if claude_md.exists() and self._has_ownership_marker(claude_md):
461
+ installed = True
462
+ files.append(str(claude_md.relative_to(self.project_root)))
463
+
464
+ return StatusResult(
465
+ target=self.target,
466
+ scope=scope,
467
+ installed=installed,
468
+ files=files,
469
+ version=self.config.version if hasattr(self.config, 'version') else "1.0.0"
470
+ )
471
+
472
+ def _update_lock_state(self, scope: str) -> None:
473
+ """Update lock state after changes."""
474
+ self.lock_manager.update_canonical_hash(self.skills_registry.skills_root)