schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0b1__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.
Files changed (73) hide show
  1. schemathesis/__init__.py +35 -27
  2. schemathesis/auths.py +85 -54
  3. schemathesis/checks.py +65 -36
  4. schemathesis/cli/commands/run/__init__.py +32 -27
  5. schemathesis/cli/commands/run/context.py +6 -1
  6. schemathesis/cli/commands/run/events.py +7 -1
  7. schemathesis/cli/commands/run/executor.py +12 -7
  8. schemathesis/cli/commands/run/handlers/output.py +188 -80
  9. schemathesis/cli/commands/run/validation.py +21 -6
  10. schemathesis/cli/constants.py +1 -1
  11. schemathesis/config/__init__.py +2 -1
  12. schemathesis/config/_generation.py +12 -13
  13. schemathesis/config/_operations.py +14 -0
  14. schemathesis/config/_phases.py +41 -5
  15. schemathesis/config/_projects.py +33 -1
  16. schemathesis/config/_report.py +6 -2
  17. schemathesis/config/_warnings.py +25 -0
  18. schemathesis/config/schema.json +49 -1
  19. schemathesis/core/errors.py +15 -19
  20. schemathesis/core/transport.py +117 -2
  21. schemathesis/engine/context.py +1 -0
  22. schemathesis/engine/errors.py +61 -2
  23. schemathesis/engine/events.py +10 -2
  24. schemathesis/engine/phases/probes.py +3 -0
  25. schemathesis/engine/phases/stateful/__init__.py +2 -1
  26. schemathesis/engine/phases/stateful/_executor.py +38 -5
  27. schemathesis/engine/phases/stateful/context.py +2 -2
  28. schemathesis/engine/phases/unit/_executor.py +36 -7
  29. schemathesis/generation/__init__.py +0 -3
  30. schemathesis/generation/case.py +153 -28
  31. schemathesis/generation/coverage.py +1 -1
  32. schemathesis/generation/hypothesis/builder.py +43 -19
  33. schemathesis/generation/metrics.py +93 -0
  34. schemathesis/generation/modes.py +0 -8
  35. schemathesis/generation/overrides.py +11 -27
  36. schemathesis/generation/stateful/__init__.py +17 -0
  37. schemathesis/generation/stateful/state_machine.py +32 -108
  38. schemathesis/graphql/loaders.py +152 -8
  39. schemathesis/hooks.py +63 -39
  40. schemathesis/openapi/checks.py +82 -20
  41. schemathesis/openapi/generation/filters.py +9 -2
  42. schemathesis/openapi/loaders.py +134 -8
  43. schemathesis/pytest/lazy.py +4 -31
  44. schemathesis/pytest/loaders.py +24 -0
  45. schemathesis/pytest/plugin.py +38 -6
  46. schemathesis/schemas.py +161 -94
  47. schemathesis/specs/graphql/scalars.py +37 -3
  48. schemathesis/specs/graphql/schemas.py +18 -9
  49. schemathesis/specs/openapi/_hypothesis.py +53 -34
  50. schemathesis/specs/openapi/checks.py +111 -47
  51. schemathesis/specs/openapi/expressions/nodes.py +1 -1
  52. schemathesis/specs/openapi/formats.py +30 -3
  53. schemathesis/specs/openapi/media_types.py +44 -1
  54. schemathesis/specs/openapi/negative/__init__.py +5 -3
  55. schemathesis/specs/openapi/negative/mutations.py +2 -2
  56. schemathesis/specs/openapi/parameters.py +0 -3
  57. schemathesis/specs/openapi/schemas.py +14 -93
  58. schemathesis/specs/openapi/stateful/__init__.py +2 -1
  59. schemathesis/specs/openapi/stateful/links.py +1 -63
  60. schemathesis/transport/__init__.py +54 -16
  61. schemathesis/transport/prepare.py +31 -7
  62. schemathesis/transport/requests.py +21 -9
  63. schemathesis/transport/serialization.py +0 -4
  64. schemathesis/transport/wsgi.py +15 -8
  65. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/METADATA +45 -87
  66. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/RECORD +69 -71
  67. schemathesis/contrib/__init__.py +0 -9
  68. schemathesis/contrib/openapi/__init__.py +0 -9
  69. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  70. schemathesis/generation/targets.py +0 -69
  71. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/WHEEL +0 -0
  72. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/entry_points.txt +0 -0
  73. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,44 +1,52 @@
1
1
  from __future__ import annotations
2
2
 
3
- from schemathesis import auths, contrib, engine, errors, graphql, hooks, openapi, pytest, python
3
+ from schemathesis import errors, graphql, openapi, pytest
4
+ from schemathesis.auths import AuthContext, AuthProvider, auth
4
5
  from schemathesis.checks import CheckContext, CheckFunction, check
5
- from schemathesis.core.output import sanitization
6
+ from schemathesis.config import SchemathesisConfig as Config
6
7
  from schemathesis.core.transport import Response
7
8
  from schemathesis.core.version import SCHEMATHESIS_VERSION
8
- from schemathesis.generation import GenerationMode
9
+ from schemathesis.generation import GenerationMode, stateful
9
10
  from schemathesis.generation.case import Case
10
- from schemathesis.generation.targets import TargetContext, TargetFunction, target
11
- from schemathesis.hooks import HookContext
12
- from schemathesis.schemas import BaseSchema
11
+ from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
12
+ from schemathesis.hooks import HookContext, hook
13
+ from schemathesis.schemas import APIOperation, BaseSchema
14
+ from schemathesis.transport import SerializationContext, serializer
13
15
 
14
16
  __version__ = SCHEMATHESIS_VERSION
15
17
 
16
- # Public API
17
- auth = auths.GLOBAL_AUTH_STORAGE
18
- hook = hooks.register
19
-
20
18
  __all__ = [
19
+ "__version__",
20
+ # Core data structures
21
21
  "Case",
22
- "CheckContext",
23
- "CheckFunction",
24
- "GenerationMode",
25
22
  "Response",
26
- "TargetContext",
27
- "TargetFunction",
28
- "HookContext",
23
+ "APIOperation",
29
24
  "BaseSchema",
30
- "__version__",
31
- "auth",
32
- "check",
33
- "contrib",
34
- "engine",
25
+ "Config",
26
+ "GenerationMode",
27
+ "stateful",
28
+ # Public errors
35
29
  "errors",
36
- "graphql",
37
- "hook",
38
- "hooks",
30
+ # Spec or usage specific namespaces
39
31
  "openapi",
32
+ "graphql",
40
33
  "pytest",
41
- "python",
42
- "sanitization",
43
- "target",
34
+ # Hooks
35
+ "hook",
36
+ "HookContext",
37
+ # Checks
38
+ "check",
39
+ "CheckContext",
40
+ "CheckFunction",
41
+ # Auth
42
+ "auth",
43
+ "AuthContext",
44
+ "AuthProvider",
45
+ # Targeted Property-based Testing
46
+ "metric",
47
+ "MetricContext",
48
+ "MetricFunction",
49
+ # Serialization
50
+ "serializer",
51
+ "SerializationContext",
44
52
  ]
schemathesis/auths.py CHANGED
@@ -34,14 +34,34 @@ Auth = TypeVar("Auth")
34
34
 
35
35
  @dataclass
36
36
  class AuthContext:
37
- """Holds state relevant for the authentication process.
37
+ """Runtime context passed to authentication providers during token generation.
38
+
39
+ Provides access to the current API operation and application instance when
40
+ auth providers need operation-specific tokens or application state.
41
+
42
+ Example:
43
+ ```python
44
+ @schemathesis.auth()
45
+ class ContextAwareAuth:
46
+ def get(self, case, context):
47
+ # Access operation details
48
+ if "/admin/" in context.operation.path:
49
+ return self.get_admin_token()
50
+ else:
51
+ return self.get_user_token()
52
+
53
+ def set(self, case, data, context):
54
+ case.headers = {"Authorization": f"Bearer {data}"}
55
+ ```
38
56
 
39
- :ivar APIOperation operation: API operation that is currently being processed.
40
- :ivar app: Optional Python application if the WSGI / ASGI integration is used.
41
57
  """
42
58
 
43
59
  operation: APIOperation
60
+ """API operation currently being processed for authentication."""
44
61
  app: Any | None
62
+ """Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
63
+
64
+ __slots__ = ("operation", "app")
45
65
 
46
66
 
47
67
  CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
@@ -49,22 +69,28 @@ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
49
69
 
50
70
  @runtime_checkable
51
71
  class AuthProvider(Generic[Auth], Protocol):
52
- """Get authentication data for an API and set it on the generated test cases."""
72
+ """Protocol for implementing custom authentication in API tests."""
53
73
 
