schemathesis 3.18.5__py3-none-any.whl → 3.19.1__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 (60) hide show
  1. schemathesis/__init__.py +1 -3
  2. schemathesis/auths.py +218 -43
  3. schemathesis/cli/__init__.py +37 -20
  4. schemathesis/cli/callbacks.py +13 -1
  5. schemathesis/cli/cassettes.py +18 -18
  6. schemathesis/cli/context.py +25 -24
  7. schemathesis/cli/debug.py +3 -3
  8. schemathesis/cli/junitxml.py +4 -4
  9. schemathesis/cli/options.py +1 -1
  10. schemathesis/cli/output/default.py +2 -0
  11. schemathesis/constants.py +3 -3
  12. schemathesis/exceptions.py +9 -9
  13. schemathesis/extra/pytest_plugin.py +1 -1
  14. schemathesis/failures.py +65 -66
  15. schemathesis/filters.py +269 -0
  16. schemathesis/hooks.py +11 -11
  17. schemathesis/lazy.py +21 -16
  18. schemathesis/models.py +149 -107
  19. schemathesis/parameters.py +12 -7
  20. schemathesis/runner/events.py +55 -55
  21. schemathesis/runner/impl/core.py +26 -26
  22. schemathesis/runner/impl/solo.py +6 -7
  23. schemathesis/runner/impl/threadpool.py +5 -5
  24. schemathesis/runner/serialization.py +50 -50
  25. schemathesis/schemas.py +38 -23
  26. schemathesis/serializers.py +3 -3
  27. schemathesis/service/ci.py +25 -25
  28. schemathesis/service/client.py +2 -2
  29. schemathesis/service/events.py +12 -13
  30. schemathesis/service/hosts.py +4 -4
  31. schemathesis/service/metadata.py +14 -15
  32. schemathesis/service/models.py +12 -13
  33. schemathesis/service/report.py +30 -31
  34. schemathesis/service/serialization.py +2 -4
  35. schemathesis/specs/graphql/loaders.py +21 -2
  36. schemathesis/specs/graphql/schemas.py +8 -8
  37. schemathesis/specs/openapi/expressions/context.py +4 -4
  38. schemathesis/specs/openapi/expressions/lexer.py +11 -12
  39. schemathesis/specs/openapi/expressions/nodes.py +16 -16
  40. schemathesis/specs/openapi/expressions/parser.py +1 -1
  41. schemathesis/specs/openapi/links.py +15 -17
  42. schemathesis/specs/openapi/loaders.py +29 -2
  43. schemathesis/specs/openapi/negative/__init__.py +5 -5
  44. schemathesis/specs/openapi/negative/mutations.py +6 -6
  45. schemathesis/specs/openapi/parameters.py +12 -13
  46. schemathesis/specs/openapi/references.py +2 -2
  47. schemathesis/specs/openapi/schemas.py +11 -15
  48. schemathesis/specs/openapi/security.py +12 -7
  49. schemathesis/specs/openapi/stateful/links.py +4 -4
  50. schemathesis/stateful.py +19 -19
  51. schemathesis/targets.py +5 -6
  52. schemathesis/throttling.py +34 -0
  53. schemathesis/types.py +11 -13
  54. schemathesis/utils.py +2 -2
  55. {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/METADATA +4 -3
  56. schemathesis-3.19.1.dist-info/RECORD +107 -0
  57. schemathesis-3.18.5.dist-info/RECORD +0 -105
  58. {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/WHEEL +0 -0
  59. {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/entry_points.txt +0 -0
  60. {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/hooks.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import inspect
2
2
  from collections import defaultdict
3
3
  from copy import deepcopy
4
+ from dataclasses import dataclass, field
4
5
  from enum import Enum, unique
5
- from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Optional, Union, cast
6
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, Dict, List, Optional, Union, cast
6
7
 
7
- import attr
8
8
  from hypothesis import strategies as st
9
9
 
10
10
  from .types import GenericTest
@@ -22,13 +22,13 @@ class HookScope(Enum):
22
22
  TEST = 3
23
23
 
24
24
 
25
- @attr.s(slots=True) # pragma: no mutate
25
+ @dataclass
26
26
  class RegisteredHook:
27
- signature: inspect.Signature = attr.ib() # pragma: no mutate
28
- scopes: List[HookScope] = attr.ib() # pragma: no mutate
27
+ signature: inspect.Signature
28
+ scopes: List[HookScope]
29
29
 
30
30
 
31
- @attr.s(slots=True) # pragma: no mutate
31
+ @dataclass
32
32
  class HookContext:
33
33
  """A context that is passed to some hook functions.
34
34
 
@@ -36,23 +36,23 @@ class HookContext:
36
36
  Might be absent in some cases.
37
37
  """
38
38
 
39
- operation: Optional["APIOperation"] = attr.ib(default=None) # pragma: no mutate
39
+ operation: Optional["APIOperation"] = None
40
40
 
41
41
  @deprecated_property(removed_in="4.0", replacement="operation")
42
42
  def endpoint(self) -> Optional["APIOperation"]:
43
43
  return self.operation
44
44
 
45
45
 
46
- @attr.s(slots=True) # pragma: no mutate
46
+ @dataclass
47
47
  class HookDispatcher:
48
48
  """Generic hook dispatcher.
49
49
 
50
50
  Provides a mechanism to extend Schemathesis in registered hook points.
51
51
  """
52
52
 
53
- scope: HookScope = attr.ib() # pragma: no mutate
54
- _hooks: DefaultDict[str, List[Callable]] = attr.ib(factory=lambda: defaultdict(list)) # pragma: no mutate
55
- _specs: Dict[str, RegisteredHook] = {} # pragma: no mutate
53
+ scope: HookScope
54
+ _hooks: DefaultDict[str, List[Callable]] = field(default_factory=lambda: defaultdict(list))
55
+ _specs: ClassVar[Dict[str, RegisteredHook]] = {}
56
56
 
57
57
  def register(self, hook: Union[str, Callable]) -> Callable:
58
58
  """Register a new hook.
schemathesis/lazy.py CHANGED
@@ -1,13 +1,14 @@
1
+ from dataclasses import dataclass, field
1
2
  from inspect import signature
2
3
  from typing import Any, Callable, Dict, Generator, Optional, Type, Union
3
4
 
4
- import attr
5
5
  import pytest
6
6
  from _pytest.fixtures import FixtureRequest
7
7
  from hypothesis.core import HypothesisHandle
8
8
  from hypothesis.errors import Flaky
9
9
  from hypothesis.internal.escalation import format_exception, get_interesting_origin, get_trimmed_traceback
10
10
  from hypothesis.internal.reflection import impersonate
11
+ from pyrate_limiter import Limiter
11
12
  from pytest_subtests import SubTests, nullcontext
12
13
 
13
14
  from ._compat import MultipleFailures
@@ -32,21 +33,22 @@ from .utils import (
32
33
  )
33
34
 
34
35
 
35
- @attr.s(slots=True) # pragma: no mutate
36
+ @dataclass
36
37
  class LazySchema:
37
- fixture_name: str = attr.ib() # pragma: no mutate
38
- base_url: Union[Optional[str], NotSet] = attr.ib(default=NOT_SET) # pragma: no mutate
39
- method: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
40
- endpoint: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
41
- tag: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
42
- operation_id: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
43
- app: Any = attr.ib(default=NOT_SET) # pragma: no mutate
44
- hooks: HookDispatcher = attr.ib(factory=lambda: HookDispatcher(scope=HookScope.SCHEMA)) # pragma: no mutate
45
- auth: AuthStorage = attr.ib(factory=AuthStorage) # pragma: no mutate
46
- validate_schema: bool = attr.ib(default=True) # pragma: no mutate
47
- skip_deprecated_operations: bool = attr.ib(default=False) # pragma: no mutate
48
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = attr.ib(default=NOT_SET)
49
- code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
38
+ fixture_name: str
39
+ base_url: Union[Optional[str], NotSet] = NOT_SET
40
+ method: Optional[Filter] = NOT_SET
41
+ endpoint: Optional[Filter] = NOT_SET
42
+ tag: Optional[Filter] = NOT_SET
43
+ operation_id: Optional[Filter] = NOT_SET
44
+ app: Any = NOT_SET
45
+ hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
46
+ auth: AuthStorage = field(default_factory=AuthStorage)
47
+ validate_schema: bool = True
48
+ skip_deprecated_operations: bool = False
49
+ data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET
50
+ code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
51
+ rate_limiter: Optional[Limiter] = None
50
52
 
51
53
  def hook(self, hook: Union[str, Callable]) -> Callable:
52
54
  return self.hooks.register(hook)
@@ -105,13 +107,14 @@ class LazySchema:
105
107
  tag=tag,
106
108
  operation_id=operation_id,
107
109
  hooks=self.hooks,
108
- auth=self.auth if self.auth.provider is not None else NOT_SET,
110
+ auth=self.auth if self.auth.providers is not None else NOT_SET,
109
111
  test_function=test,
110
112
  validate_schema=validate_schema,
111
113
  skip_deprecated_operations=skip_deprecated_operations,
112
114
  data_generation_methods=data_generation_methods,
113
115
  code_sample_style=_code_sample_style,
114
116
  app=self.app,
117
+ rate_limiter=self.rate_limiter,
115
118
  )
116
119
  fixtures = get_fixtures(test, request, given_kwargs)
117
120
  # Changing the node id is required for better reporting - the method and path will appear there
@@ -270,6 +273,7 @@ def get_schema(
270
273
  skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
271
274
  data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
272
275
  code_sample_style: CodeSampleStyle,
276
+ rate_limiter: Optional[Limiter],
273
277
  ) -> BaseSchema:
274
278
  """Loads a schema from the fixture."""
275
279
  schema = request.getfixturevalue(name)
@@ -289,6 +293,7 @@ def get_schema(
289
293
  skip_deprecated_operations=skip_deprecated_operations,
290
294
  data_generation_methods=data_generation_methods,
291
295
  code_sample_style=code_sample_style,
296
+ rate_limiter=rate_limiter,
292
297
  )
293
298
 
294
299
 
schemathesis/models.py CHANGED
@@ -4,6 +4,7 @@ import http
4
4
  import json
5
5
  from collections import Counter
6
6
  from contextlib import contextmanager
7
+ from dataclasses import dataclass, field
7
8
  from enum import Enum
8
9
  from itertools import chain
9
10
  from logging import LogRecord
@@ -28,9 +29,8 @@ from typing import (
28
29
  from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
29
30
  from uuid import uuid4
30
31
 
31
- import attr
32
32
  import curlify
33
- import requests
33
+ import requests.auth
34
34
  import werkzeug
35
35
  from hypothesis import event, note, reject
36
36
  from hypothesis import strategies as st
@@ -77,13 +77,13 @@ if TYPE_CHECKING:
77
77
  from .stateful import Stateful, StatefulTest
78
78
 
79
79
 
80
- @attr.s(slots=True) # pragma: no mutate
80
+ @dataclass
81
81
  class CaseSource:
82
82
  """Data sources, used to generate a test case."""
83
83
 
84
- case: "Case" = attr.ib() # pragma: no mutate
85
- response: GenericResponse = attr.ib() # pragma: no mutate
86
- elapsed: float = attr.ib() # pragma: no mutate
84
+ case: "Case"
85
+ response: GenericResponse
86
+ elapsed: float
87
87
 
88
88
  def partial_deepcopy(self) -> "CaseSource":
89
89
  return self.__class__(
@@ -110,26 +110,27 @@ def serialize(value: Any) -> str:
110
110
  return json.dumps(value, sort_keys=True, default=_serialize_unknown)
111
111
 
112
112
 
113
- @attr.s(slots=True, repr=False, hash=False) # pragma: no mutate
113
+ @dataclass(repr=False)
114
114
  class Case:
115
115
  """A single test case parameters."""
116
116
 
117
- operation: "APIOperation" = attr.ib() # pragma: no mutate
118
- path_parameters: Optional[PathParameters] = attr.ib(default=None) # pragma: no mutate
119
- headers: Optional[CaseInsensitiveDict] = attr.ib(default=None) # pragma: no mutate
120
- cookies: Optional[Cookies] = attr.ib(default=None) # pragma: no mutate
121
- query: Optional[Query] = attr.ib(default=None) # pragma: no mutate
117
+ operation: "APIOperation"
118
+ path_parameters: Optional[PathParameters] = None
119
+ headers: Optional[CaseInsensitiveDict] = None
120
+ cookies: Optional[Cookies] = None
121
+ query: Optional[Query] = None
122
122
  # By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
123
123
  # which is a valid payload.
124
- body: Union[Body, NotSet] = attr.ib(default=NOT_SET) # pragma: no mutate
124
+ body: Union[Body, NotSet] = NOT_SET
125
125
 
126
- source: Optional[CaseSource] = attr.ib(default=None) # pragma: no mutate
126
+ source: Optional[CaseSource] = None
127
127
  # The media type for cases with a payload. For example, "application/json"
128
- media_type: Optional[str] = attr.ib(default=None) # pragma: no mutate
128
+ media_type: Optional[str] = None
129
129
  # The way the case was generated (None for manually crafted ones)
130
- data_generation_method: Optional[DataGenerationMethod] = attr.ib(default=None) # pragma: no mutate
130
+ data_generation_method: Optional[DataGenerationMethod] = None
131
131
  # Unique test case identifier
132
- id: str = attr.ib(factory=lambda: uuid4().hex, eq=False) # pragma: no mutate
132
+ id: str = field(default_factory=lambda: uuid4().hex, compare=False)
133
+ _auth: Optional[requests.auth.AuthBase] = None
133
134
 
134
135
  def __repr__(self) -> str:
135
136
  parts = [f"{self.__class__.__name__}("]
@@ -307,7 +308,7 @@ class Case:
307
308
  if "content-type" not in {header.lower() for header in final_headers}:
308
309
  final_headers["Content-Type"] = self.media_type
309
310
  base_url = self._get_base_url(base_url)
310
- formatted_path = self.formatted_path.lstrip("/") # pragma: no mutate
311
+ formatted_path = self.formatted_path.lstrip("/")
311
312
  if not base_url.endswith("/"):
312
313
  base_url += "/"
313
314
  url = unquote(urljoin(base_url, quote(formatted_path)))
@@ -318,6 +319,8 @@ class Case:
318
319
  extra = serializer.as_requests(context, self.body)
319
320
  else:
320
321
  extra = {}
322
+ if self._auth is not None:
323
+ extra["auth"] = self._auth
321
324
  additional_headers = extra.pop("headers", None)
322
325
  if additional_headers:
323
326
  # Additional headers, needed for the serializer
@@ -337,6 +340,8 @@ class Case:
337
340
  base_url: Optional[str] = None,
338
341
  session: Optional[requests.Session] = None,
339
342
  headers: Optional[Dict[str, Any]] = None,
343
+ params: Optional[Dict[str, Any]] = None,
344
+ cookies: Optional[Dict[str, Any]] = None,
340
345
  **kwargs: Any,
341
346
  ) -> requests.Response:
342
347
  """Make a network call with `requests`."""
@@ -344,6 +349,10 @@ class Case:
344
349
  dispatch("before_call", hook_context, self)
345
350
  data = self.as_requests_kwargs(base_url, headers)
346
351
  data.update(kwargs)
352
+ if params is not None:
353
+ _merge_dict_to(data, "params", params)
354
+ if cookies is not None:
355
+ _merge_dict_to(data, "cookies", cookies)
347
356
  data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
348
357
  if session is None:
349
358
  validate_vanilla_requests_kwargs(data)
@@ -352,7 +361,8 @@ class Case:
352
361
  else:
353
362
  close_session = False
354
363
  try:
355
- response = session.request(**data) # type: ignore
364
+ with self.operation.schema.ratelimit():
365
+ response = session.request(**data) # type: ignore
356
366
  except requests.Timeout as exc:
357
367
  timeout = 1000 * data["timeout"] # It is defined and not empty, since the exception happened
358
368
  code_message = self._get_code_message(self.operation.schema.code_sample_style, exc.request)
@@ -387,7 +397,13 @@ class Case:
387
397
  **extra,
388
398
  }
389
399
 
390
- def call_wsgi(self, app: Any = None, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> WSGIResponse:
400
+ def call_wsgi(
401
+ self,
402
+ app: Any = None,
403
+ headers: Optional[Dict[str, str]] = None,
404
+ query_string: Optional[Dict[str, str]] = None,
405
+ **kwargs: Any,
406
+ ) -> WSGIResponse:
391
407
  application = app or self.app
392
408
  if application is None:
393
409
  raise RuntimeError(
@@ -397,8 +413,10 @@ class Case:
397
413
  hook_context = HookContext(operation=self.operation)
398
414
  dispatch("before_call", hook_context, self)
399
415
  data = self.as_werkzeug_kwargs(headers)
416
+ if query_string is not None:
417
+ _merge_dict_to(data, "query_string", query_string)
400
418
  client = werkzeug.Client(application, WSGIResponse)
401
- with cookie_handler(client, self.cookies):
419
+ with cookie_handler(client, self.cookies), self.operation.schema.ratelimit():
402
420
  response = client.open(**data, **kwargs)
403
421
  requests_kwargs = self.as_requests_kwargs(base_url=self.get_full_base_url(), headers=headers)
404
422
  response.request = requests.Request(**requests_kwargs).prepare()
@@ -519,6 +537,13 @@ class Case:
519
537
  )
520
538
 
521
539
 
540
+ def _merge_dict_to(data: Dict[str, Any], data_key: str, new: Dict[str, Any]) -> None:
541
+ original = data[data_key] or {}
542
+ for key, value in new.items():
543
+ original[key] = value
544
+ data[data_key] = original
545
+
546
+
522
547
  def validate_vanilla_requests_kwargs(data: Dict[str, Any]) -> None:
523
548
  """Check arguments for `requests.Session.request`.
524
549
 
@@ -569,10 +594,10 @@ def cookie_handler(client: werkzeug.Client, cookies: Optional[Cookies]) -> Gener
569
594
 
570
595
 
571
596
  P = TypeVar("P", bound=Parameter)
572
- D = TypeVar("D")
597
+ D = TypeVar("D", bound=dict)
573
598
 
574
599
 
575
- @attr.s # pragma: no mutate
600
+ @dataclass
576
601
  class OperationDefinition(Generic[P, D]):
577
602
  """A wrapper to store not resolved API operation definitions.
578
603
 
@@ -581,16 +606,25 @@ class OperationDefinition(Generic[P, D]):
581
606
  scope change to have a proper reference resolving later.
582
607
  """
583
608
 
584
- raw: D = attr.ib() # pragma: no mutate
585
- resolved: D = attr.ib() # pragma: no mutate
586
- scope: str = attr.ib() # pragma: no mutate
587
- parameters: Sequence[P] = attr.ib() # pragma: no mutate
609
+ raw: D
610
+ resolved: D
611
+ scope: str
612
+ parameters: Sequence[P]
613
+
614
+ def __contains__(self, item: Union[str, int]) -> bool:
615
+ return item in self.resolved
616
+
617
+ def __getitem__(self, item: Union[str, int]) -> Union[None, bool, float, str, list, Dict[str, Any]]:
618
+ return self.resolved[item]
619
+
620
+ def get(self, item: Union[str, int], default: Any = None) -> Union[None, bool, float, str, list, Dict[str, Any]]:
621
+ return self.resolved.get(item, default)
588
622
 
589
623
 
590
624
  C = TypeVar("C", bound=Case)
591
625
 
592
626
 
593
- @attr.s(eq=False) # pragma: no mutate
627
+ @dataclass(eq=False)
594
628
  class APIOperation(Generic[P, C]):
595
629
  """A single operation defined in an API.
596
630
 
@@ -606,23 +640,23 @@ class APIOperation(Generic[P, C]):
606
640
  # `path` does not contain `basePath`
607
641
  # Example <scheme>://<host>/<basePath>/users - "/users" is path
608
642
  # https://swagger.io/docs/specification/2-0/api-host-and-base-path/
609
- path: str = attr.ib() # pragma: no mutate
610
- method: str = attr.ib() # pragma: no mutate
611
- definition: OperationDefinition = attr.ib(repr=False) # pragma: no mutate
612
- schema: "BaseSchema" = attr.ib() # pragma: no mutate
613
- verbose_name: str = attr.ib() # pragma: no mutate
614
- app: Any = attr.ib(default=None) # pragma: no mutate
615
- base_url: Optional[str] = attr.ib(default=None) # pragma: no mutate
616
- path_parameters: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
617
- headers: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
618
- cookies: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
619
- query: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
620
- body: PayloadAlternatives[P] = attr.ib(factory=PayloadAlternatives) # pragma: no mutate
621
- case_cls: Type[C] = attr.ib(default=Case) # type: ignore
622
-
623
- @verbose_name.default
624
- def _verbose_name_default(self) -> str:
625
- return f"{self.method.upper()} {self.full_path}"
643
+ path: str
644
+ method: str
645
+ definition: OperationDefinition = field(repr=False)
646
+ schema: "BaseSchema"
647
+ verbose_name: str = None # type: ignore
648
+ app: Any = None
649
+ base_url: Optional[str] = None
650
+ path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
651
+ headers: ParameterSet[P] = field(default_factory=ParameterSet)
652
+ cookies: ParameterSet[P] = field(default_factory=ParameterSet)
653
+ query: ParameterSet[P] = field(default_factory=ParameterSet)
654
+ body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
655
+ case_cls: Type[C] = Case # type: ignore
656
+
657
+ def __post_init__(self) -> None:
658
+ if self.verbose_name is None:
659
+ self.verbose_name = f"{self.method.upper()} {self.full_path}" # type: ignore
626
660
 
627
661
  @property
628
662
  def full_path(self) -> str:
@@ -636,27 +670,35 @@ class APIOperation(Generic[P, C]):
636
670
  """Iterate over all operation's parameters."""
637
671
  return chain(self.path_parameters, self.headers, self.cookies, self.query)
638
672
 
673
+ def _lookup_container(self, location: str) -> Union[ParameterSet[P], PayloadAlternatives[P], None]:
674
+ return {
675
+ "path": self.path_parameters,
676
+ "header": self.headers,
677
+ "cookie": self.cookies,
678
+ "query": self.query,
679
+ "body": self.body,
680
+ }.get(location)
681
+
639
682
  def add_parameter(self, parameter: P) -> None:
640
683
  """Add a new processed parameter to an API operation.
641
684
 
642
685
  :param parameter: A parameter that will be used with this operation.
643
686
  :rtype: None
644
687
  """
645
- lookup_table = {
646
- "path": self.path_parameters,
647
- "header": self.headers,
648
- "cookie": self.cookies,
649
- "query": self.query,
650
- "body": self.body,
651
- }
652
688
  # If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
653
689
  # But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
654
690
  # In this case, we still might generate some tests for an API operation, but without this parameter,
655
691
  # which is better than skip the whole operation from testing.
656
- if parameter.location in lookup_table:
657
- container = lookup_table[parameter.location]
692
+ container = self._lookup_container(parameter.location)
693
+ if container is not None:
658
694
  container.add(parameter)
659
695
 
696
+ def get_parameter(self, name: str, location: str) -> Optional[P]:
697
+ container = self._lookup_container(location)
698
+ if container is not None:
699
+ return container.get(name)
700
+ return None
701
+
660
702
  def as_strategy(
661
703
  self,
662
704
  hooks: Optional["HookDispatcher"] = None,
@@ -788,35 +830,35 @@ Endpoint = APIOperation
788
830
  class Status(str, Enum):
789
831
  """Status of an action or multiple actions."""
790
832
 
791
- success = "success" # pragma: no mutate
792
- failure = "failure" # pragma: no mutate
793
- error = "error" # pragma: no mutate
794
- skip = "skip" # pragma: no mutate
833
+ success = "success"
834
+ failure = "failure"
835
+ error = "error"
836
+ skip = "skip"
795
837
 
796
838
 
797
- @attr.s(slots=True, repr=False) # pragma: no mutate
839
+ @dataclass(repr=False)
798
840
  class Check:
799
841
  """Single check run result."""
800
842
 
801
- name: str = attr.ib() # pragma: no mutate
802
- value: Status = attr.ib() # pragma: no mutate
803
- response: Optional[GenericResponse] = attr.ib() # pragma: no mutate
804
- elapsed: float = attr.ib() # pragma: no mutate
805
- example: Case = attr.ib() # pragma: no mutate
806
- message: Optional[str] = attr.ib(default=None) # pragma: no mutate
843
+ name: str
844
+ value: Status
845
+ response: Optional[GenericResponse]
846
+ elapsed: float
847
+ example: Case
848
+ message: Optional[str] = None
807
849
  # Failure-specific context
808
- context: Optional[FailureContext] = attr.ib(default=None) # pragma: no mutate
809
- request: Optional[requests.PreparedRequest] = attr.ib(default=None) # pragma: no mutate
850
+ context: Optional[FailureContext] = None
851
+ request: Optional[requests.PreparedRequest] = None
810
852
 
811
853
 
812
- @attr.s(slots=True, repr=False) # pragma: no mutate
854
+ @dataclass(repr=False)
813
855
  class Request:
814
856
  """Request data extracted from `Case`."""
815
857
 
816
- method: str = attr.ib() # pragma: no mutate
817
- uri: str = attr.ib() # pragma: no mutate
818
- body: Optional[str] = attr.ib() # pragma: no mutate
819
- headers: Headers = attr.ib() # pragma: no mutate
858
+ method: str
859
+ uri: str
860
+ body: Optional[str]
861
+ headers: Headers
820
862
 
821
863
  @classmethod
822
864
  def from_case(cls, case: Case, session: requests.Session) -> "Request":
@@ -851,17 +893,17 @@ def serialize_payload(payload: bytes) -> str:
851
893
  return base64.b64encode(payload).decode()
852
894
 
853
895
 
854
- @attr.s(slots=True, repr=False) # pragma: no mutate
896
+ @dataclass(repr=False)
855
897
  class Response:
856
898
  """Unified response data."""
857
899
 
858
- status_code: int = attr.ib() # pragma: no mutate
859
- message: str = attr.ib() # pragma: no mutate
860
- headers: Dict[str, List[str]] = attr.ib() # pragma: no mutate
861
- body: Optional[str] = attr.ib() # pragma: no mutate
862
- encoding: Optional[str] = attr.ib() # pragma: no mutate
863
- http_version: str = attr.ib() # pragma: no mutate
864
- elapsed: float = attr.ib() # pragma: no mutate
900
+ status_code: int
901
+ message: str
902
+ headers: Dict[str, List[str]]
903
+ body: Optional[str]
904
+ encoding: Optional[str]
905
+ http_version: str
906
+ elapsed: float
865
907
 
866
908
  @classmethod
867
909
  def from_requests(cls, response: requests.Response) -> "Response":
@@ -911,16 +953,16 @@ class Response:
911
953
  )
912
954
 
913
955
 
914
- @attr.s(slots=True) # pragma: no mutate
956
+ @dataclass
915
957
  class Interaction:
916
958
  """A single interaction with the target app."""
917
959
 
918
- request: Request = attr.ib() # pragma: no mutate
919
- response: Response = attr.ib() # pragma: no mutate
920
- checks: List[Check] = attr.ib() # pragma: no mutate
921
- status: Status = attr.ib() # pragma: no mutate
922
- data_generation_method: DataGenerationMethod = attr.ib() # pragma: no mutate
923
- recorded_at: str = attr.ib(factory=lambda: datetime.datetime.now().isoformat()) # pragma: no mutate
960
+ request: Request
961
+ response: Response
962
+ checks: List[Check]
963
+ status: Status
964
+ data_generation_method: DataGenerationMethod
965
+ recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
924
966
 
925
967
  @classmethod
926
968
  def from_requests(
@@ -955,28 +997,28 @@ class Interaction:
955
997
  )
956
998
 
957
999
 
958
- @attr.s(slots=True, repr=False) # pragma: no mutate
1000
+ @dataclass(repr=False)
959
1001
  class TestResult:
960
1002
  """Result of a single test."""
961
1003
 
962
1004
  __test__ = False
963
1005
 
964
- method: str = attr.ib() # pragma: no mutate
965
- path: str = attr.ib() # pragma: no mutate
966
- verbose_name: str = attr.ib() # pragma: no mutate
967
- data_generation_method: List[DataGenerationMethod] = attr.ib() # pragma: no mutate
968
- checks: List[Check] = attr.ib(factory=list) # pragma: no mutate
969
- errors: List[Tuple[Exception, Optional[Case]]] = attr.ib(factory=list) # pragma: no mutate
970
- interactions: List[Interaction] = attr.ib(factory=list) # pragma: no mutate
971
- logs: List[LogRecord] = attr.ib(factory=list) # pragma: no mutate
972
- is_errored: bool = attr.ib(default=False) # pragma: no mutate
973
- is_flaky: bool = attr.ib(default=False) # pragma: no mutate
974
- is_skipped: bool = attr.ib(default=False) # pragma: no mutate
975
- is_executed: bool = attr.ib(default=False) # pragma: no mutate
976
- seed: Optional[int] = attr.ib(default=None) # pragma: no mutate
1006
+ method: str
1007
+ path: str
1008
+ verbose_name: str
1009
+ data_generation_method: List[DataGenerationMethod]
1010
+ checks: List[Check] = field(default_factory=list)
1011
+ errors: List[Tuple[Exception, Optional[Case]]] = field(default_factory=list)
1012
+ interactions: List[Interaction] = field(default_factory=list)
1013
+ logs: List[LogRecord] = field(default_factory=list)
1014
+ is_errored: bool = False
1015
+ is_flaky: bool = False
1016
+ is_skipped: bool = False
1017
+ is_executed: bool = False
1018
+ seed: Optional[int] = None
977
1019
  # To show a proper reproduction code if an error happens and there is no way to get actual headers that were
978
1020
  # sent over the network. Or there could be no actual requests at all
979
- overridden_headers: Optional[Dict[str, Any]] = attr.ib(default=None) # pragma: no mutate
1021
+ overridden_headers: Optional[Dict[str, Any]] = None
980
1022
 
981
1023
  def mark_errored(self) -> None:
982
1024
  self.is_errored = True
@@ -1052,15 +1094,15 @@ class TestResult:
1052
1094
  self.interactions.append(Interaction.from_wsgi(case, response, headers, elapsed, status, checks))
1053
1095
 
1054
1096
 
1055
- @attr.s(slots=True, repr=False) # pragma: no mutate
1097
+ @dataclass(repr=False)
1056
1098
  class TestResultSet:
1057
1099
  """Set of multiple test results."""
1058
1100
 
1059
1101
  __test__ = False
1060
1102
 
1061
- results: List[TestResult] = attr.ib(factory=list) # pragma: no mutate
1062
- generic_errors: List[InvalidSchema] = attr.ib(factory=list) # pragma: no mutate
1063
- warnings: List[str] = attr.ib(factory=list) # pragma: no mutate
1103
+ results: List[TestResult] = field(default_factory=list)
1104
+ generic_errors: List[InvalidSchema] = field(default_factory=list)
1105
+ warnings: List[str] = field(default_factory=list)
1064
1106
 
1065
1107
  def __iter__(self) -> Iterator[TestResult]:
1066
1108
  return iter(self.results)
@@ -1127,4 +1169,4 @@ class TestResultSet:
1127
1169
  self.warnings.append(warning)
1128
1170
 
1129
1171
 
1130
- CheckFunction = Callable[[GenericResponse, Case], Optional[bool]] # pragma: no mutate
1172
+ CheckFunction = Callable[[GenericResponse, Case], Optional[bool]]
@@ -2,15 +2,14 @@
2
2
 
3
3
  These are basic entities that describe what data could be sent to the API.
4
4
  """
5
- from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, TypeVar
6
-
7
- import attr
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, Optional, TypeVar
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  from .models import APIOperation
11
10
 
12
11
 
13
- @attr.s(eq=False) # pragma: no mutate
12
+ @dataclass(eq=False)
14
13
  class Parameter:
15
14
  """A logically separate parameter bound to a location (e.g., to "query string").
16
15
 
@@ -19,7 +18,7 @@ class Parameter:
19
18
  """
20
19
 
21
20
  # The parameter definition in the language acceptable by the API
22
- definition: Any = attr.ib() # pragma: no mutate
21
+ definition: Any
23
22
 
24
23
  @property
25
24
  def location(self) -> str:
@@ -52,16 +51,22 @@ class Parameter:
52
51
  P = TypeVar("P", bound=Parameter)
53
52
 
54
53
 
55
- @attr.s # pragma: no mutate
54
+ @dataclass
56
55
  class ParameterSet(Generic[P]):
57
56
  """A set of parameters for the same location."""
58
57
 
59
- items: List[P] = attr.ib(factory=list) # pragma: no mutate
58
+ items: List[P] = field(default_factory=list)
60
59
 
61
60
  def add(self, parameter: P) -> None:
62
61
  """Add a new parameter."""
63
62
  self.items.append(parameter)
64
63
 
64
+ def get(self, name: str) -> Optional[P]:
65
+ for parameter in self:
66
+ if parameter.name == name:
67
+ return parameter
68
+ return None
69
+
65
70
  @property
66
71
  def example(self) -> Dict[str, Any]:
67
72
  """Composite example gathered from individual parameters."""