diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..17f81b9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.py] +indent_size = 4 +max_line_length = 88 + +[*.{yml,yaml,toml,json}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..df55b32 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: "pip" + cache-dependency-path: pyproject.toml + + - name: Create venv and install dev dependencies + run: | + python -m venv .venv + .venv/bin/pip install --upgrade pip + .venv/bin/pip install -e ".[dev]" + + - name: Run all checks (lint + format check + mypy + pytest + coverage) + run: make ci-check diff --git a/.gitignore b/.gitignore index 7877008..955acae 100644 --- a/.gitignore +++ b/.gitignore @@ -94,8 +94,10 @@ dmypy.json *.iml *.iws -# VSCode -.vscode/ +# VSCode (팀 공유 설정만 추적) +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json *.code-workspace # Vim diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84aa6bc..d88326b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,8 @@ default_language_version: python: python3.12 -default_stages: [pre-commit] # commit 단계에서만 실행 -fail_fast: false # 오류 발생 시 즉시 종료 +default_stages: [pre-commit] +fail_fast: false repos: # ========================================== @@ -45,6 +45,16 @@ repos: - id: detect-private-key name: "Private 키 탐지" + # AWS 자격증명 탐지 + - id: detect-aws-credentials + name: "AWS 자격증명 탐지" + args: [--allow-missing-credentials] + + # 대용량 파일 차단 (1MB) + - id: check-added-large-files + name: "대용량 파일 차단" + args: [--maxkb=1024] + # ========================================== # 2. Ruff Linter # ========================================== @@ -64,7 +74,21 @@ repos: name: "Ruff 포매터" # ========================================== - # 4. Commitizen + # 4. 자동 수정 결과를 같은 커밋에 stage + # (반드시 ruff/ruff-format 다음에 위치) + # stdlib만 사용하므로 어떤 python3든 OK. + # ========================================== + - repo: local + hooks: + - id: stage-fixes + name: "자동 수정 파일 stage" + entry: python3 scripts/stage_fixes.py + language: system + stages: [pre-commit] + pass_filenames: true + + # ========================================== + # 5. Commitizen (commit-msg 단계) # ========================================== - repo: https://github.com/commitizen-tools/commitizen rev: v3.29.1 @@ -72,3 +96,28 @@ repos: - id: commitizen name: "Commitizen 메시지 검증" stages: [commit-msg] + + # ========================================== + # 6. Pre-push 게이트 (테스트 + 타입체크) + # 빠른 모드 — 30s 이내로 --no-verify 우회 유혹 차단. + # 80% 커버리지 게이트는 CI에서만 적용. + # 프로젝트 venv 경로를 명시 (Makefile과 일관). + # ========================================== + - repo: local + hooks: + - id: pytest-push + name: "pytest (push 전, 빠른 모드)" + entry: .venv/bin/python -m pytest -x --no-cov -q + language: system + stages: [pre-push] + pass_filenames: false + always_run: true + + - id: mypy-push + name: "mypy (push 전)" + entry: .venv/bin/python -m mypy src + language: system + types: [python] + stages: [pre-push] + pass_filenames: false + always_run: true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..3583984 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "ms-python.python", + "ms-python.mypy-type-checker", + "tamasfe.even-better-toml", + "EditorConfig.EditorConfig" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fcae568 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + }, + "python.analysis.typeCheckingMode": "strict", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": ["tests"], + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true +} diff --git a/Makefile b/Makefile index fca66ae..52689d4 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,8 @@ COMMITIZEN := $(VENV_BIN)/cz .DEFAULT_GOAL := help .PHONY: help install install-dev install-hooks clean lint format \ - check ci-check validate test typecheck commit bump-version pre-commit run-all update-hooks + check ci-check validate test typecheck commit bump-version \ + pre-commit run-all update-hooks quick-check full-check list-checks init # ============================================================================ # help - 사용 가능한 명령어 목록 @@ -65,10 +66,16 @@ help: @echo "$(COLOR_GREEN)통합 명령어:$(COLOR_RESET)" @echo " make check - 코드 스타일 검사 (lint+format)" @echo " make validate - 커밋 전 전체 검증 (lint+format+test+typecheck)" - @echo " make ci-check - CI용 검사 (수정 없이 검증만)" + @echo " make ci-check - CI용 검사 (수정 없이 검증만, 80%+ coverage)" + @echo " make quick-check - commit 시뮬 (pre-commit 단계 훅)" + @echo " make full-check - push 시뮬 (pre-commit + pre-push 단계 훅)" + @echo " make list-checks - 정의된 모든 검사 목록 보기" @echo " make pre-commit - pre-commit 훅 수동 실행" @echo " make run-all - 모든 파일에 pre-commit 실행" @echo "" + @echo "$(COLOR_GREEN)프로젝트 초기화:$(COLOR_RESET)" + @echo " make init NAME=foo - your_project 플레이스홀더를 'foo'로 일괄 치환" + @echo "" @echo "$(COLOR_GREEN)Git 관련:$(COLOR_RESET)" @echo " make commit - 대화형 커밋 생성 (Conventional Commits)" @echo " make bump-version - 버전 자동 증가 및 태그 생성" @@ -105,9 +112,10 @@ install-dev: # ============================================================================ install-hooks: @echo "$(COLOR_BOLD)$(COLOR_BLUE)Git 훅 설치 중...$(COLOR_RESET)" - @$(PRE_COMMIT) install + @$(PRE_COMMIT) install --hook-type pre-commit @$(PRE_COMMIT) install --hook-type commit-msg - @echo "$(COLOR_GREEN)✓ Git 훅 설치 완료$(COLOR_RESET)" + @$(PRE_COMMIT) install --hook-type pre-push + @echo "$(COLOR_GREEN)✓ Git 훅 설치 완료 (pre-commit, commit-msg, pre-push)$(COLOR_RESET)" # ============================================================================ # clean - 임시 파일 및 캐시 삭제 @@ -161,22 +169,22 @@ validate: lint format test typecheck @echo "" # ============================================================================ -# ci-check - CI용 검사 (수정 없이 검증만) +# ci-check - CI용 검사 (수정 없이 검증만, coverage 80% 게이트) # ============================================================================ ci-check: @echo "$(COLOR_BOLD)$(COLOR_BLUE)CI 검사 중...$(COLOR_RESET)" @$(RUFF) check $(ALL_DIRS) --no-fix @$(RUFF) format $(ALL_DIRS) --check @$(VENV_BIN)/mypy $(SRC_DIR) - @$(VENV_BIN)/pytest --no-header -q + @$(VENV_BIN)/pytest --cov=$(SRC_DIR) --cov-report=term-missing --no-header -q @echo "$(COLOR_GREEN)✓ CI 검사 통과$(COLOR_RESET)" # ============================================================================ -# test - 테스트 실행 +# test - 테스트 실행 (coverage 포함, 80% 게이트) # ============================================================================ test: @echo "$(COLOR_BOLD)$(COLOR_BLUE)테스트 실행 중...$(COLOR_RESET)" - @$(VENV_BIN)/pytest + @$(VENV_BIN)/pytest --cov=$(SRC_DIR) --cov-report=term-missing @echo "$(COLOR_GREEN)✓ 테스트 완료$(COLOR_RESET)" # ============================================================================ @@ -235,6 +243,44 @@ update-hooks: @$(PRE_COMMIT) autoupdate @echo "$(COLOR_GREEN)✓ 훅 업데이트 완료$(COLOR_RESET)" +# ============================================================================ +# quick-check - commit 시뮬레이션 (pre-commit 단계 훅 전체) +# ============================================================================ +quick-check: + @echo "$(COLOR_BOLD)$(COLOR_BLUE)Quick check (commit 시뮬)...$(COLOR_RESET)" + @$(PRE_COMMIT) run --all-files --hook-stage pre-commit + @echo "$(COLOR_GREEN)✓ commit 단계 통과$(COLOR_RESET)" + +# ============================================================================ +# full-check - push 시뮬레이션 (pre-commit + pre-push 모두) +# ============================================================================ +full-check: + @echo "$(COLOR_BOLD)$(COLOR_BLUE)Full check (push 시뮬)...$(COLOR_RESET)" + @$(PRE_COMMIT) run --all-files --hook-stage pre-commit + @$(PRE_COMMIT) run --all-files --hook-stage pre-push + @echo "$(COLOR_GREEN)✓ commit + push 단계 모두 통과$(COLOR_RESET)" + +# ============================================================================ +# list-checks - 정의된 모든 검사를 한눈에 보기 +# ============================================================================ +list-checks: + @echo "$(COLOR_BOLD)$(COLOR_CYAN)정의된 pre-commit 훅 목록:$(COLOR_RESET)" + @$(PRE_COMMIT) run --all-files --verbose --hook-stage manual 2>/dev/null || true + @echo "" + @echo "$(COLOR_GREEN)단계별 훅 위치는 .pre-commit-config.yaml의 stages 필드 참고$(COLOR_RESET)" + +# ============================================================================ +# init - your_project 플레이스홀더를 새 이름으로 일괄 치환 +# 사용법: make init NAME=my_app +# ============================================================================ +init: +ifndef NAME + $(error NAME 변수가 필요합니다. 예: make init NAME=my_app) +endif + @echo "$(COLOR_BOLD)$(COLOR_BLUE)프로젝트 초기화: $(NAME)$(COLOR_RESET)" + @$(PYTHON) scripts/init_project.py $(NAME) + @echo "$(COLOR_GREEN)✓ 초기화 완료. 'make install-dev' 후 'make ci-check'로 검증하세요.$(COLOR_RESET)" + # ============================================================================ # 개발 워크플로우 예시 # ============================================================================ diff --git a/README.md b/README.md index 3ae581f..bcfa932 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,44 @@ # Python 3.12 Project Template -Automated code quality system for Python 3.12 projects. +Automated commit/push quality gate template for Python 3.12 projects. The +checking system itself is the deliverable — pre-commit and pre-push hooks +plus a CI workflow ensure broken or unformatted code never lands in main. ## Features -- **Ruff** — Linting + formatting in one tool +- **Ruff** — Linting + formatting in one tool, with auto-fix and **auto-stage** - **mypy** — Strict type checking -- **pytest** — Testing with coverage -- **pre-commit** — Automated checks on every commit +- **pytest + coverage** — Tests with 80%+ coverage gate (CI) +- **pre-commit + pre-push** — Two-stage hook pipeline - **Commitizen** — Conventional Commits enforcement +- **GitHub Actions CI** — Remote safety net for `--no-verify` bypass -### Git Hook Pipeline +### Two-Stage Hook Pipeline -```bash +``` git commit -m "feat: add user auth" -# Runs automatically: -# 1. File checks (EOF, whitespace, YAML/TOML/JSON, private keys) -# 2. Ruff linting (with auto-fix) -# 3. Ruff formatting -# 4. Commitizen message validation + ├─ pre-commit stage (fast, file-level) + │ ├─ End-of-file / trailing whitespace + │ ├─ YAML / TOML / JSON syntax check + │ ├─ Secret detection (private key, AWS credentials) + │ ├─ Large file block (>1MB) + │ ├─ Ruff lint (--fix) + │ ├─ Ruff format + │ └─ stage-fixes (auto git add the fixed files) + └─ commit-msg stage + └─ Commitizen message validation + +git push + └─ pre-push stage (project-wide) + ├─ pytest -x --no-cov -q (fast fail mode) + └─ mypy src + +GitHub Actions CI (push to main, PR) + └─ make ci-check + ├─ ruff check --no-fix + ├─ ruff format --check + ├─ mypy src + └─ pytest --cov (80% gate) ``` ## Prerequisites @@ -29,67 +49,142 @@ git commit -m "feat: add user auth" ## Getting Started ```bash -# Clone and setup +# 1. Clone git clone https://github.com/bigmooon/python-template.git cd python-template -# Install dev environment (venv + deps + git hooks) +# 2. Initialize with your project name +make init NAME=my_app + +# 3. Install dev environment (venv + deps + git hooks for commit/push/commit-msg) make install-dev ``` -## Usage +## Why might my first commit fail? -### Development Workflow +When Ruff auto-fixes code (e.g. import sort, quote style), pre-commit returns +a non-zero exit code so you can review the changes. The `stage-fixes` hook +**re-stages only the files originally staged** — protecting any partial +`git add -p` hunks you intentionally left unstaged. ```bash -# 1. After writing code -make check # lint + format (auto-fix) +git add src/foo.py +git commit -m "feat: add foo" +# ⚠️ Ruff fixed src/foo.py — first commit fails +git commit -m "feat: add foo" +# ✓ Second attempt passes (auto-fix already staged) +``` + +This is intentional — it gives you a chance to inspect the diff Ruff applied. + +## Usage -# 2. Before committing — full validation -make validate # lint + format + test + typecheck +### Daily Workflow -# 3. Commit -git add . -git commit -m "feat(auth): add login endpoint" +```bash +# Edit code → commit → push +git add src/foo.py +git commit -m "feat(foo): add bar" +git push +# pre-push runs pytest + mypy automatically +``` -# Or use interactive commit -make commit +### Manual checks before committing + +```bash +make quick-check # commit-stage hooks (lint + format + file checks) +make full-check # commit + push stages (adds pytest + mypy) +make ci-check # CI mode (no auto-fix, with coverage gate) +make list-checks # show all configured checks at a glance ``` -### All Commands +### Commands ```bash -make help # Show all commands +make help # all commands # Setup -make install # Production install -make install-dev # Dev environment (recommended) -make install-hooks # Git hooks only +make install-dev # venv + deps + git hooks (recommended) +make install-hooks # git hooks only (pre-commit + commit-msg + pre-push) +make init NAME=foo # rename placeholders to 'foo' -# Code Quality -make lint # Lint with auto-fix -make format # Format code -make test # Run tests (pytest + coverage) -make typecheck # Type check (mypy) +# Code quality +make lint # ruff check --fix +make format # ruff format +make typecheck # mypy src +make test # pytest with coverage # Combined make check # lint + format -make validate # lint + format + test + typecheck -make ci-check # CI mode (verify only, no auto-fix) +make validate # lint + format + typecheck + test +make ci-check # CI verification (--no-fix, coverage gate) +make quick-check # pre-commit stage only +make full-check # pre-commit + pre-push stages # Git -make commit # Interactive commit (Conventional Commits) -make bump-version # Auto bump version + tag +make commit # interactive commit (Conventional Commits) +make bump-version # auto bump version + tag # Maintenance -make clean # Clear caches -make update-hooks # Update pre-commit hooks +make clean # clear caches +make update-hooks # update pre-commit hook revisions ``` -## Customization +## Conventional Commits + +Allowed types: `feat`, `fix`, `docs`, `refactor`, `chore`, `revert`, `perf`. + +```bash +feat(auth): add login endpoint +fix(parser): handle empty input +``` + +**MAJOR version bumps**: use the inline `!` syntax — `feat!: rewrite API`. +The `BREAKING CHANGE:` footer is **not validated** by the commit-msg hook +(commitizen schema limitation), so the inline `!` is the reliable signal. + +## Troubleshooting + +### A pre-push check is too slow + +The `pre-push` hooks run `pytest -x --no-cov -q` and `mypy src`. If they +exceed ~30s, contributors may bypass with `--no-verify`. Mitigation: +- Mark slow tests with `@pytest.mark.slow` and skip them in pre-push +- The 80% coverage gate runs only in CI, so coverage isn't recomputed on push + +### `git push --no-verify` was used + +Local hooks can be bypassed. The GitHub Actions workflow +(`.github/workflows/ci.yml`) re-runs `make ci-check` on every push to `main` +and every PR — this is the authoritative gate. + +### `make init` didn't rename a reference + +`scripts/init_project.py` substitutes `your-project` (kebab) and `your_project` +(snake) in `pyproject.toml` and `src//__init__.py`. If you added new +files referencing the old name, run `grep -r your_project .` to find them. + +### Ruff and pre-commit ruff versions disagree + +`pyproject.toml` pins `ruff==0.8.4` exactly to match `.pre-commit-config.yaml`'s +`rev: v0.8.4`. When upgrading Ruff, **bump both in the same commit** to avoid +"local passes, CI fails" drift. + +## Architecture + +The check definitions live in `.pre-commit-config.yaml` (single source of +truth). The Makefile is a thin wrapper around `pre-commit run` plus a few +direct tool invocations for `make ci-check`. The CI workflow calls +`make ci-check` so local and CI behavior stay aligned. + +``` +.pre-commit-config.yaml ← SSOT for all check definitions + ↑ + Makefile (wrapper) + ↑ + .github/workflows/ci.yml (calls make ci-check) +``` -When using this template for a new project, update the following: +## License -1. `pyproject.toml` — `name`, `description`, `authors`, `keywords` -2. `pyproject.toml` — `known-first-party` in `[tool.ruff.lint.isort]` -3. Rename `src/your_project/` to match your package name +MIT diff --git a/pyproject.toml b/pyproject.toml index 79e218d..dc6493e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [] [project.optional-dependencies] dev = [ - "ruff>=0.8.0", + "ruff==0.8.4", # .pre-commit-config.yaml의 ruff rev와 정확히 동일하게 핀 "pre-commit>=3.8.0", "commitizen>=3.29.0", "pytest>=8.0", @@ -44,14 +44,18 @@ select = [ "RUF", # Ruff 자체 규칙 "S", # flake8-bandit (보안) "T20", # flake8-print (print 감지) + "PERF", # perflint — 루프 성능 안티패턴 + "TRY", # tryceratops — 예외 처리 안티패턴 ] -ignore = ["E501"] +# E501(line length)은 Ruff formatter가 직접 관리하므로 ignore 불필요 fixable = ["ALL"] unfixable = [] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] -"tests/**/*.py" = ["S101"] +"tests/**/*.py" = ["S101", "T20"] +# scripts/는 CLI 도구이므로 print/subprocess 사용 허용 +"scripts/**/*.py" = ["T20", "S603", "S607"] [tool.ruff.lint.isort] known-first-party = ["your_project"] # TODO: 프로젝트 패키지명으로 변경 @@ -66,7 +70,22 @@ skip-magic-trailing-comma = false [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["src"] -addopts = "--cov=src --cov-report=term-missing -v" +addopts = "--strict-markers --strict-config -v" +# coverage는 [tool.coverage.*]에서 별도 관리. +# Makefile의 test/ci-check 타겟이 명시적으로 --cov를 부착한다. + +[tool.coverage.run] +source = ["src"] +branch = true + +[tool.coverage.report] +fail_under = 80 +show_missing = true +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", +] [tool.mypy] python_version = "3.12" diff --git a/scripts/init_project.py b/scripts/init_project.py new file mode 100644 index 0000000..31e095c --- /dev/null +++ b/scripts/init_project.py @@ -0,0 +1,89 @@ +"""your-project / your_project 플레이스홀더를 새 프로젝트명으로 일괄 치환. + +크로스플랫폼(macOS/Linux/Windows) 안전. ``sed -i`` 변종 차이를 피하려고 +Python stdlib만 사용한다. + +치환 규칙: +- ``your-project`` → kebab-case 이름 (PEP 503 정규화) +- ``your_project`` → snake_case 이름 (Python 패키지명) +- ``src/your_project/`` 디렉토리 → ``src//``로 이동 + +사용: + python scripts/init_project.py my_app + # 또는 Makefile 경유: + make init NAME=my_app +""" + +from __future__ import annotations + +from pathlib import Path +import re +import shutil +import sys + +ROOT = Path(__file__).resolve().parent.parent + + +def to_snake(name: str) -> str: + """패키지명용 snake_case로 정규화.""" + cleaned = re.sub(r"[-\s]+", "_", name).lower() + if not re.fullmatch(r"[a-z_][a-z0-9_]*", cleaned): + msg = ( + f"'{name}'은 유효한 Python 패키지명이 아닙니다 " + "(영문 소문자/숫자/언더스코어만 허용, 숫자로 시작 금지)" + ) + raise ValueError(msg) + return cleaned + + +def to_kebab(name: str) -> str: + """배포용 kebab-case로 정규화.""" + return re.sub(r"[_\s]+", "-", name).lower() + + +def replace_in_file(path: Path, snake: str, kebab: str) -> None: + text = path.read_text(encoding="utf-8") + new = text.replace("your-project", kebab).replace("your_project", snake) + if new != text: + path.write_text(new, encoding="utf-8") + + +def main(name: str) -> int: + snake = to_snake(name) + kebab = to_kebab(name) + + src_old = ROOT / "src" / "your_project" + src_new = ROOT / "src" / snake + if src_old.exists() and src_old != src_new: + if src_new.exists(): + print(f"오류: {src_new}가 이미 존재합니다.", file=sys.stderr) + return 1 + shutil.move(str(src_old), str(src_new)) + + targets = [ + ROOT / "pyproject.toml", + src_new / "__init__.py", + ] + for f in targets: + if f.exists(): + replace_in_file(f, snake, kebab) + + print( + f"✓ '{name}' (snake={snake}, kebab={kebab})로 초기화 완료.\n" + "다음 단계: make install-dev && make ci-check" + ) + return 0 + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python scripts/init_project.py ", + file=sys.stderr, + ) + sys.exit(2) + try: + sys.exit(main(sys.argv[1])) + except ValueError as exc: + print(f"오류: {exc}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/stage_fixes.py b/scripts/stage_fixes.py new file mode 100644 index 0000000..1209cd4 --- /dev/null +++ b/scripts/stage_fixes.py @@ -0,0 +1,33 @@ +"""pre-commit이 넘긴 원본 staged 파일 목록만 다시 git add. + +pre-commit 프레임워크는 훅에 "원래 staged였던 파일 목록"을 인자로 넘긴다 +(즉 ``git diff --cached --name-only --diff-filter=ACMR``). Ruff/Ruff-format +같은 자동수정 훅이 그 파일들을 수정한 뒤, 이 스크립트가 동일 목록만 +정확히 다시 ``git add``하여 자동수정 결과를 같은 커밋에 포함시킨다. + +``git add -A``/``git add -u``를 의도적으로 회피하여 +``git add -p`` 부분 stage 워크플로우의 unstaged hunk가 함께 끌려가는 +사고를 방지한다. +""" + +from __future__ import annotations + +from pathlib import Path +import subprocess +import sys + + +def main(files: list[str]) -> int: + if not files: + return 0 + + existing = [f for f in files if Path(f).exists()] + if not existing: + return 0 + + subprocess.run(["git", "add", "--", *existing], check=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/src/your_project/__init__.py b/src/your_project/__init__.py index 61e5102..ce6535b 100644 --- a/src/your_project/__init__.py +++ b/src/your_project/__init__.py @@ -1,3 +1,9 @@ """your-project: Python 3.12 프로젝트 템플릿.""" -__version__ = "0.1.0" +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("your-project") +except PackageNotFoundError: # pragma: no cover + # 패키지가 설치되지 않은 환경(예: 소스만 체크아웃) fallback + __version__ = "0.0.0" diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..28b65c6 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,19 @@ +"""Smoke test — anchors the test suite so pytest never returns exit code 5. + +Replace or expand this as your project grows. The template ships with one +test on purpose: pytest treats "0 tests collected" as a failure (exit 5), +which would block the pre-push gate on every fresh clone. +""" + +from __future__ import annotations + +import your_project + + +def test_package_imports() -> None: + assert your_project is not None + + +def test_version_is_a_string() -> None: + assert isinstance(your_project.__version__, str) + assert your_project.__version__