54
- def get(self, case: Case, context: AuthContext) -> Auth | None:
55
- """Get the authentication data.
74
+ def get(self, case: Case, ctx: AuthContext) -> Auth | None:
75
+ """Obtain authentication data for the test case.
76
+
77
+ Args:
78
+ case: Generated test case requiring authentication.
79
+ ctx: Authentication state and configuration.
80
+
81
+ Returns:
82
+ Authentication data (e.g., token, credentials) or `None`.
56
83
 
57
- :param Case case: Generated test case.
58
- :param AuthContext context: Holds state relevant for the authentication process.
59
- :return: Any authentication data you find useful for your use case. For example, it could be an access token.
60
84
  """
61
85
 
62
- def set(self, case: Case, data: Auth, context: AuthContext) -> None:
63
- """Set authentication data on a generated test case.
86
+ def set(self, case: Case, data: Auth, ctx: AuthContext) -> None:
87
+ """Apply authentication data to the test case.
88
+
89
+ Args:
90
+ case: Test case to modify.
91
+ data: Authentication data from the `get` method.
92
+ ctx: Authentication state and configuration.
64
93
 
65
- :param Optional[Auth] data: Authentication data you got from the ``get`` method.
66
- :param Case case: Generated test case.
67
- :param AuthContext context: Holds state relevant for the authentication process.
68
94
  """
69
95
 
70
96
 
@@ -289,7 +315,7 @@ class AuthStorage(Generic[Auth]):
289
315
  ) -> FilterableRegisterAuth | FilterableApplyAuth:
290
316
  if provider_class is not None:
291
317
  return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
292
- return self.register(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
318
+ return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
293
319
 
294
320
  def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
295
321
  """Use `requests` auth instance as an auth provider."""
@@ -313,8 +339,9 @@ class AuthStorage(Generic[Auth]):
313
339
  ) -> None:
314
340
  if not issubclass(provider_class, AuthProvider):
315
341
  raise TypeError(
316
- f"`{provider_class.__name__}` is not a valid auth provider. "
317
- f"Check `schemathesis.auths.AuthProvider` documentation for examples."
342
+ f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
343
+ f"Auth providers must have `get` and `set` methods. "
344
+ f"See `schemathesis.AuthProvider` documentation for examples."
318
345
  )
319
346
  provider: AuthProvider
320
347
  # Apply caching if desired
@@ -333,30 +360,12 @@ class AuthStorage(Generic[Auth]):
333
360
  provider = SelectiveAuthProvider(provider, filter_set)
334
361
  self.providers.append(provider)
335
362
 
336
- def register(
363
+ def auth(
337
364
  self,
338
365
  *,
339
366
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
340
367
  cache_by_key: CacheKeyFunction | None = None,
341
368
  ) -> FilterableRegisterAuth:
342
- """Register a new auth provider.
343
-
344
- .. code-block:: python
345
-
346
- @schemathesis.auth()
347
- class TokenAuth:
348
- def get(self, context):
349
- response = requests.post(
350
- "https://example.schemathesis.io/api/token/",
351
- json={"username": "demo", "password": "test"},
352
- )
353
- data = response.json()
354
- return data["access_token"]
355
-
356
- def set(self, case, data, context):
357
- # Modify `case` the way you need
358
- case.headers = {"Authorization": f"Bearer {data}"}
359
- """
360
369
  filter_set = FilterSet()
361
370
 
362
371
  def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
@@ -387,23 +396,6 @@ class AuthStorage(Generic[Auth]):
387
396
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
388
397
  cache_by_key: CacheKeyFunction | None = None,
389
398
  ) -> FilterableApplyAuth:
390
- """Register auth provider only on one test function.
391
-
392
- :param Type[AuthProvider] provider_class: Authentication provider class.
393
- :param Optional[int] refresh_interval: Cache duration in seconds.
394
-
395
- .. code-block:: python
396
-
397
- class Auth:
398
- ...
399
-
400
-
401
- @schema.auth(Auth)
402
- @schema.parametrize()
403
- def test_api(case):
404
- ...
405
-
406
- """
407
399
  filter_set = FilterSet()
408
400
 
409
401
  def wrapper(test: Callable) -> Callable:
@@ -451,5 +443,44 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
451
443
 
452
444
  # Global auth API
453
445
  GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
454
- register = GLOBAL_AUTH_STORAGE.register
455
446
  unregister = GLOBAL_AUTH_STORAGE.unregister
