schemathesis 3.19.0__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 (52) hide show
  1. schemathesis/auths.py +20 -20
  2. schemathesis/cli/__init__.py +20 -20
  3. schemathesis/cli/cassettes.py +18 -18
  4. schemathesis/cli/context.py +25 -25
  5. schemathesis/cli/debug.py +3 -3
  6. schemathesis/cli/junitxml.py +4 -4
  7. schemathesis/constants.py +3 -3
  8. schemathesis/exceptions.py +9 -9
  9. schemathesis/extra/pytest_plugin.py +1 -1
  10. schemathesis/failures.py +65 -66
  11. schemathesis/filters.py +13 -13
  12. schemathesis/hooks.py +11 -11
  13. schemathesis/lazy.py +16 -16
  14. schemathesis/models.py +97 -97
  15. schemathesis/parameters.py +5 -6
  16. schemathesis/runner/events.py +55 -55
  17. schemathesis/runner/impl/core.py +26 -26
  18. schemathesis/runner/impl/solo.py +6 -7
  19. schemathesis/runner/impl/threadpool.py +5 -5
  20. schemathesis/runner/serialization.py +50 -50
  21. schemathesis/schemas.py +23 -23
  22. schemathesis/serializers.py +3 -3
  23. schemathesis/service/ci.py +25 -25
  24. schemathesis/service/client.py +2 -2
  25. schemathesis/service/events.py +12 -13
  26. schemathesis/service/hosts.py +4 -4
  27. schemathesis/service/metadata.py +14 -15
  28. schemathesis/service/models.py +12 -13
  29. schemathesis/service/report.py +30 -31
  30. schemathesis/service/serialization.py +2 -4
  31. schemathesis/specs/graphql/schemas.py +8 -8
  32. schemathesis/specs/openapi/expressions/context.py +4 -4
  33. schemathesis/specs/openapi/expressions/lexer.py +11 -12
  34. schemathesis/specs/openapi/expressions/nodes.py +16 -16
  35. schemathesis/specs/openapi/expressions/parser.py +1 -1
  36. schemathesis/specs/openapi/links.py +15 -17
  37. schemathesis/specs/openapi/negative/__init__.py +5 -5
  38. schemathesis/specs/openapi/negative/mutations.py +6 -6
  39. schemathesis/specs/openapi/parameters.py +12 -13
  40. schemathesis/specs/openapi/references.py +2 -2
  41. schemathesis/specs/openapi/schemas.py +11 -15
  42. schemathesis/specs/openapi/security.py +7 -7
  43. schemathesis/specs/openapi/stateful/links.py +4 -4
  44. schemathesis/stateful.py +19 -19
  45. schemathesis/targets.py +5 -6
  46. schemathesis/types.py +11 -13
  47. schemathesis/utils.py +2 -2
  48. {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/METADATA +2 -3
  49. {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/RECORD +52 -52
  50. {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/WHEEL +0 -0
  51. {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/entry_points.txt +0 -0
  52. {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/filters.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """Filtering system that allows users to filter API operations based on certain criteria."""
2
2
  import re
3
+ from dataclasses import dataclass, field
3
4
  from functools import partial
4
5
  from types import SimpleNamespace
5
6
  from typing import TYPE_CHECKING, Callable, List, Optional, Set, Tuple, Union
6
7
 
7
- import attr
8
8
  from typing_extensions import Protocol
9
9
 
10
10
  from .exceptions import UsageError
@@ -25,15 +25,15 @@ ERROR_EMPTY_FILTER = "Filter can not be empty"
25
25
  ERROR_FILTER_EXISTS = "Filter already exists"
26
26
 
27
27
 
28
- @attr.s(slots=True, repr=False, frozen=True)
28
+ @dataclass(repr=False, frozen=True)
29
29
  class Matcher:
30
30
  """Encapsulates matching logic by various criteria."""
31
31
 
32
- func: Callable[..., bool] = attr.ib(hash=False, eq=False)
32
+ func: Callable[..., bool] = field(hash=False, compare=False)
33
33
  # A short description of a matcher. Primarily exists for debugging purposes
34
- label: str = attr.ib(hash=False, eq=False)
34
+ label: str = field(hash=False, compare=False)
35
35
  # Compare & hash matchers by a pre-computed hash value
36
- _hash: int = attr.ib()
36
+ _hash: int
37
37
 
38
38
  def __repr__(self) -> str:
39
39
  return f"<{self.__class__.__name__}: {self.label}>"
@@ -41,7 +41,7 @@ class Matcher:
41
41
  @classmethod
42
42
  def for_function(cls, func: MatcherFunc) -> "Matcher":
43
43
  """Matcher that uses the given function for matching operations."""
44
- return cls(func, label=func.__name__, hash=hash(func))
44
+ return cls(func, label=func.__name__, _hash=hash(func))
45
45
 
46
46
  @classmethod
47
47
  def for_value(cls, attribute: str, expected: FilterValue) -> "Matcher":
@@ -51,7 +51,7 @@ class Matcher:
51
51
  else:
52
52
  func = partial(by_value, attribute=attribute, expected=expected)
53
53
  label = f"{attribute}={repr(expected)}"
54
- return cls(func, label=label, hash=hash(label))
54
+ return cls(func, label=label, _hash=hash(label))
55
55
 
56
56
  @classmethod
57
57
  def for_regex(cls, attribute: str, regex: RegexValue) -> "Matcher":
@@ -60,7 +60,7 @@ class Matcher:
60
60
  regex = re.compile(regex)
61
61
  func = partial(by_regex, attribute=attribute, regex=regex)
62
62
  label = f"{attribute}_regex={repr(regex)}"
63
- return cls(func, label=label, hash=hash(label))
63
+ return cls(func, label=label, _hash=hash(label))
64
64
 
65
65
  def match(self, ctx: HasAPIOperation) -> bool:
66
66
  """Whether matcher matches the given operation."""
@@ -88,11 +88,11 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
88
88
  return bool(regex.match(value))
89
89
 
90
90
 
91
- @attr.s(slots=True, repr=False, frozen=True)
91
+ @dataclass(repr=False, frozen=True)
92
92
  class Filter:
93
93
  """Match API operations against a list of matchers."""
94
94
 
95
- matchers: Tuple[Matcher, ...] = attr.ib()
95
+ matchers: Tuple[Matcher, ...]
96
96
 
97
97
  def __repr__(self) -> str:
98
98
  inner = " && ".join(matcher.label for matcher in self.matchers)
@@ -106,12 +106,12 @@ class Filter:
106
106
  return all(matcher.match(ctx) for matcher in self.matchers)
107
107
 
108
108
 
109
- @attr.s(slots=True)
109
+ @dataclass
110
110
  class FilterSet:
111
111
  """Combines multiple filters to apply inclusion and exclusion rules on API operations."""
112
112
 
113
- _includes: Set[Filter] = attr.ib(factory=set)
114
- _excludes: Set[Filter] = attr.ib(factory=set)
113
+ _includes: Set[Filter] = field(default_factory=set)
114
+ _excludes: Set[Filter] = field(default_factory=set)
115
115
 
116
116
  def apply_to(self, operations: List["APIOperation"]) -> List["APIOperation"]:
117
117
  """Get a filtered list of the given operations that match the filters."""
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,7 +1,7 @@
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
@@ -33,22 +33,22 @@ from .utils import (
33
33
  )
34
34
 
35
35
 
36
- @attr.s(slots=True) # pragma: no mutate
36
+ @dataclass
37
37
  class LazySchema:
38
- fixture_name: str = attr.ib() # pragma: no mutate
39
- base_url: Union[Optional[str], NotSet] = attr.ib(default=NOT_SET) # pragma: no mutate
40
- method: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
41
- endpoint: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
42
- tag: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
43
- operation_id: Optional[Filter] = attr.ib(default=NOT_SET) # pragma: no mutate
44
- app: Any = attr.ib(default=NOT_SET) # pragma: no mutate
45
- hooks: HookDispatcher = attr.ib(factory=lambda: HookDispatcher(scope=HookScope.SCHEMA)) # pragma: no mutate
46
- auth: AuthStorage = attr.ib(factory=AuthStorage) # pragma: no mutate
47
- validate_schema: bool = attr.ib(default=True) # pragma: no mutate
48
- skip_deprecated_operations: bool = attr.ib(default=False) # pragma: no mutate
49
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = attr.ib(default=NOT_SET)
50
- code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
51
- rate_limiter: Optional[Limiter] = attr.ib(default=None)
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
52
52
 
53
53
  def hook(self, hook: Union[str, Callable]) -> Callable:
54
54
  return self.hooks.register(hook)
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,7 +29,6 @@ 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
33
  import requests.auth
34
34
  import werkzeug
@@ -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,27 +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
133
- _auth: Optional[requests.auth.AuthBase] = attr.ib(default=None) # pragma: no mutate
132
+ id: str = field(default_factory=lambda: uuid4().hex, compare=False)
133
+ _auth: Optional[requests.auth.AuthBase] = None
134
134
 
135
135
  def __repr__(self) -> str:
136
136
  parts = [f"{self.__class__.__name__}("]
@@ -308,7 +308,7 @@ class Case:
308
308
  if "content-type" not in {header.lower() for header in final_headers}:
309
309
  final_headers["Content-Type"] = self.media_type
310
310
  base_url = self._get_base_url(base_url)
311
- formatted_path = self.formatted_path.lstrip("/") # pragma: no mutate
311
+ formatted_path = self.formatted_path.lstrip("/")
312
312
  if not base_url.endswith("/"):
313
313
  base_url += "/"
314
314
  url = unquote(urljoin(base_url, quote(formatted_path)))
@@ -537,11 +537,11 @@ class Case:
537
537
  )
538
538
 
539
539
 
540
- def _merge_dict_to(data: Dict[str, Any], key: str, new: Dict[str, Any]) -> None:
541
- original = data[key] or {}
540
+ def _merge_dict_to(data: Dict[str, Any], data_key: str, new: Dict[str, Any]) -> None:
541
+ original = data[data_key] or {}
542
542
  for key, value in new.items():
543
543
  original[key] = value
544
- data[key] = original
544
+ data[data_key] = original
545
545
 
546
546
 
547
547
  def validate_vanilla_requests_kwargs(data: Dict[str, Any]) -> None:
@@ -597,7 +597,7 @@ P = TypeVar("P", bound=Parameter)
597
597
  D = TypeVar("D", bound=dict)
598
598
 
599
599
 
600
- @attr.s # pragma: no mutate
600
+ @dataclass
601
601
  class OperationDefinition(Generic[P, D]):
602
602
  """A wrapper to store not resolved API operation definitions.
603
603
 
@@ -606,10 +606,10 @@ class OperationDefinition(Generic[P, D]):
606
606
  scope change to have a proper reference resolving later.
607
607
  """
608
608
 
609
- raw: D = attr.ib() # pragma: no mutate
610
- resolved: D = attr.ib() # pragma: no mutate
611
- scope: str = attr.ib() # pragma: no mutate
612
- parameters: Sequence[P] = attr.ib() # pragma: no mutate
609
+ raw: D
610
+ resolved: D
611
+ scope: str
612
+ parameters: Sequence[P]
613
613
 
614
614
  def __contains__(self, item: Union[str, int]) -> bool:
615
615
  return item in self.resolved
@@ -624,7 +624,7 @@ class OperationDefinition(Generic[P, D]):
624
624
  C = TypeVar("C", bound=Case)
625
625
 
626
626
 
627
- @attr.s(eq=False) # pragma: no mutate
627
+ @dataclass(eq=False)
628
628
  class APIOperation(Generic[P, C]):
629
629
  """A single operation defined in an API.
630
630
 
@@ -640,23 +640,23 @@ class APIOperation(Generic[P, C]):
640
640
  # `path` does not contain `basePath`
641
641
  # Example <scheme>://<host>/<basePath>/users - "/users" is path
642
642
  # https://swagger.io/docs/specification/2-0/api-host-and-base-path/
643
- path: str = attr.ib() # pragma: no mutate
644
- method: str = attr.ib() # pragma: no mutate
645
- definition: OperationDefinition = attr.ib(repr=False) # pragma: no mutate
646
- schema: "BaseSchema" = attr.ib() # pragma: no mutate
647
- verbose_name: str = attr.ib() # pragma: no mutate
648
- app: Any = attr.ib(default=None) # pragma: no mutate
649
- base_url: Optional[str] = attr.ib(default=None) # pragma: no mutate
650
- path_parameters: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
651
- headers: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
652
- cookies: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
653
- query: ParameterSet[P] = attr.ib(factory=ParameterSet) # pragma: no mutate
654
- body: PayloadAlternatives[P] = attr.ib(factory=PayloadAlternatives) # pragma: no mutate
655
- case_cls: Type[C] = attr.ib(default=Case) # type: ignore
656
-
657
- @verbose_name.default
658
- def _verbose_name_default(self) -> str:
659
- 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
660
660
 
661
661
  @property
662
662
  def full_path(self) -> str:
@@ -830,35 +830,35 @@ Endpoint = APIOperation
830
830
  class Status(str, Enum):
831
831
  """Status of an action or multiple actions."""
832
832
 
833
- success = "success" # pragma: no mutate
834
- failure = "failure" # pragma: no mutate
835
- error = "error" # pragma: no mutate
836
- skip = "skip" # pragma: no mutate
833
+ success = "success"
834
+ failure = "failure"
835
+ error = "error"
836
+ skip = "skip"
837
837
 
838
838
 
839
- @attr.s(slots=True, repr=False) # pragma: no mutate
839
+ @dataclass(repr=False)
840
840
  class Check:
841
841
  """Single check run result."""
842
842
 
843
- name: str = attr.ib() # pragma: no mutate
844
- value: Status = attr.ib() # pragma: no mutate
845
- response: Optional[GenericResponse] = attr.ib() # pragma: no mutate
846
- elapsed: float = attr.ib() # pragma: no mutate
847
- example: Case = attr.ib() # pragma: no mutate
848
- 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
849
849
  # Failure-specific context
850
- context: Optional[FailureContext] = attr.ib(default=None) # pragma: no mutate
851
- request: Optional[requests.PreparedRequest] = attr.ib(default=None) # pragma: no mutate
850
+ context: Optional[FailureContext] = None
851
+ request: Optional[requests.PreparedRequest] = None
852
852
 
853
853
 
854
- @attr.s(slots=True, repr=False) # pragma: no mutate
854
+ @dataclass(repr=False)
855
855
  class Request:
856
856
  """Request data extracted from `Case`."""
857
857
 
858
- method: str = attr.ib() # pragma: no mutate
859
- uri: str = attr.ib() # pragma: no mutate
860
- body: Optional[str] = attr.ib() # pragma: no mutate
861
- headers: Headers = attr.ib() # pragma: no mutate
858
+ method: str
859
+ uri: str
860
+ body: Optional[str]
861
+ headers: Headers
862
862
 
863
863
  @classmethod
864
864
  def from_case(cls, case: Case, session: requests.Session) -> "Request":
@@ -893,17 +893,17 @@ def serialize_payload(payload: bytes) -> str:
893
893
  return base64.b64encode(payload).decode()
894
894
 
895
895
 
896
- @attr.s(slots=True, repr=False) # pragma: no mutate
896
+ @dataclass(repr=False)
897
897
  class Response:
898
898
  """Unified response data."""
899
899
 
900
- status_code: int = attr.ib() # pragma: no mutate
901
- message: str = attr.ib() # pragma: no mutate
902
- headers: Dict[str, List[str]] = attr.ib() # pragma: no mutate
903
- body: Optional[str] = attr.ib() # pragma: no mutate
904
- encoding: Optional[str] = attr.ib() # pragma: no mutate
905
- http_version: str = attr.ib() # pragma: no mutate
906
- 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
907
907
 
908
908
  @classmethod
909
909
  def from_requests(cls, response: requests.Response) -> "Response":
@@ -953,16 +953,16 @@ class Response:
953
953
  )
954
954
 
955
955
 
956
- @attr.s(slots=True) # pragma: no mutate
956
+ @dataclass
957
957
  class Interaction:
958
958
  """A single interaction with the target app."""
959
959
 
960
- request: Request = attr.ib() # pragma: no mutate
961
- response: Response = attr.ib() # pragma: no mutate
962
- checks: List[Check] = attr.ib() # pragma: no mutate
963
- status: Status = attr.ib() # pragma: no mutate
964
- data_generation_method: DataGenerationMethod = attr.ib() # pragma: no mutate
965
- 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())
966
966
 
967
967
  @classmethod
968
968
  def from_requests(
@@ -997,28 +997,28 @@ class Interaction:
997
997
  )
998
998
 
999
999
 
1000
- @attr.s(slots=True, repr=False) # pragma: no mutate
1000
+ @dataclass(repr=False)
1001
1001
  class TestResult:
1002
1002
  """Result of a single test."""
1003
1003
 
1004
1004
  __test__ = False
1005
1005
 
1006
- method: str = attr.ib() # pragma: no mutate
1007
- path: str = attr.ib() # pragma: no mutate
1008
- verbose_name: str = attr.ib() # pragma: no mutate
1009
- data_generation_method: List[DataGenerationMethod] = attr.ib() # pragma: no mutate
1010
- checks: List[Check] = attr.ib(factory=list) # pragma: no mutate
1011
- errors: List[Tuple[Exception, Optional[Case]]] = attr.ib(factory=list) # pragma: no mutate
1012
- interactions: List[Interaction] = attr.ib(factory=list) # pragma: no mutate
1013
- logs: List[LogRecord] = attr.ib(factory=list) # pragma: no mutate
1014
- is_errored: bool = attr.ib(default=False) # pragma: no mutate
1015
- is_flaky: bool = attr.ib(default=False) # pragma: no mutate
1016
- is_skipped: bool = attr.ib(default=False) # pragma: no mutate
1017
- is_executed: bool = attr.ib(default=False) # pragma: no mutate
1018
- 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
1019
1019
  # To show a proper reproduction code if an error happens and there is no way to get actual headers that were
1020
1020
  # sent over the network. Or there could be no actual requests at all
1021
- overridden_headers: Optional[Dict[str, Any]] = attr.ib(default=None) # pragma: no mutate
1021
+ overridden_headers: Optional[Dict[str, Any]] = None
1022
1022
 
1023
1023
  def mark_errored(self) -> None:
1024
1024
  self.is_errored = True
@@ -1094,15 +1094,15 @@ class TestResult:
1094
1094
  self.interactions.append(Interaction.from_wsgi(case, response, headers, elapsed, status, checks))
1095
1095
 
1096
1096
 
1097
- @attr.s(slots=True, repr=False) # pragma: no mutate
1097
+ @dataclass(repr=False)
1098
1098
  class TestResultSet:
1099
1099
  """Set of multiple test results."""
1100
1100
 
1101
1101
  __test__ = False
1102
1102
 
1103
- results: List[TestResult] = attr.ib(factory=list) # pragma: no mutate
1104
- generic_errors: List[InvalidSchema] = attr.ib(factory=list) # pragma: no mutate
1105
- 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)
1106
1106
 
1107
1107
  def __iter__(self) -> Iterator[TestResult]:
1108
1108
  return iter(self.results)
@@ -1169,4 +1169,4 @@ class TestResultSet:
1169
1169
  self.warnings.append(warning)
1170
1170
 
1171
1171
 
1172
- 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 dataclasses import dataclass, field
5
6
  from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, Optional, TypeVar
6
7
 
7
- import attr
8
-
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,11 +51,11 @@ 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."""