--- title: User Project Conventions date: 2025-11-17 source: Extracted from user's production projects projects_analyzed: - pre-commit-pep723-linter-wrapper (PyPI/GitHub) - python_picotool (GitLab) - usb_powertools (GitLab) - picod (GitLab) - i2c_analyzer (GitLab) --- # User Project Conventions Conventions extracted from actual production projects. The model MUST follow these patterns when creating new Python projects. ## Asset Files Available The following template files are available in the skill's `assets/` directory for use in new projects: | File | Purpose | Usage | | ------------------------- | ---------------------------------------------------- | ----------------------------------------------------- | | `version.py` | Dual-mode version management (hatch-vcs + fallback) | Copy to `packages/{package_name}/version.py` | | `hatch_build.py` | Build hook for binary/asset handling | Copy to `scripts/hatch_build.py` | | `.markdownlint.json` | Markdown linting configuration (most rules disabled) | Copy to project root | | `.pre-commit-config.yaml` | Standard pre-commit hooks configuration | Copy to project root, run `uv run pre-commit install` | | `.editorconfig` | Editor formatting settings | Copy to project root | The model MUST copy these files when creating new Python projects to ensure consistency with established conventions documented below. ## 1. Version Management ### Pattern: Dual-mode version.py (STANDARD - 5/5 projects) **Location**: `packages/{package_name}/version.py` **Pattern**: Hatch-VCS with importlib.metadata fallback **Implementation**: ```python """Compute the version number and store it in the `__version__` variable. Based on . """ # /// script # List dependencies for linting only # dependencies = [ # "hatchling>=1.14.0", # ] # /// import os def _get_hatch_version() -> str | None: """Compute the most up-to-date version number in a development environment. Returns `None` if Hatchling is not installed, e.g. in a production environment. For more details, see . """ try: from hatchling.metadata.core import ProjectMetadata from hatchling.plugin.manager import PluginManager from hatchling.utils.fs import locate_file except ImportError: # Hatchling is not installed, so probably we are not in # a development environment. return None pyproject_toml = locate_file(__file__, "pyproject.toml") if pyproject_toml is None: raise RuntimeError("pyproject.toml not found although hatchling is installed") root = os.path.dirname(pyproject_toml) metadata = ProjectMetadata(root=root, plugin_manager=PluginManager()) # Version can be either statically set in pyproject.toml or computed dynamically: return str(metadata.core.version or metadata.hatch.version.cached) def _get_importlib_metadata_version() -> str: """Compute the version number using importlib.metadata. This is the official Pythonic way to get the version number of an installed package. However, it is only updated when a package is installed. Thus, if a package is installed in editable mode, and a different version is checked out, then the version number will not be updated. """ from importlib.metadata import version __version__ = version(__package__ or __name__) return __version__ __version__ = _get_hatch_version() or _get_importlib_metadata_version() ``` **pyproject.toml Configuration** (STANDARD - 5/5 projects): ```toml [project] dynamic = ["version"] [tool.hatch.version] source = "vcs" [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" ``` \***\*init**.py Export Pattern\*\* (STANDARD - 5/5 projects): ```python from .version import __version__ __all__ = ["__version__"] # Plus other exports ``` ## 2. Package Structure ### Pattern: src-layout with packages/ directory (STANDARD - 5/5 projects) **Directory Structure**: ```text project_root/ ├── packages/ │ └── {package_name}/ │ ├── __init__.py # Exports public API + __version__ │ ├── version.py # Version management │ ├── {modules}.py │ └── tests/ # Co-located tests ├── scripts/ │ └── hatch_build.py # Custom build hook (if needed) ├── pyproject.toml └── README.md ``` **pyproject.toml Package Mapping** (STANDARD - 5/5 projects): ```toml [tool.hatch.build.targets.wheel] packages = ["packages/{package_name}"] [tool.hatch.build.targets.wheel.sources] "packages/{package_name}" = "{package_name}" ``` ### Pattern: **init**.py exports with **all** (STANDARD - 5/5 projects) The model must export public API + `__version__` in `__init__.py` with explicit `__all__` list. **Minimal Example** (usb_powertools): ```python """Package docstring.""" from .version import __version__ __all__ = ["__version__"] ``` **Full API Example** (pep723_loader): ```python """Package docstring.""" from .pep723_checker import Pep723Checker from .version import __version__ __all__ = ["Pep723Checker", "__version__"] ``` **Evidence**: All 5 projects use this pattern consistently. ## 3. Build Configuration ### Pattern: Custom hatch_build.py Hook (STANDARD - 3/5 projects with binaries) **Location**: `scripts/hatch_build.py` **Purpose**: Execute binary build scripts (`build-binaries.sh` or `build-binaries.py`) before packaging. **Standard Implementation** (usb_powertools, picod, i2c_analyzer identical): ```python """Custom hatchling build hook for binary compilation. This hook runs before the build process to compile platform-specific binaries if build scripts are present in the project. """ from __future__ import annotations import shutil import subprocess from pathlib import Path from typing import Any from hatchling.builders.config import BuilderConfig from hatchling.builders.hooks.plugin.interface import BuildHookInterface class BinaryBuildHook(BuildHookInterface[BuilderConfig]): """Build hook that runs binary compilation scripts before packaging. This hook checks for the following scripts in order: 1. scripts/build-binaries.sh 2. scripts/build-binaries.py If either script exists, it is executed before the build process. If neither exists, the hook silently continues without error. """ PLUGIN_NAME = "binary-build" def initialize(self, version: str, build_data: dict[str, Any]) -> None: """Run binary build scripts if they exist.""" shell_script = Path(self.root) / "scripts" / "build-binaries.sh" if shell_script.exists() and shell_script.is_file(): self._run_shell_script(shell_script) return python_script = Path(self.root) / "scripts" / "build-binaries.py" if python_script.exists() and python_script.is_file(): self._run_python_script(python_script) return self.app.display_info("No binary build scripts found, skipping binary compilation") def _run_shell_script(self, script_path: Path) -> None: """Execute a shell script for binary building.""" self.app.display_info(f"Running binary build script: {script_path}") if not (bash := shutil.which("bash")): raise RuntimeError("bash not found - cannot execute shell script") try: result = subprocess.run([bash, str(script_path)], cwd=self.root, capture_output=True, text=True, check=True) if result.stdout: self.app.display_info(result.stdout) if result.stderr: self.app.display_warning(result.stderr) except subprocess.CalledProcessError as e: self.app.display_error(f"Binary build script failed with exit code {e.returncode}") if e.stdout: self.app.display_info(f"stdout: {e.stdout}") if e.stderr: self.app.display_error(f"stderr: {e.stderr}") raise def _run_python_script(self, script_path: Path) -> None: """Execute a Python script for binary building. Executes the script directly using its shebang, which honors PEP 723 inline metadata for dependency management via uv. """ self.app.display_info(f"Running binary build script: {script_path}") try: result = subprocess.run([script_path, "--clean"], cwd=self.root, capture_output=True, text=True, check=True) if result.stdout: self.app.display_info(result.stdout) if result.stderr: self.app.display_warning(result.stderr) except subprocess.CalledProcessError as e: self.app.display_error(f"Binary build script failed with exit code {e.returncode}") if e.stdout: self.app.display_info(f"stdout: {e.stdout}") if e.stderr: self.app.display_error(f"stderr: {e.stderr}") raise ``` **pyproject.toml Configuration**: ```toml [tool.hatch.build.targets.sdist.hooks.custom] path = "scripts/hatch_build.py" [tool.hatch.build] artifacts = ["builds/*/binary_name"] # If binaries included ``` ## 4. Pre-commit Configuration ### Standard Hook Set (STANDARD - 5/5 projects) **File**: `.pre-commit-config.yaml` **Core Hooks** (appear in all projects): ```yaml repos: - repo: https://github.com/mxr/sync-pre-commit-deps rev: v0.0.3 hooks: - id: sync-pre-commit-deps - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace exclude: \.lock$ - id: end-of-file-fixer exclude: \.lock$ - id: check-yaml - id: check-json - id: check-toml - id: check-added-large-files args: ["--maxkb=10000"] # 10MB limit - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: mixed-line-ending args: ["--fix=lf"] - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.3+ hooks: - id: ruff name: Lint Python with ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format name: Format Python with ruff - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier name: Format YAML, JSON, and Markdown files types_or: [yaml, json, markdown] exclude: \.lock$ - repo: https://github.com/pecigonzalo/pre-commit-shfmt rev: v2.2.0 hooks: - id: shell-fmt-go args: ["--apply-ignore", -w, -i, "4", -ci] - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.11.0.1 hooks: - id: shellcheck default_language_version: python: python3 exclude: | (?x)^( \.git/| \.venv/| __pycache__/| \.mypy_cache/| \.cache/| \.pytest_cache/| \.lock$| typings/ ) ``` ### Pattern: pep723-loader for Type Checking (STANDARD - 3/5 projects) Projects using `pep723-loader` wrapper for mypy/basedpyright: ```yaml - repo: local hooks: - id: mypy name: mypy entry: uv run -q --no-sync --with pep723-loader --with mypy pep723-loader mypy language: system types: [python] pass_filenames: true - id: pyright name: basedpyright entry: uv run -q --no-sync --with pep723-loader --with basedpyright pep723-loader basedpyright language: system types: [python] pass_filenames: true require_serial: true ``` ### Pattern: Markdown Linting (STANDARD - 4/5 projects) ```yaml - repo: https://github.com/DavidAnson/markdownlint-cli2 rev: v0.18.1 hooks: - id: markdownlint-cli2 language_version: "latest" args: ["--fix"] ``` **Evidence**: pre-commit-pep723-linter-wrapper, usb_powertools, picod all use this pattern. ## 5. Ruff Configuration ### Standard Configuration (STANDARD - 5/5 projects) **pyproject.toml Section**: ```toml [tool.ruff] target-version = "py311" line-length = 120 fix = true preview = true # Optional, 3/5 projects use [tool.ruff.format] docstring-code-format = true quote-style = "double" line-ending = "lf" skip-magic-trailing-comma = true preview = true [tool.ruff.lint] extend-select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "UP", # pyupgrade "YTT", # flake8-2020 "S", # flake8-bandit "B", # flake8-bugbear "A", # flake8-builtins "C4", # flake8-comprehensions "T10", # flake8-debugger "SIM", # flake8-simplify "C90", # mccabe "PGH", # pygrep-hooks "RUF", # ruff-specific "TRY", # tryceratops "DOC", # pydocstyle docstrings (4/5 projects) "D", # pydocstyle (4/5 projects) ] ignore = [ "COM812", # Missing trailing comma "COM819", # Missing trailing comma "D107", # Missing docstring in __init__ "D415", # First line should end with a period "E111", # Indentation is not a multiple of four "E117", # Over-indented for visual indent "E203", # whitespace before ':' "E402", # Module level import not at top of file "E501", # Line length exceeds maximum limit "ISC001", # isort configuration is missing "ISC002", # isort configuration is missing "Q000", # Remove bad quotes "Q001", # Remove bad quotes "Q002", # Remove bad quotes "Q003", # Remove bad quotes "TRY003", # Exception message should not be too long "S404", # module is possibly insecure "S603", # subprocess-without-shell-equals-true "S606", # start-process-with-no-shell "DOC201", # Missing return section in docstring "DOC501", # Missing raises section "DOC502", # Missing raises section "T201", # Allow print statements (4/5 projects) ] unfixable = ["F401", "S404", "S603", "S606", "DOC501"] [tool.ruff.lint.pycodestyle] max-line-length = 120 [tool.ruff.lint.isort] combine-as-imports = true split-on-trailing-comma = false force-single-line = false force-wrap-aliases = false [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.per-file-ignores] "**/tests/*" = ["S101", "S603", "S607", "D102", "D200", "D100"] "**/test_*.py" = ["S101", "S603", "S607", "D102", "D200", "D100"] ``` **Evidence**: All 5 projects use this exact configuration with minor variations. ## 6. Mypy Configuration ### Standard Configuration (STANDARD - 5/5 projects) ```toml [tool.mypy] python_version = "3.11" strict = true strict_equality = true extra_checks = true warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true ignore_missing_imports = true show_error_codes = true pretty = true disable_error_code = ["call-arg"] ``` **Per-module overrides pattern**: ```toml [[tool.mypy.overrides]] module = "tests.*" disable_error_code = ["misc"] ``` ## 7. Basedpyright Configuration ### Standard Configuration (STANDARD - 5/5 projects) ```toml [tool.basedpyright] pythonVersion = "3.11" typeCheckingMode = "standard" reportMissingImports = false reportMissingTypeStubs = false reportUnnecessaryTypeIgnoreComment = "error" reportPrivateImportUsage = false include = ["packages"] extraPaths = ["packages", "scripts", "tests", "."] exclude = ["**/node_modules", "**/__pycache__", ".*", "__*", "**/typings"] ignore = ["**/typings"] venvPath = "." venv = ".venv" ``` **Evidence**: All 5 projects use this configuration. ## 8. Pytest Configuration ### Standard Configuration (STANDARD - 5/5 projects) ```toml [tool.pytest.ini_options] addopts = [ "--cov=packages/{package_name}", "--cov-report=term-missing", "-v", ] testpaths = ["packages/{package_name}/tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] pythonpath = [".", "packages/"] markers = [ "hardware: tests that require USB hardware", "slow: tests that take significant time to run", "integration: integration tests", ] [tool.coverage.run] omit = ["*/tests/*"] [tool.coverage.report] show_missing = true fail_under = 70 ``` **Evidence**: All projects follow this pattern with minor marker variations. ## 9. Formatting Configuration Files ### .markdownlint.json (STANDARD - 5/5 projects) **All projects use identical configuration**: ```json { "MD003": false, "MD007": { "indent": 2 }, "MD001": false, "MD022": false, "MD024": false, "MD013": false, "MD036": false, "MD025": false, "MD031": false, "MD041": false, "MD029": false, "MD033": false, "MD046": false, "blanks-around-fences": false, "blanks-around-headings": false, "blanks-around-lists": false, "code-fence-style": false, "emphasis-style": false, "heading-start-left": false, "heading-style": false, "hr-style": false, "line-length": false, "list-indent": false, "list-marker-space": false, "no-blanks-blockquote": false, "no-hard-tabs": false, "no-missing-space-atx": false, "no-missing-space-closed-atx": false, "no-multiple-blanks": false, "no-multiple-space-atx": false, "no-multiple-space-blockquote": false, "no-multiple-space-closed-atx": false, "no-trailing-spaces": false, "ol-prefix": false, "strong-style": false, "ul-indent": false } ``` **Evidence**: Identical across all 5 projects. ### .editorconfig (COMMON - 2/5 projects have it) **Standard Pattern** (python_picotool, picod): ```ini # EditorConfig: https://editorconfig.org/ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true max_line_length = 120 [*.md] indent_style = space indent_size = 4 trim_trailing_whitespace = false [*.py] indent_style = space indent_size = 4 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.sh] indent_style = space indent_size = 4 [*.toml] indent_style = space indent_size = 2 [*.json] indent_style = space indent_size = 2 [COMMIT_EDITMSG] max_line_length = 72 ``` **Evidence**: ## 10. Semantic Release Configuration ### Standard Configuration (STANDARD - 5/5 projects) ```toml [tool.semantic_release] version_toml = [] major_on_zero = true allow_zero_version = true tag_format = "v{version}" build_command = "uv build" [tool.semantic_release.branches.main] match = "(main|master)" prerelease = false [tool.semantic_release.commit_parser_options] allowed_tags = [ "build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test", ] minor_tags = ["feat"] patch_tags = ["fix", "perf", "refactor"] ``` **Evidence**: All 5 projects use this configuration identically. ## 11. Dependency Groups ### Standard dev Dependencies (STANDARD - 5/5 projects) ```toml [dependency-groups] dev = [ "basedpyright>=1.21.1", "hatch-vcs>=0.5.0", "hatchling>=1.14.0", "mypy>=1.18.2", "pre-commit>=4.3.0", "pytest>=8.4.2", "pytest-asyncio>=1.2.0", "pytest-cov>=6.0.0", "pytest-mock>=3.14.0", "ruff>=0.9.4", "python-semantic-release>=10.4.1", "generate-changelog>=0.16.0", ] ``` **Common Pattern**: All projects include mypy, basedpyright, ruff, pytest, pre-commit, hatchling tools. **Evidence**: All 5 projects have dev dependency groups with these core tools. ## 12. GitLab Project-Specific Patterns ### Pattern: Custom PyPI Index (STANDARD - 4/4 GitLab projects) ```toml [tool.uv] publish-url = "{{gitlab_instance_url}}/api/v4/projects/{{project_id}}/packages/pypi" [[tool.uv.index]] name = "pypi" url = "https://pypi.org/simple" default = true [[tool.uv.index]] name = "gitlab" url = "{{gitlab_instance_url}}/api/v4/groups/{{group_id}}/-/packages/pypi/simple" explicit = true default = false ``` ## 13. Project Metadata Standards ### Pattern: Author and Maintainer (STANDARD - 5/5 projects) ```toml [project] authors = [{ name = "{{author_name_from_git_config_user_name}}", email = "{{author_email_from_git_config_user_email}}" }] maintainers = [{ name = "{{author_name_from_git_config_user_name}}", email = "{{author_email_from_git_config_user_email}}" }] ``` **Observation**: Email addresses differ between GitHub projects (personal email) and GitLab projects (corporate email). ### Pattern: Classifiers (STANDARD - 5/5 projects) **Common classifiers across all projects**: ```toml classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux" or "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] ``` ### Pattern: Keywords (STANDARD - 5/5 projects) All projects include domain-specific keywords related to their purpose. ### Pattern: requires-python (STANDARD - 5/5 projects) **Two variants**: - GitHub: `>=3.10` - GitLab: `>=3.11,<3.13` ## 14. CLI Entry Points ### Pattern: Typer-based CLI (STANDARD - 5/5 projects) ```toml [project.scripts] {package_name} = "{package_name}.cli:main" or "{package_name}.cli:app" [project] dependencies = [ "typer>=0.19.2", ] ``` **Evidence**: All 5 projects use Typer for CLI implementation. ## Summary of Standard Patterns **STANDARD** (5/5 projects): - Dual-mode version.py with hatch-vcs - packages/ directory structure - **all** exports in **init**.py - Ruff formatting with 120 char line length - Mypy strict mode - Basedpyright type checking - Pre-commit hooks (sync-deps, ruff, prettier, shellcheck, shfmt) - .markdownlint.json (identical config) - Semantic release configuration - Typer-based CLI - pytest with coverage **COMMON** (3-4/5 projects): - pep723-loader for type checking in pre-commit - Custom hatch_build.py hook - .editorconfig - GitLab custom PyPI index The model must follow STANDARD patterns for all new Python projects. COMMON patterns should be used when applicable (e.g., hatch_build.py only if binaries needed).