447
+
448
+
449
+ def auth(
450
+ *,
451
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
452
+ cache_by_key: CacheKeyFunction | None = None,
453
+ ) -> FilterableRegisterAuth:
454
+ """Register a dynamic authentication provider for APIs with expiring tokens.
455
+
456
+ Args:
457
+ refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
458
+ cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
459
+
460
+ Example:
461
+ ```python
462
+ import schemathesis
463
+ import requests
464
+
465
+ @schemathesis.auth()
466
+ class TokenAuth:
467
+ def get(self, case, context):
468
+ \"\"\"Fetch fresh authentication token\"\"\"
469
+ response = requests.post(
470
+ "http://localhost:8000/auth/token",
471
+ json={"username": "demo", "password": "test"}
472
+ )
473
+ return response.json()["access_token"]
474
+
475
+ def set(self, case, data, context):
476
+ \"\"\"Apply token to test case headers\"\"\"
477
+ case.headers = case.headers or {}
478
+ case.headers["Authorization"] = f"Bearer {data}"
479
+ ```
480
+
481
+ """
482
+ return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
483
+
484
+
485
+ auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
486
+ auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]
schemathesis/checks.py CHANGED
@@ -26,20 +26,21 @@ CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
26
26
 
27
27
 
28
28
  class CheckContext:
29
- """Context for Schemathesis checks.
29
+ """Runtime context passed to validation check functions during API testing.
30
30
 
