schemathesis 3.35.1__py3-none-any.whl → 3.35.2__py3-none-any.whl

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.
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
- import enum
5
4
  import io
6
5
  import os
7
6
  import sys
@@ -141,16 +140,11 @@ def schemathesis(pre_run: str | None = None) -> None:
141
140
  load_hook(hooks)
142
141
 
143
142
 
144
- class ParameterGroup(enum.Enum):
145
- filtering = "Testing scope", "Customize the scope of the API testing."
146
- validation = "Response & Schema validation", "These options specify how API responses and schemas are validated."
147
- hypothesis = "Hypothesis engine", "Configuration of the underlying Hypothesis engine."
148
- generic = "Generic", None
143
+ GROUPS: list[str] = []
149
144
 
150
145
 
151
- class CommandWithCustomHelp(click.Command):
146
+ class CommandWithGroupedOptions(click.Command):
152
147
  def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
153
- # Group options first
154
148
  groups = defaultdict(list)
155
149
  for param in self.get_params(ctx):
156
150
  rv = param.get_help_record(ctx)
@@ -158,32 +152,43 @@ class CommandWithCustomHelp(click.Command):
158
152
  if isinstance(param, GroupedOption):
159
153
  group = param.group
160
154
  else:
161
- group = ParameterGroup.generic
155
+ group = "Global options"
162
156
  groups[group].append(rv)
163
- # Then display groups separately with optional description
164
- for group in ParameterGroup:
165
- opts = groups[group]
166
- title, description = group.value
167
- with formatter.section(title):
168
- if description:
169
- formatter.write_paragraph()
170
- formatter.write_text(description)
171
- formatter.write_paragraph()
172
- formatter.write_dl(opts)
157
+ for group in GROUPS:
158
+ with formatter.section(group or "Options"):
159
+ formatter.write_dl(groups[group], col_max=40)
173
160
 
174
161
 
175
162
  class GroupedOption(click.Option):
176
- def __init__(self, *args: Any, group: ParameterGroup, **kwargs: Any):
163
+ def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
177
164
  super().__init__(*args, **kwargs)
178
165
  self.group = group
179
166
 
180
167
 
181
- with_request_proxy = click.option(
168
+ def group(name: str) -> Callable:
169
+ GROUPS.append(name)
170
+
171
+ def _inner(cmd: Callable) -> Callable:
172
+ for param in reversed(cmd.__click_params__): # type: ignore[attr-defined]
173
+ if not isinstance(param, GroupedOption) or param.group is not None:
174
+ break
175
+ param.group = name
176
+ return cmd
177
+
178
+ return _inner
179
+
180
+
181
+ def grouped_option(*args: Any, **kwargs: Any) -> Callable:
182
+ kwargs.setdefault("cls", GroupedOption)
183
+ return click.option(*args, **kwargs)
184
+
185
+
186
+ with_request_proxy = grouped_option(
182
187
  "--request-proxy",
183
188
  help="Set the proxy for all network requests.",
184
189
  type=str,
185
190
  )
186
- with_request_tls_verify = click.option(
191
+ with_request_tls_verify = grouped_option(
187
192
  "--request-tls-verify",
188
193
  help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs.",
189
194
  type=str,
@@ -191,7 +196,7 @@ with_request_tls_verify = click.option(
191
196
  show_default=True,
192
197
  callback=callbacks.convert_boolean_string,
193
198
  )
194
- with_request_cert = click.option(
199
+ with_request_cert = grouped_option(
195
200
  "--request-cert",
196
201
  help="File path of unencrypted client certificate for authentication. "
197
202
  "The certificate can be bundled with a private key (e.g. PEM) or the private "
@@ -200,7 +205,7 @@ with_request_cert = click.option(
200
205
  default=None,
201
206
  show_default=False,
202
207
  )
203
- with_request_cert_key = click.option(
208
+ with_request_cert_key = grouped_option(
204
209
  "--request-cert-key",
205
210
  help="Specifies the file path of the private key for the client certificate.",
206
211
  type=click.Path(exists=True),
@@ -208,7 +213,7 @@ with_request_cert_key = click.option(
208
213
  show_default=False,
209
214
  callback=callbacks.validate_request_cert_key,
210
215
  )
211
- with_hosts_file = click.option(
216
+ with_hosts_file = grouped_option(
212
217
  "--hosts-file",
213
218
  help="Path to a file to store the Schemathesis.io auth configuration.",
214
219
  type=click.Path(dir_okay=False, writable=True),
@@ -230,13 +235,11 @@ def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Lite
230
235
  param += f"-{modifier}"
231
236
  prop += " pattern"
232
237
  help_text = f"{prop} to {action} testing."
233
- return click.option(
238
+ return grouped_option(
234
239
  param,
235
240
  help=help_text,
236
241
  type=str,
237
242
  multiple=modifier is None,
238
- cls=GroupedOption,
239
- group=ParameterGroup.filtering,
240
243
  )
241
244
 
242
245
 
@@ -258,10 +261,58 @@ class ReportToService:
258
261
  REPORT_TO_SERVICE = ReportToService()
259
262
 
260
263
 
261
- @schemathesis.command(short_help="Execute automated tests based on API specifications.", cls=CommandWithCustomHelp)
264
+ @schemathesis.command(
265
+ short_help="Execute automated tests based on API specifications.",
266
+ cls=CommandWithGroupedOptions,
267
+ context_settings={"terminal_width": output.default.get_terminal_width(), **CONTEXT_SETTINGS},
268
+ )
262
269
  @click.argument("schema", type=str)
263
270
  @click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
264
- @click.option(
271
+ @group("Options")
272
+ @grouped_option(
273
+ "--workers",
274
+ "-w",
275
+ "workers_num",
276
+ help="Sets the number of concurrent workers for testing. Auto-adjusts if 'auto' is specified.",
277
+ type=CustomHelpMessageChoice(
278
+ ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
279
+ choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
280
+ ),
281
+ default=str(DEFAULT_WORKERS),
282
+ show_default=True,
283
+ callback=callbacks.convert_workers,
284
+ )
285
+ @grouped_option(
286
+ "--dry-run",
287
+ "dry_run",
288
+ is_flag=True,
289
+ default=False,
290
+ help="Simulates test execution without making any actual requests, useful for validating data generation.",
291
+ )
292
+ @grouped_option(
293
+ "--experimental",
294
+ "experiments",
295
+ help="Enable experimental features.",
296
+ type=click.Choice(
297
+ [
298
+ experimental.OPEN_API_3_1.name,
299
+ experimental.SCHEMA_ANALYSIS.name,
300
+ experimental.STATEFUL_TEST_RUNNER.name,
301
+ experimental.STATEFUL_ONLY.name,
302
+ experimental.COVERAGE_PHASE.name,
303
+ ]
304
+ ),
305
+ callback=callbacks.convert_experimental,
306
+ multiple=True,
307
+ )
308
+ @grouped_option(
309
+ "--fixups",
310
+ help="Applies compatibility adjustments like 'fast_api', 'utf8_bom'.",
311
+ multiple=True,
312
+ type=click.Choice(list(ALL_FIXUPS) + ["all"]),
313
+ )
314
+ @group("API validation options")
315
+ @grouped_option(
265
316
  "--checks",
266
317
  "-c",
267
318
  multiple=True,
@@ -270,55 +321,27 @@ REPORT_TO_SERVICE = ReportToService()
270
321
  f"Default is '{','.join(DEFAULT_CHECKS_NAMES)}'.",
271
322
  type=CHECKS_TYPE,
272
323
  default=DEFAULT_CHECKS_NAMES,
273
- cls=GroupedOption,
274
- group=ParameterGroup.validation,
275
324
  callback=callbacks.convert_checks,
276
325
  show_default=True,
277
326
  )
278
- @click.option(
327
+ @grouped_option(
279
328
  "--exclude-checks",
280
329
  multiple=True,
281
330
  help="Specifies the validation checks to skip during testing. "
282
331
  "Provide a comma-separated list of checks you wish to bypass.",
283
332
  type=EXCLUDE_CHECKS_TYPE,
284
333
  default=[],
285
- cls=GroupedOption,
286
- group=ParameterGroup.validation,
287
334
  callback=callbacks.convert_checks,
288
335
  show_default=True,
289
336
  )
290
- @click.option(
291
- "--data-generation-method",
292
- "-D",
293
- "data_generation_methods",
294
- help="Specifies the approach Schemathesis uses to generate test data. "
295
- "Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both. "
296
- "Default is 'positive'.",
297
- type=DATA_GENERATION_METHOD_TYPE,
298
- default=DataGenerationMethod.default().name,
299
- callback=callbacks.convert_data_generation_method,
300
- show_default=True,
301
- )
302
- @click.option(
337
+ @grouped_option(
303
338
  "--max-response-time",
304
339
  help="Sets a custom time limit for API response times. "
305
340
  "The test will fail if a response time exceeds this limit. "
306
341
  "Provide the time in milliseconds.",
307
342
  type=click.IntRange(min=1),
308
- cls=GroupedOption,
309
- group=ParameterGroup.validation,
310
343
  )
311
- @click.option(
312
- "--target",
313
- "-t",
314
- "targets",
315
- multiple=True,
316
- help="Guides input generation to values more likely to expose bugs via targeted property-based testing.",
317
- type=TARGETS_TYPE,
318
- default=DEFAULT_TARGETS_NAMES,
319
- show_default=True,
320
- )
321
- @click.option(
344
+ @grouped_option(
322
345
  "-x",
323
346
  "--exitfirst",
324
347
  "exit_first",
@@ -327,68 +350,61 @@ REPORT_TO_SERVICE = ReportToService()
327
350
  help="Terminates the test suite immediately upon the first failure or error encountered.",
328
351
  show_default=True,
329
352
  )
330
- @click.option(
353
+ @grouped_option(
331
354
  "--max-failures",
332
355
  "max_failures",
333
356
  type=click.IntRange(min=1),
334
357
  help="Terminates the test suite after reaching a specified number of failures or errors.",
335
358
  show_default=True,
336
359
  )
337
- @click.option(
338
- "--dry-run",
339
- "dry_run",
340
- is_flag=True,
341
- default=False,
342
- help="Simulates test execution without making any actual requests, useful for validating data generation.",
343
- )
344
- @click.option(
345
- "--auth",
346
- "-a",
347
- help="Provides the server authentication details in the 'USER:PASSWORD' format.",
360
+ @group("Loader options")
361
+ @grouped_option(
362
+ "--app",
363
+ help="Specifies the WSGI/ASGI application under test, provided as an importable Python path.",
348
364
  type=str,
349
- callback=callbacks.validate_auth,
365
+ callback=callbacks.validate_app,
350
366
  )
351
- @click.option(
352
- "--auth-type",
353
- "-A",
354
- type=click.Choice(["basic", "digest"], case_sensitive=False),
355
- default="basic",
356
- help="Specifies the authentication method. Default is 'basic'.",
357
- show_default=True,
367
+ @grouped_option(
368
+ "--wait-for-schema",
369
+ help="Maximum duration, in seconds, to wait for the API schema to become available. Disabled by default.",
370
+ type=click.FloatRange(1.0),
371
+ default=None,
372
+ envvar=WAIT_FOR_SCHEMA_ENV_VAR,
358
373
  )
359
- @click.option(
360
- "--set-query",
361
- "set_query",
362
- help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
363
- multiple=True,
364
- type=str,
365
- callback=callbacks.validate_set_query,
374
+ @grouped_option(
375
+ "--validate-schema",
376
+ help="Validates the input API schema. Set to 'true' to enable or 'false' to disable.",
377
+ type=bool,
378
+ default=False,
379
+ show_default=True,
366
380
  )
367
- @click.option(
368
- "--set-header",
369
- "set_header",
370
- help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
371
- multiple=True,
381
+ @group("Network requests options")
382
+ @grouped_option(
383
+ "--base-url",
384
+ "-b",
385
+ help="Provides the base URL of the API, required when schema is provided as a file.",
372
386
  type=str,
373
- callback=callbacks.validate_set_header,
387
+ callback=callbacks.validate_base_url,
388
+ envvar=BASE_URL_ENV_VAR,
374
389
  )
375
- @click.option(
376
- "--set-cookie",
377
- "set_cookie",
378
- help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
379
- multiple=True,
380
- type=str,
381
- callback=callbacks.validate_set_cookie,
390
+ @grouped_option(
391
+ "--request-timeout",
392
+ help="Sets a timeout limit, in milliseconds, for each network request during tests.",
393
+ type=click.IntRange(1),
394
+ default=DEFAULT_RESPONSE_TIMEOUT,
382
395
  )
383
- @click.option(
384
- "--set-path",
385
- "set_path",
386
- help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
387
- multiple=True,
396
+ @with_request_proxy
397
+ @with_request_tls_verify
398
+ @with_request_cert
399
+ @with_request_cert_key
400
+ @grouped_option(
401
+ "--rate-limit",
402
+ help="Specifies a rate limit for test requests in '<limit>/<duration>' format. "
403
+ "Example - `100/m` for 100 requests per minute.",
388
404
  type=str,
389
- callback=callbacks.validate_set_path,
405
+ callback=callbacks.validate_rate_limit,
390
406
  )
391
- @click.option(
407
+ @grouped_option(
392
408
  "--header",
393
409
  "-H",
394
410
  "headers",
@@ -397,234 +413,188 @@ REPORT_TO_SERVICE = ReportToService()
397
413
  type=str,
398
414
  callback=callbacks.validate_headers,
399
415
  )
416
+ @grouped_option(
417
+ "--auth",
418
+ "-a",
419
+ help="Provides the server authentication details in the 'USER:PASSWORD' format.",
420
+ type=str,
421
+ callback=callbacks.validate_auth,
422
+ )
423
+ @grouped_option(
424
+ "--auth-type",
425
+ "-A",
426
+ type=click.Choice(["basic", "digest"], case_sensitive=False),
427
+ default="basic",
428
+ help="Specifies the authentication method. Default is 'basic'.",
429
+ show_default=True,
430
+ )
431
+ @group("Filtering options")
400
432
  @with_filters
401
- @click.option(
433
+ @grouped_option(
402
434
  "--include-by",
403
435
  "include_by",
404
436
  type=str,
405
437
  help="Include API operations by expression",
406
- cls=GroupedOption,
407
- group=ParameterGroup.filtering,
408
438
  )
409
- @click.option(
439
+ @grouped_option(
410
440
  "--exclude-by",
411
441
  "exclude_by",
412
442
  type=str,
413
443
  help="Exclude API operations by expression",
414
- cls=GroupedOption,
415
- group=ParameterGroup.filtering,
416
444
  )
417
- @click.option(
445
+ @grouped_option(
418
446
  "--exclude-deprecated",
419
447
  help="Exclude deprecated API operations from testing.",
420
448
  is_flag=True,
421
449
  is_eager=True,
422
450
  default=False,
423
451
  show_default=True,
424
- cls=GroupedOption,
425
- group=ParameterGroup.filtering,
426
452
  )
427
- @click.option(
453
+ @grouped_option(
428
454
  "--endpoint",
429
455
  "-E",
430
456
  "endpoints",
431
457
  type=str,
432
458
  multiple=True,
433
- help=r"API operation path pattern (e.g., users/\d+).",
459
+ help=r"[DEPRECATED] API operation path pattern (e.g., users/\d+).",
434
460
  callback=callbacks.validate_regex,
435
- cls=GroupedOption,
436
- group=ParameterGroup.filtering,
461
+ hidden=True,
437
462
  )
438
- @click.option(
463
+ @grouped_option(
439
464
  "--method",
440
465
  "-M",
441
466
  "methods",
442
467
  type=str,
443
468
  multiple=True,
444
- help="HTTP method (e.g., GET, POST).",
469
+ help="[DEPRECATED] HTTP method (e.g., GET, POST).",
445
470
  callback=callbacks.validate_regex,
446
- cls=GroupedOption,
447
- group=ParameterGroup.filtering,
471
+ hidden=True,
448
472
  )
449
- @click.option(
473
+ @grouped_option(
450
474
  "--tag",
451
475
  "-T",
452
476
  "tags",
453
477
  type=str,
454
478
  multiple=True,
455
- help="Schema tag pattern.",
479
+ help="[DEPRECATED] Schema tag pattern.",
456
480
  callback=callbacks.validate_regex,
457
- cls=GroupedOption,
458
- group=ParameterGroup.filtering,
481
+ hidden=True,
459
482
  )
460
- @click.option(
483
+ @grouped_option(
461
484
  "--operation-id",
462
485
  "-O",
463
486
  "operation_ids",
464
487
  type=str,
465
488
  multiple=True,
466
- help="OpenAPI operationId pattern.",
489
+ help="[DEPRECATED] OpenAPI operationId pattern.",
467
490
  callback=callbacks.validate_regex,
468
- cls=GroupedOption,
469
- group=ParameterGroup.filtering,
470
- )
471
- @click.option(
472
- "--workers",
473
- "-w",
474
- "workers_num",
475
- help="Sets the number of concurrent workers for testing. Auto-adjusts if 'auto' is specified.",
476
- type=CustomHelpMessageChoice(
477
- ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
478
- choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
479
- ),
480
- default=str(DEFAULT_WORKERS),
481
- show_default=True,
482
- callback=callbacks.convert_workers,
483
- )
484
- @click.option(
485
- "--base-url",
486
- "-b",
487
- help="Provides the base URL of the API, required when schema is provided as a file.",
488
- type=str,
489
- callback=callbacks.validate_base_url,
490
- envvar=BASE_URL_ENV_VAR,
491
- )
492
- @click.option(
493
- "--app",
494
- help="Specifies the WSGI/ASGI application under test, provided as an importable Python path.",
495
- type=str,
496
- callback=callbacks.validate_app,
497
- )
498
- @click.option(
499
- "--wait-for-schema",
500
- help="Maximum duration, in seconds, to wait for the API schema to become available.",
501
- type=click.FloatRange(1.0),
502
- default=None,
503
- envvar=WAIT_FOR_SCHEMA_ENV_VAR,
504
- )
505
- @click.option(
506
- "--request-timeout",
507
- help="Sets a timeout limit, in milliseconds, for each network request during tests.",
508
- type=click.IntRange(1),
509
- default=DEFAULT_RESPONSE_TIMEOUT,
510
- )
511
- @with_request_proxy
512
- @with_request_tls_verify
513
- @with_request_cert
514
- @with_request_cert_key
515
- @click.option(
516
- "--validate-schema",
517
- help="Toggles validation of incoming payloads against the defined API schema. "
518
- "Set to 'True' to enable or 'False' to disable. "
519
- "Default is 'False'.",
520
- type=bool,
521
- default=False,
522
- show_default=True,
523
- cls=GroupedOption,
524
- group=ParameterGroup.validation,
491
+ hidden=True,
525
492
  )
526
- @click.option(
493
+ @grouped_option(
527
494
  "--skip-deprecated-operations",
528
- help="Exclude deprecated API operations from testing.",
495
+ help="[DEPRECATED] Exclude deprecated API operations from testing.",
529
496
  is_flag=True,
530
497
  is_eager=True,
531
498
  default=False,
532
499
  show_default=True,
533
- cls=GroupedOption,
534
- group=ParameterGroup.filtering,
500
+ hidden=True,
535
501
  )
536
- @click.option(
502
+ @group("Output options")
503
+ @grouped_option(
537
504
  "--junit-xml",
538
505
  help="Outputs a JUnit-XML style report at the specified file path.",
539
506
  type=click.File("w", encoding="utf-8"),
540
507
  )
541
- @click.option(
542
- "--report",
543
- "report_value",
544
- help="""Specifies how the generated report should be handled.
545
- If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
546
- If a file name is provided, the report will be stored in that file.
547
- The report data, consisting of a tar gz file with multiple JSON files, is subject to change.""",
548
- is_flag=False,
549
- flag_value="",
550
- envvar=service.REPORT_ENV_VAR,
551
- callback=callbacks.convert_report, # type: ignore
552
- )
553
- @click.option(
554
- "--debug-output-file",
555
- help="Saves debugging information in a JSONL format at the specified file path.",
556
- type=click.File("w", encoding="utf-8"),
557
- )
558
- @click.option(
559
- "--show-errors-tracebacks",
560
- help="Displays complete traceback information for internal errors.",
561
- is_flag=True,
562
- is_eager=True,
563
- default=False,
564
- hidden=True,
565
- show_default=True,
566
- )
567
- @click.option(
568
- "--show-trace",
569
- help="Displays complete traceback information for internal errors.",
570
- is_flag=True,
571
- is_eager=True,
572
- default=False,
573
- show_default=True,
574
- )
575
- @click.option(
576
- "--code-sample-style",
577
- help="Selects the code sample style for reproducing failures.",
578
- type=click.Choice([item.name for item in CodeSampleStyle]),
579
- default=CodeSampleStyle.default().name,
580
- callback=callbacks.convert_code_sample_style,
581
- )
582
- @click.option(
508
+ @grouped_option(
583
509
  "--cassette-path",
584
510
  help="Saves the test outcomes in a VCR-compatible format.",
585
511
  type=click.File("w", encoding="utf-8"),
586
512
  is_eager=True,
587
513
  )
588
- @click.option(
514
+ @grouped_option(
589
515
  "--cassette-format",
590
516
  help="Format of the saved cassettes.",
591
517
  type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
592
518
  default=cassettes.CassetteFormat.VCR.name.lower(),
593
519
  callback=callbacks.convert_cassette_format,
594
520
  )
595
- @click.option(
521
+ @grouped_option(
596
522
  "--cassette-preserve-exact-body-bytes",
597
523
  help="Retains exact byte sequence of payloads in cassettes, encoded as base64.",
598
524
  is_flag=True,
599
525
  callback=callbacks.validate_preserve_exact_body_bytes,
600
526
  )
601
- @click.option(
527
+ @grouped_option(
528
+ "--code-sample-style",
529
+ help="Selects the code sample style for reproducing failures.",
530
+ type=click.Choice([item.name for item in CodeSampleStyle]),
531
+ default=CodeSampleStyle.default().name,
532
+ callback=callbacks.convert_code_sample_style,
533
+ )
534
+ @grouped_option(
535
+ "--sanitize-output",
536
+ type=bool,
537
+ default=True,
538
+ show_default=True,
539
+ help="Enable or disable automatic output sanitization to obscure sensitive data.",
540
+ )
541
+ @grouped_option(
542
+ "--output-truncate",
543
+ help="Specifies whether to truncate schemas and responses in error messages.",
544
+ type=str,
545
+ default="true",
546
+ show_default=True,
547
+ callback=callbacks.convert_boolean_string,
548
+ )
549
+ @grouped_option(
550
+ "--show-trace",
551
+ help="Displays complete traceback information for internal errors.",
552
+ is_flag=True,
553
+ is_eager=True,
554
+ default=False,
555
+ show_default=True,
556
+ )
557
+ @grouped_option(
558
+ "--debug-output-file",
559
+ help="Saves debugging information in a JSONL format at the specified file path.",
560
+ type=click.File("w", encoding="utf-8"),
561
+ )
562
+ @grouped_option(
602
563
  "--store-network-log",
603
- help="Saves the test outcomes in a VCR-compatible format.",
564
+ help="[DEPRECATED] Saves the test outcomes in a VCR-compatible format.",
604
565
  type=click.File("w", encoding="utf-8"),
605
566
  hidden=True,
606
567
  )
607
- @click.option(
608
- "--fixups",
609
- help="Applies compatibility adjustments like 'fast_api', 'utf8_bom'.",
610
- multiple=True,
611
- type=click.Choice(list(ALL_FIXUPS) + ["all"]),
568
+ @grouped_option(
569
+ "--show-errors-tracebacks",
570
+ help="[DEPRECATED] Displays complete traceback information for internal errors.",
571
+ is_flag=True,
572
+ is_eager=True,
573
+ default=False,
574
+ hidden=True,
575
+ show_default=True,
612
576
  )
613
- @click.option(
614
- "--rate-limit",
615
- help="Specifies a rate limit for test requests in '<limit>/<duration>' format. "
616
- "Example - `100/m` for 100 requests per minute.",
617
- type=str,
618
- callback=callbacks.validate_rate_limit,
577
+ @group("Data generation options")
578
+ @grouped_option(
579
+ "--data-generation-method",
580
+ "-D",
581
+ "data_generation_methods",
582
+ help="Specifies the approach Schemathesis uses to generate test data. "
583
+ "Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both. "
584
+ "Default is 'positive'.",
585
+ type=DATA_GENERATION_METHOD_TYPE,
586
+ default=DataGenerationMethod.default().name,
587
+ callback=callbacks.convert_data_generation_method,
588
+ show_default=True,
619
589
  )
620
- @click.option(
590
+ @grouped_option(
621
591
  "--stateful",
622
- help="Enables or disables stateful testing features.",
592
+ help="Enables or disables stateful testing.",
623
593
  type=click.Choice([item.name for item in Stateful]),
624
594
  default=Stateful.links.name,
625
595
  callback=callbacks.convert_stateful,
626
596
  )
627
- @click.option(
597
+ @grouped_option(
628
598
  "--stateful-recursion-limit",
629
599
  help="Sets the recursion depth limit for stateful testing.",
630
600
  default=DEFAULT_STATEFUL_RECURSION_LIMIT,
@@ -632,19 +602,38 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
632
602
  type=click.IntRange(1, 100),
633
603
  hidden=True,
634
604
  )
635
- @click.option(
636
- "--force-schema-version",
637
- help="Forces the schema to be interpreted as a particular OpenAPI version.",
638
- type=click.Choice(["20", "30"]),
605
+ @grouped_option(
606
+ "--generation-allow-x00",
607
+ help="Determines whether to allow the generation of `\x00` bytes within strings.",
608
+ type=str,
609
+ default="true",
610
+ show_default=True,
611
+ callback=callbacks.convert_boolean_string,
639
612
  )
640
- @click.option(
641
- "--sanitize-output",
642
- type=bool,
643
- default=True,
613
+ @grouped_option(
614
+ "--generation-codec",
615
+ help="Specifies the codec used for generating strings.",
616
+ type=str,
617
+ default="utf-8",
618
+ callback=callbacks.validate_generation_codec,
619
+ )
620
+ @grouped_option(
621
+ "--generation-with-security-parameters",
622
+ help="Whether to generate security parameters.",
623
+ type=str,
624
+ default="true",
644
625
  show_default=True,
645
- help="Enable or disable automatic output sanitization to obscure sensitive data.",
626
+ callback=callbacks.convert_boolean_string,
646
627
  )
647
- @click.option(
628
+ @grouped_option(
629
+ "--generation-graphql-allow-null",
630
+ help="Whether `null` values should be used for optional arguments in GraphQL queries.",
631
+ type=str,
632
+ default="true",
633
+ show_default=True,
634
+ callback=callbacks.convert_boolean_string,
635
+ )
636
+ @grouped_option(
648
637
  "--contrib-unique-data",
649
638
  "contrib_unique_data",
650
639
  help="Forces the generation of unique test cases.",
@@ -652,7 +641,7 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
652
641
  default=False,
653
642
  show_default=True,
654
643
  )
655
- @click.option(
644
+ @grouped_option(
656
645
  "--contrib-openapi-formats-uuid",
657
646
  "contrib_openapi_formats_uuid",
658
647
  help="Enables support for the 'uuid' string format in OpenAPI.",
@@ -660,7 +649,7 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
660
649
  default=False,
661
650
  show_default=True,
662
651
  )
663
- @click.option(
652
+ @grouped_option(
664
653
  "--contrib-openapi-fill-missing-examples",
665
654
  "contrib_openapi_fill_missing_examples",
666
655
  help="Enables generation of random examples for API operations that do not have explicit examples defined.",
@@ -668,157 +657,141 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
668
657
  default=False,
669
658
  show_default=True,
670
659
  )
671
- @click.option(
660
+ @grouped_option(
661
+ "--target",
662
+ "-t",
663
+ "targets",
664
+ multiple=True,
665
+ help="Guides input generation to values more likely to expose bugs via targeted property-based testing.",
666
+ type=TARGETS_TYPE,
667
+ default=DEFAULT_TARGETS_NAMES,
668
+ show_default=True,
669
+ )
670
+ @group("Open API options")
671
+ @grouped_option(
672
+ "--force-schema-version",
673
+ help="Forces the schema to be interpreted as a particular OpenAPI version.",
674
+ type=click.Choice(["20", "30"]),
675
+ )
676
+ @grouped_option(
677
+ "--set-query",
678
+ "set_query",
679
+ help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
680
+ multiple=True,
681
+ type=str,
682
+ callback=callbacks.validate_set_query,
683
+ )
684
+ @grouped_option(
685
+ "--set-header",
686
+ "set_header",
687
+ help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
688
+ multiple=True,
689
+ type=str,
690
+ callback=callbacks.validate_set_header,
691
+ )
692
+ @grouped_option(
693
+ "--set-cookie",
694
+ "set_cookie",
695
+ help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
696
+ multiple=True,
697
+ type=str,
698
+ callback=callbacks.validate_set_cookie,
699
+ )
700
+ @grouped_option(
701
+ "--set-path",
702
+ "set_path",
703
+ help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
704
+ multiple=True,
705
+ type=str,
706
+ callback=callbacks.validate_set_path,
707
+ )
708
+ @group("Hypothesis engine options")
709
+ @grouped_option(
672
710
  "--hypothesis-database",
673
711
  help="Configures storage for examples discovered by Hypothesis. "
674
712
  f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
675
713
  f"or specify a file path for persistent storage.",
676
714
  type=str,
677
- cls=GroupedOption,
678
- group=ParameterGroup.hypothesis,
679
715
  callback=callbacks.validate_hypothesis_database,
680
716
  )
681
- @click.option(
717
+ @grouped_option(
682
718
  "--hypothesis-deadline",
683
719
  help="Sets a time limit for each test case generated by Hypothesis, in milliseconds. "
684
720
  "Exceeding this limit will cause the test to fail.",
685
- # max value to avoid overflow. It is the maximum amount of days in milliseconds
686
- type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
687
- cls=GroupedOption,
688
- group=ParameterGroup.hypothesis,
721
+ type=OptionalInt(1, 5 * 60 * 1000),
689
722
  )
690
- @click.option(
723
+ @grouped_option(
691
724
  "--hypothesis-derandomize",
692
725
  help="Enables deterministic mode in Hypothesis, which eliminates random variation between test runs.",
693
726
  is_flag=True,
694
727
  is_eager=True,
695
728
  default=None,
696
729
  show_default=True,
697
- cls=GroupedOption,
698
- group=ParameterGroup.hypothesis,
699
730
  )
700
- @click.option(
731
+ @grouped_option(
701
732
  "--hypothesis-max-examples",
702
733
  help="Sets the cap on the number of examples generated by Hypothesis for each API method/path pair.",
703
734
  type=click.IntRange(1),
704
- cls=GroupedOption,
705
- group=ParameterGroup.hypothesis,
706
735
  )
707
- @click.option(
736
+ @grouped_option(
708
737
  "--hypothesis-phases",
709
738
  help="Specifies which testing phases to execute.",
710
739
  type=CsvEnumChoice(Phase),
711
- cls=GroupedOption,
712
- group=ParameterGroup.hypothesis,
713
740
  )
714
- @click.option(
741
+ @grouped_option(
715
742
  "--hypothesis-no-phases",
716
743
  help="Specifies which testing phases to exclude from execution.",
717
744
  type=CsvEnumChoice(Phase),
718
- cls=GroupedOption,
719
- group=ParameterGroup.hypothesis,
720
745
  )
721
- @click.option(
746
+ @grouped_option(
722
747
  "--hypothesis-report-multiple-bugs",
723
748
  help="If set, only the most easily reproducible exception will be reported when multiple issues are found.",
724
749
  type=bool,
725
- cls=GroupedOption,
726
- group=ParameterGroup.hypothesis,
727
750
  )
728
- @click.option(
751
+ @grouped_option(
729
752
  "--hypothesis-seed",
730
753
  help="Sets a seed value for Hypothesis, ensuring reproducibility across test runs.",
731
754
  type=int,
732
- cls=GroupedOption,
733
- group=ParameterGroup.hypothesis,
734
755
  )
735
- @click.option(
756
+ @grouped_option(
736
757
  "--hypothesis-suppress-health-check",
737
758
  help="Disables specified health checks from Hypothesis like 'data_too_large', 'filter_too_much', etc. "
738
759
  "Provide a comma-separated list",
739
760
  type=CsvEnumChoice(HealthCheck),
740
- cls=GroupedOption,
741
- group=ParameterGroup.hypothesis,
742
761
  )
743
- @click.option(
762
+ @grouped_option(
744
763
  "--hypothesis-verbosity",
745
764
  help="Controls the verbosity level of Hypothesis output.",
746
765
  type=click.Choice([item.name for item in Verbosity]),
747
766
  callback=callbacks.convert_verbosity,
748
- cls=GroupedOption,
749
- group=ParameterGroup.hypothesis,
750
- )
751
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
752
- @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
753
- @click.option(
754
- "--experimental",
755
- "experiments",
756
- help="Enable experimental support for specific features.",
757
- type=click.Choice(
758
- [
759
- experimental.OPEN_API_3_1.name,
760
- experimental.SCHEMA_ANALYSIS.name,
761
- experimental.STATEFUL_TEST_RUNNER.name,
762
- experimental.STATEFUL_ONLY.name,
763
- experimental.COVERAGE_PHASE.name,
764
- ]
765
- ),
766
- callback=callbacks.convert_experimental,
767
- multiple=True,
768
- )
769
- @click.option(
770
- "--output-truncate",
771
- help="Specifies whether to truncate schemas and responses in error messages.",
772
- type=str,
773
- default="true",
774
- show_default=True,
775
- callback=callbacks.convert_boolean_string,
776
767
  )
777
- @click.option(
778
- "--generation-allow-x00",
779
- help="Determines whether to allow the generation of `\x00` bytes within strings.",
780
- type=str,
781
- default="true",
782
- show_default=True,
783
- callback=callbacks.convert_boolean_string,
784
- )
785
- @click.option(
786
- "--generation-codec",
787
- help="Specifies the codec used for generating strings.",
788
- type=str,
789
- default="utf-8",
790
- callback=callbacks.validate_generation_codec,
791
- )
792
- @click.option(
793
- "--generation-with-security-parameters",
794
- help="Whether to generate security parameters.",
795
- type=str,
796
- default="true",
797
- show_default=True,
798
- callback=callbacks.convert_boolean_string,
799
- )
800
- @click.option(
801
- "--generation-graphql-allow-null",
802
- help="Whether `null` values should be used for optional arguments in GraphQL queries.",
803
- type=str,
804
- default="true",
805
- show_default=True,
806
- callback=callbacks.convert_boolean_string,
768
+ @group("Schemathesis.io options")
769
+ @grouped_option(
770
+ "--report",
771
+ "report_value",
772
+ help="""Specifies how the generated report should be handled.
773
+ If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
774
+ If a file name is provided, the report will be stored in that file.
775
+ The report data, consisting of a tar gz file with multiple JSON files, is subject to change.""",
776
+ is_flag=False,
777
+ flag_value="",
778
+ envvar=service.REPORT_ENV_VAR,
779
+ callback=callbacks.convert_report, # type: ignore
807
780
  )
808
- @click.option(
781
+ @grouped_option(
809
782
  "--schemathesis-io-token",
810
783
  help="Schemathesis.io authentication token.",
811
784
  type=str,
812
785
  envvar=service.TOKEN_ENV_VAR,
813
786
  )
814
- @click.option(
787
+ @grouped_option(
815
788
  "--schemathesis-io-url",
816
789
  help="Schemathesis.io base URL.",
817
790
  default=service.DEFAULT_URL,
818
791
  type=str,
819
792
  envvar=service.URL_ENV_VAR,
820
793
  )
821
- @click.option(
794
+ @grouped_option(
822
795
  "--schemathesis-io-telemetry",
823
796
  help="Controls whether you send anonymized CLI usage data to Schemathesis.io along with your report.",
824
797
  type=str,
@@ -828,7 +801,10 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
828
801
  envvar=service.TELEMETRY_ENV_VAR,
829
802
  )
830
803
  @with_hosts_file
831
- @click.option("--verbosity", "-v", help="Increase verbosity of the output.", count=True)
804
+ @group("Global options")
805
+ @grouped_option("--verbosity", "-v", help="Increase verbosity of the output.", count=True)
806
+ @grouped_option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
807
+ @grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
832
808
  @click.pass_context
833
809
  def run(
834
810
  ctx: click.Context,
@@ -2012,6 +1988,25 @@ def add_option(*args: Any, cls: Type = click.Option, **kwargs: Any) -> None:
2012
1988
  run.params.append(cls(args, **kwargs))
2013
1989
 
2014
1990
 
1991
+ @dataclass
1992
+ class Group:
1993
+ name: str
1994
+
1995
+ def add_option(self, *args: Any, **kwargs: Any) -> None:
1996
+ kwargs["cls"] = GroupedOption
1997
+ kwargs["group"] = self.name
1998
+ add_option(*args, **kwargs)
1999
+
2000
+
2001
+ def add_group(name: str, *, index: int | None = None) -> Group:
2002
+ """Add a custom options group to `st run`."""
2003
+ if index is not None:
2004
+ GROUPS.insert(index, name)
2005
+ else:
2006
+ GROUPS.append(name)
2007
+ return Group(name)
2008
+
2009
+
2015
2010
  def handler() -> Callable[[Type], None]:
2016
2011
  """Register a new CLI event handler."""
2017
2012