schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0a12__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 (62) hide show
  1. schemathesis/__init__.py +28 -25
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +60 -36
  4. schemathesis/cli/commands/run/__init__.py +23 -21
  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 +175 -80
  9. schemathesis/cli/commands/run/validation.py +21 -6
  10. schemathesis/config/__init__.py +2 -1
  11. schemathesis/config/_generation.py +12 -13
  12. schemathesis/config/_operations.py +14 -0
  13. schemathesis/config/_phases.py +41 -5
  14. schemathesis/config/_projects.py +28 -0
  15. schemathesis/config/_report.py +6 -2
  16. schemathesis/config/_warnings.py +25 -0
  17. schemathesis/config/schema.json +49 -1
  18. schemathesis/core/errors.py +5 -2
  19. schemathesis/core/transport.py +36 -1
  20. schemathesis/engine/context.py +1 -0
  21. schemathesis/engine/errors.py +60 -1
  22. schemathesis/engine/events.py +10 -2
  23. schemathesis/engine/phases/probes.py +3 -0
  24. schemathesis/engine/phases/stateful/__init__.py +2 -1
  25. schemathesis/engine/phases/stateful/_executor.py +38 -5
  26. schemathesis/engine/phases/stateful/context.py +2 -2
  27. schemathesis/engine/phases/unit/_executor.py +36 -7
  28. schemathesis/generation/__init__.py +0 -3
  29. schemathesis/generation/case.py +1 -0
  30. schemathesis/generation/coverage.py +1 -1
  31. schemathesis/generation/hypothesis/builder.py +31 -7
  32. schemathesis/generation/metrics.py +93 -0
  33. schemathesis/generation/modes.py +0 -8
  34. schemathesis/generation/stateful/__init__.py +4 -0
  35. schemathesis/generation/stateful/state_machine.py +1 -0
  36. schemathesis/graphql/loaders.py +138 -4
  37. schemathesis/hooks.py +62 -35
  38. schemathesis/openapi/loaders.py +120 -4
  39. schemathesis/pytest/loaders.py +24 -0
  40. schemathesis/pytest/plugin.py +22 -0
  41. schemathesis/schemas.py +9 -6
  42. schemathesis/specs/graphql/scalars.py +37 -3
  43. schemathesis/specs/graphql/schemas.py +12 -3
  44. schemathesis/specs/openapi/_hypothesis.py +14 -20
  45. schemathesis/specs/openapi/checks.py +21 -18
  46. schemathesis/specs/openapi/formats.py +30 -3
  47. schemathesis/specs/openapi/media_types.py +44 -1
  48. schemathesis/specs/openapi/schemas.py +8 -2
  49. schemathesis/specs/openapi/stateful/__init__.py +2 -1
  50. schemathesis/transport/__init__.py +54 -16
  51. schemathesis/transport/prepare.py +31 -7
  52. schemathesis/transport/requests.py +9 -8
  53. schemathesis/transport/wsgi.py +8 -8
  54. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +44 -90
  55. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/RECORD +58 -60
  56. schemathesis/contrib/__init__.py +0 -9
  57. schemathesis/contrib/openapi/__init__.py +0 -9
  58. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  59. schemathesis/generation/targets.py +0 -69
  60. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  61. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  62. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,44 +1,47 @@
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, auth
4
5
  from schemathesis.checks import CheckContext, CheckFunction, check
5
- from schemathesis.core.output import sanitization
6
6
  from schemathesis.core.transport import Response
7
7
  from schemathesis.core.version import SCHEMATHESIS_VERSION
8
8
  from schemathesis.generation import GenerationMode
9
9
  from schemathesis.generation.case import Case
10
- from schemathesis.generation.targets import TargetContext, TargetFunction, target
11
- from schemathesis.hooks import HookContext
10
+ from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
11
+ from schemathesis.hooks import HookContext, hook
12
12
  from schemathesis.schemas import BaseSchema
13
+ from schemathesis.transport import SerializationContext, serializer
13
14
 
14
15
  __version__ = SCHEMATHESIS_VERSION
15
16
 
16
- # Public API
17
- auth = auths.GLOBAL_AUTH_STORAGE
18
- hook = hooks.register
19
-
20
17
  __all__ = [
18
+ "__version__",
19
+ # Core data structures
21
20
  "Case",
22
- "CheckContext",
23
- "CheckFunction",
24
- "GenerationMode",
25
21
  "Response",
26
- "TargetContext",
27
- "TargetFunction",
28
- "HookContext",
22
+ "GenerationMode",
29
23
  "BaseSchema",
30
- "__version__",
31
- "auth",
32
- "check",
33
- "contrib",
34
- "engine",
24
+ # Public errors
35
25
  "errors",
36
- "graphql",
37
- "hook",
38
- "hooks",
26
+ # Spec or usage specific namespaces
39
27
  "openapi",
28
+ "graphql",
40
29
  "pytest",
41
- "python",
42
- "sanitization",
43
- "target",
30
+ # Hooks
31
+ "hook",
32
+ "HookContext",
33
+ # Checks
34
+ "check",
35
+ "CheckContext",
36
+ "CheckFunction",
37
+ # Auth
38
+ "auth",
39
+ "AuthContext",
40
+ # Targeted Property-based Testing
41
+ "metric",
42
+ "MetricContext",
43
+ "MetricFunction",
44
+ # Serialization
45
+ "serializer",
46
+ "SerializationContext",
44
47
  ]
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]]
@@ -289,7 +309,7 @@ class AuthStorage(Generic[Auth]):
289
309
  ) -> FilterableRegisterAuth | FilterableApplyAuth:
290
310
  if provider_class is not None:
291
311
  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)
312
+ return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
293
313
 
294
314
  def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
295
315
  """Use `requests` auth instance as an auth provider."""
@@ -333,30 +353,12 @@ class AuthStorage(Generic[Auth]):
333
353
  provider = SelectiveAuthProvider(provider, filter_set)
334
354
  self.providers.append(provider)
335
355
 
