git-commit-guard 0.8.0__tar.gz → 0.9.0__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.
Files changed (26) hide show
  1. git_commit_guard-0.9.0/.github/workflows/lint-python.yml +24 -0
  2. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/PKG-INFO +34 -8
  3. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/README.md +33 -7
  4. git_commit_guard-0.9.0/ruff.toml +13 -0
  5. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/src/git_commit_guard/__init__.py +53 -6
  6. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/tests/test_git_commit_guard.py +193 -3
  7. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/.github/workflows/lint-commits.yml +0 -0
  8. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/.github/workflows/lint-md.yml +0 -0
  9. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/.github/workflows/test.yml +0 -0
  10. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/.gitignore +0 -0
  11. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/.pre-commit-hooks.yaml +0 -0
  12. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/.python-version +0 -0
  13. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/LICENSE +0 -0
  14. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/.gitignore +0 -0
  15. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/class_index.html +0 -0
  16. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
  17. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
  18. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/function_index.html +0 -0
  19. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/index.html +0 -0
  20. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
  21. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/status.json +0 -0
  22. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/style_cb_9ff733b0.css +0 -0
  23. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/htmlcov/z_262b75d81d1cf686___init___py.html +0 -0
  24. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/pyproject.toml +0 -0
  25. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/tests/__init__.py +0 -0
  26. {git_commit_guard-0.8.0 → git_commit_guard-0.9.0}/uv.lock +0 -0
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: Lint Python
3
+ on: # yamllint disable-line rule:truthy
4
+ pull_request:
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ lint-python:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ checks: write
12
+ pull-requests: write
13
+ steps:
14
+ - name: Checkout code
15
+ # yamllint disable-line rule:line-length
16
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
+ with:
18
+ persist-credentials: false
19
+ - name: Run ruff
20
+ # yamllint disable-line rule:line-length
21
+ uses: benner/action-ruff@b36eb590165f0ea36700bb92de15235c121f24f0 # v0.1.0
22
+ with:
23
+ fail_level: error
24
+ reporter: github-pr-review
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-guard
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Opinionated conventional commit message linter with imperative mood detection
5
5
  Project-URL: Homepage, https://github.com/benner/commit-guard
6
6
  Project-URL: Repository, https://github.com/benner/commit-guard
@@ -101,6 +101,28 @@ Available checks:
101
101
  * `signed-off` - `Signed-off-by:` trailer exists
102
102
  * `signature` - Verify GPG or SSH signature
103
103
 
104
+ ### Subject length
105
+
106
+ The default maximum subject line length is 72 characters. Override with
107
+ `--max-subject-length`:
108
+
109
+ ```bash
110
+ commit-guard --max-subject-length 100
111
+ ```
112
+
113
+ ### Type validation
114
+
115
+ By default the standard conventional commit types are accepted. Use `--types`
116
+ to replace the allowed set entirely:
117
+
118
+ ```bash
119
+ # restrict to a subset
120
+ commit-guard --types feat,fix,chore
121
+
122
+ # add a project-specific type
123
+ commit-guard --types feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert,wip
124
+ ```
125
+
104
126
  ### Scope validation
105
127
 
106
128
  By default any scope is accepted and scope is optional. Use `--scopes` to
@@ -121,15 +143,17 @@ commit-guard --scopes auth,api --require-scope
121
143
  ### Configuration file
122
144
 
123
145
  Place `.commit-guard.toml` in your project root (or any parent directory) to
124
- set defaults for `enable`, `disable`, `scopes`, and `require-scope`.
125
- commit-guard searches upward from the working directory and uses the first file
126
- found.
146
+ set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`, and
147
+ `max-subject-length`. commit-guard searches upward from the working directory
148
+ and uses the first file found.
127
149
 
128
150
  ```toml
129
151
  # .commit-guard.toml
130
152
  disable = ["signature", "body"]
131
153
  scopes = ["auth", "api", "db"]
132
154
  require-scope = true
155
+ types = ["feat", "fix", "chore", "wip"]
156
+ max-subject-length = 100
133
157
  ```
134
158
 
135
159
  ```toml
@@ -137,8 +161,9 @@ require-scope = true
137
161
  enable = ["subject", "imperative"]
138
162
  ```
139
163
 
140
- CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`) take full
141
- precedence and ignore config file values when provided.
164
+ CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
165
+ `--max-subject-length`) take full precedence and ignore config file values when
166
+ provided.
142
167
 
