Style rules for Python code under
skills/ppt-master/scripts/and any Python that ships with the skill. Derived from the de facto patterns in the existing codebase.
These rules are pragmatic, not exhaustive. They capture the conventions readers actually encounter — anything PEP 8 hands you for free is assumed.
Every script under scripts/ starts with:
#!/usr/bin/env python3
"""
PPT Master - Short Tool Name
One-paragraph description of what this script does.
Usage:
python3 scripts/<name>.py <required_arg> [options]
Examples:
python3 scripts/<name>.py projects/<project_name> -o output_dir
Dependencies:
None (only uses standard library) <-- or list third-party deps
"""
| Element | Rule |
|---|---|
| Shebang | #!/usr/bin/env python3 (always — even for non-CLI helper modules) |
| Module docstring | Tool name + purpose + Usage + Examples + Dependencies |
| Internal helper modules | May add an early --help short-circuit (see §4) |
# 1. Standard library
import os
import sys
import argparse
import re
from pathlib import Path
from typing import Optional
# 2. Third-party
import requests
# 3. Local — sometimes need sys.path injection first (see §3)
from image_sources.provider_common import (
AssetCandidate,
ImageSearchRequest,
)
| Rule | Note |
|---|---|
| Group order | std → third-party → local, blank line between groups |
| Within a group | Sorted by length when short; alphabetical when ≥ 4 imports |
from x import lists |
One name per line if ≥ 4 names, with trailing comma |
from __future__ import annotations |
Add at top when the file uses X \| Y union syntax (PEP 604) and may run on Python < 3.10 |
scripts/ is not a Python package — it’s a flat directory of scripts. Each entry-point script that imports a sibling module injects scripts/ onto sys.path itself:
import sys
from pathlib import Path
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from image_backends.backend_common import download_image # noqa: E402
| Rule | Why |
|---|---|
| Inject only in entry-points | Library modules under image_sources/ / image_backends/ import each other normally |
Use Path(__file__).resolve().parent |
Robust under symlinks and aliasing |
Annotate post-injection imports with # noqa: E402 |
Suppress the lint warning honestly, not via per-file noqa |
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="One-line description.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("query", help="...")
parser.add_argument("-o", "--output", default=".", help="...")
return parser
def main(argv: Optional[list[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
# ... do the thing ...
return 0
if __name__ == "__main__":
raise SystemExit(main())
| Rule | Note |
|---|---|
main(argv=None) -> int |
Returns exit code; testable by passing argv |
raise SystemExit(main()) |
Preferred over sys.exit(main()) |
formatter_class=argparse.RawDescriptionHelpFormatter |
Preserves docstring formatting in --help |
Internal helpers --help |
Module-level: if __name__ == "__main__" and any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]): print(__doc__); raise SystemExit(0) |
| Output | Progress / status to stderr; the script’s primary output (if any) to stdout |
Help and argument validation requirements:
-h / --help must be handled by argparse (preferred) or an explicit early helper guard before any side effect: no directory creation, file writes, network calls, package installs, or long-running servers.init --help must not create a project named --help; export --help must not write a file named --help.argparse subparsers. Each subcommand must provide its own help (<script> <subcmd> --help) and validate its own required arguments before doing work.Console encoding:
configure_utf8_stdio() (from console_encoding.py, §9) once at startup, before any user-facing print or before importing optional dependencies that may print. It forces stdout/stderr to UTF-8 with errors="replace" so non-UTF-8 Windows locales (e.g. GBK) do not crash on Unicode status output. On systems already using UTF-8 it leaves the effective encoding unchanged.sys.path first (§3), then import the helper. Pure library modules with no __main__ entry do not need it. File I/O still passes encoding="utf-8" explicitly (§13); this rule only covers the console.Required for all new public functions; optional for internal _helpers.
| Pattern | Use |
|---|---|
def f(x: str, *, y: int = 0) -> bool: |
Public functions |
tuple[int, int] \| None |
PEP 604 unions (with from __future__ import annotations if needed for compat) |
Optional[X] from typing |
Acceptable alternative to X \| None |
list[X], dict[K, V] |
Built-in generics (Python 3.9+) |
Any |
Sparingly — only when interfacing with truly heterogeneous data (raw: Any in dataclasses for upstream JSON) |
Forbidden — over-specification:
Callable[[int, str], dict[str, list[Optional[Union[int, str]]]]] — break this into typed dataclassesLiteral["a", "b", "c"] everywhere — use a constant + plain str unless the type itself is the API| Kind | Convention | Examples |
|---|---|---|
| Module file | snake_case.py |
image_search.py, svg_to_pptx.py |
| Script entrypoint | verb or noun phrase | finalize_svg.py, notes_to_audio.py |
| Public function | snake_case |
download_image, parse_results |
| Private helper | _snake_case |
_load_dotenv_if_available, _measure_actual_image |
| Constant | UPPER_SNAKE_CASE |
API_URL, DEFAULT_PAGE_SIZE, LICENSE_TIER_NO_ATTRIBUTION |
| Class | PascalCase |
AssetCandidate, SVGQualityChecker |
| Dataclass field | snake_case |
license_tier, download_url |
| Module-private regex | _PATTERN_RE (private + _RE suffix) |
_TAG_RE, HEADING_RE |
| Situation | Pattern |
|---|---|
| Optional dependency | try: import x; HAS_X = True\nexcept ImportError: HAS_X = False |
| Optional sibling module | try: from project_utils import CANVAS_FORMATS\nexcept ImportError: CANVAS_FORMATS = {}; print("Warning: ...") |
| Recoverable runtime failure | Catch specific exceptions, log to stderr, return / continue — do NOT halt the pipeline |
| User-facing error | print("...", file=sys.stderr); return 1 from main() |
| Programming error | Raise — don’t paper over a bug |
Hard rule: never bare-except:. Always name the exception class.
Forbidden — silent fallbacks for security-relevant code:
| Tier | Where it can be required |
|---|---|
| Standard library | Anywhere |
requests, Pillow, lxml |
Common dependencies; safe to require in main scripts |
Provider SDKs (google-genai, openai, anthropic, etc.) |
Lazy import inside the function that uses it; soft-fail with ImportError → RuntimeError containing install instructions |
python-dotenv |
Optional — wrap the import in try/except, no-op if unavailable |
def _require_api_key() -> str:
key = os.environ.get("PEXELS_API_KEY") or ""
if not key:
raise RuntimeError(
"PEXELS_API_KEY is not set. Add it to your environment or .env file. "
"Get one at https://www.pexels.com/api/"
)
return key
Error messages must include the fix — “what env var to set”, “where to get a key”, “which package to install”.
Common functionality lives in these designated submodules. New scripts use these, not their own copies:
| Module | Owns |
|---|---|
image_backends/backend_common.py |
HTTP download, retry, image format detection, save-with-Pillow-transcode |
image_sources/provider_common.py |
License classification, query simplification, scoring, attribution text, dataclasses |
project_utils.py |
Canvas formats, project path conventions |
slide_roster.py |
Numeric slide-filename ordering and SVG roster discovery |
error_helper.py |
User-facing error message templates |
console_encoding.py |
configure_utf8_stdio() — force UTF-8 console for CLI entries (§4) |
Forbidden — duplicating logic that exists in a shared helper. If a helper is missing a feature, extend the helper, don’t fork it inside your new script.
Short and imperative. No Args/Returns/Raises sections unless the signature is genuinely complex.
def classify_license(
license_name: str,
license_url: str = "",
provider: str = "",
) -> Optional[str]:
"""Classify a license string into one of the two tiers, or reject it.
Returns:
``"no-attribution"`` / ``"attribution-required"`` / ``None``.
The provider hint lets us treat Pexels and Pixabay's own licenses as
``no-attribution`` even when the upstream API only returns a short
label like ``"Pexels"``.
"""
| Use a Google/Sphinx-style block | Skip the block |
|---|---|
| Function returns multiple branches with semantic differences | One-liner that explains itself in the function name |
| Has more than 3 parameters with non-obvious roles | Single-purpose helper |
| Maintains a non-trivial invariant | Pure formatter / accessor |
Default — tests are allowed only under skills/ppt-master/scripts/tests/ (may override only by editing this rule). The repository stays a workflow/skill package: no CI, no pytest, no coverage tooling, no merge gate. Tests exist so that a checker/exporter fix can be re-run in seconds after the next fix lands; they are a regression net, not a substitute for running the real thing.
Allowed:
skills/ppt-master/scripts/tests/test_<topic>.py modules on the standard library unittest, run one module at a time: cd skills/ppt-master/scripts && python3 -m unittest tests.test_<topic> (discover is not supported here)scripts/ Python through its public entry points (CLI main(argv), the converter, the checker) on minimal fixtures built inside the test — a short SVG or JSON string, a temporary directoryrequirements.txtForbidden:
tests/ directories anywhere else, test_*.py next to the code, pytest / third-party test frameworks, if __name__ == "__main__": self-test blocksprompt_audit.py and reviewtemplates/)python3 -c "...", svg_quality_checker.py, svg_to_pptx.py on a /tmp copy) and the output is shown in the conversation / PR descriptionContributors may include tests for the scripts they change; reviewers never require them, and a PR that claims “tests pass” without a real-project smoke run is reviewed as untested (see CONTRIBUTING.md).
Prefer plain @dataclass over pydantic / attrs for value types. Keep them simple:
@dataclass
class AssetCandidate:
provider: str
title: str
asset_id: str = ""
license_tier: str = ""
width: int = 0
height: int = 0
raw: Any = None
| Rule | Note |
|---|---|
@dataclass |
Default; no need for frozen=True unless mutation is a real risk |
| Fields | All required fields first, then optional with defaults |
field(default_factory=...) |
Only when the default needs to be a new container per instance |
| No legacy positional-arg shims | New dataclass = keyword-arg API. YAGNI on positional support |
| Methods | Keep dataclasses dumb; computation goes in module-level functions |
| Property | Value |
|---|---|
| Encoding | UTF-8 |
| Line endings | LF |
| Final newline | Always present |
| BOM | Forbidden |
| Indentation | 4 spaces (no tabs) |
| Max line length | Soft 100; hard 120. Prose in docstrings can flow longer |
When a Python file mirrors a reference doc, cross-link both ways:
See references/image-searcher.md for the on-slide attribution rules.This keeps prompt-style.md and code-style.md (this file) operating as a pair — neither layer drifts away from the other.
Existing files take precedence. If a current script contradicts a rule here, decide whether to (a) update this guide, or (b) refactor the script. The canonical exemplars to model new scripts after:
| If you’re writing… | Model after |
|---|---|
| A small CLI utility | total_md_split.py, gemini_watermark_remover.py |
| A multi-backend / dispatcher CLI | image_search.py, image_gen.py |
| A library / shared helper | image_sources/provider_common.py, image_backends/backend_common.py |
| A class-based checker / validator | svg_quality/checker.py |