Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
274 lines (248 loc) · 9.7 KB
/
Copy pathpyproject.toml
File metadata and controls
274 lines (248 loc) · 9.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
[build-system]
requires = ["setuptools>=77", "setuptools-scm>=8", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "git-nested"
dynamic = ["version"]
description = "Git Submodule Alternative"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
dependencies = [
"PyYAML>=6.0,<7",
]
authors = [
{name = "Thorsten Klein", email = "thorsten.klein.git@gmail.com"}
]
keywords = [
"cli",
"dependency-management",
"developer-tools",
"git",
"git-extension",
"git-submodule",
"git-submodules",
"git-subrepo",
"git-subtree",
"git-tools",
"monorepo",
"multi-repo",
"nested-repositories",
"python",
"repository-management",
"submodules",
"subrepo",
"subtree",
"vendoring",
"version-control",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Version Control :: Git",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
[project.scripts]
git-nested = "git_nested:main"
[project.urls]
Homepage = "https://github.com/thorsten-klein/git-nested"
Documentation = "https://thorsten-klein.github.io/git-nested/"
Repository = "https://github.com/thorsten-klein/git-nested"
Issues = "https://github.com/thorsten-klein/git-nested/issues"
[dependency-groups]
# The Sphinx toolchain doc/conf.py needs. See doc/contributing/documentation.md.
docs = [
"myst-parser>=4.0.0,<5",
"Sphinx>=8.0.0,<9",
"sphinx-copybutton>=0.5.2,<1",
"sphinx-markdown-builder>=0.6.8,<1",
"sphinx-rtd-theme>=3.0.0,<4",
"sphinxcontrib-mermaid>=1.0.0,<3",
]
lint = [
"bandit>=1.8.0",
"complexipy>=7.0.0",
"mypy>=1.14,<3.0",
"pip-audit>=2.9.0",
"pre-commit>=4.3.0",
"pyright~=1.1",
"ruff~=0.13",
"types-pyyaml>=6.0.12.20250915",
]
test = [
"coverage>=7.10",
"pytest>=8.4,<10.0",
"pytest-xdist>=3.0.0",
"pytest-cov>=7.0",
"pytest-mock>=3.6.1",
"pytest-flakefinder>=1.1.0",
]
dev = [
"poethepoet~=0.37",
{include-group = "docs"},
{include-group = "lint"},
{include-group = "test"},
]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
# Applied by path in tests/conftest.py's pytest_collection_modifyitems, not by
# decorator -- the directory a test lives in already says which it is.
markers = [
"unit: fast, no real git (tests/unit)",
"e2e: shells out to real git (tests/e2e)",
]
addopts = "--strict-markers"
[tool.coverage.run]
# `--cov=git_nested` resolves through the editable install's site-packages
# path. Without this remapping coverage.xml records site-packages filenames,
# and SonarQube -- which is told `sonar.sources=src` -- silently reports 0%.
relative_files = true
[tool.coverage.paths]
source = ["src/git_nested", "*/site-packages/git_nested"]
[tool.coverage.report]
fail_under = 100
show_missing = true
# `if TYPE_CHECKING:` bodies never execute at runtime, so they are not
# reachable statements -- without this they would count against the 100% gate.
exclude_also = [
"if TYPE_CHECKING:",
"@overload",
]
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools_scm]
fallback_version = "1.0.0"
[tool.setuptools.package-data]
# The previous glob ("*" = ["*.sh", "*.bash"]) matched nothing: there was no
# package, and no shell scripts under the module's directory.
"git_nested" = ["py.typed"]
[tool.ruff]
line-length = 120
indent-width = 4
[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP", "SIM", "PTH", "RET", "ARG", "C4", "RUF", "D", "T20", "TRY", "PL", "ANN"]
ignore = [
"E501", # line length is enforced by the formatter, not the linter
"TRY003", # GitNestedError carries the user-facing message; that IS the long
# message outside the exception class, and there is one exception class
"PLR0913", # complexipy caps cognitive complexity at 5, which forces functions to
# be split, which pushes their state out into parameters. The two rules
# pull opposite ways and complexipy is the one measuring readability
"PLR0917", # the positional-only twin of PLR0913 (stable since ruff 0.16), same reasoning
"PLR2004", # what it finds here is `len(parts) != 2` -- naming that 2 hides it
]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
# unused fixture/mock-callback arguments are idiomatic pytest, not a smell;
# a test's own name is its documentation, so docstrings aren't required; and
# an end-to-end test is a long straight line of steps by nature (PLR0915).
# ANN is off here too: a test takes fixtures and returns None, so annotating
# 730 signatures would say nothing the fixture names do not already say.
"tests/**" = ["ARG001", "ARG002", "ARG005", "D", "PLR0915", "ANN"]
# the one place allowed to write to stdout -- everything else goes through it.
"src/git_nested/output.py" = ["T201"]
[tool.ruff.format]
quote-style = "preserve"
line-ending = "lf"
# Enable preview mode for Github Annotations added in 0.13.3
preview = true
[tool.complexipy]
paths = ["src", "tests"]
max-complexity-allowed = 8
failed = true
color = "auto"
sort = "asc"
[tool.mypy]
python_version = "3.10"
files = ["src", "tests"]
# Both conftest.py files would otherwise claim the module name "conftest" and
# mypy stops at the collision. With explicit bases they are tests.conftest and
# tests.unit.conftest instead -- at the cost of the sibling imports pytest's
# own sys.path insertion makes work, which the override below waves through.
mypy_path = "src:."
explicit_package_bases = true
# The tests are checked for the mistakes that matter in a test -- a call with
# the wrong arguments, an attribute that is not there -- but not required to
# annotate themselves; see the ANN per-file-ignore for why.
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = ["conftest", "fakes"]
ignore_missing_imports = true
[tool.pyright]
pythonVersion = "3.10"
include = ["src", "tests"]
extraPaths = ["src", "tests"]
[tool.bandit]
# git-nested's whole job is orchestrating git as a subprocess -- these fire on
# every single one of those call sites:
# B404 -- merely importing subprocess
# B603 -- subprocess.run called with a list argv and no shell=True (that's
# the *safe* pattern these checks exist to push people towards)
# B607 -- invoking a PATH-resolved executable ("git") rather than a
# hardcoded absolute path, which would break on every dev machine
skips = ["B404", "B603", "B607"]
[tool.poe.tasks]
all = ["lock-check", "pre-commit", "lint", "format", "mypy", "pyright", "bandit", "pip-audit", "complexity", "test", "docs"]
# Every workflow installs with `uv sync --frozen`, so a pyproject dependency
# edit without a matching `uv lock` fails in CI as an opaque sync error. This
# turns that into a local, named failure instead. It is a poe task rather than
# a pre-commit hook on purpose: this file's header rules out duplicating a
# pinned tool there, and a uv-pre-commit hook would be a second, independently
# pinned uv drifting from the one CI runs.
"lock-check" = "uv lock --check"
"pre-commit" = "pre-commit run --all-files --show-diff-on-failure"
format = "ruff format ."
lint = "ruff check ."
mypy = "mypy"
pyright = "pyright"
bandit = "bandit -c pyproject.toml -r src"
# Audits only what ships: the locked runtime dependencies, no dependency
# groups. A CVE in a dev tool (e.g. filelock via pre-commit, whose fix needed a
# newer Python than we support) must not fail the build. Markers in the export
# are evaluated for the running Python, so each CI version audits its own set.
"pip-audit" = {shell = """
set -e
req="$(mktemp)"
trap 'rm -f "$req"' EXIT
uv export --frozen --no-emit-project --no-default-groups --quiet --output-file "$req" > /dev/null
pip-audit --disable-pip --requirement "$req"
"""}
complexity = "complexipy"
# Fast inner loop: the unit tests only, and deliberately WITHOUT --cov --
# `fail_under = 100` would otherwise fail every partial run.
test-unit = {cmd = "pytest -v -n=auto -W error -m unit", env = {PYTHONIOENCODING = "utf-8"}}
flakefinder = "pytest -n=auto -W error --flake-finder --flake-runs=200"
test = {cmd = "pytest -v -n=auto -W error --cov-config=pyproject.toml --cov=git_nested --junitxml=junit.xml -o junit_family=legacy --cov-report=term --cov-report=html --cov-report=xml", env = {PYTHONIOENCODING = "utf-8"}}
# The docs site, built twice: HTML for people, Markdown for AI tools. The
# Markdown copy goes under html/markdown/, so one publish step carries both.
# -W --keep-going: every warning fails the build, and all of them are shown.
docs = {shell = """
set -e
sphinx-build -b html -W --keep-going doc doc/_build/html
sphinx-build -b markdown -W --keep-going doc doc/_build/markdown
rm -rf doc/_build/html/.doctrees doc/_build/markdown/.doctrees doc/_build/html/markdown
cp -r doc/_build/markdown doc/_build/html/markdown
"""}
"docs-clean" = "rm -rf doc/_build"
# Build the standalone executable, then run the whole suite through it
# (GIT_NESTED_EXE makes conftest drive `git nested` on PATH instead of the
# module). Same two steps CI runs; see scripts/create-python-exe.sh.
exe = "scripts/create-python-exe.sh"
test-exe = ["exe", {cmd = "pytest -v -n=auto -W error", env = {PYTHONIOENCODING = "utf-8", GIT_NESTED_EXE = "dist/git-nested"}}]
clean = "rm -rf dist/ build/ *.egg-info src/*.egg-info"
build = ["clean", {cmd = "uv build"}]
publish-test = {cmd = "uv publish --publish-url https://test.pypi.org/legacy/"}
publish = "uv publish"