src-py-lib 0.1.6__tar.gz → 0.1.8__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.
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/.github/workflows/release.yml +55 -23
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/.github/workflows/validate.yml +2 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/.gitignore +2 -1
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/AGENTS.md +14 -97
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/PKG-INFO +1 -1
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/pyproject.toml +5 -2
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/__init__.py +4 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/sourcegraph.py +5 -3
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/config.py +121 -18
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/logging.py +4 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/tests/test_import.py +2 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/tests/test_logging_http_clients.py +107 -2
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/uv.lock +0 -1
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/.github/workflows/ci.yml +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/.markdownlint-cli2.yaml +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/.python-version +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/LICENSE +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/README.md +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/SECURITY.md +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/renovate.json +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/__init__.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/github.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/google_sheets.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/graphql.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/linear.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/one_password.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/clients/slack.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/py.typed +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/__init__.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/http.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/json_cache.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/json_types.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/src/src_py_lib/utils/tsv.py +0 -0
- {src_py_lib-0.1.6 → src_py_lib-0.1.8}/tests/test_tsv.py +0 -0
|
@@ -6,8 +6,8 @@ on:
|
|
|
6
6
|
- "v*"
|
|
7
7
|
workflow_dispatch:
|
|
8
8
|
inputs:
|
|
9
|
-
|
|
10
|
-
description: "
|
|
9
|
+
version:
|
|
10
|
+
description: "Package version to publish, for example 0.1.0 or v0.1.0"
|
|
11
11
|
required: true
|
|
12
12
|
type: string
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ permissions:
|
|
|
16
16
|
pull-requests: read
|
|
17
17
|
|
|
18
18
|
concurrency:
|
|
19
|
-
group: release-${{ github.
|
|
19
|
+
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
|
20
20
|
cancel-in-progress: false
|
|
21
21
|
|
|
22
22
|
defaults:
|
|
@@ -24,15 +24,41 @@ defaults:
|
|
|
24
24
|
shell: bash
|
|
25
25
|
|
|
26
26
|
jobs:
|
|
27
|
+
release_ref:
|
|
28
|
+
name: Resolve release tag
|
|
29
|
+
runs-on: ubuntu-24.04
|
|
30
|
+
outputs:
|
|
31
|
+
tag: ${{ steps.release.outputs.tag }}
|
|
32
|
+
version: ${{ steps.release.outputs.version }}
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- name: Resolve release tag
|
|
36
|
+
id: release
|
|
37
|
+
env:
|
|
38
|
+
RELEASE_INPUT: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
|
39
|
+
run: |
|
|
40
|
+
release_input="${RELEASE_INPUT}"
|
|
41
|
+
if [[ ! "${release_input}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
42
|
+
echo "::error title=Invalid release version::Use MAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH, got '${release_input}'."
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
release_version="${release_input#v}"
|
|
47
|
+
release_tag="v${release_version}"
|
|
48
|
+
echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
|
|
49
|
+
echo "version=${release_version}" >> "${GITHUB_OUTPUT}"
|
|
50
|
+
|
|
27
51
|
validate:
|
|
28
52
|
name: Validate
|
|
53
|
+
needs: release_ref
|
|
29
54
|
uses: ./.github/workflows/validate.yml
|
|
30
55
|
with:
|
|
31
|
-
ref: ${{
|
|
56
|
+
ref: ${{ needs.release_ref.outputs.tag }}
|
|
32
57
|
build-package: false
|
|
33
58
|
|
|
34
59
|
wheel:
|
|
35
60
|
name: Build wheel
|
|
61
|
+
needs: release_ref
|
|
36
62
|
runs-on: ubuntu-24.04
|
|
37
63
|
env:
|
|
38
64
|
IMPORT_NAME: src_py_lib
|
|
@@ -45,7 +71,7 @@ jobs:
|
|
|
45
71
|
with:
|
|
46
72
|
fetch-depth: 0
|
|
47
73
|
persist-credentials: false
|
|
48
|
-
ref: ${{
|
|
74
|
+
ref: ${{ needs.release_ref.outputs.tag }}
|
|
49
75
|
|
|
50
76
|
- name: Set up Python
|
|
51
77
|
uses: actions/setup-python@v6
|
|
@@ -66,7 +92,7 @@ jobs:
|
|
|
66
92
|
- name: Validate release inputs
|
|
67
93
|
id: release
|
|
68
94
|
env:
|
|
69
|
-
RELEASE_TAG: ${{
|
|
95
|
+
RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
|
|
70
96
|
run: |
|
|
71
97
|
release_tag="${RELEASE_TAG}"
|
|
72
98
|
if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
@@ -80,29 +106,19 @@ jobs:
|
|
|
80
106
|
tag_revision="$(git rev-list -n 1 "${release_tag}")"
|
|
81
107
|
git fetch --no-tags origin main
|
|
82
108
|
main_revision="$(git rev-parse origin/main)"
|
|
83
|
-
if
|
|
84
|
-
echo "::error title=Tag is not
|
|
85
|
-
echo "::error::
|
|
86
|
-
exit 1
|
|
87
|
-
fi
|
|
88
|
-
|
|
89
|
-
project_version=$(uv run --frozen python - <<'PY'
|
|
90
|
-
import tomllib
|
|
91
|
-
|
|
92
|
-
with open("pyproject.toml", "rb") as pyproject_file:
|
|
93
|
-
print(tomllib.load(pyproject_file)["project"]["version"])
|
|
94
|
-
PY
|
|
95
|
-
)
|
|
96
|
-
if [[ "v${project_version}" != "${release_tag}" ]]; then
|
|
97
|
-
echo "::error title=Version mismatch::pyproject.toml version '${project_version}' does not match tag '${release_tag}'."
|
|
109
|
+
if [[ "${tag_revision}" != "${main_revision}" ]]; then
|
|
110
|
+
echo "::error title=Tag is not origin/main::Tag '${release_tag}' points at ${tag_revision}, but origin/main is ${main_revision}."
|
|
111
|
+
echo "::error::Tag the remote head of main, then rerun the release."
|
|
98
112
|
exit 1
|
|
99
113
|
fi
|
|
100
114
|
|
|
101
115
|
echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
|
|
116
|
+
echo "version=${release_tag#v}" >> "${GITHUB_OUTPUT}"
|
|
102
117
|
|
|
103
118
|
- name: Build distributions
|
|
104
119
|
id: build
|
|
105
120
|
run: |
|
|
121
|
+
release_version="${{ steps.release.outputs.version }}"
|
|
106
122
|
dist_dir="build/release/dist"
|
|
107
123
|
rm -rf build/release
|
|
108
124
|
mkdir -p "${dist_dir}"
|
|
@@ -123,6 +139,22 @@ jobs:
|
|
|
123
139
|
wheel_name="$(basename "${wheel_path}")"
|
|
124
140
|
source_distribution_path="${source_distributions[0]}"
|
|
125
141
|
source_distribution_name="$(basename "${source_distribution_path}")"
|
|
142
|
+
case "${wheel_name}" in
|
|
143
|
+
src_py_lib-"${release_version}"-*.whl)
|
|
144
|
+
;;
|
|
145
|
+
*)
|
|
146
|
+
echo "::error title=Wheel version mismatch::Expected wheel version ${release_version}, got '${wheel_name}'."
|
|
147
|
+
exit 1
|
|
148
|
+
;;
|
|
149
|
+
esac
|
|
150
|
+
case "${source_distribution_name}" in
|
|
151
|
+
src_py_lib-"${release_version}".tar.gz)
|
|
152
|
+
;;
|
|
153
|
+
*)
|
|
154
|
+
echo "::error title=Source distribution version mismatch::Expected source distribution version ${release_version}, got '${source_distribution_name}'."
|
|
155
|
+
exit 1
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
126
158
|
wheel_checksum_path="${wheel_path}.sha256"
|
|
127
159
|
source_distribution_checksum_path="${source_distribution_path}.sha256"
|
|
128
160
|
|
|
@@ -214,7 +246,7 @@ jobs:
|
|
|
214
246
|
|
|
215
247
|
github-release:
|
|
216
248
|
name: Publish GitHub release assets
|
|
217
|
-
needs: [validate, wheel]
|
|
249
|
+
needs: [release_ref, validate, wheel]
|
|
218
250
|
runs-on: ubuntu-24.04
|
|
219
251
|
permissions:
|
|
220
252
|
contents: write
|
|
@@ -230,7 +262,7 @@ jobs:
|
|
|
230
262
|
env:
|
|
231
263
|
GH_TOKEN: ${{ github.token }}
|
|
232
264
|
GH_REPO: ${{ github.repository }}
|
|
233
|
-
RELEASE_TAG: ${{
|
|
265
|
+
RELEASE_TAG: ${{ needs.release_ref.outputs.tag }}
|
|
234
266
|
run: |
|
|
235
267
|
release_tag="${RELEASE_TAG}"
|
|
236
268
|
notes_path="$(find release-assets -name release-notes.md -print -quit)"
|
|
@@ -158,6 +158,7 @@ jobs:
|
|
|
158
158
|
- name: Check out code
|
|
159
159
|
uses: actions/checkout@v6
|
|
160
160
|
with:
|
|
161
|
+
fetch-depth: 0
|
|
161
162
|
persist-credentials: false
|
|
162
163
|
ref: ${{ inputs.ref || github.ref }}
|
|
163
164
|
|
|
@@ -217,6 +218,7 @@ jobs:
|
|
|
217
218
|
- name: Check out code
|
|
218
219
|
uses: actions/checkout@v6
|
|
219
220
|
with:
|
|
221
|
+
fetch-depth: 0
|
|
220
222
|
persist-credentials: false
|
|
221
223
|
ref: ${{ inputs.ref || github.ref }}
|
|
222
224
|
|
|
@@ -51,111 +51,28 @@ uv run python -m unittest discover -s tests
|
|
|
51
51
|
|
|
52
52
|
## Release process
|
|
53
53
|
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
- Package versions are derived from Git tags through `hatch-vcs`.
|
|
55
|
+
- `pyproject.toml` must use `dynamic = ["version"]`; do not add a hard-coded
|
|
56
|
+
`project.version` for releases.
|
|
57
|
+
- The release tag must be `vMAJOR.MINOR.PATCH` and point at a commit reachable
|
|
58
|
+
from `origin/main`.
|
|
59
|
+
- The release workflow builds from the tag and checks that wheel and source
|
|
60
|
+
distribution filenames match the tag version before publishing.
|
|
61
|
+
- Do not make the release workflow edit `pyproject.toml` or `uv.lock`.
|
|
62
|
+
- Tag the remote head of `main` directly:
|
|
63
63
|
|
|
64
64
|
```sh
|
|
65
65
|
set -euo pipefail
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
git fetch origin --tags --prune
|
|
71
|
-
git switch main
|
|
72
|
-
git pull --ff-only
|
|
73
|
-
git switch -c "${BRANCH}"
|
|
74
|
-
|
|
75
|
-
uv run python - "${VERSION}" <<'PY'
|
|
76
|
-
from pathlib import Path
|
|
77
|
-
import re
|
|
78
|
-
import sys
|
|
79
|
-
|
|
80
|
-
version = sys.argv[1]
|
|
81
|
-
path = Path("pyproject.toml")
|
|
82
|
-
text = path.read_text()
|
|
83
|
-
new_text = re.sub(
|
|
84
|
-
r'(?m)^version = "[^"]+"$',
|
|
85
|
-
f'version = "{version}"',
|
|
86
|
-
text,
|
|
87
|
-
count=1,
|
|
88
|
-
)
|
|
89
|
-
if new_text == text:
|
|
90
|
-
raise SystemExit("pyproject.toml version was not updated")
|
|
91
|
-
path.write_text(new_text)
|
|
92
|
-
PY
|
|
93
|
-
|
|
94
|
-
uv lock
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
- Validate before opening the PR:
|
|
98
|
-
|
|
99
|
-
```sh
|
|
100
|
-
set -euo pipefail
|
|
101
|
-
|
|
102
|
-
uv lock --check
|
|
103
|
-
actionlint
|
|
104
|
-
npx --yes markdownlint-cli2@0.22.1
|
|
105
|
-
uv run ruff check .
|
|
106
|
-
uv run ruff format --check .
|
|
107
|
-
uv run pyright
|
|
108
|
-
uv run python -m unittest discover -s tests
|
|
109
|
-
uv build --wheel --sdist --out-dir /tmp/src-py-lib-release-check --no-create-gitignore
|
|
110
|
-
rm -rf /tmp/src-py-lib-release-check
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
- Commit, push, open the PR, wait for checks, then merge it. If review is
|
|
114
|
-
required, stop after `gh pr checks` and ask for review before merging.
|
|
115
|
-
|
|
116
|
-
```sh
|
|
117
|
-
set -euo pipefail
|
|
118
|
-
|
|
119
|
-
VERSION=0.1.6
|
|
120
|
-
BRANCH="release-v${VERSION}"
|
|
67
|
+
VERSION_INPUT=<next-version>
|
|
68
|
+
VERSION="${VERSION_INPUT#v}"
|
|
121
69
|
GH_REPO="sourcegraph/src-py-lib"
|
|
122
70
|
|
|
123
|
-
|
|
124
|
-
git commit -m "Release v${VERSION}"
|
|
125
|
-
git push -u origin "${BRANCH}"
|
|
126
|
-
|
|
127
|
-
gh pr create \
|
|
128
|
-
--repo "${GH_REPO}" \
|
|
129
|
-
--base main \
|
|
130
|
-
--head "${BRANCH}" \
|
|
131
|
-
--title "Release v${VERSION}" \
|
|
132
|
-
--body "Bump src-py-lib package metadata to ${VERSION}."
|
|
133
|
-
|
|
134
|
-
gh pr checks "${BRANCH}" --repo "${GH_REPO}" --watch --fail-fast
|
|
135
|
-
gh pr merge "${BRANCH}" --repo "${GH_REPO}" --squash --delete-branch
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
- Tag the merged `main` commit. Do not tag a branch commit.
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
set -euo pipefail
|
|
142
|
-
|
|
143
|
-
VERSION=0.1.6
|
|
144
|
-
|
|
71
|
+
[[ "${VERSION_INPUT}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
|
145
72
|
git fetch origin --tags --prune
|
|
146
|
-
git
|
|
147
|
-
git
|
|
148
|
-
git tag "v${VERSION}"
|
|
73
|
+
MAIN_COMMIT="$(git rev-parse origin/main)"
|
|
74
|
+
git tag -a "v${VERSION}" "${MAIN_COMMIT}" -m "Release v${VERSION}"
|
|
149
75
|
git push origin "v${VERSION}"
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
- Watch the release workflow and confirm the GitHub release and PyPI project.
|
|
153
|
-
|
|
154
|
-
```sh
|
|
155
|
-
set -euo pipefail
|
|
156
|
-
|
|
157
|
-
VERSION=0.1.6
|
|
158
|
-
GH_REPO="sourcegraph/src-py-lib"
|
|
159
76
|
|
|
160
77
|
RUN_ID="$(
|
|
161
78
|
gh run list \
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: src-py-lib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Reusable libraries for Sourcegraph projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/sourcegraph/src-py-lib
|
|
6
6
|
Project-URL: Issues, https://github.com/sourcegraph/src-py-lib/issues
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[dependency-groups]
|
|
@@ -10,7 +10,7 @@ dev = [
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "src-py-lib"
|
|
13
|
-
|
|
13
|
+
dynamic = ["version"]
|
|
14
14
|
description = "Reusable libraries for Sourcegraph projects"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.11"
|
|
@@ -44,6 +44,9 @@ Issues = "https://github.com/sourcegraph/src-py-lib/issues"
|
|
|
44
44
|
[tool.hatch.build.targets.wheel]
|
|
45
45
|
packages = ["src/src_py_lib"]
|
|
46
46
|
|
|
47
|
+
[tool.hatch.version]
|
|
48
|
+
source = "vcs"
|
|
49
|
+
|
|
47
50
|
[tool.pyright]
|
|
48
51
|
include = ["src/src_py_lib", "tests"]
|
|
49
52
|
typeCheckingMode = "strict"
|
|
@@ -52,6 +52,8 @@ from src_py_lib.utils.config import (
|
|
|
52
52
|
Config,
|
|
53
53
|
ConfigError,
|
|
54
54
|
config_field,
|
|
55
|
+
config_field_names,
|
|
56
|
+
config_help_formatter,
|
|
55
57
|
config_snapshot,
|
|
56
58
|
)
|
|
57
59
|
from src_py_lib.utils.config import (
|
|
@@ -150,6 +152,8 @@ __all__ = [
|
|
|
150
152
|
"TraceContext",
|
|
151
153
|
"aliased_batched_query",
|
|
152
154
|
"config_field",
|
|
155
|
+
"config_field_names",
|
|
156
|
+
"config_help_formatter",
|
|
153
157
|
"config_snapshot",
|
|
154
158
|
"configure_logging",
|
|
155
159
|
"critical",
|
|
@@ -25,7 +25,6 @@ from src_py_lib.utils.logging import (
|
|
|
25
25
|
traceparent_header,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
DEFAULT_SOURCEGRAPH_ENDPOINT = "https://sourcegraph.com"
|
|
29
28
|
SOURCEGRAPH_EXTERNAL_SERVICE_NODE_TYPE: Final[str] = "ExternalService"
|
|
30
29
|
SOURCEGRAPH_REPOSITORY_NODE_TYPE: Final[str] = "Repository"
|
|
31
30
|
REQUEST_TRACE_HEADER: Final[str] = "X-Sourcegraph-Request-Trace"
|
|
@@ -158,11 +157,13 @@ class SourcegraphClientConfig(Config):
|
|
|
158
157
|
"""Config fields needed to build a Sourcegraph API client."""
|
|
159
158
|
|
|
160
159
|
src_endpoint: str = config_field(
|
|
161
|
-
default=
|
|
160
|
+
default="",
|
|
162
161
|
env_var="SRC_ENDPOINT",
|
|
163
162
|
cli_flag="--src-endpoint",
|
|
164
163
|
metavar="URL",
|
|
165
|
-
help=
|
|
164
|
+
help="Sourcegraph instance URL",
|
|
165
|
+
help_group="Sourcegraph",
|
|
166
|
+
required=True,
|
|
166
167
|
)
|
|
167
168
|
src_access_token: str = config_field(
|
|
168
169
|
default="",
|
|
@@ -170,6 +171,7 @@ class SourcegraphClientConfig(Config):
|
|
|
170
171
|
cli_flag="--src-access-token",
|
|
171
172
|
metavar="TOKEN",
|
|
172
173
|
help="Sourcegraph access token, or op:// secret reference",
|
|
174
|
+
help_group="Sourcegraph",
|
|
173
175
|
secret=True,
|
|
174
176
|
required=True,
|
|
175
177
|
)
|
|
@@ -16,7 +16,7 @@ from collections.abc import Iterable, Mapping, Sequence
|
|
|
16
16
|
from dataclasses import dataclass, replace
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from types import UnionType
|
|
19
|
-
from typing import Any, Final, Literal, TypeVar, Union, cast, get_args, get_origin
|
|
19
|
+
from typing import Any, Final, Literal, TypeAlias, TypeVar, Union, cast, get_args, get_origin
|
|
20
20
|
|
|
21
21
|
from dotenv import dotenv_values
|
|
22
22
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
@@ -33,6 +33,7 @@ DEFAULT_CONFIG_ENV_FILE: Final[Path] = Path(".env")
|
|
|
33
33
|
CONFIG_HELP_MIN_POSITION: Final[int] = 24
|
|
34
34
|
CONFIG_HELP_MAX_POSITION_LIMIT: Final[int] = 48
|
|
35
35
|
CONFIG_HELP_PADDING: Final[int] = 4
|
|
36
|
+
DEFAULT_CONFIG_HELP_GROUP: Final[str] = "Config"
|
|
36
37
|
_CONFIG_OPTION_KEY: Final[str] = "src_py_lib_config_option"
|
|
37
38
|
_MISSING: Final[object] = object()
|
|
38
39
|
|
|
@@ -67,6 +68,7 @@ class ConfigOption:
|
|
|
67
68
|
cli_const: object | None = None
|
|
68
69
|
metavar: str | None = None
|
|
69
70
|
help: str = ""
|
|
71
|
+
help_group: str = DEFAULT_CONFIG_HELP_GROUP
|
|
70
72
|
secret: bool = False
|
|
71
73
|
required: bool = False
|
|
72
74
|
|
|
@@ -78,6 +80,7 @@ class Config(BaseModel):
|
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
ConfigType = TypeVar("ConfigType", bound=Config)
|
|
83
|
+
ConfigFieldSource: TypeAlias = str | type[Config]
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
def config_field(
|
|
@@ -91,6 +94,7 @@ def config_field(
|
|
|
91
94
|
cli_const: object | None = None,
|
|
92
95
|
metavar: str | None = None,
|
|
93
96
|
help: str = "",
|
|
97
|
+
help_group: str = DEFAULT_CONFIG_HELP_GROUP,
|
|
94
98
|
secret: bool = False,
|
|
95
99
|
required: bool = False,
|
|
96
100
|
gt: int | float | None = None,
|
|
@@ -110,6 +114,7 @@ def config_field(
|
|
|
110
114
|
cli_const=cli_const,
|
|
111
115
|
metavar=metavar,
|
|
112
116
|
help=help,
|
|
117
|
+
help_group=help_group,
|
|
113
118
|
secret=secret,
|
|
114
119
|
required=required,
|
|
115
120
|
)
|
|
@@ -140,6 +145,38 @@ def config_options(config_cls: type[Config]) -> tuple[ConfigOption, ...]:
|
|
|
140
145
|
return tuple(options)
|
|
141
146
|
|
|
142
147
|
|
|
148
|
+
def config_field_names(*sources: ConfigFieldSource) -> tuple[str, ...]:
|
|
149
|
+
"""Return Config field names from Config classes and explicit field names.
|
|
150
|
+
|
|
151
|
+
Use this to define reusable CLI argument sets from Config mixins while
|
|
152
|
+
keeping the field metadata defined once on the Config classes.
|
|
153
|
+
"""
|
|
154
|
+
names: list[str] = []
|
|
155
|
+
for source in sources:
|
|
156
|
+
if isinstance(source, str):
|
|
157
|
+
names.append(source)
|
|
158
|
+
continue
|
|
159
|
+
names.extend(option.field_name for option in config_options(source))
|
|
160
|
+
return tuple(dict.fromkeys(names))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def config_help_formatter(
|
|
164
|
+
config_cls: type[Config],
|
|
165
|
+
*,
|
|
166
|
+
include_env_file: bool = True,
|
|
167
|
+
include_fields: Iterable[str] | None = None,
|
|
168
|
+
exclude_fields: Iterable[str] = (),
|
|
169
|
+
) -> type[argparse.HelpFormatter]:
|
|
170
|
+
"""Return a help formatter aligned for the selected Config fields."""
|
|
171
|
+
max_help_position = _config_help_max_position(
|
|
172
|
+
config_cls,
|
|
173
|
+
include_env_file=include_env_file,
|
|
174
|
+
include_fields=include_fields,
|
|
175
|
+
exclude_fields=exclude_fields,
|
|
176
|
+
)
|
|
177
|
+
return _config_help_formatter(max_help_position)
|
|
178
|
+
|
|
179
|
+
|
|
143
180
|
def load_config_env_file(path: Path | None) -> dict[str, str]:
|
|
144
181
|
"""Load key/value pairs from a `.env` file.
|
|
145
182
|
|
|
@@ -192,22 +229,19 @@ def add_config_arguments(
|
|
|
192
229
|
config_cls: type[Config],
|
|
193
230
|
*,
|
|
194
231
|
include_env_file: bool = True,
|
|
232
|
+
include_fields: Iterable[str] | None = None,
|
|
233
|
+
exclude_fields: Iterable[str] = (),
|
|
195
234
|
) -> None:
|
|
196
235
|
"""Add Config CLI flags to an argparse parser."""
|
|
197
|
-
|
|
198
|
-
"Config",
|
|
199
|
-
"These options override matching environment variables and .env values",
|
|
200
|
-
)
|
|
201
|
-
if include_env_file:
|
|
202
|
-
group.add_argument(
|
|
203
|
-
"--env-file",
|
|
204
|
-
dest="env_file",
|
|
205
|
-
default=None,
|
|
206
|
-
metavar="PATH",
|
|
207
|
-
help="Read Config .env values from PATH (default: .env)",
|
|
208
|
-
)
|
|
236
|
+
groups: dict[str, Any] = {}
|
|
209
237
|
|
|
210
|
-
|
|
238
|
+
def argument_group(title: str) -> Any:
|
|
239
|
+
if title not in groups:
|
|
240
|
+
groups[title] = parser.add_argument_group(title)
|
|
241
|
+
return groups[title]
|
|
242
|
+
|
|
243
|
+
for option in _selected_config_options(config_cls, include_fields, exclude_fields):
|
|
244
|
+
group = argument_group(option.help_group or DEFAULT_CONFIG_HELP_GROUP)
|
|
211
245
|
field_info = config_cls.model_fields[option.field_name]
|
|
212
246
|
argument_kwargs: dict[str, Any] = {
|
|
213
247
|
"dest": option.field_name,
|
|
@@ -227,6 +261,15 @@ def add_config_arguments(
|
|
|
227
261
|
argument_kwargs["action"] = option.cli_action
|
|
228
262
|
group.add_argument(option.cli_flag, *option.cli_aliases, **argument_kwargs)
|
|
229
263
|
|
|
264
|
+
if include_env_file:
|
|
265
|
+
argument_group(DEFAULT_CONFIG_HELP_GROUP).add_argument(
|
|
266
|
+
"--env-file",
|
|
267
|
+
dest="env_file",
|
|
268
|
+
default=None,
|
|
269
|
+
metavar="PATH",
|
|
270
|
+
help="Read Config .env values from PATH (default: .env)",
|
|
271
|
+
)
|
|
272
|
+
|
|
230
273
|
|
|
231
274
|
def config_parse_args(
|
|
232
275
|
config_cls: type[ConfigType],
|
|
@@ -235,6 +278,8 @@ def config_parse_args(
|
|
|
235
278
|
argv: Sequence[str] | None = None,
|
|
236
279
|
description: str | None = None,
|
|
237
280
|
include_env_file: bool = True,
|
|
281
|
+
include_fields: Iterable[str] | None = None,
|
|
282
|
+
exclude_fields: Iterable[str] = (),
|
|
238
283
|
env: Mapping[str, str] | None = None,
|
|
239
284
|
base_dir: Path | None = None,
|
|
240
285
|
resolve_op_refs: bool = True,
|
|
@@ -242,12 +287,23 @@ def config_parse_args(
|
|
|
242
287
|
require: Iterable[str] = (),
|
|
243
288
|
) -> ConfigType:
|
|
244
289
|
"""Parse Config CLI flags and return a validated Config model."""
|
|
245
|
-
|
|
290
|
+
formatter_class = config_help_formatter(
|
|
291
|
+
config_cls,
|
|
292
|
+
include_env_file=include_env_file,
|
|
293
|
+
include_fields=include_fields,
|
|
294
|
+
exclude_fields=exclude_fields,
|
|
295
|
+
)
|
|
246
296
|
argument_parser = parser or argparse.ArgumentParser(
|
|
247
297
|
description=description,
|
|
248
|
-
formatter_class=
|
|
298
|
+
formatter_class=formatter_class,
|
|
299
|
+
)
|
|
300
|
+
add_config_arguments(
|
|
301
|
+
argument_parser,
|
|
302
|
+
config_cls,
|
|
303
|
+
include_env_file=include_env_file,
|
|
304
|
+
include_fields=include_fields,
|
|
305
|
+
exclude_fields=exclude_fields,
|
|
249
306
|
)
|
|
250
|
-
add_config_arguments(argument_parser, config_cls, include_env_file=include_env_file)
|
|
251
307
|
args = argument_parser.parse_args(argv)
|
|
252
308
|
try:
|
|
253
309
|
return load_config_from_args(
|
|
@@ -277,12 +333,14 @@ def _config_help_max_position(
|
|
|
277
333
|
config_cls: type[Config],
|
|
278
334
|
*,
|
|
279
335
|
include_env_file: bool,
|
|
336
|
+
include_fields: Iterable[str] | None = None,
|
|
337
|
+
exclude_fields: Iterable[str] = (),
|
|
280
338
|
) -> int:
|
|
281
339
|
"""Return help-column width based on this Config's CLI arguments."""
|
|
282
340
|
invocation_lengths = [len("--env-file PATH")] if include_env_file else []
|
|
283
341
|
invocation_lengths.extend(
|
|
284
342
|
_config_option_invocation_length(config_cls, option)
|
|
285
|
-
for option in
|
|
343
|
+
for option in _selected_config_options(config_cls, include_fields, exclude_fields)
|
|
286
344
|
)
|
|
287
345
|
longest_invocation = max(invocation_lengths, default=0)
|
|
288
346
|
return min(
|
|
@@ -291,6 +349,48 @@ def _config_help_max_position(
|
|
|
291
349
|
)
|
|
292
350
|
|
|
293
351
|
|
|
352
|
+
def _selected_config_options(
|
|
353
|
+
config_cls: type[Config],
|
|
354
|
+
include_fields: Iterable[str] | None,
|
|
355
|
+
exclude_fields: Iterable[str],
|
|
356
|
+
) -> tuple[ConfigOption, ...]:
|
|
357
|
+
"""Return options selected by field or env-var names.
|
|
358
|
+
|
|
359
|
+
When include_fields is set, its order controls the returned option order.
|
|
360
|
+
Without include_fields, Config model field order is preserved.
|
|
361
|
+
"""
|
|
362
|
+
options = config_options(config_cls)
|
|
363
|
+
excluded = _selected_config_field_names(options, exclude_fields) or set()
|
|
364
|
+
if include_fields is None:
|
|
365
|
+
return tuple(option for option in options if not _option_is_selected(option, excluded))
|
|
366
|
+
|
|
367
|
+
options_by_field_name = {option.field_name: option for option in options}
|
|
368
|
+
return tuple(
|
|
369
|
+
options_by_field_name[field_name]
|
|
370
|
+
for field_name in _selected_config_field_names_in_order(options, include_fields)
|
|
371
|
+
if field_name not in excluded
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _selected_config_field_names_in_order(
|
|
376
|
+
options: tuple[ConfigOption, ...],
|
|
377
|
+
selected: Iterable[str],
|
|
378
|
+
) -> tuple[str, ...]:
|
|
379
|
+
"""Return selected field names in caller order after validating names."""
|
|
380
|
+
names = (_option_by_name(options, name).field_name for name in selected)
|
|
381
|
+
return tuple(dict.fromkeys(names))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _selected_config_field_names(
|
|
385
|
+
options: tuple[ConfigOption, ...],
|
|
386
|
+
selected: Iterable[str] | None,
|
|
387
|
+
) -> set[str] | None:
|
|
388
|
+
"""Return selected field names after validating field or env-var names."""
|
|
389
|
+
if selected is None:
|
|
390
|
+
return None
|
|
391
|
+
return {_option_by_name(options, name).field_name for name in selected}
|
|
392
|
+
|
|
393
|
+
|
|
294
394
|
def _config_option_invocation_length(config_cls: type[Config], option: ConfigOption) -> int:
|
|
295
395
|
"""Return argparse-style option invocation length for help alignment."""
|
|
296
396
|
field_info = config_cls.model_fields[option.field_name]
|
|
@@ -452,6 +552,7 @@ def _config_option_payload(option: ConfigOption) -> dict[str, object]:
|
|
|
452
552
|
"cli_const": option.cli_const,
|
|
453
553
|
"metavar": option.metavar,
|
|
454
554
|
"help": option.help,
|
|
555
|
+
"help_group": option.help_group,
|
|
455
556
|
"secret": option.secret,
|
|
456
557
|
"required": option.required,
|
|
457
558
|
}
|
|
@@ -467,6 +568,7 @@ def _config_option_from_payload(payload: Mapping[str, object]) -> ConfigOption |
|
|
|
467
568
|
cli_nargs = payload.get("cli_nargs")
|
|
468
569
|
metavar = payload.get("metavar")
|
|
469
570
|
help_text = payload.get("help")
|
|
571
|
+
help_group = payload.get("help_group")
|
|
470
572
|
return ConfigOption(
|
|
471
573
|
field_name="",
|
|
472
574
|
env_var=env_var,
|
|
@@ -477,6 +579,7 @@ def _config_option_from_payload(payload: Mapping[str, object]) -> ConfigOption |
|
|
|
477
579
|
cli_const=payload.get("cli_const"),
|
|
478
580
|
metavar=metavar if isinstance(metavar, str) else None,
|
|
479
581
|
help=help_text if isinstance(help_text, str) else "",
|
|
582
|
+
help_group=help_group if isinstance(help_group, str) else DEFAULT_CONFIG_HELP_GROUP,
|
|
480
583
|
secret=payload.get("secret") is True,
|
|
481
584
|
required=payload.get("required") is True,
|
|
482
585
|
)
|
|
@@ -100,6 +100,7 @@ class LoggingConfig(Config):
|
|
|
100
100
|
cli_flag="--src-log-level",
|
|
101
101
|
metavar="LEVEL",
|
|
102
102
|
help="Log level (default: INFO)",
|
|
103
|
+
help_group="Logging",
|
|
103
104
|
)
|
|
104
105
|
verbose: bool = config_field(
|
|
105
106
|
default=False,
|
|
@@ -108,6 +109,7 @@ class LoggingConfig(Config):
|
|
|
108
109
|
cli_aliases=("-v",),
|
|
109
110
|
cli_action="store_true",
|
|
110
111
|
help="Alias for --src-log-level DEBUG",
|
|
112
|
+
help_group="Logging",
|
|
111
113
|
)
|
|
112
114
|
quiet: bool = config_field(
|
|
113
115
|
default=False,
|
|
@@ -116,6 +118,7 @@ class LoggingConfig(Config):
|
|
|
116
118
|
cli_aliases=("-q",),
|
|
117
119
|
cli_action="store_true",
|
|
118
120
|
help="Alias for --src-log-level WARNING",
|
|
121
|
+
help_group="Logging",
|
|
119
122
|
)
|
|
120
123
|
silent: bool = config_field(
|
|
121
124
|
default=False,
|
|
@@ -124,6 +127,7 @@ class LoggingConfig(Config):
|
|
|
124
127
|
cli_aliases=("-s",),
|
|
125
128
|
cli_action="store_true",
|
|
126
129
|
help="Alias for --src-log-level ERROR",
|
|
130
|
+
help_group="Logging",
|
|
127
131
|
)
|
|
128
132
|
|
|
129
133
|
@model_validator(mode="after")
|
|
@@ -27,6 +27,8 @@ class PackageImportTest(unittest.TestCase):
|
|
|
27
27
|
self.assertIsNotNone(src_py_lib.SourcegraphClient)
|
|
28
28
|
self.assertIsNotNone(src_py_lib.SourcegraphClientConfig)
|
|
29
29
|
self.assertIsNotNone(src_py_lib.config_field)
|
|
30
|
+
self.assertIsNotNone(src_py_lib.config_field_names)
|
|
31
|
+
self.assertIsNotNone(src_py_lib.config_help_formatter)
|
|
30
32
|
self.assertIsNotNone(src_py_lib.gh_cli_token)
|
|
31
33
|
self.assertIsNotNone(src_py_lib.gcloud_adc_access_token)
|
|
32
34
|
self.assertIsNotNone(src_py_lib.info)
|
|
@@ -50,6 +50,7 @@ from src_py_lib.utils.config import (
|
|
|
50
50
|
add_config_arguments,
|
|
51
51
|
config_env_file_from_args,
|
|
52
52
|
config_field,
|
|
53
|
+
config_field_names,
|
|
53
54
|
config_overrides_from_args,
|
|
54
55
|
config_parse_args,
|
|
55
56
|
config_snapshot,
|
|
@@ -198,6 +199,32 @@ class MultilineHelpConfig(Config):
|
|
|
198
199
|
)
|
|
199
200
|
|
|
200
201
|
|
|
202
|
+
class GroupedHelpConfig(Config):
|
|
203
|
+
"""Config model with grouped help sections."""
|
|
204
|
+
|
|
205
|
+
alpha: str = config_field(
|
|
206
|
+
default="",
|
|
207
|
+
env_var="GROUPED_HELP_ALPHA",
|
|
208
|
+
cli_flag="--alpha",
|
|
209
|
+
help="Alpha option",
|
|
210
|
+
help_group="First group",
|
|
211
|
+
)
|
|
212
|
+
beta: str = config_field(
|
|
213
|
+
default="",
|
|
214
|
+
env_var="GROUPED_HELP_BETA",
|
|
215
|
+
cli_flag="--beta",
|
|
216
|
+
help="Beta option",
|
|
217
|
+
help_group="Second group",
|
|
218
|
+
)
|
|
219
|
+
gamma: str = config_field(
|
|
220
|
+
default="",
|
|
221
|
+
env_var="GROUPED_HELP_GAMMA",
|
|
222
|
+
cli_flag="--gamma",
|
|
223
|
+
help="Gamma option",
|
|
224
|
+
help_group="First group",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
201
228
|
class SnapshotOrderConfig(Config):
|
|
202
229
|
"""Config model whose field names and env-var names sort differently."""
|
|
203
230
|
|
|
@@ -340,6 +367,8 @@ class ConfigTest(unittest.TestCase):
|
|
|
340
367
|
add_config_arguments(parser, SourcegraphExampleConfig)
|
|
341
368
|
args = parser.parse_args(
|
|
342
369
|
[
|
|
370
|
+
"--src-endpoint",
|
|
371
|
+
"https://sourcegraph.example.com",
|
|
343
372
|
"--src-access-token",
|
|
344
373
|
"test-token",
|
|
345
374
|
"--repo-query",
|
|
@@ -355,10 +384,10 @@ class ConfigTest(unittest.TestCase):
|
|
|
355
384
|
)
|
|
356
385
|
client = sourcegraph_client_from_config(config)
|
|
357
386
|
|
|
358
|
-
self.assertEqual(config.src_endpoint, "https://sourcegraph.com")
|
|
387
|
+
self.assertEqual(config.src_endpoint, "https://sourcegraph.example.com")
|
|
359
388
|
self.assertEqual(config.src_access_token, "test-token")
|
|
360
389
|
self.assertEqual(config.repo_query, "repo:example")
|
|
361
|
-
self.assertEqual(client.endpoint, "https://sourcegraph.com")
|
|
390
|
+
self.assertEqual(client.endpoint, "https://sourcegraph.example.com")
|
|
362
391
|
self.assertEqual(client.token, "test-token")
|
|
363
392
|
|
|
364
393
|
def test_load_config_uses_precedence_and_pydantic_types(self) -> None:
|
|
@@ -456,6 +485,82 @@ class ConfigTest(unittest.TestCase):
|
|
|
456
485
|
},
|
|
457
486
|
)
|
|
458
487
|
|
|
488
|
+
def test_config_field_names_combines_config_classes_and_fields(self) -> None:
|
|
489
|
+
self.assertEqual(
|
|
490
|
+
config_field_names(SourcegraphClientConfig, LoggingConfig, "page_size"),
|
|
491
|
+
(
|
|
492
|
+
"src_endpoint",
|
|
493
|
+
"src_access_token",
|
|
494
|
+
"src_log_level",
|
|
495
|
+
"verbose",
|
|
496
|
+
"quiet",
|
|
497
|
+
"silent",
|
|
498
|
+
"page_size",
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def test_add_config_arguments_can_select_reusable_field_sets(self) -> None:
|
|
503
|
+
parser = argparse.ArgumentParser()
|
|
504
|
+
add_config_arguments(
|
|
505
|
+
parser,
|
|
506
|
+
ExampleConfig,
|
|
507
|
+
include_fields=("token", "page_size", "EXAMPLE_LABELS"),
|
|
508
|
+
exclude_fields=("page_size",),
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
args = parser.parse_args(["--token", "raw-token", "--labels", "one,two"])
|
|
512
|
+
|
|
513
|
+
self.assertEqual(
|
|
514
|
+
config_overrides_from_args(ExampleConfig, args),
|
|
515
|
+
{
|
|
516
|
+
"token": "raw-token",
|
|
517
|
+
"labels": "one,two",
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
with redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
|
|
521
|
+
parser.parse_args(["--page-size", "50"])
|
|
522
|
+
|
|
523
|
+
def test_config_parse_args_help_only_shows_selected_fields(self) -> None:
|
|
524
|
+
stdout = io.StringIO()
|
|
525
|
+
|
|
526
|
+
with redirect_stdout(stdout), self.assertRaises(SystemExit) as raised:
|
|
527
|
+
config_parse_args(
|
|
528
|
+
ExampleConfig,
|
|
529
|
+
argv=["--help"],
|
|
530
|
+
env={},
|
|
531
|
+
resolve_op_refs=False,
|
|
532
|
+
include_fields=("labels", "token"),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
self.assertEqual(raised.exception.code, 0)
|
|
536
|
+
help_text = stdout.getvalue()
|
|
537
|
+
self.assertIn("--token TOKEN", help_text)
|
|
538
|
+
self.assertIn("--labels CSV", help_text)
|
|
539
|
+
self.assertLess(help_text.index("--labels CSV"), help_text.index("--token TOKEN"))
|
|
540
|
+
self.assertNotIn("--page-size", help_text)
|
|
541
|
+
self.assertNotIn("--include-archived", help_text)
|
|
542
|
+
|
|
543
|
+
def test_config_parse_args_groups_help_by_field_metadata(self) -> None:
|
|
544
|
+
stdout = io.StringIO()
|
|
545
|
+
|
|
546
|
+
with redirect_stdout(stdout), self.assertRaises(SystemExit) as raised:
|
|
547
|
+
config_parse_args(
|
|
548
|
+
GroupedHelpConfig,
|
|
549
|
+
argv=["--help"],
|
|
550
|
+
env={},
|
|
551
|
+
resolve_op_refs=False,
|
|
552
|
+
include_fields=("beta", "alpha", "gamma"),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
self.assertEqual(raised.exception.code, 0)
|
|
556
|
+
help_text = stdout.getvalue()
|
|
557
|
+
self.assertLess(help_text.index("Second group:"), help_text.index("First group:"))
|
|
558
|
+
self.assertLess(help_text.index("First group:"), help_text.index("Config:"))
|
|
559
|
+
self.assertLess(help_text.index("--alpha"), help_text.index("--gamma"))
|
|
560
|
+
self.assertIn("Second group:\n --beta", help_text)
|
|
561
|
+
self.assertIn("First group:\n --alpha", help_text)
|
|
562
|
+
self.assertNotIn("override matching environment variables", help_text)
|
|
563
|
+
|
|
459
564
|
def test_config_arguments_support_aliases_actions_and_optional_values(self) -> None:
|
|
460
565
|
parser = argparse.ArgumentParser()
|
|
461
566
|
add_config_arguments(parser, CommandStyleConfig)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|