microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
.github/instructions/coding-standards/python-script.instructions.md
263lines · modecode
| 1 | --- |
| 2 | applyTo: '**/*.py' |
| 3 | description: 'Instructions for Python scripting implementation - Brought to you by microsoft/hve-core' |
| 4 | --- |
| 5 | |
| 6 | # Python Script Instructions |
| 7 | |
| 8 | Conventions for Python 3.11+ scripts used in automation, tooling, and CLI applications. |
| 9 | |
| 10 | ## Entry Points and Exit Codes |
| 11 | |
| 12 | ```python |
| 13 | import sys |
| 14 | |
| 15 | EXIT_SUCCESS = 0 # Successful execution |
| 16 | EXIT_FAILURE = 1 # General failure |
| 17 | EXIT_ERROR = 2 # Arguments or configuration error |
| 18 | |
| 19 | |
| 20 | def main() -> int: |
| 21 | """Main entry point for the script.""" |
| 22 | return EXIT_SUCCESS |
| 23 | |
| 24 | |
| 25 | if __name__ == "__main__": |
| 26 | sys.exit(main()) |
| 27 | ``` |
| 28 | |
| 29 | Standard exit codes: 0 success, 1 failure, 2 configuration error, 130 user interrupt (SIGINT). |
| 30 | |
| 31 | ## CLI Argument Parsing |
| 32 | |
| 33 | ### argparse |
| 34 | |
| 35 | Extract parser creation into a separate function for testability. |
| 36 | |
| 37 | ```python |
| 38 | import argparse |
| 39 | from pathlib import Path |
| 40 | |
| 41 | |
| 42 | def create_parser() -> argparse.ArgumentParser: |
| 43 | """Create and configure argument parser.""" |
| 44 | parser = argparse.ArgumentParser(description="Process files") |
| 45 | parser.add_argument("-v", "--verbose", action="store_true") |
| 46 | parser.add_argument("-o", "--output", type=Path, default=Path("output.txt")) |
| 47 | parser.add_argument("input_file", type=Path) |
| 48 | return parser |
| 49 | ``` |
| 50 | |
| 51 | Use `type=Path` for file arguments and `action="store_true"` for boolean flags. |
| 52 | |
| 53 | ### click |
| 54 | |
| 55 | For complex CLIs with subcommands or interactive prompts, use the *click* framework. |
| 56 | |
| 57 | ```python |
| 58 | import click |
| 59 | |
| 60 | |
| 61 | @click.command() |
| 62 | @click.option("-v", "--verbose", is_flag=True) |
| 63 | @click.argument("input_file", type=click.Path(exists=True)) |
| 64 | @click.pass_context |
| 65 | def main(ctx: click.Context, verbose: bool, input_file: str) -> None: |
| 66 | """Process input files.""" |
| 67 | ctx.exit(0) # Explicit exit code |
| 68 | ``` |
| 69 | |
| 70 | Use `@click.group()` for subcommands, `ctx.exit(code)` for exit codes, and `ctx.fail(message)` for errors. |
| 71 | |
| 72 | ## Logging Configuration |
| 73 | |
| 74 | ```python |
| 75 | import logging |
| 76 | |
| 77 | logger = logging.getLogger(__name__) |
| 78 | |
| 79 | |
| 80 | def configure_logging(verbose: bool = False) -> None: |
| 81 | """Configure logging based on verbosity level.""" |
| 82 | level = logging.DEBUG if verbose else logging.INFO |
| 83 | logging.basicConfig(level=level, format="%(levelname)s: %(message)s") |
| 84 | ``` |
| 85 | |
| 86 | Create module-level logger, configure early in main. For file logging, add *FileHandler* to the root logger. |
| 87 | |
| 88 | ## Path Handling |
| 89 | |
| 90 | Use *pathlib.Path* exclusively; avoid *os.path*. |
| 91 | |
| 92 | ```python |
| 93 | from pathlib import Path |
| 94 | |
| 95 | |
| 96 | def process_file(path: Path) -> None: |
| 97 | """Read, process, and write file content.""" |
| 98 | content = path.read_text(encoding="utf-8") |
| 99 | processed = transform_content(content) |
| 100 | output_path = path.with_suffix(".out") |
| 101 | output_path.parent.mkdir(parents=True, exist_ok=True) |
| 102 | output_path.write_text(processed, encoding="utf-8") |
| 103 | ``` |
| 104 | |
| 105 | Common patterns: `cwd()`, `resolve()`, `exists()`, `is_dir()`, `is_file()`, `iterdir()`, `glob()`, `rglob()`, `read_text()`, `write_text()`, `mkdir(parents=True, exist_ok=True)`, `parent`, `name`, `stem`, `suffix`. |
| 106 | |
| 107 | ## Subprocess Execution |
| 108 | |
| 109 | Use *subprocess.run()* with error handling. |
| 110 | |
| 111 | ```python |
| 112 | import subprocess |
| 113 | import os |
| 114 | from pathlib import Path |
| 115 | |
| 116 | |
| 117 | def run_command(cmd: list[str], cwd: Path | None = None, extra_env: dict[str, str] | None = None) -> str: |
| 118 | """Run command and return stdout, raising on failure.""" |
| 119 | env = os.environ.copy() |
| 120 | if extra_env: |
| 121 | env.update(extra_env) |
| 122 | try: |
| 123 | result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=cwd, env=env) |
| 124 | return result.stdout |
| 125 | except subprocess.CalledProcessError as e: |
| 126 | logger.error("Command failed: %s\nstderr: %s", e.returncode, e.stderr) |
| 127 | raise |
| 128 | except FileNotFoundError: |
| 129 | logger.error("Command not found: %s", cmd[0]) |
| 130 | raise |
| 131 | ``` |
| 132 | |
| 133 | Use `capture_output=True` and `text=True` for string output. Use `check=True` to raise on non-zero exit. |
| 134 | |
| 135 | ## Type Hints |
| 136 | |
| 137 | Use Python 3.11+ syntax with built-in generics. |
| 138 | |
| 139 | ```python |
| 140 | from pathlib import Path |
| 141 | from typing import Literal, Self |
| 142 | |
| 143 | |
| 144 | def process_items(items: list[str]) -> dict[str, int]: # Built-in generics |
| 145 | return {item: len(item) for item in items} |
| 146 | |
| 147 | |
| 148 | def read_file(path: str | Path) -> str: # Union with pipe |
| 149 | return Path(path).read_text(encoding="utf-8") |
| 150 | |
| 151 | |
| 152 | def find_config(name: str) -> Path | None: # Optional with pipe |
| 153 | config = Path(name) |
| 154 | return config if config.exists() else None |
| 155 | |
| 156 | |
| 157 | def set_level(level: Literal["debug", "info", "warning"]) -> None: # Constrained values |
| 158 | pass |
| 159 | |
| 160 | |
| 161 | class Builder: |
| 162 | def add(self, item: str) -> Self: # Fluent interface |
| 163 | self.items.append(item) |
| 164 | return self |
| 165 | ``` |
| 166 | |
| 167 | Use `list[str]` not `typing.List[str]`, `str | None` not `Optional[str]`, `Literal` for constrained values, `Self` for chained methods. |
| 168 | |
| 169 | ## Error Handling |
| 170 | |
| 171 | Handle interrupts and pipe errors at the top level. |
| 172 | |
| 173 | ```python |
| 174 | import sys |
| 175 | |
| 176 | |
| 177 | def main() -> int: |
| 178 | """Main entry point with error handling.""" |
| 179 | try: |
| 180 | return run() |
| 181 | except KeyboardInterrupt: |
| 182 | print("\nInterrupted by user", file=sys.stderr) |
| 183 | return 130 |
| 184 | except BrokenPipeError: |
| 185 | sys.stderr.close() |
| 186 | return 1 |
| 187 | except Exception as e: |
| 188 | print(f"Error: {e}", file=sys.stderr) |
| 189 | return 1 |
| 190 | ``` |
| 191 | |
| 192 | Custom exceptions can carry exit codes: |
| 193 | |
| 194 | ```python |
| 195 | class ScriptError(Exception): |
| 196 | def __init__(self, message: str, exit_code: int = 1) -> None: |
| 197 | super().__init__(message) |
| 198 | self.exit_code = exit_code |
| 199 | ``` |
| 200 | |
| 201 | ## Documentation |
| 202 | |
| 203 | Use Google-style docstrings with Args, Returns, Raises, and Example sections. |
| 204 | |
| 205 | ```python |
| 206 | def process_data(data: list[str], *, normalize: bool = False) -> dict[str, int]: |
| 207 | """Process input data and return statistics. |
| 208 | |
| 209 | Args: |
| 210 | data: List of strings to process. |
| 211 | normalize: If True, normalize values before processing. |
| 212 | |
| 213 | Returns: |
| 214 | Dictionary mapping processed items to their counts. |
| 215 | |
| 216 | Raises: |
| 217 | ValueError: If data is empty. |
| 218 | |
| 219 | Example: |
| 220 | >>> process_data(["a", "b", "a"]) |
| 221 | {'a': 2, 'b': 1} |
| 222 | """ |
| 223 | ``` |
| 224 | |
| 225 | Include module docstrings with description, usage, and examples. |
| 226 | |
| 227 | ## Script Organization |
| 228 | |
| 229 | Organize scripts in this order: |
| 230 | |
| 231 | 1. Shebang: `#!/usr/bin/env python3` |
| 232 | 2. Copyright header: `# Copyright (c) Microsoft Corporation.` |
| 233 | 3. SPDX license identifier: `# SPDX-License-Identifier: MIT` |
| 234 | 4. PEP 723 inline script metadata (if applicable) |
| 235 | 5. Future imports: `from __future__ import annotations` |
| 236 | 6. Imports: standard library, third-party, local (separated by blank lines) |
| 237 | 7. Constants and exit codes |
| 238 | 8. Module-level logger |
| 239 | 9. Helper functions |
| 240 | 10. Parser creation function |
| 241 | 11. Logging configuration function |
| 242 | 12. Run logic function |
| 243 | 13. Main entry point |
| 244 | 14. Module guard: `if __name__ == "__main__": sys.exit(main())` |
| 245 | |
| 246 | ## Inline Script Metadata |
| 247 | |
| 248 | PEP 723 inline metadata enables automatic dependency installation with *uv*. |
| 249 | |
| 250 | ```python |
| 251 | #!/usr/bin/env python3 |
| 252 | # Copyright (c) Microsoft Corporation. |
| 253 | # SPDX-License-Identifier: MIT |
| 254 | # /// script |
| 255 | # requires-python = ">=3.11" |
| 256 | # dependencies = [ |
| 257 | # "click>=8.0", |
| 258 | # "rich>=13.0", |
| 259 | # ] |
| 260 | # /// |
| 261 | ``` |
| 262 | |
| 263 | Place after copyright and SPDX headers, before module docstring. Run with `uv run script.py`. |