143
168
  ### Checking a range of commits
144
169
 
@@ -206,8 +231,9 @@ body
206
231
  trailers
207
232
  ```
208
233
 
209
- Supported types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
210
- `build`, `ci`, `chore`, `revert`.
234
+ Default types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
235
+ `build`, `ci`, `chore`, `revert`. Override with `--types` or the `types` config
236
+ key.
211
237
 
212
238
  Scope is optional. Mark breaking changes with `!` before
213
239
  the colon.
@@ -80,6 +80,28 @@ Available checks:
80
80
  * `signed-off` - `Signed-off-by:` trailer exists
81
81
  * `signature` - Verify GPG or SSH signature
82
82
 
83
+ ### Subject length
84
+
85
+ The default maximum subject line length is 72 characters. Override with
86
+ `--max-subject-length`:
87
+
88
+ ```bash
89
+ commit-guard --max-subject-length 100
90
+ ```
91
+
92
+ ### Type validation
93
+
94
+ By default the standard conventional commit types are accepted. Use `--types`
95
+ to replace the allowed set entirely:
96
+
97
+ ```bash
98
+ # restrict to a subset
99
+ commit-guard --types feat,fix,chore
100
+
101
+ # add a project-specific type
102
+ commit-guard --types feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert,wip
103
+ ```
104
+
83
105
  ### Scope validation
84
106
 
85
107
  By default any scope is accepted and scope is optional. Use `--scopes` to
@@ -100,15 +122,17 @@ commit-guard --scopes auth,api --require-scope
100
122
  ### Configuration file
101
123
 
102
124
  Place `.commit-guard.toml` in your project root (or any parent directory) to
103
- set defaults for `enable`, `disable`, `scopes`, and `require-scope`.
104
- commit-guard searches upward from the working directory and uses the first file
105
- found.
125
+ set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`, and
126
+ `max-subject-length`. commit-guard searches upward from the working directory
127
+ and uses the first file found.
106
128
 
107
129
  ```toml
108
130
  # .commit-guard.toml
109
131
  disable = ["signature", "body"]
110
132
  scopes = ["auth", "api", "db"]
111
133
  require-scope = true
134
+ types = ["feat", "fix", "chore", "wip"]
135
+ max-subject-length = 100
112
136
  ```
113
137
 
114
138
  ```toml
@@ -116,8 +140,9 @@ require-scope = true
116
140
  enable = ["subject", "imperative"]
117
141
  ```
118
142
 
119
- CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`) take full
120
- precedence and ignore config file values when provided.
143
+ CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
144
+ `--max-subject-length`) take full precedence and ignore config file values when
145
+ provided.
121
146
 
122
147
  ### Checking a range of commits
123
148
 
@@ -185,8 +210,9 @@ body
185
210
  trailers
186
211
  ```
187
212
 
188
- Supported types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
189
- `build`, `ci`, `chore`, `revert`.
213
+ Default types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
214
+ `build`, `ci`, `chore`, `revert`. Override with `--types` or the `types` config
215
+ key.
190
216
 
191
217
  Scope is optional. Mark breaking changes with `!` before
192
218
  the colon.
@@ -0,0 +1,13 @@
1
+ target-version = "py312"
2
+
3
+ [lint]
4
+ select = ["ALL"]
5
+ ignore = [
6
+ "ANN", # flake8-annotations (ANN)
7
+ "COM812", # missing-trailing-comma
8
+ "D", # pydocstyle (D)
9
+ "RET504", # Unnecessary assignment to X before `return` statement
10
+ ]
11
+
12
+ [lint.per-file-ignores]
13
+ "tests/**" = ["S101"] # assert is expected in tests
@@ -1,13 +1,13 @@
1
1
  import re
2
2
  import subprocess
3
3
  import sys
4
+ import tomllib
4
5
  from argparse import ArgumentParser
5
6
  from dataclasses import dataclass, field
6
7
  from enum import StrEnum
7
8
  from pathlib import Path
8
9
 
9
10
  import nltk
10
- import tomllib
11
11
  from nltk.corpus import wordnet
12
12
 
13
13
  TYPES = frozenset(
@@ -120,13 +120,21 @@ def _strip_comments(message):
120
120
  )
121
121
 
122
122
 
123
- def check_subject(line, result, allowed_scopes=frozenset(), *, require_scope=False):
123
+ def check_subject( # noqa: PLR0913 Too many arguments in function definition (6 > 5)
124
+ line,
125
+ result,
126
+ allowed_scopes=frozenset(),
127
+ allowed_types=TYPES,
128
+ max_subject_length=MAX_SUBJECT_LEN,
129
+ *,
130
+ require_scope=False,
131
+ ):
124
132
  m = SUBJECT_RE.match(line)
125
133
  if not m:
126
134
  result.error(f"subject does not match 'type(scope): description': {line}")
127
135
  return None
128
136
 
129
- if m.group("type") not in TYPES:
137
+ if m.group("type") not in allowed_types:
130
138
  result.error(f"unknown type: {m.group('type')}")
131
139
 
132
140
  scope = m.group("scope")
@@ -140,8 +148,8 @@ def check_subject(line, result, allowed_scopes=frozenset(), *, require_scope=Fal
140
148
  result.error("description must not start with uppercase")
141
149
  if desc.endswith("."):
142
150
  result.error("description must not end with period")
143
- if len(line) > MAX_SUBJECT_LEN:
144
- result.error(f"subject too long: {len(line)} > {MAX_SUBJECT_LEN}")
151
+ if len(line) > max_subject_length:
152
+ result.error(f"subject too long: {len(line)} > {max_subject_length}")
145
153
  return desc
146
154
 
147
155
 
@@ -223,6 +231,8 @@ class Args:
223
231
  enabled: frozenset
224
232
  allowed_scopes: frozenset
225
233
  require_scope: bool
234
+ allowed_types: frozenset
235
+ max_subject_length: int
226
236
 
227
237
 
228
238
  def _resolve_enabled(args, config, parser):
@@ -241,6 +251,22 @@ def _resolve_enabled(args, config, parser):
241
251
  return enabled
242
252
 
243
253
 
254
+ def _resolve_max_subject_length(args, config):
255
+ if args.max_subject_length is not None:
256
+ return args.max_subject_length
257
+ if "max-subject-length" in config:
258
+ return config["max-subject-length"]
259
+ return MAX_SUBJECT_LEN
260
+
261
+
262
+ def _resolve_types(args, config):
263
+ if args.types:
264
+ return frozenset(t.strip() for t in args.types.split(","))
265
+ if config.get("types"):
266
+ return frozenset(config["types"])
267
+ return TYPES
268
+
269
+
244
270
  def _resolve_scopes(args, config):
245
271
  if args.scopes:
246
272
  allowed_scopes = frozenset(s.strip() for s in args.scopes.split(","))
@@ -292,10 +318,24 @@ def _parse_args():
292
318
  default=False,
293
319
  help="require a scope in the subject line",
294
320
  )
321
+ parser.add_argument(
322
+ "--types",
323
+ metavar="TYPE[,TYPE,...]",
324
+ help="allowed commit types (replaces defaults when set)",
325
+ )
326
+ parser.add_argument(
327
+ "--max-subject-length",
328
+ type=int,
329
+ default=None,
330
+ metavar="N",
331
+ help=f"maximum subject line length (default: {MAX_SUBJECT_LEN})",
332
+ )
295
333
  args = parser.parse_args()
296
334
  config = _load_config()
297
335
  enabled = _resolve_enabled(args, config, parser)
298
336
  allowed_scopes, require_scope = _resolve_scopes(args, config)
337
+ allowed_types = _resolve_types(args, config)
338
+ max_subject_length = _resolve_max_subject_length(args, config)
299
339
 
300
340
  if args.message_file:
301
341
  rev = None
@@ -316,6 +356,8 @@ def _parse_args():
316
356
  enabled=enabled,
317
357
  allowed_scopes=allowed_scopes,
318
358
  require_scope=require_scope,
359
+ allowed_types=allowed_types,
360
+ max_subject_length=max_subject_length,
319
361
  )
320
362
 
321
363
 
@@ -341,7 +383,12 @@ def main():
341
383
  desc = None
342
384
  if Check.SUBJECT in args.enabled:
343
385
  desc = check_subject(
344
- lines[0], result, args.allowed_scopes, require_scope=args.require_scope
386
+ lines[0],
387
+ result,
388
+ args.allowed_scopes,
389
+ args.allowed_types,
390
+ args.max_subject_length,
391
+ require_scope=args.require_scope,
345
392
  )