31
- Provides access to broader test execution data beyond individual test cases.
31
+ Provides access to configuration for currently checked endpoint.
32
32
  """
33
33
 
34
- override: Override | None
35
- auth: tuple[str, str] | None
36
- headers: CaseInsensitiveDict | None
34
+ _override: Override | None
35
+ _auth: tuple[str, str] | None
36
+ _headers: CaseInsensitiveDict | None
37
37
  config: ChecksConfig
38
- transport_kwargs: dict[str, Any] | None
39
- recorder: ScenarioRecorder | None
40
- checks: list[CheckFunction]
38
+ """Configuration settings for validation checks."""
39
+ _transport_kwargs: dict[str, Any] | None
40
+ _recorder: ScenarioRecorder | None
41
+ _checks: list[CheckFunction]
41
42
 
42
- __slots__ = ("override", "auth", "headers", "config", "transport_kwargs", "recorder", "checks")
43
+ __slots__ = ("_override", "_auth", "_headers", "config", "_transport_kwargs", "_recorder", "_checks")
43
44
 
44
45
  def __init__(
45
46
  self,
@@ -50,55 +51,83 @@ class CheckContext:
50
51
  transport_kwargs: dict[str, Any] | None,
51
52
  recorder: ScenarioRecorder | None = None,
52
53
  ) -> None:
53
- self.override = override
54
- self.auth = auth
55
- self.headers = headers
54
+ self._override = override
55
+ self._auth = auth
56
+ self._headers = headers
56
57
  self.config = config
57
- self.transport_kwargs = transport_kwargs
58
- self.recorder = recorder
59
- self.checks = []
58
+ self._transport_kwargs = transport_kwargs
59
+ self._recorder = recorder
60
+ self._checks = []
60
61
  for check in CHECKS.get_all():
61
62
  name = check.__name__
62
63
  if self.config.get_by_name(name=name).enabled:
63
- self.checks.append(check)
64
+ self._checks.append(check)
64
65
  if self.config.max_response_time.enabled:
65
- self.checks.append(max_response_time)
66
+ self._checks.append(max_response_time)
66
67
 
67
- def find_parent(self, *, case_id: str) -> Case | None:
68
- if self.recorder is not None:
69
- return self.recorder.find_parent(case_id=case_id)
68
+ def _find_parent(self, *, case_id: str) -> Case | None:
69
+ if self._recorder is not None:
70
+ return self._recorder.find_parent(case_id=case_id)
70
71
  return None
71
72
 
72
- def find_related(self, *, case_id: str) -> Iterator[Case]:
73
- if self.recorder is not None:
74
- yield from self.recorder.find_related(case_id=case_id)
73
+ def _find_related(self, *, case_id: str) -> Iterator[Case]:
74
+ if self._recorder is not None:
75
+ yield from self._recorder.find_related(case_id=case_id)
75
76
 
76
- def find_response(self, *, case_id: str) -> Response | None:
77
- if self.recorder is not None:
78
- return self.recorder.find_response(case_id=case_id)
77
+ def _find_response(self, *, case_id: str) -> Response | None:
78
+ if self._recorder is not None:
79
+ return self._recorder.find_response(case_id=case_id)
79
80
  return None
80
81
 
81
- def record_case(self, *, parent_id: str, case: Case) -> None:
82
- if self.recorder is not None:
83
- self.recorder.record_case(parent_id=parent_id, transition=None, case=case)
82
+ def _record_case(self, *, parent_id: str, case: Case) -> None:
83
+ if self._recorder is not None:
84
+ self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
84
85
 
85
- def record_response(self, *, case_id: str, response: Response) -> None:
86
- if self.recorder is not None:
87
- self.recorder.record_response(case_id=case_id, response=response)
86
+ def _record_response(self, *, case_id: str, response: Response) -> None:
87
+ if self._recorder is not None:
88
+ self._recorder.record_response(case_id=case_id, response=response)
88
89
 
89
90
 
90
91
  CHECKS = Registry[CheckFunction]()
91
- check = CHECKS.register
92
+
93
+
94
+ def load_all_checks() -> None:
95
+ # NOTE: Trigger registering all Open API checks
96
+ from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401, F403
97
+
98
+
99
+ def check(func: CheckFunction) -> CheckFunction:
100
+ """Register a custom validation check to run against API responses.
101
+
102
+ Args:
103
+ func: Function that takes `(ctx: CheckContext, response: Response, case: Case)` and raises `AssertionError` on validation failure
104
+
105
+ Example:
106
+ ```python
107
+ import schemathesis
108
+
109
+ @schemathesis.check
110
+ def check_cors_headers(ctx, response, case):
111
+ \"\"\"Verify CORS headers are present\"\"\"
112
+ if "Access-Control-Allow-Origin" not in response.headers:
113
+ raise AssertionError("Missing CORS headers")
114
+ ```
115
+
116
+ """
117
+ return CHECKS.register(func)
92
118
 
93
119
 
94
120
  @check
95
121
  def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
96
122
  """A check to verify that the response is not a server-side error."""
97
- from .specs.graphql.schemas import GraphQLSchema
98
- from .specs.graphql.validation import validate_graphql_response
123
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
124
+ from schemathesis.specs.graphql.validation import validate_graphql_response
125
+ from schemathesis.specs.openapi.utils import expand_status_codes
126
+
127
+ expected_statuses = expand_status_codes(ctx.config.not_a_server_error.expected_statuses or [])
99
128
 
100
129
  status_code = response.status_code
101
- if status_code >= 500:
130
+ if status_code not in expected_statuses:
102
131
  raise ServerError(operation=case.operation.label, status_code=status_code)
103
132
  if isinstance(case.operation.schema, GraphQLSchema):
104
133
  try:
@@ -6,8 +6,7 @@ from typing import Any, Callable
6
6
  import click
7
7
  from click.utils import LazyFile
8
8
 
9
- from schemathesis import contrib
10
- from schemathesis.checks import CHECKS
9
+ from schemathesis.checks import CHECKS, load_all_checks
11
10
  from schemathesis.cli.commands.run import executor, validation
12
11
  from schemathesis.cli.commands.run.filters import with_filters
13
12
  from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
@@ -19,14 +18,19 @@ from schemathesis.cli.ext.options import (
19
18
  CustomHelpMessageChoice,
20
19
  RegistryChoice,
21
20
  )
22
- from schemathesis.config import DEFAULT_REPORT_DIRECTORY, HealthCheck, ReportFormat, SchemathesisConfig
21
+ from schemathesis.config import (
22
+ DEFAULT_REPORT_DIRECTORY,
23
+ HealthCheck,
24
+ ReportFormat,
25
+ SchemathesisConfig,
26
+ SchemathesisWarning,
27
+ )
23
28
  from schemathesis.core import HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER
24
29
  from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
25
- from schemathesis.generation import DEFAULT_GENERATOR_MODES, GenerationMode
26
- from schemathesis.generation.targets import TARGETS, TargetFunction
30
+ from schemathesis.generation import GenerationMode
31
+ from schemathesis.generation.metrics import METRICS, MetricFunction
27
32
 
28
- # NOTE: Need to explicitly import all registered checks
29
- from schemathesis.specs.openapi.checks import * # noqa: F401, F403
33
+ load_all_checks()
30
34
 
31
35
  COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
32
36
 
@@ -83,6 +87,14 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
83
87
  default=None,
84
88
  envvar="SCHEMATHESIS_WAIT_FOR_SCHEMA",
85
89
  )
90
+ @grouped_option(
91
+ "--warnings",
92
+ help="Control warning display: 'off' to disable all, or comma-separated list of warning types to enable",
93
+ type=str,
94
+ default=None,
95
+ callback=validation.validate_warnings,
96
+ metavar="WARNINGS",
97
+ )
86
98
  @group("API validation options")
87
99
  @grouped_option(
88
100
  "--checks",
@@ -296,7 +308,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
296
308
  "generation_modes",
297
309
  help="Test data generation mode",
298
310
  type=click.Choice([item.value for item in GenerationMode] + ["all"]),
299
- default=GenerationMode.default().value,
311
+ default="all",
300
312
  callback=validation.convert_generation_mode,
301
313
  show_default=True,
302
314
  metavar="",
@@ -349,7 +361,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
349
361
  "generation_maximize",
350
362
  multiple=True,
351
363
  help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
352
- type=RegistryChoice(TARGETS),
364
+ type=RegistryChoice(METRICS),
353
365
  default=None,
354
366
  callback=validation.convert_maximize,
355
367
  show_default=True,
@@ -390,15 +402,6 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
390
402
  show_default=True,
391
403
  metavar="BOOLEAN",
392
404
  )
393
- @grouped_option(
394
- "--contrib-openapi-fill-missing-examples",
395
- "contrib_openapi_fill_missing_examples",
396
- help="Enable generation of random examples for API operations that do not have explicit examples",
397
- is_flag=True,
398
- default=False,
399
- show_default=True,
400
- metavar="BOOLEAN",
401
- )
402
405
  @group("Global options")
403
406
  @grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
404
407
  @grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
@@ -442,6 +445,7 @@ def run(
442
445
  base_url: str | None,
443
446
  wait_for_schema: float | None = None,
444
447
  suppress_health_check: list[HealthCheck] | None,
448
+ warnings: bool | list[SchemathesisWarning] | None,
445
449
  rate_limit: str | None = None,
446
450
  request_timeout: int | None = None,
447
451
  request_tls_verify: bool = True,
@@ -456,11 +460,10 @@ def run(
456
460
  report_preserve_bytes: bool = False,
457
461
  output_sanitize: bool = True,
458
462
  output_truncate: bool = True,
459
- contrib_openapi_fill_missing_examples: bool = False,
460
- generation_modes: list[GenerationMode] = DEFAULT_GENERATOR_MODES,
463
+ generation_modes: list[GenerationMode],
461
464
  generation_seed: int | None = None,
462
465
  generation_max_examples: int | None = None,
463
- generation_maximize: list[TargetFunction] | None,
466
+ generation_maximize: list[MetricFunction] | None,
464
467
  generation_deterministic: bool = False,
465
468
  generation_database: str | None = None,
466
469
  generation_unique_inputs: bool = False,
@@ -473,10 +476,14 @@ def run(
473
476
  no_color: bool = False,
474
477
  **__kwargs: Any,
475
478
  ) -> None:
476
- """Run tests against an API using a specified SCHEMA.
479
+ """Generate and run property-based tests against your API.
477
480
 
478
- [Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications
479
- """
481
+ \b
482
+ LOCATION can be:
483
+ - Local file: ./openapi.json, ./schema.yaml, ./schema.graphql
484
+ - OpenAPI URL: https://api.example.com/openapi.json
485
+ - GraphQL URL: https://api.example.com/graphql/
486
+ """ # noqa: D301
480
487
  if no_color and force_color:
481
488
  raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
482
489
 
@@ -494,9 +501,6 @@ def run(
494
501
 
495
502
  validation.validate_auth_overlap(auth, headers)
496
503
 
497
- if contrib_openapi_fill_missing_examples:
498
- contrib.openapi.fill_missing_examples.install()
499
-
500
504
  # Then override the global config from CLI options
501
505
  config.update(
502
506
  color=color,
@@ -528,6 +532,7 @@ def run(
528
532
  request_cert=request_cert,
529
533
  request_cert_key=request_cert_key,
530
534
  proxy=request_proxy,
535
+ warnings=warnings,
531
536
  )
532
537
  # These are filters for what API operations should be tested
533
538
  filter_set = {
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Generator
4
+ from typing import TYPE_CHECKING, Callable, Generator
5
5
 
6
+ from schemathesis.cli.commands.run.events import LoadingFinished
6
7
  from schemathesis.config import ProjectConfig
7
8
  from schemathesis.core.failures import Failure
8
9
  from schemathesis.core.result import Err, Ok
@@ -11,6 +12,7 @@ from schemathesis.core.transport import Response
11
12
  from schemathesis.engine import Status, events
12
13
  from schemathesis.engine.recorder import CaseNode, ScenarioRecorder
13
14
  from schemathesis.generation.case import Case
15
+ from schemathesis.schemas import APIOperation
14
16
 
15
17
  if TYPE_CHECKING:
16
18
  from schemathesis.generation.stateful.state_machine import ExtractionFailure
@@ -177,6 +179,7 @@ class ExecutionContext:
177
179
  """Storage for the current context of the execution."""
178
180
 
179
181
  config: ProjectConfig
182
+ find_operation_by_label: Callable[[str], APIOperation | None] | None = None
180
183
  statistic: Statistic = field(default_factory=Statistic)
181
184
  exit_code: int = 0
182
185
  initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
@@ -189,6 +192,8 @@ class ExecutionContext:
189
192
  self.summary_lines.append(line)
190
193
 
191
194
  def on_event(self, event: events.EngineEvent) -> None:
195
+ if isinstance(event, LoadingFinished):
196
+ self.find_operation_by_label = event.find_operation_by_label
192
197
  if isinstance(event, events.ScenarioFinished):
193
198
  self.statistic.on_scenario_finished(event.recorder)
194
199
  elif isinstance(event, events.NonFatalError) or (
@@ -1,10 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  import uuid
5
+ from typing import Callable
3
6
 
4
7
  from schemathesis.config import ProjectConfig
5
8
  from schemathesis.core import Specification
6
9
  from schemathesis.engine import events
7
- from schemathesis.schemas import ApiStatistic
10
+ from schemathesis.schemas import APIOperation, ApiStatistic
8
11
 
9
12
 
10
13
  class LoadingStarted(events.EngineEvent):
@@ -28,6 +31,7 @@ class LoadingFinished(events.EngineEvent):
28
31
  "statistic",
29
32
  "schema",
30
33
  "config",
34
+ "find_operation_by_label",
31
35
  )
32
36
 
33
37
  def __init__(
@@ -41,6 +45,7 @@ class LoadingFinished(events.EngineEvent):
41
45
  statistic: ApiStatistic,
42
46
  schema: dict,
43
47
  config: ProjectConfig,
48
+ find_operation_by_label: Callable[[str], APIOperation | None],
44
49
  ) -> None:
45
50
  self.id = uuid.uuid4()
46
51
  self.timestamp = time.time()
@@ -52,3 +57,4 @@ class LoadingFinished(events.EngineEvent):
52
57
  self.schema = schema
53
58
  self.base_path = base_path
54
59
  self.config = config
60
+ self.find_operation_by_label = find_operation_by_label
@@ -76,6 +76,7 @@ def into_event_stream(*, location: str, config: ProjectConfig, filter_set: dict[
76
76
  schema=schema.raw_schema,
77
77
  config=schema.config,
78
78
  base_path=schema.base_path,
79
+ find_operation_by_label=schema.find_operation_by_label,
79
80
  )
80
81
 
81
82
  try:
@@ -121,17 +122,21 @@ def _execute(
121
122
  args: list[str],
122
123
  params: dict[str, Any],
123
124
  ) -> None:
124
- handlers = initialize_handlers(config=config, args=args, params=params)
125
- ctx = ExecutionContext(config=config)
125
+ handlers: list[EventHandler] = []
126
+ ctx: ExecutionContext | None = None
126
127
 
127
128
  def shutdown() -> None:
128
- for _handler in handlers:
129
- _handler.shutdown(ctx)
130
-
131
- for handler in handlers:
132
- handler.start(ctx)
129
+ if ctx is not None:
130
+ for _handler in handlers:
131
+ _handler.shutdown(ctx)
133
132
 
134
133
  try:
134
+ handlers = initialize_handlers(config=config, args=args, params=params)
135
+ ctx = ExecutionContext(config=config)
136
+
137
+ for handler in handlers:
138
+ handler.start(ctx)
139
+
135
140
  for event in event_stream:
136
141
  ctx.on_event(event)
137
142
  for handler in handlers: