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
@@ -1,12 +1,12 @@
1
- import attr
1
+ from dataclasses import dataclass
2
2
 
3
3
  from ....models import Case
4
4
  from ....utils import GenericResponse
5
5
 
6
6
 
7
- @attr.s(slots=True) # pragma: no mutate
7
+ @dataclass
8
8
  class ExpressionContext:
9
9
  """Context in what an expression are evaluated."""
10
10
 
11
- response: GenericResponse = attr.ib() # pragma: no mutate
12
- case: Case = attr.ib() # pragma: no mutate
11
+ response: GenericResponse
12
+ case: Case
@@ -1,26 +1,25 @@
1
1
  """Lexical analysis of runtime expressions."""
2
+ from dataclasses import dataclass
2
3
  from enum import Enum, unique
3
4
  from typing import Callable, Generator
4
5
 
5
- import attr
6
6
 
7
-
8
- @unique # pragma: no mutate
7
+ @unique
9
8
  class TokenType(Enum):
10
- VARIABLE = 1 # pragma: no mutate
11
- STRING = 2 # pragma: no mutate
12
- POINTER = 3 # pragma: no mutate
13
- DOT = 4 # pragma: no mutate
14
- LBRACKET = 5 # pragma: no mutate
15
- RBRACKET = 6 # pragma: no mutate
9
+ VARIABLE = 1
10
+ STRING = 2
11
+ POINTER = 3
12
+ DOT = 4
13
+ LBRACKET = 5
14
+ RBRACKET = 6
16
15
 
17
16
 
18
- @attr.s(slots=True) # pragma: no mutate
17
+ @dataclass
19
18
  class Token:
20
19
  """Lexical token that may occur in a runtime expression."""
21
20
 
22
- value: str = attr.ib() # pragma: no mutate
23
- type_: TokenType = attr.ib() # pragma: no mutate
21
+ value: str
22
+ type_: TokenType
24
23
 
25
24
  # Helpers for cleaner instantiation
26
25
 
@@ -1,8 +1,8 @@
1
1
  """Expression nodes description and evaluation logic."""
2
+ from dataclasses import dataclass
2
3
  from enum import Enum, unique
3
4
  from typing import Any, Dict, Optional, Union
4
5
 
5
- import attr
6
6
  from requests.structures import CaseInsensitiveDict
7
7
 
8
8
  from ....utils import WSGIResponse
@@ -10,7 +10,7 @@ from .. import references
10
10
  from .context import ExpressionContext
11
11
 
12
12
 
13
- @attr.s(slots=True) # pragma: no mutate
13
+ @dataclass
14
14
  class Node:
15
15
  """Generic expression node."""
16
16
 
@@ -27,11 +27,11 @@ class NodeType(Enum):
27
27
  RESPONSE = "$response"
28
28
 
29
29
 
30
- @attr.s(slots=True) # pragma: no mutate
30
+ @dataclass
31
31
  class String(Node):
32
32
  """A simple string that is not evaluated somehow specifically."""
33
33
 
34
- value: str = attr.ib() # pragma: no mutate
34
+ value: str
35
35
 
36
36
  def evaluate(self, context: ExpressionContext) -> str:
37
37
  """String tokens are passed as they are.
@@ -43,7 +43,7 @@ class String(Node):
43
43
  return self.value
44
44
 
45
45
 
46
- @attr.s(slots=True) # pragma: no mutate
46
+ @dataclass
47
47
  class URL(Node):
48
48
  """A node for `$url` expression."""
49
49
 
@@ -51,7 +51,7 @@ class URL(Node):
51
51
  return context.case.get_full_url()
52
52
 
53
53
 
54
- @attr.s(slots=True) # pragma: no mutate
54
+ @dataclass
55
55
  class Method(Node):
56
56
  """A node for `$method` expression."""
57
57
 
@@ -59,7 +59,7 @@ class Method(Node):
59
59
  return context.case.operation.method.upper()
60
60
 
61
61
 
62
- @attr.s(slots=True) # pragma: no mutate
62
+ @dataclass
63
63
  class StatusCode(Node):
64
64
  """A node for `$statusCode` expression."""
65
65
 
@@ -67,12 +67,12 @@ class StatusCode(Node):
67
67
  return str(context.response.status_code)
68
68
 
69
69
 
70
- @attr.s(slots=True) # pragma: no mutate
70
+ @dataclass
71
71
  class NonBodyRequest(Node):
72
72
  """A node for `$request` expressions where location is not `body`."""
73
73
 
74
- location: str = attr.ib() # pragma: no mutate
75
- parameter: str = attr.ib() # pragma: no mutate
74
+ location: str
75
+ parameter: str
76
76
 
77
77
  def evaluate(self, context: ExpressionContext) -> str:
78
78
  container: Union[Dict, CaseInsensitiveDict] = {
@@ -85,11 +85,11 @@ class NonBodyRequest(Node):
85
85
  return container[self.parameter]
86
86
 
87
87
 
88
- @attr.s(slots=True) # pragma: no mutate
88
+ @dataclass
89
89
  class BodyRequest(Node):
90
90
  """A node for `$request` expressions where location is `body`."""
91
91
 
92
- pointer: Optional[str] = attr.ib(default=None) # pragma: no mutate
92
+ pointer: Optional[str] = None
93
93
 
94
94
  def evaluate(self, context: ExpressionContext) -> Any:
95
95
  document = context.case.body
@@ -98,21 +98,21 @@ class BodyRequest(Node):
98
98
  return references.resolve_pointer(document, self.pointer[1:])
99
99
 
100
100
 
101
- @attr.s(slots=True) # pragma: no mutate
101
+ @dataclass
102
102
  class HeaderResponse(Node):
103
103
  """A node for `$response.header` expressions."""
104
104
 
105
- parameter: str = attr.ib() # pragma: no mutate
105
+ parameter: str
106
106
 
107
107
  def evaluate(self, context: ExpressionContext) -> str:
108
108
  return context.response.headers[self.parameter]
109
109
 
110
110
 
111
- @attr.s(slots=True) # pragma: no mutate
111
+ @dataclass
112
112
  class BodyResponse(Node):
113
113
  """A node for `$response.body` expressions."""
114
114
 
115
- pointer: Optional[str] = attr.ib(default=None) # pragma: no mutate
115
+ pointer: Optional[str] = None
116
116
 
117
117
  def evaluate(self, context: ExpressionContext) -> Any:
118
118
  if isinstance(context.response, WSGIResponse):
@@ -5,7 +5,7 @@ from . import lexer, nodes
5
5
  from .errors import RuntimeExpressionError, UnknownToken
6
6
 
7
7
 
8
- @lru_cache() # pragma: no mutate
8
+ @lru_cache()
9
9
  def parse(expr: str) -> List[nodes.Node]:
10
10
  """Parse lexical tokens into concrete expression nodes."""
11
11
  return list(_parse(expr))
@@ -2,11 +2,10 @@
2
2
 
3
3
  Based on https://swagger.io/docs/specification/links/
4
4
  """
5
+ from dataclasses import dataclass, field
5
6
  from difflib import get_close_matches
6
7
  from typing import Any, Dict, Generator, List, NoReturn, Optional, Sequence, Tuple, Union
7
8
 
8
- import attr
9
-
10
9
  from ...models import APIOperation, Case
11
10
  from ...parameters import ParameterSet
12
11
  from ...stateful import Direction, ParsedData, StatefulTest
@@ -17,15 +16,14 @@ from .constants import LOCATION_TO_CONTAINER
17
16
  from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
18
17
 
19
18
 
20
- @attr.s(slots=True, repr=False) # pragma: no mutate
19
+ @dataclass(repr=False)
21
20
  class Link(StatefulTest):
