schemathesis 3.37.1__py3-none-any.whl → 3.38.0__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.
@@ -1,21 +1,25 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
- import json
5
4
  import re
6
5
  from contextlib import contextmanager, suppress
7
6
  from dataclasses import dataclass
8
7
  from functools import lru_cache, partial
9
8
  from itertools import combinations
10
- from typing import Any, Generator, Iterator, TypeVar, cast
9
+ from json.encoder import _make_iterencode, c_make_encoder, encode_basestring_ascii # type: ignore
10
+ from typing import Any, Callable, Generator, Iterator, TypeVar, cast
11
11
 
12
12
  import jsonschema
13
13
  from hypothesis import strategies as st
14
14
  from hypothesis.errors import InvalidArgument, Unsatisfiable
15
15
  from hypothesis_jsonschema import from_schema
16
16
  from hypothesis_jsonschema._canonicalise import canonicalish
17
+ from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
17
18
 
18
19
  from ..constants import NOT_SET
20
+ from ..internal.copy import fast_deepcopy
21
+ from ..specs.openapi.converter import update_pattern_in_schema
22
+ from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
19
23
  from ..specs.openapi.patterns import update_quantifier
20
24
  from ._hypothesis import get_single_example
21
25
  from ._methods import DataGenerationMethod
@@ -38,6 +42,18 @@ JSON_STRATEGY: st.SearchStrategy = st.recursive(
38
42
  ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
39
43
  OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
40
44
 
45
+
46
+ STRATEGIES_FOR_TYPE = {
47
+ "integer": st.integers(),
48
+ "number": NUMERIC_STRATEGY,
49
+ "boolean": st.booleans(),
50
+ "null": st.none(),
51
+ "string": st.text(),
52
+ "array": ARRAY_STRATEGY,
53
+ "object": OBJECT_STRATEGY,
54
+ }
55
+ FORMAT_STRATEGIES = {**BUILT_IN_STRING_FORMATS, **get_default_format_strategies(), **STRING_FORMATS}
56
+
41
57
  UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
42
58
  UNKNOWN_PROPERTY_VALUE = 42
43
59
 
@@ -47,16 +63,24 @@ class GeneratedValue:
47
63
  value: Any
48
64
  data_generation_method: DataGenerationMethod
49
65
  description: str
66
+ location: str | None
50
67
 
51
- __slots__ = ("value", "data_generation_method", "description")
68
+ __slots__ = ("value", "data_generation_method", "description", "location")
52
69
 
53
70
  @classmethod
54
71
  def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
55
- return cls(value=value, data_generation_method=DataGenerationMethod.positive, description=description)
72
+ return cls(
73
+ value=value, data_generation_method=DataGenerationMethod.positive, description=description, location=None
74
+ )
56
75
 
57
76
  @classmethod
58
- def with_negative(cls, value: Any, *, description: str) -> GeneratedValue:
59
- return cls(value=value, data_generation_method=DataGenerationMethod.negative, description=description)
77
+ def with_negative(cls, value: Any, *, description: str, location: str) -> GeneratedValue:
78
+ return cls(
79
+ value=value,
80
+ data_generation_method=DataGenerationMethod.negative,
81
+ description=description,
82
+ location=location,
83
+ )
60
84
 
61
85
 
62
86
  PositiveValue = GeneratedValue.with_positive
@@ -71,35 +95,132 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
71
95
  @dataclass
72
96
  class CoverageContext:
73
97
  data_generation_methods: list[DataGenerationMethod]
98
+ location_stack: list[str | int]
74
99
 
75
- __slots__ = ("data_generation_methods",)
100
+ __slots__ = ("data_generation_methods", "location_stack")
76
101
 
77
- def __init__(self, data_generation_methods: list[DataGenerationMethod] | None = None) -> None:
102
+ def __init__(
103
+ self,
104
+ data_generation_methods: list[DataGenerationMethod] | None = None,
105
+ location_stack: list[str | int] | None = None,
106
+ ) -> None:
78
107
  self.data_generation_methods = (
79
108
  data_generation_methods if data_generation_methods is not None else DataGenerationMethod.all()
80
109
  )
110
+ self.location_stack = location_stack or []
111
+
112
+ @contextmanager
113
+ def location(self, key: str | int) -> Generator[None, None, None]:
114
+ self.location_stack.append(key)
115
+ try:
116
+ yield
117
+ finally:
118
+ self.location_stack.pop()
119
+
120
+ @property
121
+ def current_location(self) -> str:
122
+ return "/" + "/".join(str(key) for key in self.location_stack)
123
+
124
+ def with_positive(self) -> CoverageContext:
125
+ return CoverageContext(
126
+ data_generation_methods=[DataGenerationMethod.positive], location_stack=self.location_stack
127
+ )
81
128
 
82
- @classmethod
83
- def with_positive(cls) -> CoverageContext:
84
- return CoverageContext(data_generation_methods=[DataGenerationMethod.positive])
85
-
86
- @classmethod
87
- def with_negative(cls) -> CoverageContext:
88
- return CoverageContext(data_generation_methods=[DataGenerationMethod.negative])
129
+ def with_negative(self) -> CoverageContext:
130
+ return CoverageContext(
131
+ data_generation_methods=[DataGenerationMethod.negative], location_stack=self.location_stack
132
+ )
89
133
 
90
134
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
91
135
  return cached_draw(strategy)
92
136
 
93
- def generate_from_schema(self, schema: dict) -> Any:
137
+ def generate_from_schema(self, schema: dict | bool) -> Any:
138
+ if isinstance(schema, bool):
139
+ return 0
140
+ keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example"]])
141
+ if keys == ["type"] and isinstance(schema["type"], str) and schema["type"] in STRATEGIES_FOR_TYPE:
142
+ return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
143
+ if keys == ["format", "type"]:
144
+ if schema["type"] != "string":
145
+ return cached_draw(STRATEGIES_FOR_TYPE[schema["type"]])
146
+ elif schema["format"] in FORMAT_STRATEGIES:
147
+ return cached_draw(FORMAT_STRATEGIES[schema["format"]])
148
+ if (keys == ["maxLength", "minLength", "type"] or keys == ["maxLength", "type"]) and schema["type"] == "string":
149
+ return cached_draw(st.text(min_size=schema.get("minLength", 0), max_size=schema["maxLength"]))
150
+ if (
151
+ keys == ["properties", "required", "type"]
152
+ or keys == ["properties", "required"]
153
+ or keys == ["properties", "type"]
154
+ or keys == ["properties"]
155
+ ):
156
+ obj = {}
157
+ for key, sub_schema in schema["properties"].items():
158
+ if isinstance(sub_schema, dict) and "const" in sub_schema:
159
+ obj[key] = sub_schema["const"]
160
+ else:
161
+ obj[key] = self.generate_from_schema(sub_schema)
162
+ return obj
163
+ if (
164
+ keys == ["maximum", "minimum", "type"] or keys == ["maximum", "type"] or keys == ["minimum", "type"]
165
+ ) and schema["type"] == "integer":
166
+ return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
167
+ if "enum" in schema:
168
+ return cached_draw(st.sampled_from(schema["enum"]))
169
+ if "pattern" in schema:
170
+ pattern = schema["pattern"]
171
+ try:
172
+ re.compile(pattern)
173
+ except re.error:
174
+ raise Unsatisfiable from None
175
+ return cached_draw(st.from_regex(pattern))
176
+ if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
177
+ items = schema["items"]
178
+ min_items = schema.get("minItems", 0)
179
+ if "enum" in items:
180
+ return cached_draw(st.lists(st.sampled_from(items["enum"]), min_size=min_items))
181
+ sub_keys = sorted([k for k in items if not k.startswith("x-") and k not in ["description", "example"]])
182
+ if sub_keys == ["type"] and items["type"] == "string":
183
+ return cached_draw(st.lists(st.text(), min_size=min_items))
184
+ if (
185
+ sub_keys == ["properties", "required", "type"]
186
+ or sub_keys == ["properties", "type"]
187
+ or sub_keys == ["properties"]
188
+ ):
189
+ return cached_draw(
190
+ st.lists(
191
+ st.fixed_dictionaries(
192
+ {key: from_schema(sub_schema) for key, sub_schema in items["properties"].items()}
193
+ ),
194
+ min_size=min_items,
195
+ )
196
+ )
197
+
198
+ if keys == ["allOf"]:
199
+ schema = canonicalish(schema)
200
+ if isinstance(schema, dict) and "allOf" not in schema:
201
+ return self.generate_from_schema(schema)
202
+
94
203
  return self.generate_from(from_schema(schema))
95
204
 
96
205
 
97
206
  T = TypeVar("T")
98
207
 
99
208
 
100
- def _to_hashable_key(value: T) -> T | tuple[type, str]:
209
+ if c_make_encoder is not None:
210
+ _iterencode = c_make_encoder(None, None, encode_basestring_ascii, None, ":", ",", True, False, False)
211
+ else:
212
+ _iterencode = _make_iterencode(
213
+ None, None, encode_basestring_ascii, None, float.__repr__, ":", ",", True, False, True
214
+ )
215
+
216
+
217
+ def _encode(o: Any) -> str:
218
+ return "".join(_iterencode(o, 0))
219
+
220
+
221
+ def _to_hashable_key(value: T, _encode: Callable = _encode) -> T | tuple[type, str]:
101
222
  if isinstance(value, (dict, list)):
102
- serialized = json.dumps(value, sort_keys=True)
223
+ serialized = _encode(value)
103
224
  return (type(value), serialized)
104
225
  return value
105
226
 
@@ -193,7 +314,7 @@ def cover_schema_iter(
193
314
  if DataGenerationMethod.negative in ctx.data_generation_methods:
194
315
  template = None
195
316
  for key, value in schema.items():
196
- with _ignore_unfixable():
317
+ with _ignore_unfixable(), ctx.location(key):
197
318
  if key == "enum":
198
319
  yield from _negative_enum(ctx, value)
199
320
  elif key == "const":
@@ -207,24 +328,35 @@ def cover_schema_iter(
207
328
  elif key == "properties":
208
329
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
209
330
  yield from _negative_properties(ctx, template, value)
331
+ elif key == "patternProperties":
332
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
333
+ yield from _negative_pattern_properties(ctx, template, value)
334
+ elif key == "items" and isinstance(value, dict):
335
+ yield from _negative_items(ctx, value)
210
336
  elif key == "pattern":
211
- yield from _negative_pattern(ctx, value)
337
+ min_length = schema.get("minLength")
338
+ max_length = schema.get("maxLength")
339
+ yield from _negative_pattern(ctx, value, min_length=min_length, max_length=max_length)
212
340
  elif key == "format" and ("string" in types or not types):
213
341
  yield from _negative_format(ctx, schema, value)
214
342
  elif key == "maximum":
215
343
  next = value + 1
216
344
  if next not in seen:
217
- yield NegativeValue(next, description="Value greater than maximum")
345
+ yield NegativeValue(
346
+ next, description="Value greater than maximum", location=ctx.current_location
347
+ )
218
348
  seen.add(next)
219
349
  elif key == "minimum":
220
350
  next = value - 1
221
351
  if next not in seen:
222
- yield NegativeValue(next, description="Value smaller than minimum")
352
+ yield NegativeValue(
353
+ next, description="Value smaller than minimum", location=ctx.current_location
354
+ )
223
355
  seen.add(next)
224
356
  elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
225
357
  verb = "greater" if key == "exclusiveMaximum" else "smaller"
226
358
  limit = "maximum" if key == "exclusiveMaximum" else "minimum"
227
- yield NegativeValue(value, description=f"Value {verb} than {limit}")
359
+ yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_location)
228
360
  seen.add(value)
229
361
  elif key == "multipleOf":
230
362
  for value_ in _negative_multiple_of(ctx, schema, value):
@@ -236,35 +368,59 @@ def cover_schema_iter(
236
368
  with suppress(InvalidArgument):
237
369
  min_length = max_length = value - 1
238
370
  new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
371
+ new_schema.setdefault("type", "string")
239
372
  if "pattern" in new_schema:
240
373
  new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
241
- value = ctx.generate_from_schema(new_schema)
374
+ if new_schema["pattern"] == schema["pattern"]:
375
+ # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
376
+ del new_schema["minLength"]
377
+ del new_schema["maxLength"]
378
+ value = ctx.generate_from_schema(new_schema)[:max_length]
379
+ else:
380
+ value = ctx.generate_from_schema(new_schema)
381
+ else:
382
+ value = ctx.generate_from_schema(new_schema)
242
383
  k = _to_hashable_key(value)
243
384
  if k not in seen:
244
- yield NegativeValue(value, description="String smaller than minLength")
385
+ yield NegativeValue(
386
+ value, description="String smaller than minLength", location=ctx.current_location
387
+ )
245
388
  seen.add(k)
246
389
  elif key == "maxLength" and value < BUFFER_SIZE:
247
- with suppress(InvalidArgument, Unsatisfiable):
248
- min_length = value + 1
249
- max_length = value + 1
390
+ try:
391
+ min_length = max_length = value + 1
250
392
  new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
393
+ new_schema.setdefault("type", "string")
251
394
  if "pattern" in new_schema:
252
395
  new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
253
- value = ctx.generate_from_schema(new_schema)
396
+ if new_schema["pattern"] == schema["pattern"]:
397
+ # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
398
+ del new_schema["minLength"]
399
+ del new_schema["maxLength"]
400
+ value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
401
+ else:
402
+ value = ctx.generate_from_schema(new_schema)
403
+ else:
404
+ value = ctx.generate_from_schema(new_schema)
254
405
  k = _to_hashable_key(value)
255
406
  if k not in seen:
256
- yield NegativeValue(value, description="String larger than maxLength")
407
+ yield NegativeValue(
408
+ value, description="String larger than maxLength", location=ctx.current_location
409
+ )
257
410
  seen.add(k)
411
+ except (InvalidArgument, Unsatisfiable):
412
+ pass
258
413
  elif key == "uniqueItems" and value:
259
414
  yield from _negative_unique_items(ctx, schema)
260
415
  elif key == "required":
261
416
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
262
417
  yield from _negative_required(ctx, template, value)
263
- elif key == "additionalProperties" and not value:
418
+ elif key == "additionalProperties" and not value and "pattern" not in schema:
264
419
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
265
420
  yield NegativeValue(
266
421
  {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
267
422
  description="Object with unexpected properties",
423
+ location=ctx.current_location,
268
424
  )
269
425
  elif key == "allOf":
270
426
  nctx = ctx.with_negative()
@@ -277,8 +433,9 @@ def cover_schema_iter(
277
433
  elif key == "anyOf" or key == "oneOf":
278
434
  nctx = ctx.with_negative()
279
435
  # NOTE: Other sub-schemas are not filtered out
280
- for sub_schema in value:
281
- yield from cover_schema_iter(nctx, sub_schema, seen)
436
+ for idx, sub_schema in enumerate(value):
437
+ with nctx.location(idx):
438
+ yield from cover_schema_iter(nctx, sub_schema, seen)
282
439
 
283
440
 
284
441
  def _get_properties(schema: dict | bool) -> dict | bool:
@@ -291,6 +448,9 @@ def _get_properties(schema: dict | bool) -> dict | bool:
291
448
  return {"enum": schema["examples"]}
292
449
  if schema.get("type") == "object":
293
450
  return _get_template_schema(schema, "object")
451
+ _schema = fast_deepcopy(schema)
452
+ update_pattern_in_schema(_schema)
453
+ return _schema
294
454
  return schema
295
455
 
296
456
 
@@ -581,7 +741,7 @@ def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValu
581
741
 
582
742
  strategy = JSON_STRATEGY.filter(is_not_in_value)
583
743
  # The exact negative value is not important here
584
- yield NegativeValue(ctx.generate_from(strategy), description="Invalid enum value")
744
+ yield NegativeValue(ctx.generate_from(strategy), description="Invalid enum value", location=ctx.current_location)
585
745
 
586
746
 
587
747
  def _negative_properties(
@@ -589,21 +749,63 @@ def _negative_properties(
589
749
  ) -> Generator[GeneratedValue, None, None]:
590
750
  nctx = ctx.with_negative()
591
751
  for key, sub_schema in properties.items():
592
- for value in cover_schema_iter(nctx, sub_schema):
593
- yield NegativeValue(
594
- {**template, key: value.value},
595
- description=f"Object with invalid '{key}' value: {value.description}",
596
- )
752
+ with nctx.location(key):
753
+ for value in cover_schema_iter(nctx, sub_schema):
754
+ yield NegativeValue(
755
+ {**template, key: value.value},
756
+ description=f"Object with invalid '{key}' value: {value.description}",
757
+ location=nctx.current_location,
758
+ )
759
+
760
+
761
+ def _negative_pattern_properties(
762
+ ctx: CoverageContext, template: dict, pattern_properties: dict
763
+ ) -> Generator[GeneratedValue, None, None]:
764
+ nctx = ctx.with_negative()
765
+ for pattern, sub_schema in pattern_properties.items():
766
+ try:
767
+ key = ctx.generate_from(st.from_regex(pattern))
768
+ except re.error:
769
+ continue
770
+ with nctx.location(pattern):
771
+ for value in cover_schema_iter(nctx, sub_schema):
772
+ yield NegativeValue(
773
+ {**template, key: value.value},
774
+ description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
775
+ location=nctx.current_location,
776
+ )
777
+
597
778
 
779
+ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Generator[GeneratedValue, None, None]:
780
+ """Arrays not matching the schema."""
781
+ nctx = ctx.with_negative()
782
+ for value in cover_schema_iter(nctx, schema):
783
+ yield NegativeValue(
784
+ [value.value],
785
+ description=f"Array with invalid items: {value.description}",
786
+ location=nctx.current_location,
787
+ )
598
788
 
599
- def _not_matching_pattern(value: str, pattern: str) -> bool:
600
- return re.search(pattern, value) is None
601
789
 
790
+ def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
791
+ return pattern.search(value) is None
602
792
 
603
- def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator[GeneratedValue, None, None]:
793
+
794
+ def _negative_pattern(
795
+ ctx: CoverageContext, pattern: str, min_length: int | None = None, max_length: int | None = None
796
+ ) -> Generator[GeneratedValue, None, None]:
797
+ try:
798
+ compiled = re.compile(pattern)
799
+ except re.error:
800
+ return
604
801
  yield NegativeValue(
605
- ctx.generate_from(st.text().filter(partial(_not_matching_pattern, pattern=pattern))),
802
+ ctx.generate_from(
803
+ st.text(min_size=min_length or 0, max_size=max_length).filter(
804
+ partial(_not_matching_pattern, pattern=compiled)
805
+ )
806
+ ),
606
807
  description=f"Value not matching the '{pattern}' pattern",
808
+ location=ctx.current_location,
607
809
  )
608
810
 
609
811
 
@@ -617,12 +819,13 @@ def _negative_multiple_of(
617
819
  yield NegativeValue(
618
820
  ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
619
821
  description=f"Non-multiple of {multiple_of}",
822
+ location=ctx.current_location,
620
823
  )
621
824
 
622
825
 
623
826
  def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
624
827
  unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
625
- yield NegativeValue(unique + unique, description="Non-unique items")
828
+ yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_location)
626
829
 
627
830
 
628
831
  def _negative_required(
@@ -632,6 +835,7 @@ def _negative_required(
632
835
  yield NegativeValue(
633
836
  {k: v for k, v in template.items() if k != key},
634
837
  description=f"Missing required property: {key}",
838
+ location=ctx.current_location,
635
839
  )
636
840
 
637
841
 
@@ -653,7 +857,11 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
653
857
  strategy = strategy.filter(_is_invalid_hostname)
654
858
  else:
655
859
  strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
656
- yield NegativeValue(ctx.generate_from(strategy), description=f"Value not matching the '{format}' format")
860
+ yield NegativeValue(
861
+ ctx.generate_from(strategy),
862
+ description=f"Value not matching the '{format}' format",
863
+ location=ctx.current_location,
864
+ )
657
865
 
658
866
 
659
867
  def _is_non_integer_float(x: float) -> bool:
@@ -661,31 +869,21 @@ def _is_non_integer_float(x: float) -> bool:
661
869
 
662
870
 
663
871
  def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
664
- strategies = {
665
- "integer": st.integers(),
666
- "number": NUMERIC_STRATEGY,
667
- "boolean": st.booleans(),
668
- "null": st.none(),
669
- "string": st.text(),
670
- "array": ARRAY_STRATEGY,
671
- "object": OBJECT_STRATEGY,
672
- }
673
872
  if isinstance(ty, str):
674
873
  types = [ty]
675
874
  else:
676
875
  types = ty
677
- for ty_ in types:
678
- strategies.pop(ty_)
876
+ strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
679
877
  if "number" in types:
680
878
  del strategies["integer"]
681
879
  if "integer" in types:
682
880
  strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
683
- for strat in strategies.values():
684
- value = ctx.generate_from(strat)
881
+ for strategy in strategies.values():
882
+ value = ctx.generate_from(strategy)
685
883
  hashed = _to_hashable_key(value)
686
884
  if hashed in seen:
687
885
  continue
688
- yield NegativeValue(value, description="Incorrect type")
886
+ yield NegativeValue(value, description="Incorrect type", location=ctx.current_location)
689
887
  seen.add(hashed)
690
888
 
691
889
 
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
9
9
  from requests.auth import HTTPDigestAuth
10
10
  from requests.structures import CaseInsensitiveDict
11
11
 
12
+ from .._override import CaseOverride
12
13
  from ..models import Case
13
14
  from ..transports.responses import GenericResponse
14
15
  from ..types import RawAuth
@@ -41,8 +42,9 @@ class CheckContext:
41
42
  Provides access to broader test execution data beyond individual test cases.
42
43
  """
43
44
 
44
- auth: HTTPDigestAuth | RawAuth | None = None
45
- headers: CaseInsensitiveDict | None = None
45
+ override: CaseOverride | None
46
+ auth: HTTPDigestAuth | RawAuth | None
47
+ headers: CaseInsensitiveDict | None
46
48
  config: CheckConfig = field(default_factory=CheckConfig)
47
49
 
48
50
 
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+
6
+ def diff(left: Mapping[str, Any], right: Mapping[str, Any]) -> dict[str, Any]:
7
+ """Calculate the difference between two dictionaries."""
8
+ diff = {}
9
+ for key, value in right.items():
10
+ if key not in left or left[key] != value:
11
+ diff[key] = value
12
+ for key in left:
13
+ if key not in right:
14
+ diff[key] = None # Mark deleted items as None
15
+ return diff