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,226 @@
1
+ """Aider adapter for python-skills.
2
+
3
+ Aider uses a "declare-everything" model — no auto-discovery of instruction files.
4
+ Integration is via `read:` key in `.aider.conf.yml` or `--read` CLI flag.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from ..adapters.base import (
10
+ AgentAdapter,
11
+ DetectionResult,
12
+ InstallResult,
13
+ StatusResult,
14
+ SyncResult,
15
+ UninstallResult,
16
+ )
17
+ from ..config import Target
18
+ from ..skills.registry import SkillRegistry
19
+ from ..state import LockManager
20
+
21
+
22
+ class AiderAdapter(AgentAdapter):
23
+ """Adapter for Aider.
24
+
25
+ Aider does not auto-discover instruction files. We generate:
26
+ 1. A PYTHON_SKILLS.md summary file
27
+ 2. Update .aider.conf.yml to read it
28
+ """
29
+
30
+ name = "Aider"
31
+ target = Target.AIDER.value
32
+
33
+ def __init__(self, project_root: Path, skills_registry: SkillRegistry,
34
+ lock_manager: LockManager, config):
35
+ super().__init__(project_root, skills_registry, lock_manager, config)
36
+
37
+ def _get_global_path(self) -> Path:
38
+ return Path.home()
39
+
40
+ def detect(self) -> DetectionResult:
41
+ import shutil
42
+ app_detected = shutil.which("aider") is not None
43
+ project_config = (self.project_root / ".aider.conf.yml").exists()
44
+ global_config = (Path.home() / ".aider.conf.yml").exists()
45
+
46
+ return DetectionResult(
47
+ application_detected=app_detected,
48
+ project_config_detected=project_config,
49
+ adapter_available=True,
50
+ details=f"App: {'yes' if app_detected else 'no'}, "
51
+ f"Project: {'yes' if project_config else 'no'}, "
52
+ f"Global: {'yes' if global_config else 'no'}",
53
+ )
54
+
55
+ def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
56
+ result = InstallResult(success=True, target=self.target, scope=scope)
57
+ target_path = self._get_scope_path(scope)
58
+
59
+ try:
60
+ # Generate PYTHON_SKILLS.md
61
+ skills_md = target_path / "PYTHON_SKILLS.md"
62
+ content = self._generate_skills_summary()
63
+
64
+ if dry_run:
65
+ result.files_created.append(str(skills_md.relative_to(self.project_root)))
66
+ else:
67
+ self._safe_write_file(skills_md, content, dry_run)
68
+ result.files_created.append(str(skills_md.relative_to(self.project_root)))
69
+ self._record_file(self.target, scope, skills_md)
70
+
71
+ # Update .aider.conf.yml
72
+ config_path = target_path / ".aider.conf.yml"
73
+ config_created = self._update_aider_config(config_path, dry_run)
74
+ if config_created:
75
+ result.files_created.append(config_created)
76
+
77
+ if not dry_run:
78
+ self._update_lock_state(scope)
79
+
80
+ except Exception as e:
81
+ result.success = False
82
+ result.errors.append(str(e))
83
+
84
+ return result
85
+
86
+ def _generate_skills_summary(self) -> str:
87
+ categories = {}
88
+ if self.skills_registry:
89
+ for skill in self.skills_registry.get_all_skills():
90
+ if skill.category not in categories:
91
+ categories[skill.category] = []
92
+ categories[skill.category].append(skill.name)
93
+
94
+ parts = [
95
+ "<!-- BEGIN PYTHON-SKILLS MANAGED -->",
96
+ "# Python Skills Integration",
97
+ "",
98
+ "This project uses python-skills for Python engineering standards.",
99
+ "",
100
+ "## Available Skills",
101
+ "",
102
+ ]
103
+
104
+ for category, skills in sorted(categories.items()):
105
+ parts.append(f"### {category.title()}")
106
+ parts.append("")
107
+ for skill in sorted(skills):
108
+ parts.append(f"- `{skill}`")
109
+ parts.append("")
110
+
111
+ parts.extend([
112
+ "## Key Patterns",
113
+ "",
114
+ "- **Type hints**: Use built-in generics, `str | None`, `Protocol`",
115
+ "- **Async**: `asyncio.gather`, `Semaphore`, `TaskGroup` (3.11+)",
116
+ "- **HTTP**: `httpx` with retries, timeouts, connection pooling",
117
+ "- **Security**: Parameterized queries, `pathlib` for paths, `yaml.safe_load`",
118
+ "- **Testing**: `pytest` with parametrized tests, edge cases",
119
+ "",
120
+ "<!-- END PYTHON-SKILLS MANAGED -->",
121
+ ])
122
+
123
+ return "\n".join(parts)
124
+
125
+ def _update_aider_config(self, config_path: Path, dry_run: bool) -> str | None:
126
+ """Update .aider.conf.yml to read PYTHON_SKILLS.md."""
127
+ import yaml
128
+
129
+ existing = {}
130
+ if config_path.exists():
131
+ try:
132
+ existing = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
133
+ except Exception:
134
+ existing = {}
135
+
136
+ read_list = existing.get("read", [])
137
+ if not isinstance(read_list, list):
138
+ read_list = [read_list] if read_list else []
139
+
140
+ if "PYTHON_SKILLS.md" not in read_list:
141
+ read_list.append("PYTHON_SKILLS.md")
142
+ existing["read"] = read_list
143
+
144
+ if not dry_run:
145
+ content = yaml.dump(existing, default_flow_style=False, sort_keys=True)
146
+ self._safe_write_file(config_path, content, dry_run)
147
+
148
+ return str(config_path.relative_to(self.project_root))
149
+
150
+ return None
151
+
152
+ def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
153
+ result = SyncResult(success=True, target=self.target, scope=scope)
154
+ target_path = self._get_scope_path(scope)
155
+
156
+ try:
157
+ skills_md = target_path / "PYTHON_SKILLS.md"
158
+ if not skills_md.exists():
159
+ result.success = False
160
+ result.errors.append("PYTHON_SKILLS.md not found")
161
+ return result
162
+
163
+ current = skills_md.read_text(encoding="utf-8")
164
+ new_content = self._generate_skills_summary()
165
+
166
+ if current.strip() != new_content.strip():
167
+ if not dry_run:
168
+ self._safe_write_file(skills_md, new_content, dry_run)
169
+ result.modified.append(str(skills_md.relative_to(self.project_root)))
170
+ else:
171
+ result.skipped.append(str(skills_md.relative_to(self.project_root)))
172
+
173
+ if not dry_run:
174
+ self._update_lock_state(scope)
175
+
176
+ except Exception as e:
177
+ result.success = False
178
+ result.errors.append(str(e))
179
+
180
+ return result
181
+
182
+ def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
183
+ result = UninstallResult(success=True, target=self.target, scope=scope)
184
+ target_path = self._get_scope_path(scope)
185
+
186
+ try:
187
+ skills_md = target_path / "PYTHON_SKILLS.md"
188
+ if skills_md.exists():
189
+ content = skills_md.read_text(encoding="utf-8")
190
+ begin, end = self._get_markers(skills_md)
191
+ if begin in content:
192
+ if not dry_run:
193
+ skills_md.unlink()
194
+ result.files_removed.append(str(skills_md.relative_to(self.project_root)))
195
+
196
+ if not dry_run:
197
+ self._update_lock_state(scope)
198
+
199
+ except Exception as e:
200
+ result.success = False
201
+ result.errors.append(str(e))
202
+
203
+ return result
204
+
205
+ def status(self, scope: str = "project") -> StatusResult:
206
+ target_path = self._get_scope_path(scope)
207
+ skills_md = target_path / "PYTHON_SKILLS.md"
208
+
209
+ installed = False
210
+ files = []
211
+ if skills_md.exists():
212
+ content = skills_md.read_text(encoding="utf-8")
213
+ begin, end = self._get_markers(skills_md)
214
+ if begin in content:
215
+ installed = True
216
+ files.append(str(skills_md.relative_to(self.project_root)))
217
+
218
+ return StatusResult(
219
+ target=self.target, scope=scope, installed=installed,
220
+ files=files, version="2.0.0",
221
+ )
222
+
223
+ def _update_lock_state(self, scope: str) -> None:
224
+ skills_root = self.skills_registry.skills_root
225
+ if skills_root.exists():
226
+ self.lock_manager.update_canonical_hash(skills_root)
@@ -0,0 +1,153 @@
1
+ """Base adapter contract for python-skills adapters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass
9
+ class DetectionResult:
10
+ """Result of environment detection."""
11
+ application_detected: bool
12
+ project_config_detected: bool
13
+ adapter_available: bool = True
14
+ details: str = ""
15
+
16
+
17
+ @dataclass
18
+ class InstallResult:
19
+ """Result of installation."""
20
+ success: bool
21
+ target: str
22
+ scope: str
23
+ files_created: list[str] = field(default_factory=list)
24
+ files_modified: list[str] = field(default_factory=list)
25
+ errors: list[str] = field(default_factory=list)
26
+ warnings: list[str] = field(default_factory=list)
27
+
28
+
29
+ @dataclass
30
+ class SyncResult:
31
+ """Result of synchronization."""
32
+ success: bool
33
+ target: str
34
+ scope: str
35
+ added: list[str] = field(default_factory=list)
36
+ modified: list[str] = field(default_factory=list)
37
+ removed: list[str] = field(default_factory=list)
38
+ skipped: list[str] = field(default_factory=list)
39
+ errors: list[str] = field(default_factory=list)
40
+ warnings: list[str] = field(default_factory=list)
41
+
42
+
43
+ @dataclass
44
+ class UninstallResult:
45
+ """Result of uninstallation."""
46
+ success: bool
47
+ target: str
48
+ scope: str
49
+ files_removed: list[str] = field(default_factory=list)
50
+ regions_removed: list[str] = field(default_factory=list)
51
+ skipped: list[str] = field(default_factory=list)
52
+ errors: list[str] = field(default_factory=list)
53
+ warnings: list[str] = field(default_factory=list)
54
+
55
+
56
+ @dataclass
57
+ class StatusResult:
58
+ """Result of status check."""
59
+ target: str
60
+ scope: str
61
+ installed: bool
62
+ files: list[str] = field(default_factory=list)
63
+ version: str = ""
64
+ last_sync: str = ""
65
+
66
+
67
+ class AgentAdapter(ABC):
68
+ """Abstract base class for agent adapters."""
69
+
70
+ name: str = ""
71
+ target: str = ""
72
+
73
+ def __init__(self, project_root: Path, skills_registry, lock_manager, config):
74
+ self.project_root = Path(project_root).resolve()
75
+ self.skills_registry = skills_registry
76
+ self.lock_manager = lock_manager
77
+ self.config = config
78
+
79
+ @abstractmethod
80
+ def detect(self) -> DetectionResult:
81
+ """Detect if this agent is available and configured."""
82
+ pass
83
+
84
+ @abstractmethod
85
+ def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
86
+ """Install integration for this agent."""
87
+ pass
88
+
89
+ @abstractmethod
90
+ def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
91
+ """Synchronize installed integration with current skills."""
92
+ pass
93
+
94
+ @abstractmethod
95
+ def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
96
+ """Uninstall integration for this agent."""
97
+ pass
98
+
99
+ @abstractmethod
100
+ def status(self, scope: str = "project") -> StatusResult:
101
+ """Get installation status."""
102
+ pass
103
+
104
+ def _get_scope_path(self, scope: str) -> Path:
105
+ """Get the path for a given scope."""
106
+ if scope == "global":
107
+ return self._get_global_path()
108
+ return self.project_root
109
+
110
+ @abstractmethod
111
+ def _get_global_path(self) -> Path:
112
+ """Get the global config path for this agent."""
113
+ pass
114
+
115
+ def _get_markers(self, file_path: Path) -> tuple[str, str]:
116
+ """Get ownership markers for a file."""
117
+ from ..markers import get_markers
118
+ return get_markers(str(file_path))
119
+
120
+ def _wrap_managed(self, content: str, file_path: Path) -> str:
121
+ """Wrap content with ownership markers."""
122
+ from ..markers import wrap_managed_content
123
+ return wrap_managed_content(content, str(file_path))
124
+
125
+ def _replace_managed_region(self, content: str, new_content: str, file_path: Path) -> tuple[str, bool]:
126
+ """Replace managed region in content."""
127
+ from ..markers import replace_managed_region
128
+ return replace_managed_region(content, new_content, str(file_path))
129
+
130
+ def _remove_managed_region(self, content: str, file_path: Path) -> tuple[str, bool]:
131
+ """Remove managed region from content."""
132
+ from ..markers import remove_managed_region
133
+ return remove_managed_region(content, str(file_path))
134
+
135
+ def _extract_managed_region(self, content: str, file_path: Path) -> str | None:
136
+ """Extract managed region from content."""
137
+ from ..markers import extract_managed_region
138
+ return extract_managed_region(content, str(file_path))
139
+
140
+ def _safe_write_file(self, path: Path, content: str, dry_run: bool = False) -> bool:
141
+ """Safely write a file with ownership markers."""
142
+ if dry_run:
143
+ return True
144
+
145
+ path.parent.mkdir(parents=True, exist_ok=True)
146
+ path.write_text(content, encoding="utf-8")
147
+ return True
148
+
149
+ def _record_file(self, target: str, scope: str, path: Path, region_markers: tuple = None) -> None:
150
+ """Record a managed file in the lock state."""
151
+ self.lock_manager.record_file(target, scope, path, region_markers)
152
+
153
+