22
- operation: APIOperation = attr.ib() # pragma: no mutate
23
- parameters: Dict[str, Any] = attr.ib() # pragma: no mutate
24
- request_body: Any = attr.ib(default=NOT_SET) # pragma: no mutate
21
+ operation: APIOperation
22
+ parameters: Dict[str, Any]
23
+ request_body: Any = NOT_SET
25
24
 
26
- @request_body.validator
27
- def is_defined(self, attribute: attr.Attribute, value: Any) -> None:
28
- if value is not NOT_SET and not self.operation.body:
25
+ def __post_init__(self) -> None:
26
+ if self.request_body is not NOT_SET and not self.operation.body:
29
27
  # Link defines `requestBody` for a parameter that does not accept one
30
28
  raise ValueError(
31
29
  f"Request body is not defined in API operation {self.operation.method.upper()} {self.operation.path}"
@@ -159,21 +157,21 @@ def get_links(response: GenericResponse, operation: APIOperation, field: str) ->
159
157
  return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
160
158
 
161
159
 
162
- @attr.s(slots=True, repr=False) # pragma: no mutate
160
+ @dataclass(repr=False)
163
161
  class OpenAPILink(Direction):
164
162
  """Alternative approach to link processing.
165
163
 
166
164
  NOTE. This class will replace `Link` in the future.
167
165
  """
168
166
 
169
- name: str = attr.ib() # pragma: no mutate
170
- status_code: str = attr.ib() # pragma: no mutate
171
- definition: Dict[str, Any] = attr.ib() # pragma: no mutate
172
- operation: APIOperation = attr.ib() # pragma: no mutate
173
- parameters: List[Tuple[Optional[str], str, str]] = attr.ib(init=False) # pragma: no mutate
174
- body: Union[Dict[str, Any], NotSet] = attr.ib(init=False) # pragma: no mutate
167
+ name: str
168
+ status_code: str
169
+ definition: Dict[str, Any]
170
+ operation: APIOperation
171
+ parameters: List[Tuple[Optional[str], str, str]] = field(init=False)
172
+ body: Union[Dict[str, Any], NotSet] = field(init=False)
175
173
 
176
- def __attrs_post_init__(self) -> None:
174
+ def __post_init__(self) -> None:
177
175
  self.parameters = [
178
176
  normalize_parameter(parameter, expression)
179
177
  for parameter, expression in self.definition.get("parameters", {}).items()
@@ -9,6 +9,7 @@ import jsonschema
9
9
  import requests
10
10
  import yaml
11
11
  from jsonschema import ValidationError
12
+ from pyrate_limiter import Limiter
12
13
  from starlette.applications import Starlette
13
14
  from starlette_testclient import TestClient as ASGIClient
14
15
  from werkzeug.test import Client
@@ -18,6 +19,7 @@ from ...constants import DEFAULT_DATA_GENERATION_METHODS, WAIT_FOR_SCHEMA_INTERV
18
19
  from ...exceptions import HTTPError, SchemaLoadingError
19
20
  from ...hooks import HookContext, dispatch
20
21
  from ...lazy import LazySchema
22
+ from ...throttling import build_limiter
21
23
  from ...types import DataGenerationMethodInput, Filter, NotSet, PathLike
22
24
  from ...utils import (
23
25
  NOT_SET,
@@ -61,6 +63,7 @@ def from_path(
61
63
  force_schema_version: Optional[str] = None,
62
64
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
63
65
  code_sample_style: str = CodeSampleStyle.default().name,
66
+ rate_limit: Optional[str] = None,
64
67
  encoding: str = "utf8",
65
68
  ) -> BaseOpenAPISchema:
66
69
  """Load Open API schema via a file from an OS path.
@@ -83,6 +86,7 @@ def from_path(
83
86
  data_generation_methods=data_generation_methods,
84
87
  code_sample_style=code_sample_style,
85
88
  location=pathlib.Path(path).absolute().as_uri(),
89
+ rate_limit=rate_limit,
86
90
  __expects_json=_is_json_path(path),
87
91
  )
88
92
 
@@ -103,6 +107,7 @@ def from_uri(
103
107
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
104
108
  code_sample_style: str = CodeSampleStyle.default().name,
105
109
  wait_for_schema: Optional[float] = None,
110
+ rate_limit: Optional[str] = None,
106
111
  **kwargs: Any,
107
112
  ) -> BaseOpenAPISchema:
108
113
  """Load Open API schema from the network.
@@ -110,8 +115,10 @@ def from_uri(
110
115
  :param str uri: Schema URL.
111
116
  """
112
117
  setup_headers(kwargs)
113
- if not base_url and port:
114
- base_url = str(URL(uri).with_port(port))
118
+ if port:
119
+ uri = str(URL(uri).with_port(port))
120
+ if not base_url:
121
+ base_url = uri
115
122
 
116
123
  if wait_for_schema is not None:
117
124
 
@@ -144,6 +151,7 @@ def from_uri(
144
151
  data_generation_methods=data_generation_methods,
145
152
  code_sample_style=code_sample_style,
146
153
  location=uri,
154
+ rate_limit=rate_limit,
147
155
  __expects_json=_is_json_response(response),
148
156
  )
149
157
  except SchemaLoadingError as exc:
@@ -181,6 +189,7 @@ def from_file(
181
189
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
182
190
  code_sample_style: str = CodeSampleStyle.default().name,
183
191
  location: Optional[str] = None,
192
+ rate_limit: Optional[str] = None,
184
193
  __expects_json: bool = False,
185
194
  **kwargs: Any, # needed in the runner to have compatible API across all loaders
186
195
  ) -> BaseOpenAPISchema:
@@ -216,6 +225,7 @@ def from_file(
216
225
  data_generation_methods=data_generation_methods,
217
226
  code_sample_style=code_sample_style,
218
227
  location=location,
228
+ rate_limit=rate_limit,
219
229
  )
220
230
 
221
231
 
@@ -234,6 +244,7 @@ def from_dict(
234
244
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
235
245
  code_sample_style: str = CodeSampleStyle.default().name,
236
246
  location: Optional[str] = None,
247
+ rate_limit: Optional[str] = None,
237
248
  ) -> BaseOpenAPISchema:
238
249
  """Load Open API schema from a Python dictionary.
239
250
 
@@ -242,6 +253,9 @@ def from_dict(
242
253
  _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
243
254
  hook_context = HookContext()
244
255
  dispatch("before_load_schema", hook_context, raw_schema)
256
+ rate_limiter: Optional[Limiter] = None
257
+ if rate_limit is not None:
258
+ rate_limiter = build_limiter(rate_limit)
245
259
 
246
260
  def init_openapi_2() -> SwaggerV20:
247
261
  _maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
@@ -258,6 +272,7 @@ def from_dict(
258
272
  data_generation_methods=prepare_data_generation_methods(data_generation_methods),
259
273
  code_sample_style=_code_sample_style,
260
274
  location=location,
275
+ rate_limiter=rate_limiter,
261
276
  )
262
277
  dispatch("after_load_schema", hook_context, instance)
263
278
  return instance
@@ -277,6 +292,7 @@ def from_dict(
277
292
  data_generation_methods=prepare_data_generation_methods(data_generation_methods),
278
293
  code_sample_style=_code_sample_style,
279
294
  location=location,
295
+ rate_limiter=rate_limiter,
280
296
  )
281
297
  dispatch("after_load_schema", hook_context, instance)
282
298
  return instance
@@ -344,6 +360,7 @@ def from_pytest_fixture(
344
360
  validate_schema: bool = False,
345
361
  data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
346
362
  code_sample_style: str = CodeSampleStyle.default().name,
363
+ rate_limit: Optional[str] = None,
347
364
  ) -> LazySchema:
348
365
  """Load schema from a ``pytest`` fixture.
349
366
 
@@ -361,6 +378,9 @@ def from_pytest_fixture(
361
378
  _data_generation_methods = prepare_data_generation_methods(data_generation_methods)
362
379
  else:
363
380
  _data_generation_methods = data_generation_methods
381
+ rate_limiter: Optional[Limiter] = None
382
+ if rate_limit is not None:
383
+ rate_limiter = build_limiter(rate_limit)
364
384
  return LazySchema(
365
385
  fixture_name,
366
386
  app=app,
@@ -373,6 +393,7 @@ def from_pytest_fixture(
373
393
  validate_schema=validate_schema,
374
394
  data_generation_methods=_data_generation_methods,
375
395
  code_sample_style=_code_sample_style,
396
+ rate_limiter=rate_limiter,
376
397
  )
377
398
 
378
399
 
@@ -390,6 +411,7 @@ def from_wsgi(
390
411
  force_schema_version: Optional[str] = None,
391
412
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
392
413
  code_sample_style: str = CodeSampleStyle.default().name,
414
+ rate_limit: Optional[str] = None,
393
415
  **kwargs: Any,
394
416
  ) -> BaseOpenAPISchema:
395
417
  """Load Open API schema from a WSGI app.
@@ -416,6 +438,7 @@ def from_wsgi(
416
438
  data_generation_methods=data_generation_methods,
417
439
  code_sample_style=code_sample_style,
418
440
  location=schema_path,
441
+ rate_limit=rate_limit,
419
442
  __expects_json=_is_json_response(response),
420
443
  )
421
444
 
@@ -442,6 +465,7 @@ def from_aiohttp(
442
465
  force_schema_version: Optional[str] = None,
443
466
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
444
467
  code_sample_style: str = CodeSampleStyle.default().name,
468
+ rate_limit: Optional[str] = None,
445
469
  **kwargs: Any,
446
470
  ) -> BaseOpenAPISchema:
447
471
  """Load Open API schema from an AioHTTP app.
@@ -466,6 +490,7 @@ def from_aiohttp(
466
490
  force_schema_version=force_schema_version,
467
491
  data_generation_methods=data_generation_methods,
468
492
  code_sample_style=code_sample_style,
493
+ rate_limit=rate_limit,
469
494
  **kwargs,
470
495
  )
471
496
 
@@ -484,6 +509,7 @@ def from_asgi(
484
509
  force_schema_version: Optional[str] = None,
485
510
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
486
511
  code_sample_style: str = CodeSampleStyle.default().name,
512
+ rate_limit: Optional[str] = None,
487
513
  **kwargs: Any,
488
514
  ) -> BaseOpenAPISchema:
489
515
  """Load Open API schema from an ASGI app.
@@ -510,5 +536,6 @@ def from_asgi(
510
536
  data_generation_methods=data_generation_methods,
511
537
  code_sample_style=code_sample_style,
512
538
  location=schema_path,
539
+ rate_limit=rate_limit,
513
540
  __expects_json=_is_json_response(response),
514
541
  )
@@ -1,8 +1,8 @@
1
+ from dataclasses import dataclass
1
2
  from functools import lru_cache
2
3
  from typing import Any, Dict, Optional, Tuple
3
4
  from urllib.parse import urlencode
4
5
 
5
- import attr
6
6
  import jsonschema
7
7
  from hypothesis import strategies as st
8
8
  from hypothesis_jsonschema import from_schema
@@ -12,16 +12,16 @@ from .mutations import MutationContext
12
12
  from .types import Draw, Schema
13
13
 
14
14
 
15
- @attr.s(slots=True, hash=False)
15
+ @dataclass
16
16
  class CacheKey:
17
17
  """A cache key for API Operation / location.
18
18
 
19
19
  Carries the schema around but don't use it for hashing to simplify LRU cache usage.
20
20
  """
21
21
 
22
- operation_name: str = attr.ib()
23
- location: str = attr.ib()
24
- schema: Schema = attr.ib()
22
+ operation_name: str
23
+ location: str
24
+ schema: Schema
25
25
 
26
26
  def __hash__(self) -> int:
27
27
  return hash((self.operation_name, self.location))
@@ -1,9 +1,9 @@
1
1
  """Schema mutations."""
2
2
  import enum
3
+ from dataclasses import dataclass
3
4
  from functools import wraps
4
5
  from typing import Any, Callable, List, Optional, Sequence, Set, Tuple, TypeVar
5
6
 
6
- import attr
7
7
  from hypothesis import reject
8
8
  from hypothesis import strategies as st
9
9
  from hypothesis.strategies._internal.featureflags import FeatureStrategy
@@ -58,17 +58,17 @@ TYPE_SPECIFIC_KEYS = {
58
58
  }
59
59
 
60
60
 
61
- @attr.s(slots=True)
61
+ @dataclass
62
62
  class MutationContext:
63
63
  """Meta information about the current mutation state."""
64
64
 
65
65
  # The original schema
66
- keywords: Schema = attr.ib() # only keywords
67
- non_keywords: Schema = attr.ib() # everything else
66
+ keywords: Schema # only keywords
67
+ non_keywords: Schema # everything else
68
68
  # Schema location within API operation (header, query, etc)
69
- location: str = attr.ib()
69
+ location: str
70
70
  # Payload media type, if available
71
- media_type: Optional[str] = attr.ib()
71
+ media_type: Optional[str]
72
72
 
73
73
  @property
74
74
  def is_header_location(self) -> bool:
@@ -1,15 +1,14 @@
1
1
  import json
2
+ from dataclasses import dataclass
2
3
  from typing import Any, ClassVar, Dict, Iterable, List, Optional, Tuple
3
4
 
4
- import attr
5
-
6
5
  from ...exceptions import InvalidSchema
7
6
  from ...models import APIOperation
8
7
  from ...parameters import Parameter
9
8
  from .converter import to_json_schema_recursive
10
9
 
11
10
 
12
- @attr.s(eq=False)
11
+ @dataclass(eq=False)
13
12
  class OpenAPIParameter(Parameter):
14
13
  """A single Open API operation parameter."""
15
14
 
@@ -122,7 +121,7 @@ class OpenAPIParameter(Parameter):
122
121
  return json.dumps(self.as_json_schema(operation), sort_keys=True)
123
122
 
124
123
 
125
- @attr.s(eq=False)
124
+ @dataclass(eq=False)
126
125
  class OpenAPI20Parameter(OpenAPIParameter):
127
126
  """Open API 2.0 parameter.
128
127
 
@@ -167,7 +166,7 @@ class OpenAPI20Parameter(OpenAPIParameter):
167
166
  return None
168
167
 
169
168
 
170
- @attr.s(eq=False)
169
+ @dataclass(eq=False)
171
170
  class OpenAPI30Parameter(OpenAPIParameter):
172
171
  """Open API 3.0 parameter.
173
172
 
@@ -217,9 +216,9 @@ class OpenAPI30Parameter(OpenAPIParameter):
217
216
  return super().from_open_api_to_json_schema(operation, open_api_schema)
218
217
 
219
218
 
220
- @attr.s(eq=False)
219
+ @dataclass(eq=False)
221
220
  class OpenAPIBody(OpenAPIParameter):
222
- media_type: str = attr.ib()
221
+ media_type: str
223
222
 
224
223
  @property
225
224
  def location(self) -> str:
@@ -231,7 +230,7 @@ class OpenAPIBody(OpenAPIParameter):
231
230
  return "body"
232
231
 
233
232
 
234
- @attr.s(slots=True, eq=False)
233
+ @dataclass(eq=False)
235
234
  class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
236
235
  """Open API 2.0 body variant."""
237
236
 
@@ -280,7 +279,7 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
280
279
  FORM_MEDIA_TYPES = ("multipart/form-data", "application/x-www-form-urlencoded")
281
280
 
282
281
 
283
- @attr.s(slots=True, eq=False)
282
+ @dataclass(eq=False)
284
283
  class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
285
284
  """Open API 3.0 body variant.
286
285
 
@@ -290,8 +289,8 @@ class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
290
289
 
291
290
  # The `required` keyword is located above the schema for concrete media-type;
292
291
  # Therefore, it is passed here explicitly
293
- required: bool = attr.ib(default=False)
294
- description: Optional[str] = attr.ib(default=None)
292
+ required: bool = False
293
+ description: Optional[str] = None
295
294
 
296
295
  def as_json_schema(self, operation: APIOperation) -> Dict[str, Any]:
297
296
  """Convert body definition to JSON Schema."""
@@ -315,11 +314,11 @@ class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
315
314
  return self.required
316
315
 
317
316
 
318
- @attr.s(slots=True, eq=False)
317
+ @dataclass(eq=False)
319
318
  class OpenAPI20CompositeBody(OpenAPIBody, OpenAPI20Parameter):
320
319
  """A special container to abstract over multiple `formData` parameters."""
321
320
 
322
- definition: List[OpenAPI20Parameter] = attr.ib()
321
+ definition: List[OpenAPI20Parameter]
323
322
 
324
323
  @classmethod
325
324
  def from_parameters(cls, *parameters: Dict[str, Any], media_type: str) -> "OpenAPI20CompositeBody":
@@ -52,11 +52,11 @@ class InliningResolver(jsonschema.RefResolver):
52
52
  )
53
53
  super().__init__(*args, **kwargs)
54
54
 
55
- @overload # pragma: no mutate
55
+ @overload
56
56
  def resolve_all(self, item: Dict[str, Any], recursion_level: int = 0) -> Dict[str, Any]:
57
57
  pass
58
58
 
59
- @overload # pragma: no mutate
59
+ @overload
60
60
  def resolve_all(self, item: List, recursion_level: int = 0) -> List:
61
61
  pass
62
62
 
@@ -2,6 +2,7 @@ import itertools
2
2
  import json
3
3
  from collections import defaultdict
4
4
  from contextlib import ExitStack, contextmanager
5
+ from dataclasses import dataclass, field
5
6
  from difflib import get_close_matches
6
7
  from hashlib import sha1
7
8
  from json import JSONDecodeError
@@ -23,7 +24,6 @@ from typing import (
23
24
  )
24
25
  from urllib.parse import urlsplit
25
26
 
26
- import attr
27
27
  import jsonschema
28
28
  import requests
29
29
  from hypothesis.strategies import SearchStrategy
@@ -82,24 +82,20 @@ SCHEMA_ERROR_MESSAGE = "Schema parsing failed. Please check your schema."
82
82
  SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
83
83
 
84
84
 
85
- @attr.s(eq=False, repr=False)
85
+ @dataclass(eq=False, repr=False)
86
86
  class BaseOpenAPISchema(BaseSchema):
87
- nullable_name: str
88
- links_field: str
89
- header_required_field: str
90
- security: BaseSecurityProcessor
91
- component_locations: ClassVar[Tuple[Tuple[str, ...], ...]] = ()
92
- _operations_by_id: Dict[str, APIOperation]
93
- _inline_reference_cache: Dict[str, Any]
87
+ nullable_name: ClassVar[str] = ""
88
+ links_field: ClassVar[str] = ""
89
+ header_required_field: ClassVar[str] = ""
90
+ security: ClassVar[BaseSecurityProcessor] = None # type: ignore
91
+ _operations_by_id: Dict[str, APIOperation] = field(init=False)
92
+ _inline_reference_cache: Dict[str, Any] = field(default_factory=dict)
94
93
  # Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
95
94
  # excessive resolving
96
- _inline_reference_cache_lock: RLock
97
-
98
- def __attrs_post_init__(self) -> None:
99
- self._inline_reference_cache = {}
100
- self._inline_reference_cache_lock = RLock()
95
+ _inline_reference_cache_lock: RLock = field(default_factory=RLock)
96
+ component_locations: ClassVar[Tuple[Tuple[str, ...], ...]] = ()
101
97
 
102
- @property # pragma: no mutate
98
+ @property
103
99
  def spec_version(self) -> str:
104
100
  raise NotImplementedError
105
101