src-py-lib 0.1.6__tar.gz → 0.1.7__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 (34) hide show
  1. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/PKG-INFO +1 -1
  2. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/pyproject.toml +1 -1
  3. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/__init__.py +4 -0
  4. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/sourcegraph.py +5 -3
  5. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/config.py +121 -18
  6. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/logging.py +4 -0
  7. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/tests/test_import.py +2 -0
  8. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/tests/test_logging_http_clients.py +107 -2
  9. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/uv.lock +1 -1
  10. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/.github/workflows/ci.yml +0 -0
  11. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/.github/workflows/release.yml +0 -0
  12. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/.github/workflows/validate.yml +0 -0
  13. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/.gitignore +0 -0
  14. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/.markdownlint-cli2.yaml +0 -0
  15. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/.python-version +0 -0
  16. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/AGENTS.md +0 -0
  17. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/LICENSE +0 -0
  18. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/README.md +0 -0
  19. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/SECURITY.md +0 -0
  20. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/renovate.json +0 -0
  21. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/__init__.py +0 -0
  22. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/github.py +0 -0
  23. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/google_sheets.py +0 -0
  24. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/graphql.py +0 -0
  25. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/linear.py +0 -0
  26. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/one_password.py +0 -0
  27. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/clients/slack.py +0 -0
  28. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/py.typed +0 -0
  29. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/__init__.py +0 -0
  30. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/http.py +0 -0
  31. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/json_cache.py +0 -0
  32. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/json_types.py +0 -0
  33. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/src/src_py_lib/utils/tsv.py +0 -0
  34. {src_py_lib-0.1.6 → src_py_lib-0.1.7}/tests/test_tsv.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: src-py-lib
3
- Version: 0.1.6
3
+ Version: 0.1.7
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
@@ -10,7 +10,7 @@ dev = [
10
10
 
11
11
  [project]
12
12
  name = "src-py-lib"
13
- version = "0.1.6"
13
+ version = "0.1.7"
14
14
  description = "Reusable libraries for Sourcegraph projects"
15
15
  readme = "README.md"
16
16
  requires-python = ">=3.11"
@@ -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=DEFAULT_SOURCEGRAPH_ENDPOINT,
160
+ default="",
162
161
  env_var="SRC_ENDPOINT",
163
162
  cli_flag="--src-endpoint",
164
163
  metavar="URL",
165
- help=f"Sourcegraph instance URL (default: {DEFAULT_SOURCEGRAPH_ENDPOINT})",
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
- group = parser.add_argument_group(
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
- for option in config_options(config_cls):
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
- max_help_position = _config_help_max_position(config_cls, include_env_file=include_env_file)
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=_config_help_formatter(max_help_position),
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 config_options(config_cls)
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)
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "src-py-lib"
257
- version = "0.1.6"
257
+ version = "0.1.7"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes