schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a11__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 (92) hide show
  1. schemathesis/__init__.py +3 -7
  2. schemathesis/checks.py +17 -7
  3. schemathesis/cli/commands/__init__.py +51 -3
  4. schemathesis/cli/commands/data.py +10 -0
  5. schemathesis/cli/commands/run/__init__.py +147 -260
  6. schemathesis/cli/commands/run/context.py +2 -3
  7. schemathesis/cli/commands/run/events.py +4 -0
  8. schemathesis/cli/commands/run/executor.py +60 -73
  9. schemathesis/cli/commands/run/filters.py +15 -165
  10. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  11. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  12. schemathesis/cli/commands/run/handlers/output.py +26 -47
  13. schemathesis/cli/commands/run/loaders.py +35 -50
  14. schemathesis/cli/commands/run/validation.py +36 -161
  15. schemathesis/cli/core.py +5 -3
  16. schemathesis/cli/ext/fs.py +7 -5
  17. schemathesis/cli/ext/options.py +0 -21
  18. schemathesis/config/__init__.py +188 -0
  19. schemathesis/config/_auth.py +51 -0
  20. schemathesis/config/_checks.py +268 -0
  21. schemathesis/config/_diff_base.py +99 -0
  22. schemathesis/config/_env.py +21 -0
  23. schemathesis/config/_error.py +156 -0
  24. schemathesis/config/_generation.py +150 -0
  25. schemathesis/config/_health_check.py +24 -0
  26. schemathesis/config/_operations.py +313 -0
  27. schemathesis/config/_output.py +171 -0
  28. schemathesis/config/_parameters.py +19 -0
  29. schemathesis/config/_phases.py +151 -0
  30. schemathesis/config/_projects.py +495 -0
  31. schemathesis/config/_rate_limit.py +17 -0
  32. schemathesis/config/_report.py +116 -0
  33. schemathesis/config/_validator.py +9 -0
  34. schemathesis/config/schema.json +837 -0
  35. schemathesis/core/__init__.py +2 -0
  36. schemathesis/core/compat.py +16 -9
  37. schemathesis/core/errors.py +19 -2
  38. schemathesis/core/failures.py +6 -7
  39. schemathesis/core/hooks.py +20 -0
  40. schemathesis/core/output/__init__.py +14 -37
  41. schemathesis/core/output/sanitization.py +3 -146
  42. schemathesis/core/validation.py +16 -0
  43. schemathesis/engine/__init__.py +2 -4
  44. schemathesis/engine/context.py +41 -43
  45. schemathesis/engine/core.py +7 -5
  46. schemathesis/engine/phases/__init__.py +10 -0
  47. schemathesis/engine/phases/probes.py +8 -8
  48. schemathesis/engine/phases/stateful/_executor.py +68 -43
  49. schemathesis/engine/phases/unit/__init__.py +23 -15
  50. schemathesis/engine/phases/unit/_executor.py +77 -17
  51. schemathesis/engine/phases/unit/_pool.py +1 -1
  52. schemathesis/errors.py +2 -0
  53. schemathesis/filters.py +2 -3
  54. schemathesis/generation/__init__.py +6 -31
  55. schemathesis/generation/case.py +5 -3
  56. schemathesis/generation/coverage.py +153 -123
  57. schemathesis/generation/hypothesis/builder.py +40 -14
  58. schemathesis/generation/meta.py +3 -3
  59. schemathesis/generation/overrides.py +37 -1
  60. schemathesis/generation/stateful/state_machine.py +8 -1
  61. schemathesis/graphql/loaders.py +21 -12
  62. schemathesis/openapi/checks.py +12 -8
  63. schemathesis/openapi/generation/filters.py +10 -8
  64. schemathesis/openapi/loaders.py +22 -13
  65. schemathesis/pytest/lazy.py +2 -5
  66. schemathesis/pytest/plugin.py +11 -2
  67. schemathesis/schemas.py +13 -61
  68. schemathesis/specs/graphql/schemas.py +11 -15
  69. schemathesis/specs/openapi/_hypothesis.py +12 -8
  70. schemathesis/specs/openapi/checks.py +16 -18
  71. schemathesis/specs/openapi/examples.py +4 -3
  72. schemathesis/specs/openapi/formats.py +2 -2
  73. schemathesis/specs/openapi/negative/__init__.py +2 -2
  74. schemathesis/specs/openapi/patterns.py +46 -16
  75. schemathesis/specs/openapi/references.py +2 -3
  76. schemathesis/specs/openapi/schemas.py +11 -20
  77. schemathesis/specs/openapi/stateful/__init__.py +10 -5
  78. schemathesis/transport/prepare.py +7 -6
  79. schemathesis/transport/requests.py +3 -1
  80. schemathesis/transport/wsgi.py +3 -4
  81. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
  82. schemathesis-4.0.0a11.dist-info/RECORD +166 -0
  83. schemathesis/cli/commands/run/checks.py +0 -79
  84. schemathesis/cli/commands/run/hypothesis.py +0 -78
  85. schemathesis/cli/commands/run/reports.py +0 -72
  86. schemathesis/cli/hooks.py +0 -36
  87. schemathesis/engine/config.py +0 -59
  88. schemathesis/experimental/__init__.py +0 -72
  89. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  90. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
  91. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
  92. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import os
5
4
  from dataclasses import dataclass, field
6
5
  from enum import Enum
7
6
  from functools import wraps
@@ -15,15 +14,17 @@ from hypothesis import strategies as st
15
14
  from hypothesis._settings import all_settings
16
15
  from hypothesis.errors import Unsatisfiable
17
16
  from jsonschema.exceptions import SchemaError
17
+ from requests.models import CaseInsensitiveDict
18
18
 
19
19
  from schemathesis import auths
20
20
  from schemathesis.auths import AuthStorage, AuthStorageMark
21
- from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types, string_to_boolean
21
+ from schemathesis.config import ProjectConfig
22
+ from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
22
23
  from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
23
24
  from schemathesis.core.marks import Mark
24
25
  from schemathesis.core.transport import prepare_urlencoded
25
26
  from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
26
- from schemathesis.generation import GenerationConfig, GenerationMode, coverage
27
+ from schemathesis.generation import GenerationMode, coverage
27
28
  from schemathesis.generation.case import Case
28
29
  from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
29
30
  from schemathesis.generation.hypothesis.given import GivenInput
@@ -49,7 +50,7 @@ class HypothesisTestMode(str, Enum):
49
50
 
50
51
  @dataclass
51
52
  class HypothesisTestConfig:
52
- generation: GenerationConfig
53
+ project: ProjectConfig
53
54
  modes: list[HypothesisTestMode]
54
55
  settings: hypothesis.settings | None = None
55
56
  seed: int | None = None
@@ -71,11 +72,11 @@ def create_test(
71
72
  strategy_kwargs = {
72
73
  "hooks": hook_dispatcher,
73
74
  "auth_storage": auth_storage,
74
- "generation_config": config.generation,
75
75
  **config.as_strategy_kwargs,
76
76
  }
77
+ generation = config.project.generation_for(operation=operation)
77
78
  strategy = strategies.combine(
78
- [operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in config.generation.modes]
79
+ [operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes]
79
80
  )
80
81
 
81
82
  hypothesis_test = create_base_test(
@@ -127,23 +128,21 @@ def create_test(
127
128
  ):
128
129
  hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
129
130
 
130
- disable_coverage = string_to_boolean(os.getenv("SCHEMATHESIS_DISABLE_COVERAGE", ""))
131
-
132
131
  if (
133
- not disable_coverage
134
- and HypothesisTestMode.COVERAGE in config.modes
132
+ HypothesisTestMode.COVERAGE in config.modes
135
133
  and Phase.explicit in settings.phases
136
134
  and specification.supports_feature(SpecificationFeature.COVERAGE)
137
135
  and not config.given_args
138
136
  and not config.given_kwargs
139
137
  ):
138
+ phases_config = config.project.phases_for(operation=operation)
140
139
  hypothesis_test = add_coverage(
141
140
  hypothesis_test,
142
141
  operation,
143
- config.generation.modes,
142
+ generation.modes,
144
143
  auth_storage,
145
144
  config.as_strategy_kwargs,
146
- config.generation.unexpected_methods,
145
+ phases_config.coverage.unexpected_methods,
147
146
  )
148
147
 
149
148
  setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
@@ -388,7 +387,7 @@ def _stringify_value(val: Any, container_name: str) -> Any:
388
387
  # Having a list here ensures there will be multiple query parameters wit the same name
389
388
  return [_stringify_value(item, container_name) for item in val]
390
389
  # use comma-separated values style for arrays
391
- return ",".join(_stringify_value(sub, container_name) for sub in val)
390
+ return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
392
391
  if isinstance(val, dict):
393
392
  return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
394
393
  return val
@@ -411,6 +410,10 @@ def _iter_coverage_cases(
411
410
  responses = find_in_responses(operation)
412
411
  # NOTE: The HEAD method is excluded
413
412
  unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
413
+
414
+ seen_negative = coverage.HashSet()
415
+ seen_positive = coverage.HashSet()
416
+
414
417
  for parameter in operation.iter_parameters():
415
418
  location = parameter.location
416
419
  name = parameter.name
@@ -473,7 +476,7 @@ def _iter_coverage_cases(
473
476
  meta=CaseMetadata(
474
477
  generation=GenerationInfo(
475
478
  time=instant.elapsed,
476
- mode=value.generation_mode,
479
+ mode=next_value.generation_mode,
477
480
  ),
478
481
  components=data.components,
479
482
  phase=PhaseInfo.coverage(
@@ -488,6 +491,7 @@ def _iter_coverage_cases(
488
491
  break
489
492
  elif GenerationMode.POSITIVE in generation_modes:
490
493
  data = template.unmodified()
494
+ seen_positive.insert(data.kwargs)
491
495
  yield operation.Case(
492
496
  **data.kwargs,
493
497
  meta=CaseMetadata(
@@ -510,6 +514,12 @@ def _iter_coverage_cases(
510
514
  except StopIteration:
511
515
  break
512
516
 
517
+ if value.generation_mode == GenerationMode.NEGATIVE:
518
+ seen_negative.insert(data.kwargs)
519
+ elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
520
+ # Was already generated before
521
+ continue
522
+
513
523
  yield operation.Case(
514
524
  **data.kwargs,
515
525
  meta=CaseMetadata(
@@ -689,6 +699,9 @@ def _iter_coverage_cases(
689
699
  if GenerationMode.NEGATIVE in generation_modes:
690
700
  subschema = _combination_schema(only_required, required, parameter_set)
691
701
  for case in _yield_negative(subschema, location, container_name):
702
+ kwargs = _case_to_kwargs(case)
703
+ if not seen_negative.insert(kwargs):
704
+ continue
692
705
  assert case.meta is not None
693
706
  assert isinstance(case.meta.phase.data, CoveragePhaseData)
694
707
  # Already generated in one of the blocks above
@@ -738,6 +751,19 @@ def _iter_coverage_cases(
738
751
  )
739
752
 
740
753
 
754
+ def _case_to_kwargs(case: Case) -> dict:
755
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
756
+
757
+ kwargs = {}
758
+ for container_name in LOCATION_TO_CONTAINER.values():
759
+ value = getattr(case, container_name)
760
+ if isinstance(value, CaseInsensitiveDict):
761
+ kwargs[container_name] = dict(value)
762
+ elif value and value is not NOT_SET:
763
+ kwargs[container_name] = value
764
+ return kwargs
765
+
766
+
741
767
  def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
742
768
  for name, value in headers.items():
743
769
  if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
@@ -9,9 +9,9 @@ from schemathesis.generation import GenerationMode
9
9
  class TestPhase(str, Enum):
10
10
  __test__ = False
11
11
 
12
- EXPLICIT = "explicit"
12
+ EXAMPLES = "examples"
13
13
  COVERAGE = "coverage"
14
- GENERATE = "generate"
14
+ FUZZING = "fuzzing"
15
15
 
16
16
 
17
17
  class ComponentKind(str, Enum):
@@ -81,7 +81,7 @@ class PhaseInfo:
81
81
 
82
82
  @classmethod
83
83
  def generate(cls) -> PhaseInfo:
84
- return cls(name=TestPhase.GENERATE, data=GeneratePhaseData())
84
+ return cls(name=TestPhase.FUZZING, data=GeneratePhaseData())
85
85
 
86
86
 
87
87
  @dataclass
@@ -4,6 +4,7 @@ from collections.abc import Mapping
4
4
  from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Any, Callable
6
6
 
7
+ from schemathesis.config import ProjectConfig
7
8
  from schemathesis.core.errors import IncorrectUsage
8
9
  from schemathesis.core.marks import Mark
9
10
  from schemathesis.core.transforms import diff
@@ -11,7 +12,7 @@ from schemathesis.generation.meta import ComponentKind
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from schemathesis.generation.case import Case
14
- from schemathesis.schemas import APIOperation, ParameterSet
15
+ from schemathesis.schemas import APIOperation, Parameter, ParameterSet
15
16
 
16
17
 
17
18
  @dataclass
@@ -41,6 +42,41 @@ class Override:
41
42
  )
42
43
 
43
44
 
45
+ def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override:
46
+ operation_config = config.operations.get_for_operation(operation)
47
+
48
+ output = Override(query={}, headers={}, cookies={}, path_parameters={})
49
+ groups = [
50
+ (output.query, operation.query),
51
+ (output.headers, operation.headers),
52
+ (output.cookies, operation.cookies),
53
+ (output.path_parameters, operation.path_parameters),
54
+ ]
55
+ for container, params in groups:
56
+ for param in params:
57
+ # Attempt to get the override from the operation-specific configuration.
58
+ value = None
59
+ if operation_config:
60
+ value = _get_override_value(param, operation_config.parameters)
61
+ # Fallback to the global project configuration.
62
+ if value is None:
63
+ value = _get_override_value(param, config.parameters)
64
+ if value is not None:
65
+ container[param.name] = value
66
+
67
+ return output
68
+
69
+
70
+ def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
71
+ key = param.name
72
+ full_key = f"{param.location}.{param.name}"
73
+ if key in parameters:
74
+ return parameters[key]
75
+ elif full_key in parameters:
76
+ return parameters[full_key]
77
+ return None
78
+
79
+
44
80
  def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
45
81
  output = {}
46
82
  for param in defined:
@@ -10,6 +10,7 @@ from hypothesis.errors import InvalidDefinition
10
10
  from hypothesis.stateful import RuleBasedStateMachine
11
11
 
12
12
  from schemathesis.checks import CheckFunction
13
+ from schemathesis.core import DEFAULT_STATEFUL_STEP_COUNT
13
14
  from schemathesis.core.errors import NoLinksFound
14
15
  from schemathesis.core.result import Result
15
16
  from schemathesis.core.transport import Response
@@ -22,7 +23,6 @@ if TYPE_CHECKING:
22
23
  from schemathesis.schemas import BaseSchema
23
24
 
24
25
 
25
- DEFAULT_STATEFUL_STEP_COUNT = 6
26
26
  DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
27
27
  phases=[hypothesis.Phase.generate],
28
28
  deadline=None,
@@ -184,6 +184,13 @@ class APIStateMachine(RuleBasedStateMachine):
184
184
  if target is not None:
185
185
  super()._add_result_to_targets((target,), result)
186
186
 
187
+ def _add_results_to_targets(self, targets: tuple[str, ...], results: list[StepOutput]) -> None:
188
+ # Hypothesis >6.131.15
189
+ for result in results:
190
+ target = self._get_target_for_result(result)
191
+ if target is not None:
192
+ super()._add_results_to_targets((target,), [result])
193
+
187
194
  @classmethod
188
195
  def run(cls, *, settings: hypothesis.settings | None = None) -> None:
189
196
  """Run state machine as a test."""
@@ -6,6 +6,7 @@ from os import PathLike
6
6
  from pathlib import Path
7
7
  from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, TypeVar, cast
8
8
 
9
+ from schemathesis.config import SchemathesisConfig
9
10
  from schemathesis.core.errors import LoaderError, LoaderErrorKind
10
11
  from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
11
12
  from schemathesis.hooks import HookContext, dispatch
@@ -17,16 +18,16 @@ if TYPE_CHECKING:
17
18
  from schemathesis.specs.graphql.schemas import GraphQLSchema
18
19
 
19
20
 
20
- def from_asgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
21
+ def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
21
22
  require_relative_url(path)
22
23
  kwargs.setdefault("json", {"query": get_introspection_query()})
23
24
  client = asgi.get_client(app)
24
25
  response = load_from_url(client.post, url=path, **kwargs)
25
26
  schema = extract_schema_from_response(response, lambda r: r.json())
26
- return from_dict(schema=schema).configure(app=app, location=path)
27
+ return from_dict(schema=schema, config=config).configure(app=app, location=path)
27
28
 
28
29
 
29
- def from_wsgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
30
+ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
30
31
  require_relative_url(path)
31
32
  prepare_request_kwargs(kwargs)
32
33
  kwargs.setdefault("json", {"query": get_introspection_query()})
@@ -34,26 +35,30 @@ def from_wsgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
34
35
  response = client.post(path=path, **kwargs)
35
36
  raise_for_status(response)
36
37
  schema = extract_schema_from_response(response, lambda r: r.json)
37
- return from_dict(schema=schema).configure(app=app, location=path)
38
+ return from_dict(schema=schema, config=config).configure(app=app, location=path)
38
39
 
39
40
 
40
- def from_url(url: str, *, wait_for_schema: float | None = None, **kwargs: Any) -> GraphQLSchema:
41
+ def from_url(
42
+ url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
43
+ ) -> GraphQLSchema:
41
44
  """Load from URL."""
42
45
  import requests
43
46
 
44
47
  kwargs.setdefault("json", {"query": get_introspection_query()})
45
48
  response = load_from_url(requests.post, url=url, wait_for_schema=wait_for_schema, **kwargs)
46
49
  schema = extract_schema_from_response(response, lambda r: r.json())
47
- return from_dict(schema).configure(location=url)
50
+ return from_dict(schema, config=config).configure(location=url)
48
51
 
49
52
 
50
- def from_path(path: PathLike | str, *, encoding: str = "utf-8") -> GraphQLSchema:
53
+ def from_path(
54
+ path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
55
+ ) -> GraphQLSchema:
51
56
  """Load from a filesystem path."""
52
57
  with open(path, encoding=encoding) as file:
53
- return from_file(file=file).configure(location=Path(path).absolute().as_uri())
58
+ return from_file(file=file, config=config).configure(location=Path(path).absolute().as_uri())
54
59
 
55
60
 
56
- def from_file(file: IO[str] | str) -> GraphQLSchema:
61
+ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
57
62
  """Load from file-like object or string."""
58
63
  import graphql
59
64
 
@@ -78,10 +83,10 @@ def from_file(file: IO[str] | str) -> GraphQLSchema:
78
83
  _on_invalid_schema(exc)
79
84
  except json.JSONDecodeError:
80
85
  _on_invalid_schema(exc, extras=[entry for entry in str(exc).splitlines() if entry])
81
- return from_dict(schema)
86
+ return from_dict(schema, config=config)
82
87
 
83
88
 
84
- def from_dict(schema: dict[str, Any]) -> GraphQLSchema:
89
+ def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
85
90
  """Base loader that others build upon."""
86
91
  from schemathesis.specs.graphql.schemas import GraphQLSchema
87
92
 
@@ -89,7 +94,11 @@ def from_dict(schema: dict[str, Any]) -> GraphQLSchema:
89
94
  schema = schema["data"]
90
95
  hook_context = HookContext()
91
96
  dispatch("before_load_schema", hook_context, schema)
92
- instance = GraphQLSchema(schema)
97
+
98
+ if config is None:
99
+ config = SchemathesisConfig.discover()
100
+ project_config = config.projects.get(schema)
101
+ instance = GraphQLSchema(schema, config=project_config)
93
102
  dispatch("after_load_schema", hook_context, instance)
94
103
  return instance
95
104
 
@@ -4,8 +4,9 @@ import textwrap
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ from schemathesis.config import OutputConfig
7
8
  from schemathesis.core.failures import Failure, Severity
8
- from schemathesis.core.output import OutputConfig, truncate_json
9
+ from schemathesis.core.output import truncate_json
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from jsonschema import ValidationError
@@ -141,11 +142,14 @@ class JsonSchemaError(Failure):
141
142
  title: str = "Response violates schema",
142
143
  operation: str,
143
144
  exc: ValidationError,
144
- output_config: OutputConfig | None = None,
145
+ config: OutputConfig | None = None,
145
146
  ) -> JsonSchemaError:
146
- output_config = OutputConfig.from_parent(output_config, max_lines=20)
147
- schema = textwrap.indent(truncate_json(exc.schema, config=output_config), prefix=" ")
148
- value = textwrap.indent(truncate_json(exc.instance, config=output_config), prefix=" ")
147
+ schema = textwrap.indent(
148
+ truncate_json(exc.schema, config=config or OutputConfig(), max_lines=20), prefix=" "
149
+ )
150
+ value = textwrap.indent(
151
+ truncate_json(exc.instance, config=config or OutputConfig(), max_lines=20), prefix=" "
152
+ )
149
153
  schema_path = list(exc.absolute_schema_path)
150
154
  if len(schema_path) > 1:
151
155
  # Exclude the last segment, which is already in the schema
@@ -336,7 +340,7 @@ class IgnoredAuth(Failure):
336
340
  class AcceptedNegativeData(Failure):
337
341
  """Response with negative data was accepted."""
338
342
 
339
- __slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "case_id", "severity")
343
+ __slots__ = ("operation", "message", "status_code", "expected_statuses", "title", "case_id", "severity")
340
344
 
341
345
  def __init__(
342
346
  self,
@@ -344,14 +348,14 @@ class AcceptedNegativeData(Failure):
344
348
  operation: str,
345
349
  message: str,
346
350
  status_code: int,
347
- allowed_statuses: list[str],
351
+ expected_statuses: list[str],
348
352
  title: str = "Accepted negative data",
349
353
  case_id: str | None = None,
350
354
  ) -> None:
351
355
  self.operation = operation
352
356
  self.message = message
353
357
  self.status_code = status_code
354
- self.allowed_statuses = allowed_statuses
358
+ self.expected_statuses = expected_statuses
355
359
  self.title = title
356
360
  self.case_id = case_id
357
361
  self.severity = Severity.MEDIUM
@@ -1,4 +1,5 @@
1
1
  from collections.abc import Mapping
2
+ from typing import Any
2
3
 
3
4
  from schemathesis.core import NOT_SET
4
5
  from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
@@ -21,14 +22,15 @@ def is_valid_path(parameters: dict[str, object]) -> bool:
21
22
  In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
22
23
  Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
23
24
  """
24
- return not any(
25
- (
26
- value in ("/", "")
27
- or contains_unicode_surrogate_pair(value)
28
- or isinstance(value, str)
29
- and ("/" in value or "}" in value or "{" in value)
30
- )
31
- for value in parameters.values()
25
+ return not any(is_invalid_path_parameter(value) for value in parameters.values())
26
+
27
+
28
+ def is_invalid_path_parameter(value: Any) -> bool:
29
+ return (
30
+ value in ("/", "")
31
+ or contains_unicode_surrogate_pair(value)
32
+ or isinstance(value, str)
33
+ and ("/" in value or "}" in value or "{" in value)
32
34
  )
33
35
 
34
36
 
@@ -7,6 +7,7 @@ from os import PathLike
7
7
  from pathlib import Path
8
8
  from typing import IO, TYPE_CHECKING, Any, Mapping
9
9
 
10
+ from schemathesis.config import SchemathesisConfig
10
11
  from schemathesis.core import media_types
11
12
  from schemathesis.core.deserialization import deserialize_yaml
12
13
  from schemathesis.core.errors import LoaderError, LoaderErrorKind
@@ -18,16 +19,16 @@ if TYPE_CHECKING:
18
19
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
19
20
 
20
21
 
21
- def from_asgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
22
+ def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
22
23
  require_relative_url(path)
23
24
  client = asgi.get_client(app)
24
25
  response = load_from_url(client.get, url=path, **kwargs)
25
26
  content_type = detect_content_type(headers=response.headers, path=path)
26
27
  schema = load_content(response.text, content_type)
27
- return from_dict(schema=schema).configure(app=app, location=path)
28
+ return from_dict(schema=schema, config=config).configure(app=app, location=path)
28
29
 
29
30
 
30
- def from_wsgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
31
+ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
31
32
  require_relative_url(path)
32
33
  prepare_request_kwargs(kwargs)
33
34
  client = wsgi.get_client(app)
@@ -35,28 +36,32 @@ def from_wsgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
35
36
  raise_for_status(response)
36
37
  content_type = detect_content_type(headers=response.headers, path=path)
37
38
  schema = load_content(response.text, content_type)
38
- return from_dict(schema=schema).configure(app=app, location=path)
39
+ return from_dict(schema=schema, config=config).configure(app=app, location=path)
39
40
 
40
41
 
41
- def from_url(url: str, *, wait_for_schema: float | None = None, **kwargs: Any) -> BaseOpenAPISchema:
42
+ def from_url(
43
+ url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
44
+ ) -> BaseOpenAPISchema:
42
45
  """Load from URL."""
43
46
  import requests
44
47
 
45
48
  response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
46
49
  content_type = detect_content_type(headers=response.headers, path=url)
47
50
  schema = load_content(response.text, content_type)
48
- return from_dict(schema=schema).configure(location=url)
51
+ return from_dict(schema=schema, config=config).configure(location=url)
49
52
 
50
53
 
51
- def from_path(path: PathLike | str, *, encoding: str = "utf-8") -> BaseOpenAPISchema:
54
+ def from_path(
55
+ path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
56
+ ) -> BaseOpenAPISchema:
52
57
  """Load from a filesystem path."""
53
58
  with open(path, encoding=encoding) as file:
54
59
  content_type = detect_content_type(headers=None, path=str(path))
55
60
  schema = load_content(file.read(), content_type)
56
- return from_dict(schema=schema).configure(location=Path(path).absolute().as_uri())
61
+ return from_dict(schema=schema, config=config).configure(location=Path(path).absolute().as_uri())
57
62
 
58
63
 
59
- def from_file(file: IO[str] | str) -> BaseOpenAPISchema:
64
+ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
60
65
  """Load from file-like object or string."""
61
66
  if isinstance(file, str):
62
67
  data = file
@@ -66,10 +71,10 @@ def from_file(file: IO[str] | str) -> BaseOpenAPISchema:
66
71
  schema = json.loads(data)
67
72
  except json.JSONDecodeError:
68
73
  schema = _load_yaml(data)
69
- return from_dict(schema)
74
+ return from_dict(schema, config=config)
70
75
 
71
76
 
72
- def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
77
+ def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
73
78
  """Base loader that others build upon."""
74
79
  from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
75
80
 
@@ -78,8 +83,12 @@ def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
78
83
  hook_context = HookContext()
79
84
  dispatch("before_load_schema", hook_context, schema)
80
85
 
86
+ if config is None:
87
+ config = SchemathesisConfig.discover()
88
+ project_config = config.projects.get(schema)
89
+
81
90
  if "swagger" in schema:
82
- instance = SwaggerV20(schema)
91
+ instance = SwaggerV20(raw_schema=schema, config=project_config)
83
92
  elif "openapi" in schema:
84
93
  version = schema["openapi"]
85
94
  if not OPENAPI_VERSION_RE.match(version):
@@ -87,7 +96,7 @@ def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
87
96
  LoaderErrorKind.OPEN_API_UNSUPPORTED_VERSION,
88
97
  f"The provided schema uses Open API {version}, which is currently not supported.",
89
98
  )
90
- instance = OpenApi30(schema)
99
+ instance = OpenApi30(raw_schema=schema, config=project_config)
91
100
  else:
92
101
  raise LoaderError(
93
102
  LoaderErrorKind.OPEN_API_UNSPECIFIED_VERSION,
@@ -12,7 +12,6 @@ from pytest_subtests import SubTests
12
12
  from schemathesis.core.errors import InvalidSchema
13
13
  from schemathesis.core.result import Ok, Result
14
14
  from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
15
- from schemathesis.generation import GenerationConfig
16
15
  from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
17
16
  from schemathesis.generation.hypothesis.given import (
18
17
  GivenArgsMark,
@@ -38,7 +37,6 @@ def get_all_tests(
38
37
  *,
39
38
  schema: BaseSchema,
40
39
  test_func: Callable,
41
- generation_config: GenerationConfig,
42
40
  modes: list[HypothesisTestMode],
43
41
  settings: hypothesis.settings | None = None,
44
42
  seed: int | None = None,
@@ -46,7 +44,7 @@ def get_all_tests(
46
44
  given_kwargs: dict[str, GivenInput] | None = None,
47
45
  ) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
48
46
  """Generate all operations and Hypothesis tests for them."""
49
- for result in schema.get_all_operations(generation_config=generation_config):
47
+ for result in schema.get_all_operations():
50
48
  if isinstance(result, Ok):
51
49
  operation = result.ok()
52
50
  if callable(as_strategy_kwargs):
@@ -60,7 +58,7 @@ def get_all_tests(
60
58
  settings=settings,
61
59
  modes=modes,
62
60
  seed=seed,
63
- generation=generation_config,
61
+ project=schema.config,
64
62
  as_strategy_kwargs=_as_strategy_kwargs,
65
63
  given_kwargs=given_kwargs or {},
66
64
  ),
@@ -194,7 +192,6 @@ class LazySchema:
194
192
  test_func=test_func,
195
193
  settings=settings,
196
194
  modes=list(HypothesisTestMode),
197
- generation_config=schema.generation_config,
198
195
  as_strategy_kwargs=as_strategy_kwargs,
199
196
  given_kwargs=given_kwargs,
200
197
  )
@@ -134,13 +134,22 @@ class SchemathesisCase(PyCollector):
134
134
  as_strategy_kwargs[location] = entry
135
135
  else:
136
136
  as_strategy_kwargs = {}
137
+ modes = []
138
+ phases = self.schema.config.phases_for(operation=operation)
139
+ if phases.examples.enabled:
140
+ modes.append(HypothesisTestMode.EXAMPLES)
141
+ if phases.fuzzing.enabled:
142
+ modes.append(HypothesisTestMode.FUZZING)
143
+ if phases.coverage.enabled:
144
+ modes.append(HypothesisTestMode.COVERAGE)
145
+
137
146
  funcobj = create_test(
138
147
  operation=operation,
139
148
  test_func=self.test_function,
140
149
  config=HypothesisTestConfig(
141
- modes=list(HypothesisTestMode),
150
+ modes=modes,
142
151
  given_kwargs=self.given_kwargs,
143
- generation=self.schema.generation_config,
152
+ project=self.schema.config,
144
153
  as_strategy_kwargs=as_strategy_kwargs,
145
154
  ),
146
155
  )