346
393
  if Check.IMPERATIVE in args.enabled:
347
394
  if desc is None:
@@ -1,9 +1,12 @@
1
1
  import subprocess
2
- from argparse import ArgumentParser
2
+ from argparse import ArgumentParser, Namespace
3
3
  from unittest.mock import MagicMock, patch
4
4
 
5
5
  import pytest
6
+
6
7
  from git_commit_guard import (
8
+ MAX_SUBJECT_LEN,
9
+ TYPES,
7
10
  Result,
8
11
  _download_if_missing,
9
12
  _ensure_nltk_data,
@@ -12,6 +15,8 @@ from git_commit_guard import (
12
15
  _parse_checks,
13
16
  _parse_config_checks,
14
17
  _report,
18
+ _resolve_max_subject_length,
19
+ _resolve_types,
15
20
  _strip_comments,
16
21
  check_body,
17
22
  check_imperative,
@@ -21,8 +26,6 @@ from git_commit_guard import (
21
26
  main,
22
27
  )
23
28
 
24
- # ruff: noqa: S101 # Use of `assert` detected
25
-
26
29
 
27
30
  @pytest.fixture(scope="session", autouse=True)
28
31
  def nltk_data():
@@ -147,6 +150,26 @@ class TestCheckSubject:
147
150
  check_subject("fix(anything): add token", r, allowed_scopes=frozenset())
148
151
  assert r.ok
149
152
 
153
+ def test_custom_max_length_enforced(self):
154
+ r = Result()
155
+ check_subject("fix: add thing", r, max_subject_length=10)
156
+ assert not r.ok
157
+
158
+ def test_custom_max_length_passes(self):
159
+ r = Result()
160
+ check_subject("fix: ok", r, max_subject_length=10)
161
+ assert r.ok
162
+
163
+ def test_custom_type_passes(self):
164
+ r = Result()
165
+ check_subject("wip: add thing", r, allowed_types=frozenset(["wip"]))
166
+ assert r.ok
167
+
168
+ def test_type_not_in_custom_list_fails(self):
169
+ r = Result()
170
+ check_subject("feat: add thing", r, allowed_types=frozenset(["wip"]))
171
+ assert not r.ok
172
+
150
173
  @pytest.mark.parametrize(
151
174
  "type_",
152
175
  [
@@ -409,6 +432,45 @@ class TestParseConfigChecks:
409
432
  _parse_config_checks({"disable": ["bogus"]}, "disable")
410
433
 
411
434
 
435
+ class TestResolveMaxSubjectLength:
436
+ def test_defaults_when_no_config_or_flag(self):
437
+ result = _resolve_max_subject_length(Namespace(max_subject_length=None), {})
438
+ assert result == MAX_SUBJECT_LEN
439
+
440
+ def test_cli_flag_overrides_default(self):
441
+ result = _resolve_max_subject_length(Namespace(max_subject_length=50), {})
442
+ assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable
443
+
444
+ def test_config_overrides_default(self):
445
+ result = _resolve_max_subject_length(
446
+ Namespace(max_subject_length=None), {"max-subject-length": 60}
447
+ )
448
+ assert result == 60 # noqa: PLR2004 Magic value used in comparison, consider replacing 60 with a constant variable
449
+
450
+ def test_cli_overrides_config(self):
451
+ result = _resolve_max_subject_length(
452
+ Namespace(max_subject_length=50), {"max-subject-length": 60}
453
+ )
454
+ assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable
455
+
456
+
457
+ class TestResolveTypes:
458
+ def test_defaults_when_no_config_or_flag(self):
459
+ assert _resolve_types(Namespace(types=None), {}) == TYPES
460
+
461
+ def test_cli_flag_replaces_defaults(self):
462
+ result = _resolve_types(Namespace(types="wip,deploy"), {})
463
+ assert result == frozenset({"wip", "deploy"})
464
+
465
+ def test_config_replaces_defaults(self):
466
+ result = _resolve_types(Namespace(types=None), {"types": ["wip", "deploy"]})
467
+ assert result == frozenset({"wip", "deploy"})
468
+
469
+ def test_cli_overrides_config(self):
470
+ result = _resolve_types(Namespace(types="wip"), {"types": ["deploy"]})
471
+ assert result == frozenset({"wip"})
472
+
473
+
412
474
  class TestParseChecks:
413
475
  def test_invalid_check_name(self):
414
476
  parser = ArgumentParser()
@@ -641,3 +703,131 @@ class TestMain:
641
703
  ),
642
704
  ):
643
705
  assert main() == 0
706
+
707
+ def test_types_flag_valid(self, tmp_path):
708
+ f = tmp_path / "msg"
709
+ f.write_text("wip: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")
710
+ argv = [
711
+ "cg",
712
+ "--message-file",
713
+ str(f),
714
+ "--disable",
715
+ "signature",
716
+ "--types",
717
+ "wip,feat,fix",
718
+ ]
719
+ with patch("sys.argv", argv):
720
+ assert main() == 0
721
+
722
+ def test_types_flag_invalid(self, tmp_path):
723
+ f = tmp_path / "msg"
724
+ f.write_text("chore: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")
725
+ argv = [
726
+ "cg",
727
+ "--message-file",
728
+ str(f),
729
+ "--disable",
730
+ "signature",
731
+ "--types",
732
+ "feat,fix",
733
+ ]
734
+ with patch("sys.argv", argv):
735
+ assert main() == 1
736
+
737
+ def test_types_from_config(self, tmp_path):
738
+ f = tmp_path / "msg"
739
+ f.write_text("wip: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")
740
+ argv = ["cg", "--message-file", str(f), "--disable", "signature"]
741
+ with (
742
+ patch("sys.argv", argv),
743
+ patch(
744
+ "git_commit_guard._load_config",
745
+ return_value={"types": ["wip", "feat"]},
746
+ ),
747
+ ):
748
+ assert main() == 0
749
+
750
+ def test_max_subject_length_flag_passes(self, tmp_path):
751
+ f = tmp_path / "msg"
752
+ f.write_text("fix: ok\n\nbody\n\nSigned-off-by: A User <a@b.com>")
753
+ argv = [
754
+ "cg",
755
+ "--message-file",
756
+ str(f),
757
+ "--disable",
758
+ "signature",
759
+ "--max-subject-length",
760
+ "10",
761
+ ]
762
+ with patch("sys.argv", argv):
763
+ assert main() == 0
764
+
765
+ def test_max_subject_length_flag_fails(self, tmp_path):
766
+ f = tmp_path / "msg"
767
+ f.write_text(_VALID_MSG)
768
+ argv = [
769
+ "cg",
770
+ "--message-file",
771
+ str(f),
772
+ "--disable",
773
+ "signature",
774
+ "--max-subject-length",
775
+ "5",
776
+ ]
777
+ with patch("sys.argv", argv):
778
+ assert main() == 1
779
+
780
+ def test_max_subject_length_from_config(self, tmp_path):
781
+ f = tmp_path / "msg"
782
+ f.write_text(_VALID_MSG)
783
+ argv = ["cg", "--message-file", str(f), "--disable", "signature"]
784
+ with (
785
+ patch("sys.argv", argv),
786
+ patch(
787
+ "git_commit_guard._load_config",
788
+ return_value={"max-subject-length": 5},
789
+ ),
790
+ ):
791
+ assert main() == 1
792
+
793
+ def test_max_subject_length_cli_overrides_config(self, tmp_path):
794
+ f = tmp_path / "msg"
795
+ f.write_text(_VALID_MSG)
796
+ argv = [
797
+ "cg",
798
+ "--message-file",
799
+ str(f),
800
+ "--disable",
801
+ "signature",
802
+ "--max-subject-length",
803
+ "100",
804
+ ]
805
+ with (
806
+ patch("sys.argv", argv),
807
+ patch(
808
+ "git_commit_guard._load_config",
809
+ return_value={"max-subject-length": 5},
810
+ ),
811
+ ):
812
+ assert main() == 0
813
+
814
+ def test_types_cli_overrides_config(self, tmp_path):
815
+ f = tmp_path / "msg"
816
+ f.write_text("wip: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")
817
+ argv = [
818
+ "cg",
819
+ "--message-file",
820
+ str(f),
821
+ "--disable",
822
+ "signature",
823
+ "--types",
824
+ "wip",
825
+ ]
826
+ with (
827
+ patch("sys.argv", argv),
828
+ patch(
829
+ "git_commit_guard._load_config",
830
+ return_value={"types": ["deploy"]},
831
+ ),
832
+ ):
833
+ assert main() == 0