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