Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3fe6fb9

Browse files
committedApr 19, 2025
Create a cookiecutter template
1 parent 9e11bc3 commit 3fe6fb9

File tree

6 files changed

+648
-0
lines changed

6 files changed

+648
-0
lines changed
 

‎cookiecutter.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"project_name": "my_cli_tool",
3+
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}",
4+
"project_description": "CLI tool for developers",
5+
"project_short_description": "CLI tool for developers",
6+
"package_name": "{{ cookiecutter.project_slug }}",
7+
"author_name": "Your Name",
8+
"author_email": "your.email@example.com",
9+
"github_username": "username",
10+
"github_repo": "{{ cookiecutter.project_slug }}",
11+
"version": "0.0.1",
12+
"python_version": "3.9",
13+
"license": ["MIT", "BSD-3", "GPL-3.0", "Apache-2.0"],
14+
"include_docs": ["y", "n"],
15+
"include_github_actions": ["y", "n"],
16+
"include_tests": ["y", "n"],
17+
"supported_vcs": ["git", "svn", "hg"],
18+
"create_author_file": ["y", "n"]
19+
}

‎hooks/post_gen_project.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
#!/usr/bin/env python
2+
"""Post-generation script for cookiecutter."""
3+
4+
import os
5+
import datetime
6+
7+
license_type = "{{cookiecutter.license}}"
8+
author = "{{cookiecutter.author_name}}"
9+
year = datetime.datetime.now().year
10+
11+
12+
def generate_mit_license():
13+
"""Generate MIT license file."""
14+
mit_license = f"""MIT License
15+
16+
Copyright (c) {year} {author}
17+
18+
Permission is hereby granted, free of charge, to any person obtaining a copy
19+
of this software and associated documentation files (the "Software"), to deal
20+
in the Software without restriction, including without limitation the rights
21+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22+
copies of the Software, and to permit persons to whom the Software is
23+
furnished to do so, subject to the following conditions:
24+
25+
The above copyright notice and this permission notice shall be included in all
26+
copies or substantial portions of the Software.
27+
28+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34+
SOFTWARE.
35+
"""
36+
with open("LICENSE", "w") as f:
37+
f.write(mit_license)
38+
39+
40+
def generate_bsd3_license():
41+
"""Generate BSD-3 license file."""
42+
bsd3_license = f"""BSD 3-Clause License
43+
44+
Copyright (c) {year}, {author}
45+
All rights reserved.
46+
47+
Redistribution and use in source and binary forms, with or without
48+
modification, are permitted provided that the following conditions are met:
49+
50+
1. Redistributions of source code must retain the above copyright notice, this
51+
list of conditions and the following disclaimer.
52+
53+
2. Redistributions in binary form must reproduce the above copyright notice,
54+
this list of conditions and the following disclaimer in the documentation
55+
and/or other materials provided with the distribution.
56+
57+
3. Neither the name of the copyright holder nor the names of its
58+
contributors may be used to endorse or promote products derived from
59+
this software without specific prior written permission.
60+
61+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
62+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
63+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
64+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
65+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
66+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
67+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
68+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
69+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
70+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
71+
"""
72+
with open("LICENSE", "w") as f:
73+
f.write(bsd3_license)
74+
75+
76+
def generate_gpl3_license():
77+
"""Generate GPL-3.0 license file."""
78+
# This would be the full GPL-3.0 license, but it's very long
79+
# Here we'll just write a reference to the standard license
80+
gpl3_license = f"""Copyright (C) {year} {author}
81+
82+
This program is free software: you can redistribute it and/or modify
83+
it under the terms of the GNU General Public License as published by
84+
the Free Software Foundation, either version 3 of the License, or
85+
(at your option) any later version.
86+
87+
This program is distributed in the hope that it will be useful,
88+
but WITHOUT ANY WARRANTY; without even the implied warranty of
89+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
90+
GNU General Public License for more details.
91+
92+
You should have received a copy of the GNU General Public License
93+
along with this program. If not, see <https://www.gnu.org/licenses/>.
94+
"""
95+
with open("LICENSE", "w") as f:
96+
f.write(gpl3_license)
97+
98+
99+
def generate_apache2_license():
100+
"""Generate Apache-2.0 license file."""
101+
apache2_license = f""" Apache License
102+
Version 2.0, January 2004
103+
http://www.apache.org/licenses/
104+
105+
Copyright {year} {author}
106+
107+
Licensed under the Apache License, Version 2.0 (the "License");
108+
you may not use this file except in compliance with the License.
109+
You may obtain a copy of the License at
110+
111+
http://www.apache.org/licenses/LICENSE-2.0
112+
113+
Unless required by applicable law or agreed to in writing, software
114+
distributed under the License is distributed on an "AS IS" BASIS,
115+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
116+
See the License for the specific language governing permissions and
117+
limitations under the License.
118+
"""
119+
with open("LICENSE", "w") as f:
120+
f.write(apache2_license)
121+
122+
123+
if __name__ == "__main__":
124+
if license_type == "MIT":
125+
generate_mit_license()
126+
elif license_type == "BSD-3":
127+
generate_bsd3_license()
128+
elif license_type == "GPL-3.0":
129+
generate_gpl3_license()
130+
elif license_type == "Apache-2.0":
131+
generate_apache2_license()
132+
else:
133+
print(f"Unsupported license type: {license_type}")
134+
135+
# Create test directory if tests are included
136+
if "{{cookiecutter.include_tests}}" == "y":
137+
if not os.path.exists("tests"):
138+
os.makedirs("tests")
139+
with open("tests/__init__.py", "w") as f:
140+
f.write("""Test package for {{cookiecutter.package_name}}.""")
141+
142+
# Create a basic test file
143+
with open("tests/test_cli.py", "w") as f:
144+
f.write("""#!/usr/bin/env python
145+
"""Test CLI for {{cookiecutter.package_name}}."""
146+
147+
from __future__ import annotations
148+
149+
import os
150+
import pathlib
151+
import subprocess
152+
import sys
153+
154+
import pytest
155+
156+
import {{cookiecutter.package_name}}
157+
158+
159+
def test_run():
160+
"""Test run."""
161+
# Test that the function doesn't error
162+
proc = {{cookiecutter.package_name}}.run(cmd="echo", cmd_args=["hello"])
163+
assert proc is None
164+
165+
# Test when G_IS_TEST is set, it returns the proc
166+
os.environ["{{cookiecutter.package_name.upper()}}_IS_TEST"] = "1"
167+
proc = {{cookiecutter.package_name}}.run(cmd="echo", cmd_args=["hello"])
168+
assert isinstance(proc, subprocess.Popen)
169+
assert proc.returncode == 0
170+
del os.environ["{{cookiecutter.package_name.upper()}}_IS_TEST"]
171+
""")
172+
173+
# Create docs directory if docs are included
174+
if "{{cookiecutter.include_docs}}" == "y":
175+
if not os.path.exists("docs"):
176+
os.makedirs("docs")
177+
with open("docs/index.md", "w") as f:
178+
f.write("""# {{cookiecutter.project_name}}
179+
180+
{{cookiecutter.project_description}}
181+
182+
## Installation
183+
184+
```bash
185+
pip install {{cookiecutter.package_name}}
186+
```
187+
188+
## Usage
189+
190+
```bash
191+
{{cookiecutter.package_name}}
192+
```
193+
194+
This will detect the type of repository in your current directory and run the appropriate VCS command.
195+
""")
196+
197+
# Create GitHub Actions workflows if included
198+
if "{{cookiecutter.include_github_actions}}" == "y":
199+
if not os.path.exists(".github/workflows"):
200+
os.makedirs(".github/workflows")
201+
with open(".github/workflows/tests.yml", "w") as f:
202+
f.write("""name: tests
203+
204+
on:
205+
push:
206+
branches: [main]
207+
pull_request:
208+
branches: [main]
209+
210+
jobs:
211+
build:
212+
runs-on: ubuntu-latest
213+
strategy:
214+
matrix:
215+
python-version: ['3.9', '3.10', '3.11']
216+
217+
steps:
218+
- uses: actions/checkout@v3
219+
- name: Set up Python ${{ matrix.python-version }}
220+
uses: actions/setup-python@v4
221+
with:
222+
python-version: ${{ matrix.python-version }}
223+
- name: Install dependencies
224+
run: |
225+
python -m pip install --upgrade pip
226+
pip install uv
227+
uv pip install -e .
228+
uv pip install pytest pytest-cov
229+
- name: Test with pytest
230+
run: |
231+
uv pip install pytest
232+
pytest
233+
""")
234+
235+
print("Project generated successfully!")
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# `$ {{cookiecutter.package_name}}`
2+
3+
{{cookiecutter.project_description}}
4+
5+
[![Python Package](https://img.shields.io/pypi/v/{{cookiecutter.package_name}}.svg)](https://pypi.org/project/{{cookiecutter.package_name}}/)
6+
{% if cookiecutter.include_docs == "y" %}
7+
[![Docs](https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/workflows/docs/badge.svg)](https://{{cookiecutter.package_name}}.git-pull.com)
8+
{% endif %}
9+
{% if cookiecutter.include_github_actions == "y" %}
10+
[![Build Status](https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/workflows/tests/badge.svg)](https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/actions?query=workflow%3A%22tests%22)
11+
[![Code Coverage](https://codecov.io/gh/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/branch/master/graph/badge.svg)](https://codecov.io/gh/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}})
12+
{% endif %}
13+
[![License](https://img.shields.io/github/license/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}.svg)](https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/blob/master/LICENSE)
14+
15+
Shortcut / powertool for developers to access current repos' VCS, whether it's
16+
{% for vcs in cookiecutter.supported_vcs.split(',') %}
17+
{% if loop.first %}{{vcs.strip()}}{% elif loop.last %} or {{vcs.strip()}}{% else %}, {{vcs.strip()}}{% endif %}{% endfor %}.
18+
19+
```console
20+
$ pip install --user {{cookiecutter.package_name}}
21+
```
22+
23+
```console
24+
$ {{cookiecutter.package_name}}
25+
```
26+
27+
### Developmental releases
28+
29+
You can test the unpublished version of {{cookiecutter.package_name}} before its released.
30+
31+
- [pip](https://pip.pypa.io/en/stable/):
32+
33+
```console
34+
$ pip install --user --upgrade --pre {{cookiecutter.package_name}}
35+
```
36+
37+
- [pipx](https://pypa.github.io/pipx/docs/):
38+
39+
```console
40+
$ pipx install --suffix=@next {{cookiecutter.package_name}} --pip-args '\--pre' --force
41+
```
42+
43+
Then use `{{cookiecutter.package_name}}@next --help`.
44+
45+
# More information
46+
47+
- Python support: >= {{cookiecutter.python_version}}, pypy
48+
- VCS supported: {% for vcs in cookiecutter.supported_vcs.split(',') %}{{vcs.strip()}}(1){% if not loop.last %}, {% endif %}{% endfor %}
49+
- Source: <https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}>
50+
{% if cookiecutter.include_docs == "y" %}
51+
- Docs: <https://{{cookiecutter.package_name}}.git-pull.com>
52+
- Changelog: <https://{{cookiecutter.package_name}}.git-pull.com/history.html>
53+
- API: <https://{{cookiecutter.package_name}}.git-pull.com/api.html>
54+
{% endif %}
55+
- Issues: <https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/issues>
56+
{% if cookiecutter.include_github_actions == "y" %}
57+
- Test Coverage: <https://codecov.io/gh/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}>
58+
{% endif %}
59+
- pypi: <https://pypi.python.org/pypi/{{cookiecutter.package_name}}>
60+
- License: [{{cookiecutter.license}}](https://opensource.org/licenses/{{cookiecutter.license}})
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
[project]
2+
name = "{{cookiecutter.package_name}}"
3+
version = "{{cookiecutter.version}}"
4+
description = "{{cookiecutter.project_description}}"
5+
requires-python = ">=3.9,<4.0"
6+
authors = [
7+
{name = "{{cookiecutter.author_name}}", email = "{{cookiecutter.author_email}}"}
8+
]
9+
license = { text = "{{cookiecutter.license}}" }
10+
classifiers = [
11+
"Development Status :: 4 - Beta",
12+
{% if cookiecutter.license == "MIT" %}
13+
"License :: OSI Approved :: MIT License",
14+
{% elif cookiecutter.license == "BSD-3" %}
15+
"License :: OSI Approved :: BSD License",
16+
{% elif cookiecutter.license == "GPL-3.0" %}
17+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
18+
{% elif cookiecutter.license == "Apache-2.0" %}
19+
"License :: OSI Approved :: Apache Software License",
20+
{% endif %}
21+
"Environment :: Web Environment",
22+
"Intended Audience :: Developers",
23+
"Operating System :: POSIX",
24+
"Operating System :: MacOS :: MacOS X",
25+
"Programming Language :: Python",
26+
"Programming Language :: Python :: 3",
27+
"Programming Language :: Python :: 3.9",
28+
"Programming Language :: Python :: 3.10",
29+
"Programming Language :: Python :: 3.11",
30+
"Programming Language :: Python :: 3.12",
31+
"Topic :: Utilities",
32+
"Topic :: System :: Shells",
33+
]
34+
packages = [
35+
{ include = "*", from = "src" },
36+
]
37+
{% if cookiecutter.include_tests == "y" %}
38+
include = [
39+
{ path = "tests", format = "sdist" },
40+
]
41+
{% endif %}
42+
readme = 'README.md'
43+
keywords = [
44+
"{{cookiecutter.package_name}}",
45+
{% for vcs in cookiecutter.supported_vcs.split(',') %}
46+
"{{vcs.strip()}}",
47+
{% endfor %}
48+
"vcs",
49+
"cli",
50+
"sync",
51+
"pull",
52+
"update",
53+
]
54+
homepage = "https://{{cookiecutter.package_name}}.git-pull.com"
55+
56+
[project.urls]
57+
"Bug Tracker" = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/issues"
58+
Documentation = "https://{{cookiecutter.package_name}}.git-pull.com"
59+
Repository = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}"
60+
Changes = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/blob/master/CHANGES"
61+
62+
[project.scripts]
63+
{{cookiecutter.package_name}} = '{{cookiecutter.package_name}}:run'
64+
65+
[tool.uv]
66+
dev-dependencies = [
67+
{% if cookiecutter.include_docs == "y" %}
68+
# Docs
69+
"aafigure",
70+
"pillow",
71+
"sphinx",
72+
"furo",
73+
"gp-libs",
74+
"sphinx-autobuild",
75+
"sphinx-autodoc-typehints",
76+
"sphinx-inline-tabs",
77+
"sphinxext-opengraph",
78+
"sphinx-copybutton",
79+
"sphinxext-rediraffe",
80+
"sphinx-argparse",
81+
"myst-parser",
82+
"linkify-it-py",
83+
{% endif %}
84+
{% if cookiecutter.include_tests == "y" %}
85+
# Testing
86+
"gp-libs",
87+
"pytest",
88+
"pytest-rerunfailures",
89+
"pytest-mock",
90+
"pytest-watcher",
91+
# Coverage
92+
"codecov",
93+
"coverage",
94+
"pytest-cov",
95+
{% endif %}
96+
# Lint
97+
"ruff",
98+
"mypy",
99+
]
100+
101+
{% if cookiecutter.include_docs == "y" or cookiecutter.include_tests == "y" %}
102+
[dependency-groups]
103+
{% if cookiecutter.include_docs == "y" %}
104+
docs = [
105+
"aafigure",
106+
"pillow",
107+
"sphinx",
108+
"furo",
109+
"gp-libs",
110+
"sphinx-autobuild",
111+
"sphinx-autodoc-typehints",
112+
"sphinx-inline-tabs",
113+
"sphinxext-opengraph",
114+
"sphinx-copybutton",
115+
"sphinxext-rediraffe",
116+
"myst-parser",
117+
"linkify-it-py",
118+
]
119+
{% endif %}
120+
{% if cookiecutter.include_tests == "y" %}
121+
testing = [
122+
"gp-libs",
123+
"pytest",
124+
"pytest-rerunfailures",
125+
"pytest-mock",
126+
"pytest-watcher",
127+
]
128+
coverage =[
129+
"codecov",
130+
"coverage",
131+
"pytest-cov",
132+
]
133+
{% endif %}
134+
lint = [
135+
"ruff",
136+
"mypy",
137+
]
138+
{% endif %}
139+
140+
[build-system]
141+
requires = ["hatchling"]
142+
build-backend = "hatchling.build"
143+
144+
[tool.mypy]
145+
strict = true
146+
python_version = "{{cookiecutter.python_version}}"
147+
files = [
148+
"src/",
149+
{% if cookiecutter.include_tests == "y" %}
150+
"tests/",
151+
{% endif %}
152+
]
153+
154+
[tool.ruff]
155+
target-version = "py39"
156+
157+
[tool.ruff.lint]
158+
select = [
159+
"E", # pycodestyle
160+
"F", # pyflakes
161+
"I", # isort
162+
"UP", # pyupgrade
163+
"A", # flake8-builtins
164+
"B", # flake8-bugbear
165+
"C4", # flake8-comprehensions
166+
"COM", # flake8-commas
167+
"EM", # flake8-errmsg
168+
"Q", # flake8-quotes
169+
"PTH", # flake8-use-pathlib
170+
"SIM", # flake8-simplify
171+
"TRY", # Trycertatops
172+
"PERF", # Perflint
173+
"RUF", # Ruff-specific rules
174+
"D", # pydocstyle
175+
"FA100", # future annotations
176+
]
177+
ignore = [
178+
"COM812", # missing trailing comma, ruff format conflict
179+
]
180+
extend-safe-fixes = [
181+
"UP006",
182+
"UP007",
183+
]
184+
pyupgrade.keep-runtime-typing = false
185+
186+
[tool.ruff.lint.pydocstyle]
187+
convention = "numpy"
188+
189+
[tool.ruff.lint.isort]
190+
known-first-party = [
191+
"{{cookiecutter.package_name}}",
192+
]
193+
combine-as-imports = true
194+
required-imports = [
195+
"from __future__ import annotations",
196+
]
197+
198+
[tool.ruff.lint.per-file-ignores]
199+
"*/__init__.py" = ["F401"]
200+
201+
{% if cookiecutter.include_tests == "y" %}
202+
[tool.pytest.ini_options]
203+
addopts = "--tb=short --no-header --showlocals --doctest-modules"
204+
doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE"
205+
testpaths = [
206+
"src/{{cookiecutter.package_name}}",
207+
"tests",
208+
{% if cookiecutter.include_docs == "y" %}
209+
"docs",
210+
{% endif %}
211+
]
212+
filterwarnings = [
213+
"ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::",
214+
]
215+
216+
[tool.pytest-watcher]
217+
now = true
218+
ignore_patterns = ["*.py.*.py"]
219+
220+
[tool.coverage.report]
221+
exclude_also = [
222+
"def __repr__",
223+
"raise AssertionError",
224+
"raise NotImplementedError",
225+
"if __name__ == .__main__.:",
226+
"if TYPE_CHECKING:",
227+
"class .*\\bProtocol\\):",
228+
"@(abc\\.)?abstractmethod",
229+
"from __future__ import annotations",
230+
]
231+
{% endif %}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Metadata package for {{cookiecutter.package_name}}."""
2+
3+
from __future__ import annotations
4+
5+
__title__ = "{{cookiecutter.project_name}}"
6+
__package_name__ = "{{cookiecutter.package_name}}"
7+
__description__ = "{{cookiecutter.project_description}}"
8+
__version__ = "{{cookiecutter.version}}"
9+
__author__ = "{{cookiecutter.author_name}}"
10+
__github__ = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}"
11+
__docs__ = "https://{{cookiecutter.package_name}}.git-pull.com"
12+
__tracker__ = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.github_repo}}/issues"
13+
__pypi__ = "https://pypi.org/project/{{cookiecutter.package_name}}/"
14+
__email__ = "{{cookiecutter.author_email}}"
15+
__license__ = "{{cookiecutter.license}}"
16+
__copyright__ = "Copyright {% now 'local', '%Y' %}- {{cookiecutter.author_name}}"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python
2+
"""Package for {{cookiecutter.package_name}}."""
3+
4+
from __future__ import annotations
5+
6+
import io
7+
import logging
8+
import os
9+
import pathlib
10+
import subprocess
11+
import sys
12+
import typing as t
13+
from os import PathLike
14+
15+
__all__ = ["DEFAULT", "run", "sys", "vcspath_registry"]
16+
17+
{% set vcs_dict = {} %}
18+
{% for vcs in cookiecutter.supported_vcs.split(',') %}
19+
{% if vcs.strip() == 'git' %}
20+
{% set _ = vcs_dict.update({'.git': 'git'}) %}
21+
{% elif vcs.strip() == 'svn' %}
22+
{% set _ = vcs_dict.update({'.svn': 'svn'}) %}
23+
{% elif vcs.strip() == 'hg' %}
24+
{% set _ = vcs_dict.update({'.hg': 'hg'}) %}
25+
{% endif %}
26+
{% endfor %}
27+
28+
vcspath_registry = {{ vcs_dict }}
29+
30+
log = logging.getLogger(__name__)
31+
32+
33+
def find_repo_type(path: pathlib.Path | str) -> str | None:
34+
"""Detect repo type looking upwards."""
35+
for _path in [*list(pathlib.Path(path).parents), pathlib.Path(path)]:
36+
for p in _path.iterdir():
37+
if p.is_dir() and p.name in vcspath_registry:
38+
return vcspath_registry[p.name]
39+
return None
40+
41+
42+
DEFAULT = object()
43+
44+
45+
def run(
46+
cmd: str | bytes | PathLike[str] | PathLike[bytes] | object = DEFAULT,
47+
cmd_args: object = DEFAULT,
48+
wait: bool = False,
49+
*args: object,
50+
**kwargs: t.Any,
51+
) -> subprocess.Popen[str] | None:
52+
"""CLI Entrypoint for {{cookiecutter.package_name}}, overlay for current directory's VCS utility.
53+
54+
Environment variables
55+
---------------------
56+
{{cookiecutter.package_name.upper()}}_IS_TEST :
57+
Control whether run() returns proc so function can be tested. If proc was always
58+
returned, it would print *<Popen: returncode: 1 args: ['git']>* after command.
59+
"""
60+
# Interpret default kwargs lazily for mockability of argv
61+
if cmd is DEFAULT:
62+
cmd = find_repo_type(pathlib.Path.cwd())
63+
if cmd_args is DEFAULT:
64+
cmd_args = sys.argv[1:]
65+
66+
logging.basicConfig(level=logging.INFO, format="%(message)s")
67+
68+
if cmd is None:
69+
msg = "No VCS found in current directory."
70+
log.info(msg)
71+
return None
72+
73+
assert isinstance(cmd_args, (tuple, list))
74+
assert isinstance(cmd, (str, bytes, pathlib.Path))
75+
76+
proc = subprocess.Popen([cmd, *cmd_args], **kwargs)
77+
if wait:
78+
proc.wait()
79+
else:
80+
proc.communicate()
81+
if os.getenv("{{cookiecutter.package_name.upper()}}_IS_TEST") and __name__ != "__main__":
82+
return proc
83+
return None
84+
85+
86+
if __name__ == "__main__":
87+
run()

0 commit comments

Comments
 (0)
Please sign in to comment.