python-package-copier-template 0.4.3__tar.gz

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.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.3
2
+ Name: python-package-copier-template
3
+ Version: 0.4.3
4
+ Summary: A Copier template for modern Python projects.
5
+ Author: Martín Gaitán
6
+ Author-email: Martín Gaitán <gaitan@gmail.com>
7
+ Requires-Dist: copier>=9.11.0
8
+ Requires-Dist: copier-templates-extensions>=0.3.2
9
+ Requires-Dist: ruff>=0.14.9
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Modern Python package template
14
+
15
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-purple.json)](https://github.com/copier-org/copier)
16
+ [![CI](https://github.com/mgaitan/python-package-copier-template/actions/workflows/ci.yml/badge.svg)](https://github.com/mgaitan/python-package-copier-template/actions/workflows/ci.yml)
17
+ [![Changelog](https://img.shields.io/github/v/release/mgaitan/python-package-copier-template)](https://github.com/mgaitan/python-package-copier-template/releases)
18
+
19
+ A [Copier](https://github.com/copier-org/copier) template
20
+ for modern Python projects.
21
+
22
+ Demo repo generated from this template: [mgaitan/yet-another-demo](https://github.com/mgaitan/yet-another-demo)
23
+
24
+ ## Features
25
+
26
+ - 🐍 Modern Python package (3.12+)
27
+ - 📦 Build and dependency management with [uv](https://docs.astral.sh/uv/), split by groups (dev/qa/docs)
28
+ - 🧊 Dependency cooldowns enabled by default in `uv` (`[tool.uv].exclude-newer = "1 week"`), with targeted overrides when needed (for example `ty`) to reduce supply-chain risk without blocking QA tools
29
+ - 🧹 Linting and formatting via [Ruff](https://docs.astral.sh/ruff/) with a broad set of rules enabled
30
+ - ✅ Type checking via [ty](https://github.com/astral-sh/ty)
31
+ - 🪝 Optional pre-commit style QA orchestration via [prek](https://github.com/j178/prek) as an external tool (`uv tool install prek`; hooks include `check-ast`, `check-yaml`, `check-toml`, Ruff, Ty)
32
+ - 🧪 Tests with [pytest](https://docs.pytest.org/en/stable/), [coverage.py](https://coverage.readthedocs.io/en/latest/) and extensions
33
+ - 📚 Docs with [Sphinx](https://www.sphinx-doc.org/en/master/), [MyST](https://myst-parser.readthedocs.io/en/stable/) and a few extensions, deployed to [GitHub Pages](https://pages.github.com/)
34
+ - 👀 Automatic docs preview deployment for PRs that modify documentation (`/_preview/pr-<PR_NUMBER>/` on GitHub Pages)
35
+ - 🧭 Generated docs follow [Diataxis](https://diataxis.fr/) and document rationale in [`about_the_docs`](project/docs/about_the_docs.md.jinja)
36
+ - 🏗️ Use of [GitHub CLI](https://cli.github.com/) for autotic project creation
37
+ - ⚙️ CI workflow on [GitHub Actions](https://github.com/features/actions)
38
+ - 🚀 Automated releases to PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
39
+ - 🧠 Sensible defaults via introspection to minimize answers during the initial setup
40
+ - 🛠️ Makefile with shortcuts for common tasks
41
+ - 📄 Generation of generic docs such as `LICENSE`, `CODE_OF_CONDUCT`, etc.
42
+ - 🤖 Heavily curated [AGENTS.md](https://agents.md/)
43
+ - 🌀 Initial setup of the development environment and git repo
44
+ - 🔁 Scheduled template refresh workflow that opens a PR every 20 days when updates are available
45
+ - ♻️ Projects updatable with [`copier update`](https://copier.readthedocs.io/en/stable/updating/)
46
+
47
+ Please read [my blog post](https://mgaitan.github.io/en/posts/opinionated-python-project-scaffolding/) to learn about the details of the decisions I made and the alternatives I considered.
48
+
49
+ ## Package Documentation
50
+
51
+ The package now includes a Diataxis-based docs set under `docs/` that expands this README:
52
+
53
+ - [Tutorial: Getting started](docs/getting_started.md)
54
+ - [How-to: Maintain the template](docs/maintain_template.md)
55
+ - [Reference: Configuration](docs/configuration.md)
56
+ - [Explanation: About these docs](docs/about_the_docs.md)
57
+ - [Explanation: Design decisions](docs/design_decisions.md)
58
+
59
+ ## Quick setup and usage
60
+
61
+ Quick shortcut (auto-detects copy vs update):
62
+
63
+ ```bash
64
+ uvx python-package-copier-template [PATH_TO_PROJECT]
65
+ ```
66
+
67
+ This runs `copier copy --defaults --unsafe` to `PATH_TO_PROJECT` (or `.` if omitted) when no `.copier-answers.yml` is present, or `copier update --defaults --unsafe` when it is.
68
+ Check the installed version with `uvx python-package-copier-template --version`.
69
+ When installed from PyPI, the wrapper copies from the matching template release instead of always using `main`.
70
+
71
+ If you want the latest development version straight from this repository instead of the latest published release, use:
72
+
73
+ ```bash
74
+ uvx git+https://github.com/mgaitan/python-package-copier-template [PATH_TO_PROJECT]
75
+ ```
76
+
77
+ You can check that wrapper version with `uvx git+https://github.com/mgaitan/python-package-copier-template -- --version`.
78
+
79
+ Start a new project explicitly with Copier if you want to bypass the wrapper:
80
+
81
+ ```bash
82
+ uvx --with=copier-template-extensions copier copy --trust "gh:mgaitan/python-package-copier-template" /path/to/your/new/project
83
+ ```
84
+
85
+ To upgrade an existing project created from this template to the latest version, run:
86
+
87
+ ```bash
88
+ uvx --with=copier-template-extensions copier update . --trust
89
+ ```
90
+
91
+ This will fetch the latest template version and guide you through updating your project, preserving your customizations whenever possible.
92
+
93
+ The generated project also ships a `Template Update` GitHub Actions workflow that runs every 20 days (or on manual dispatch) to execute `uvx copier update --trust --defaults .` and open a pull request with the changes and template version bump.
94
+
95
+ To test a local development version of the template, clone the repository and run:
96
+
97
+ ```bash
98
+ uv sync
99
+ uv run copier copy --trust --vcs-ref=HEAD . /path/to/your/test/project
100
+ ```
101
+
102
+ If you create the GitHub repository via the `gh` CLI prompt, the template will attempt to enable GitHub Pages (using the Actions build type) so documentation deployments succeed. If Pages is unavailable (for example, with some private repositories or account policies), the docs workflow will keep failing until Pages is allowed.
103
+
104
+
105
+ To publish a release of your project to PyPI, you need to [register the project with trusted publishing](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/). Read more about how this workflow works [here](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/).
106
+
107
+ This repository's own release workflow also refreshes the demo repository after a successful publish. That step requires the `DEMO_REPO_TOKEN` GitHub Actions secret, documented in [Configuration](docs/configuration.md).
108
+
109
+ Then
110
+
111
+ ```
112
+ $ make bump # optional
113
+ $ make release
114
+ ```
115
+
116
+
117
+ ### Acknowledgement
118
+
119
+ This project template started as a fork of [pawamoy/copier-uv](https://github.com/pawamoy/copier-uv). Then I simplified and changed it to fit my needs.
@@ -0,0 +1,107 @@
1
+ # Modern Python package template
2
+
3
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-purple.json)](https://github.com/copier-org/copier)
4
+ [![CI](https://github.com/mgaitan/python-package-copier-template/actions/workflows/ci.yml/badge.svg)](https://github.com/mgaitan/python-package-copier-template/actions/workflows/ci.yml)
5
+ [![Changelog](https://img.shields.io/github/v/release/mgaitan/python-package-copier-template)](https://github.com/mgaitan/python-package-copier-template/releases)
6
+
7
+ A [Copier](https://github.com/copier-org/copier) template
8
+ for modern Python projects.
9
+
10
+ Demo repo generated from this template: [mgaitan/yet-another-demo](https://github.com/mgaitan/yet-another-demo)
11
+
12
+ ## Features
13
+
14
+ - 🐍 Modern Python package (3.12+)
15
+ - 📦 Build and dependency management with [uv](https://docs.astral.sh/uv/), split by groups (dev/qa/docs)
16
+ - 🧊 Dependency cooldowns enabled by default in `uv` (`[tool.uv].exclude-newer = "1 week"`), with targeted overrides when needed (for example `ty`) to reduce supply-chain risk without blocking QA tools
17
+ - 🧹 Linting and formatting via [Ruff](https://docs.astral.sh/ruff/) with a broad set of rules enabled
18
+ - ✅ Type checking via [ty](https://github.com/astral-sh/ty)
19
+ - 🪝 Optional pre-commit style QA orchestration via [prek](https://github.com/j178/prek) as an external tool (`uv tool install prek`; hooks include `check-ast`, `check-yaml`, `check-toml`, Ruff, Ty)
20
+ - 🧪 Tests with [pytest](https://docs.pytest.org/en/stable/), [coverage.py](https://coverage.readthedocs.io/en/latest/) and extensions
21
+ - 📚 Docs with [Sphinx](https://www.sphinx-doc.org/en/master/), [MyST](https://myst-parser.readthedocs.io/en/stable/) and a few extensions, deployed to [GitHub Pages](https://pages.github.com/)
22
+ - 👀 Automatic docs preview deployment for PRs that modify documentation (`/_preview/pr-<PR_NUMBER>/` on GitHub Pages)
23
+ - 🧭 Generated docs follow [Diataxis](https://diataxis.fr/) and document rationale in [`about_the_docs`](project/docs/about_the_docs.md.jinja)
24
+ - 🏗️ Use of [GitHub CLI](https://cli.github.com/) for autotic project creation
25
+ - ⚙️ CI workflow on [GitHub Actions](https://github.com/features/actions)
26
+ - 🚀 Automated releases to PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
27
+ - 🧠 Sensible defaults via introspection to minimize answers during the initial setup
28
+ - 🛠️ Makefile with shortcuts for common tasks
29
+ - 📄 Generation of generic docs such as `LICENSE`, `CODE_OF_CONDUCT`, etc.
30
+ - 🤖 Heavily curated [AGENTS.md](https://agents.md/)
31
+ - 🌀 Initial setup of the development environment and git repo
32
+ - 🔁 Scheduled template refresh workflow that opens a PR every 20 days when updates are available
33
+ - ♻️ Projects updatable with [`copier update`](https://copier.readthedocs.io/en/stable/updating/)
34
+
35
+ Please read [my blog post](https://mgaitan.github.io/en/posts/opinionated-python-project-scaffolding/) to learn about the details of the decisions I made and the alternatives I considered.
36
+
37
+ ## Package Documentation
38
+
39
+ The package now includes a Diataxis-based docs set under `docs/` that expands this README:
40
+
41
+ - [Tutorial: Getting started](docs/getting_started.md)
42
+ - [How-to: Maintain the template](docs/maintain_template.md)
43
+ - [Reference: Configuration](docs/configuration.md)
44
+ - [Explanation: About these docs](docs/about_the_docs.md)
45
+ - [Explanation: Design decisions](docs/design_decisions.md)
46
+
47
+ ## Quick setup and usage
48
+
49
+ Quick shortcut (auto-detects copy vs update):
50
+
51
+ ```bash
52
+ uvx python-package-copier-template [PATH_TO_PROJECT]
53
+ ```
54
+
55
+ This runs `copier copy --defaults --unsafe` to `PATH_TO_PROJECT` (or `.` if omitted) when no `.copier-answers.yml` is present, or `copier update --defaults --unsafe` when it is.
56
+ Check the installed version with `uvx python-package-copier-template --version`.
57
+ When installed from PyPI, the wrapper copies from the matching template release instead of always using `main`.
58
+
59
+ If you want the latest development version straight from this repository instead of the latest published release, use:
60
+
61
+ ```bash
62
+ uvx git+https://github.com/mgaitan/python-package-copier-template [PATH_TO_PROJECT]
63
+ ```
64
+
65
+ You can check that wrapper version with `uvx git+https://github.com/mgaitan/python-package-copier-template -- --version`.
66
+
67
+ Start a new project explicitly with Copier if you want to bypass the wrapper:
68
+
69
+ ```bash
70
+ uvx --with=copier-template-extensions copier copy --trust "gh:mgaitan/python-package-copier-template" /path/to/your/new/project
71
+ ```
72
+
73
+ To upgrade an existing project created from this template to the latest version, run:
74
+
75
+ ```bash
76
+ uvx --with=copier-template-extensions copier update . --trust
77
+ ```
78
+
79
+ This will fetch the latest template version and guide you through updating your project, preserving your customizations whenever possible.
80
+
81
+ The generated project also ships a `Template Update` GitHub Actions workflow that runs every 20 days (or on manual dispatch) to execute `uvx copier update --trust --defaults .` and open a pull request with the changes and template version bump.
82
+
83
+ To test a local development version of the template, clone the repository and run:
84
+
85
+ ```bash
86
+ uv sync
87
+ uv run copier copy --trust --vcs-ref=HEAD . /path/to/your/test/project
88
+ ```
89
+
90
+ If you create the GitHub repository via the `gh` CLI prompt, the template will attempt to enable GitHub Pages (using the Actions build type) so documentation deployments succeed. If Pages is unavailable (for example, with some private repositories or account policies), the docs workflow will keep failing until Pages is allowed.
91
+
92
+
93
+ To publish a release of your project to PyPI, you need to [register the project with trusted publishing](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/). Read more about how this workflow works [here](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/).
94
+
95
+ This repository's own release workflow also refreshes the demo repository after a successful publish. That step requires the `DEMO_REPO_TOKEN` GitHub Actions secret, documented in [Configuration](docs/configuration.md).
96
+
97
+ Then
98
+
99
+ ```
100
+ $ make bump # optional
101
+ $ make release
102
+ ```
103
+
104
+
105
+ ### Acknowledgement
106
+
107
+ This project template started as a fork of [pawamoy/copier-uv](https://github.com/pawamoy/copier-uv). Then I simplified and changed it to fit my needs.
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "python-package-copier-template"
3
+ version = "0.4.3"
4
+ description = "A Copier template for modern Python projects."
5
+ readme = "README.md"
6
+ authors = [{ name = "Martín Gaitán", email = "gaitan@gmail.com" }]
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "copier>=9.11.0",
10
+ "copier-templates-extensions>=0.3.2",
11
+ "ruff>=0.14.9",
12
+ ]
13
+
14
+ [project.scripts]
15
+ python-package-copier-template = "python_package_copier_template.cli:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.18,<0.10.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.uv.build-backend]
22
+ module-name = "python_package_copier_template"
23
+ module-root = ""
24
+
25
+ [tool.ty.src]
26
+ exclude = [".worktrees/", ".external-worktrees/"]
27
+
28
+ [tool.ruff]
29
+ line-length = 120
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "pytest>=9.0.2",
34
+ ]
35
+ docs = [
36
+ "myst-parser>=4.0.1",
37
+ "richterm[sphinx]>=0.2.0",
38
+ "sphinx>=8.2.3",
39
+ "sphinx-book-theme>=1.1.4",
40
+ "sphinxcontrib-mermaid>=1.0.0",
41
+ ]
42
+ qa = [
43
+ "ty>=0.0.27",
44
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,119 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from importlib.metadata import PackageNotFoundError, distribution, version
7
+ from pathlib import Path
8
+ from urllib.parse import unquote, urlparse
9
+
10
+ from copier import run_copy, run_update
11
+
12
+ from python_package_copier_template import extensions
13
+
14
+ TEMPLATE_SRC = "gh:mgaitan/python-package-copier-template"
15
+ ANSWER_FILES: tuple[str, ...] = (".copier-answers.yml", ".copier-answers.yaml")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class TemplateTarget:
20
+ src_path: str
21
+ vcs_ref: str | None = None
22
+
23
+
24
+ def get_version() -> str:
25
+ try:
26
+ return version("python-package-copier-template")
27
+ except PackageNotFoundError:
28
+ return "0.0.0"
29
+
30
+
31
+ def has_answers(dst: Path) -> bool:
32
+ return any((dst / filename).exists() for filename in ANSWER_FILES)
33
+
34
+
35
+ def get_local_git_head(path: str) -> str | None:
36
+ try:
37
+ result = subprocess.run(
38
+ ["git", "-C", path, "rev-parse", "HEAD"],
39
+ check=True,
40
+ capture_output=True,
41
+ text=True,
42
+ )
43
+ except (FileNotFoundError, subprocess.CalledProcessError):
44
+ return None
45
+
46
+ return result.stdout.strip() or None
47
+
48
+
49
+ def resolve_template_target() -> TemplateTarget:
50
+ try:
51
+ dist = distribution("python-package-copier-template")
52
+ except PackageNotFoundError:
53
+ return TemplateTarget(src_path=TEMPLATE_SRC)
54
+
55
+ direct_url_text = dist.read_text("direct_url.json")
56
+ if direct_url_text:
57
+ direct_url = json.loads(direct_url_text)
58
+ url = direct_url.get("url", "")
59
+ parsed_url = urlparse(url)
60
+
61
+ if parsed_url.scheme == "file":
62
+ path = unquote(parsed_url.path)
63
+ return TemplateTarget(src_path=path, vcs_ref=get_local_git_head(path))
64
+
65
+ if vcs_info := direct_url.get("vcs_info"):
66
+ return TemplateTarget(
67
+ src_path=TEMPLATE_SRC,
68
+ vcs_ref=vcs_info.get("commit_id") or vcs_info.get("requested_revision"),
69
+ )
70
+
71
+ return TemplateTarget(src_path=TEMPLATE_SRC, vcs_ref=dist.version)
72
+
73
+
74
+ def build_parser() -> argparse.ArgumentParser:
75
+ parser = argparse.ArgumentParser(
76
+ prog="python-package-copier-template",
77
+ description=(
78
+ "Apply the template via Copier. If the destination has a .copier-answers file, run update; "
79
+ "otherwise run copy."
80
+ ),
81
+ )
82
+ parser.add_argument(
83
+ "destination",
84
+ nargs="?",
85
+ help="Destination directory.",
86
+ )
87
+ parser.add_argument("--version", action="version", version=f"%(prog)s {get_version()}")
88
+ return parser
89
+
90
+
91
+ def main(argv: list[str] | None = None) -> int:
92
+ parser = build_parser()
93
+ args = parser.parse_args(argv)
94
+
95
+ copy_defaults = os.environ.get("COPIER_TEMPLATE_DEFAULTS") == "1"
96
+ if not args.destination:
97
+ parser.error("Destination path is required. Use Copier directly if you want interactive prompts.")
98
+
99
+ dst = Path(args.destination).expanduser()
100
+ if has_answers(dst):
101
+ with extensions.update_mode():
102
+ run_update(
103
+ dst_path=str(dst),
104
+ defaults=True,
105
+ unsafe=True,
106
+ overwrite=True,
107
+ skip_answered=True,
108
+ )
109
+ else:
110
+ template_target = resolve_template_target()
111
+ run_copy(
112
+ src_path=template_target.src_path,
113
+ dst_path=str(dst),
114
+ defaults=copy_defaults,
115
+ unsafe=True,
116
+ vcs_ref=template_target.vcs_ref,
117
+ )
118
+
119
+ return 0
@@ -0,0 +1,140 @@
1
+ import re
2
+ import shutil
3
+ import subprocess
4
+ import unicodedata
5
+ import urllib.error
6
+ import urllib.request
7
+ from contextlib import contextmanager
8
+ from contextvars import ContextVar
9
+ from datetime import date
10
+ from pathlib import Path
11
+
12
+ from jinja2.ext import Extension
13
+
14
+ _UPDATE_MODE: ContextVar[bool] = ContextVar("copier_template_update_mode", default=False)
15
+
16
+
17
+ @contextmanager
18
+ def update_mode():
19
+ """Mark the current execution context as an update operation."""
20
+
21
+ token = _UPDATE_MODE.set(True)
22
+ try:
23
+ yield
24
+ finally:
25
+ _UPDATE_MODE.reset(token)
26
+
27
+
28
+ def git_user_name(default: str) -> str:
29
+ return subprocess.getoutput("git config user.name").strip() or default
30
+
31
+
32
+ def git_user_email(default: str) -> str:
33
+ return subprocess.getoutput("git config user.email").strip() or default
34
+
35
+
36
+ def gh_user_login(default: str) -> str:
37
+ """Return the authenticated GitHub username via the GH CLI when available."""
38
+
39
+ try:
40
+ completed = subprocess.run(
41
+ ["gh", "api", "user", "-q", ".login"],
42
+ check=True,
43
+ capture_output=True,
44
+ text=True,
45
+ )
46
+ login = completed.stdout.strip()
47
+ if login:
48
+ return login
49
+ except (FileNotFoundError, subprocess.CalledProcessError):
50
+ return default
51
+
52
+ return default
53
+
54
+
55
+ def command_available(command: str) -> bool:
56
+ """Return True if the command exists on PATH."""
57
+
58
+ return shutil.which(command) is not None
59
+
60
+
61
+ def slugify(value, separator="-"):
62
+ value = unicodedata.normalize("NFKD", str(value)).encode("ascii", "ignore").decode("ascii")
63
+ value = re.sub(r"[^\w\s-]", "", value.lower())
64
+ return re.sub(r"[-_\s]+", separator, value).strip("-_")
65
+
66
+
67
+ def path_exists(path: str) -> bool:
68
+ """Return True when ``path`` exists relative to the destination."""
69
+
70
+ return Path(path).expanduser().exists()
71
+
72
+
73
+ def is_update(defaults: bool | None = None) -> bool:
74
+ """Return True when running under `copier update`."""
75
+
76
+ return bool(defaults)
77
+
78
+
79
+ def pypi_distribution_exists(name: str) -> bool:
80
+ """Return True if a distribution with ``name`` is present on PyPI.
81
+
82
+ Uses the lightweight JSON endpoint and handles network failures gracefully
83
+ by treating them as "not found" so template execution is not blocked.
84
+ """
85
+
86
+ # During updates we keep the previously selected distribution name and
87
+ # should not block on global PyPI availability checks.
88
+ if _UPDATE_MODE.get():
89
+ return False
90
+
91
+ if not name:
92
+ return False
93
+
94
+ url = f"https://pypi.org/pypi/{name}/json"
95
+ request = urllib.request.Request(url, method="HEAD")
96
+ try:
97
+ with urllib.request.urlopen(request, timeout=3):
98
+ return True
99
+ except (urllib.error.HTTPError, OSError):
100
+ return False
101
+
102
+
103
+ def suggest_pypi_distribution_name(name: str) -> str:
104
+ """Return a PyPI-safe distribution name, adding a suffix if needed."""
105
+
106
+ base = slugify(name)
107
+ if not base:
108
+ base = "package"
109
+
110
+ candidate = base
111
+ suffix = 1
112
+ while pypi_distribution_exists(candidate) and suffix < 50:
113
+ candidate = f"{base}-{suffix}"
114
+ suffix += 1
115
+ return candidate
116
+
117
+
118
+ class GitExtension(Extension):
119
+ def __init__(self, environment):
120
+ super().__init__(environment)
121
+ environment.filters["git_user_name"] = git_user_name
122
+ environment.filters["git_user_email"] = git_user_email
123
+ environment.filters["gh_user_login"] = gh_user_login
124
+ environment.filters["command_available"] = command_available
125
+ environment.filters["path_exists"] = path_exists
126
+ environment.filters["is_update"] = is_update
127
+
128
+
129
+ class SlugifyExtension(Extension):
130
+ def __init__(self, environment):
131
+ super().__init__(environment)
132
+ environment.filters["slugify"] = slugify
133
+ environment.filters["pypi_exists"] = pypi_distribution_exists
134
+ environment.filters["pypi_suggest_name"] = suggest_pypi_distribution_name
135
+
136
+
137
+ class CurrentYearExtension(Extension):
138
+ def __init__(self, environment):
139
+ super().__init__(environment)
140
+ environment.globals["current_year"] = date.today().year