336
- def register(
356
+ def auth(
337
357
  self,
338
358
  *,
339
359
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
340
360
  cache_by_key: CacheKeyFunction | None = None,
341
361
  ) -> 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
362
  filter_set = FilterSet()
361
363
 
362
364
  def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
@@ -451,5 +453,44 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
451
453
 
452
454
  # Global auth API
453
455
  GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
454
- register = GLOBAL_AUTH_STORAGE.register
455
456
  unregister = GLOBAL_AUTH_STORAGE.unregister
457
+
458
+
459
+ def auth(
460
+ *,
461
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
462
+ cache_by_key: CacheKeyFunction | None = None,
463
+ ) -> FilterableRegisterAuth:
464
+ """Register a dynamic authentication provider for APIs with expiring tokens.
465
+
466
+ Args:
467
+ refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
468
+ cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
469
+
470
+ Example:
471
+ ```python
472
+ import schemathesis
473
+ import requests
474
+
475
+ @schemathesis.auth()
476
+ class TokenAuth:
477
+ def get(self, case, context):
478
+ \"\"\"Fetch fresh authentication token\"\"\"
479
+ response = requests.post(
480
+ "http://localhost:8000/auth/token",
481
+ json={"username": "demo", "password": "test"}
482
+ )
483
+ return response.json()["access_token"]
484
+
485
+ def set(self, case, data, context):
486
+ \"\"\"Apply token to test case headers\"\"\"
487
+ case.headers = case.headers or {}
488
+ case.headers["Authorization"] = f"Bearer {data}"
489
+ ```
490
+
491
+ """
492
+ return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
493
+
494
+
495
+ auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
496
+ 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,78 @@ 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 check(func: CheckFunction) -> CheckFunction:
95
+ """Register a custom validation check to run against API responses.
96
+
97
+ Args:
98
+ func: Function that takes `(ctx: CheckContext, response: Response, case: Case)` and raises `AssertionError` on validation failure
99
+
100
+ Example:
101
+ ```python
102
+ import schemathesis
103
+
104
+ @schemathesis.check
105
+ def check_cors_headers(ctx, response, case):
106
+ \"\"\"Verify CORS headers are present\"\"\"
107
+ if "Access-Control-Allow-Origin" not in response.headers:
108
+ raise AssertionError("Missing CORS headers")
109
+ ```
110
+
111
+ """
112
+ return CHECKS.register(func)
92
113
 
93
114
 
94
115
  @check
95
116
  def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
96
117
  """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
118
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
119
+ from schemathesis.specs.graphql.validation import validate_graphql_response
120
+ from schemathesis.specs.openapi.utils import expand_status_codes
121
+
122
+ expected_statuses = expand_status_codes(ctx.config.not_a_server_error.expected_statuses or [])
99
123
 
100
124
  status_code = response.status_code
101
- if status_code >= 500:
125
+ if status_code not in expected_statuses:
102
126
  raise ServerError(operation=case.operation.label, status_code=status_code)
103
127
  if isinstance(case.operation.schema, GraphQLSchema):
104
128
  try:
@@ -6,7 +6,6 @@ from typing import Any, Callable
6
6
  import click
7
7
  from click.utils import LazyFile
8
8
 
9
- from schemathesis import contrib
10
9
  from schemathesis.checks import CHECKS
11
10
  from schemathesis.cli.commands.run import executor, validation
12
11
  from schemathesis.cli.commands.run.filters import with_filters
@@ -19,11 +18,17 @@ 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
33
  # NOTE: Need to explicitly import all registered checks
29
34
  from schemathesis.specs.openapi.checks import * # noqa: F401, F403
@@ -83,6 +88,14 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
83
88
  default=None,
84
89
  envvar="SCHEMATHESIS_WAIT_FOR_SCHEMA",
85
90
  )
91
+ @grouped_option(
92
+ "--warnings",
93
+ help="Control warning display: 'off' to disable all, or comma-separated list of warning types to enable",
94
+ type=str,
95
+ default=None,
96
+ callback=validation.validate_warnings,
97
+ metavar="WARNINGS",
98
+ )
86
99
  @group("API validation options")
87
100
  @grouped_option(
88
101
  "--checks",
@@ -296,7 +309,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
296
309
  "generation_modes",
297
310
  help="Test data generation mode",
298
311
  type=click.Choice([item.value for item in GenerationMode] + ["all"]),
299
- default=GenerationMode.default().value,
312
+ default="all",
300
313
  callback=validation.convert_generation_mode,
301
314
  show_default=True,
302
315
  metavar="",
@@ -349,7 +362,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
349
362
  "generation_maximize",
350
363
  multiple=True,
351
364
  help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
352
- type=RegistryChoice(TARGETS),
365
+ type=RegistryChoice(METRICS),
353
366
  default=None,
354
367
  callback=validation.convert_maximize,
355
368
  show_default=True,
@@ -390,15 +403,6 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
390
403
  show_default=True,
391
404
  metavar="BOOLEAN",
392
405
  )
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
406
  @group("Global options")
403
407
  @grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
404
408
  @grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
@@ -442,6 +446,7 @@ def run(
442
446
  base_url: str | None,
443
447
  wait_for_schema: float | None = None,
444
448
  suppress_health_check: list[HealthCheck] | None,
449
+ warnings: bool | list[SchemathesisWarning] | None,
445
450
  rate_limit: str | None = None,
446
451
  request_timeout: int | None = None,
447
452
  request_tls_verify: bool = True,
@@ -456,11 +461,10 @@ def run(
456
461
  report_preserve_bytes: bool = False,
457
462
  output_sanitize: bool = True,
458
463
  output_truncate: bool = True,
459
- contrib_openapi_fill_missing_examples: bool = False,
460
- generation_modes: list[GenerationMode] = DEFAULT_GENERATOR_MODES,
464
+ generation_modes: list[GenerationMode],
461
465
  generation_seed: int | None = None,
462
466
  generation_max_examples: int | None = None,
463
- generation_maximize: list[TargetFunction] | None,
467
+ generation_maximize: list[MetricFunction] | None,
464
468
  generation_deterministic: bool = False,
465
469
  generation_database: str | None = None,
466
470
  generation_unique_inputs: bool = False,
@@ -494,9 +498,6 @@ def run(
494
498
 
495
499
  validation.validate_auth_overlap(auth, headers)
496
500
 
497
- if contrib_openapi_fill_missing_examples:
498
- contrib.openapi.fill_missing_examples.install()
499
-
500
501
  # Then override the global config from CLI options
501
502
  config.update(
502
503
  color=color,
@@ -528,6 +529,7 @@ def run(
528
529
  request_cert=request_cert,
529
530
  request_cert_key=request_cert_key,
530
531
  proxy=request_proxy,
532
+ warnings=warnings,
531
533
  )
532
534
  # These are filters for what API operations should be tested
533
535
  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: