pytest-jsonschema-snapshot 0.2.4__py3-none-any.whl → 0.2.6__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.
@@ -8,5 +8,5 @@ pytest-typed-schema-shot
8
8
 
9
9
  from .core import SchemaShot
10
10
 
11
- __version__ = "0.2.4"
11
+ __version__ = "0.2.6"
12
12
  __all__ = ["SchemaShot"]
@@ -5,7 +5,7 @@ Core logic of the plugin.
5
5
  import json
6
6
  import logging
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Any, Callable, Optional, Set
8
+ from typing import TYPE_CHECKING, Any, Callable, Literal, Optional
9
9
 
10
10
  import pathvalidate
11
11
 
@@ -13,10 +13,17 @@ if TYPE_CHECKING:
13
13
  from jsonschema_diff import JsonSchemaDiff
14
14
 
15
15
  import pytest
16
+ from genschema import Converter, PseudoArrayHandler
17
+ from genschema.comparators import (
18
+ DeleteElement,
19
+ FormatComparator,
20
+ RequiredComparator,
21
+ SchemaVersionComparator,
22
+ )
16
23
  from jsonschema import FormatChecker, ValidationError, validate
17
24
 
18
25
  from .stats import GLOBAL_STATS
19
- from .tools import JsonToSchemaConverter, NameMaker
26
+ from .tools import NameMaker
20
27
 
21
28
 
22
29
  class SchemaShot:
@@ -52,7 +59,18 @@ class SchemaShot:
52
59
  self.save_original: bool = save_original
53
60
  self.debug_mode: bool = debug_mode
54
61
  self.snapshot_dir: Path = root_dir / snapshot_dir_name
55
- self.used_schemas: Set[str] = set()
62
+ self.used_schemas: set[str] = set()
63
+
64
+ self.conv = Converter(
65
+ pseudo_handler=PseudoArrayHandler(),
66
+ base_of="anyOf",
67
+ )
68
+ self.conv.register(FormatComparator())
69
+ self.conv.register(RequiredComparator())
70
+ # self.conv.register(EmptyComparator())
71
+ self.conv.register(SchemaVersionComparator())
72
+ self.conv.register(DeleteElement())
73
+ self.conv.register(DeleteElement("isPseudoArray"))
56
74
 
57
75
  self.logger = logging.getLogger(__name__)
58
76
  # добавляем вывод в stderr
@@ -144,13 +162,7 @@ class SchemaShot:
144
162
 
145
163
  real_name = self._process_name(name)
146
164
 
147
- builder = JsonToSchemaConverter(
148
- format_mode=self.format_mode # type: ignore[arg-type]
149
- ) # , examples=self.examples_limit)
150
- builder.add_object(data)
151
- current_schema = builder.to_schema()
152
-
153
- real_name, status = self._base_match(data, current_schema, real_name)
165
+ real_name, status = self._base_match(data, data, "json", real_name)
154
166
 
155
167
  if self.update_mode or self.reset_mode:
156
168
  self._save_process_original(real_name=real_name, status=status, data=data)
@@ -175,7 +187,7 @@ class SchemaShot:
175
187
 
176
188
  real_name = self._process_name(name)
177
189
 
178
- real_name, status = self._base_match(data, schema, real_name)
190
+ real_name, status = self._base_match(data, schema, "schema", real_name)
179
191
 
180
192
  if self.update_mode and data is not None:
181
193
  self._save_process_original(real_name=real_name, status=status, data=data)
@@ -185,7 +197,8 @@ class SchemaShot:
185
197
  def _base_match(
186
198
  self,
187
199
  data: Optional[dict],
188
- current_schema: dict,
200
+ current_data: dict,
201
+ type_data: Literal["json", "schema"],
189
202
  name: str,
190
203
  ) -> tuple[str, Optional[bool]]:
191
204
  """
@@ -208,6 +221,16 @@ class SchemaShot:
208
221
  # --- состояние ДО проверки ---
209
222
  schema_exists_before = schema_path.exists()
210
223
 
224
+ def make_schema(value: dict | list, type: Literal["json", "schema"]) -> dict:
225
+ if type_data == "schema":
226
+ return current_data
227
+ elif type_data == "json":
228
+ self.conv.clear_data()
229
+ self.conv.add_json(current_data)
230
+ return self.conv.run()
231
+ else:
232
+ raise ValueError("Not correct type argument")
233
+
211
234
  # --- когда схемы ещё нет ---
212
235
  if not schema_exists_before:
213
236
  if not self.update_mode and not self.reset_mode:
@@ -220,6 +243,8 @@ class SchemaShot:
220
243
  f"Schema `{name}` not found and adding new schemas is disabled."
221
244
  )
222
245
 
246
+ current_schema = make_schema(current_data, type_data)
247
+
223
248
  with open(schema_path, "w", encoding="utf-8") as f:
224
249
  json.dump(current_schema, f, indent=2, ensure_ascii=False)
225
250
 
@@ -233,45 +258,59 @@ class SchemaShot:
233
258
  # --- схема уже была: сравнение и валидация --------------------------------
234
259
  schema_updated = False
235
260
 
236
- def merge_schemas(old: dict, new: dict) -> dict:
237
- builder = JsonToSchemaConverter(
238
- format_mode=self.format_mode # type: ignore[arg-type]
239
- ) # , examples=self.examples_limit)
240
- builder.add_schema(old)
241
- builder.add_schema(new)
242
- return builder.to_schema()
261
+ def merge_schemas(
262
+ old: dict, new: dict | list, type_data: Literal["json", "schema"]
263
+ ) -> dict:
264
+ self.conv.clear_data()
265
+ self.conv.add_schema(old)
266
+ if type_data == "schema":
267
+ self.conv.add_schema(dict(new))
268
+ elif type_data == "json":
269
+ self.conv.add_json(new)
270
+ else:
271
+ raise ValueError("Not correct type argument")
272
+ result = self.conv.run()
273
+ return result
243
274
 
244
- if existing_schema != current_schema: # есть отличия
275
+ if (
276
+ type_data == "json" or existing_schema != current_data
277
+ ): # есть отличия или могут быть
245
278
  if (self.update_mode or self.reset_mode) and self.update_actions.get("update"):
246
279
  # обновляем файл
247
280
  if self.reset_mode and not self.update_mode:
281
+ current_schema = make_schema(current_data, type_data)
282
+
248
283
  differences = self.differ.compare(
249
284
  dict(existing_schema), current_schema
250
285
  ).render()
251
- GLOBAL_STATS.add_updated(schema_path.name, differences)
286
+ diff_count = self.differ.property.calc_diff()
287
+ if any(diff_count[key] > 0 for key in diff_count if key != "UNKNOWN"):
288
+ GLOBAL_STATS.add_updated(schema_path.name, differences)
252
289
 
253
- with open(schema_path, "w", encoding="utf-8") as f:
254
- json.dump(current_schema, f, indent=2, ensure_ascii=False)
255
- self.logger.warning(f"Schema `{name}` updated (reset).\n\n{differences}")
290
+ with open(schema_path, "w", encoding="utf-8") as f:
291
+ json.dump(current_schema, f, indent=2, ensure_ascii=False)
292
+ self.logger.warning(f"Schema `{name}` reseted.\n\n{differences}")
256
293
  elif self.update_mode and not self.reset_mode:
257
- merged_schema = merge_schemas(existing_schema, current_schema)
294
+ merged_schema = merge_schemas(existing_schema, current_data, type_data)
258
295
 
259
296
  differences = self.differ.compare(
260
297
  dict(existing_schema), merged_schema
261
298
  ).render()
262
- GLOBAL_STATS.add_updated(schema_path.name, differences)
299
+ diff_count = self.differ.property.calc_diff()
300
+ if any(diff_count[key] > 0 for key in diff_count if key != "UNKNOWN"):
301
+ GLOBAL_STATS.add_updated(schema_path.name, differences)
263
302
 
264
- with open(schema_path, "w", encoding="utf-8") as f:
265
- json.dump(merged_schema, f, indent=2, ensure_ascii=False)
303
+ with open(schema_path, "w", encoding="utf-8") as f:
304
+ json.dump(merged_schema, f, indent=2, ensure_ascii=False)
266
305
 
267
- self.logger.warning(f"Schema `{name}` updated (update).\n\n{differences}")
306
+ self.logger.warning(f"Schema `{name}` updated.\n\n{differences}")
268
307
  else: # both update_mode and reset_mode are True
269
308
  raise ValueError(
270
309
  "Both update_mode and reset_mode cannot be True at the same time."
271
310
  )
272
311
  schema_updated = True
273
312
  elif data is not None:
274
- merged_schema = merge_schemas(existing_schema, current_schema)
313
+ merged_schema = merge_schemas(existing_schema, current_data, type_data)
275
314
 
276
315
  differences = self.differ.compare(dict(existing_schema), merged_schema).render()
277
316
  GLOBAL_STATS.add_uncommitted(schema_path.name, differences)
@@ -287,7 +326,7 @@ class SchemaShot:
287
326
  pytest.fail(
288
327
  f"\n\n{differences}\n\nValidation error in `{name}`: {e.message}"
289
328
  )
290
- elif data is not None:
329
+ elif data is not None and type_data == "schema":
291
330
  # схемы совпали – всё равно валидируем на случай формальных ошибок
292
331
  try:
293
332
  validate(
@@ -296,7 +335,7 @@ class SchemaShot:
296
335
  format_checker=FormatChecker(),
297
336
  )
298
337
  except ValidationError as e:
299
- merged_schema = merge_schemas(existing_schema, current_schema)
338
+ merged_schema = merge_schemas(existing_schema, current_data, type_data)
300
339
 
301
340
  differences = self.differ.compare(dict(existing_schema), merged_schema).render()
302
341
  pytest.fail(f"\n\n{differences}\n\nValidation error in `{name}`: {e.message}")
@@ -1,4 +1,3 @@
1
- from .genson_addon import JsonToSchemaConverter
2
1
  from .name_maker import NameMaker
3
2
 
4
- __all__ = ["JsonToSchemaConverter", "NameMaker"]
3
+ __all__ = ["NameMaker"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-jsonschema-snapshot
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Pytest plugin for automatic JSON Schema generation and validation from examples
5
5
  Project-URL: Homepage, https://miskler.github.io/pytest-jsonschema-snapshot/basic/quick_start.html
6
6
  Project-URL: Repository, https://github.com/Miskler/pytest-jsonschema-snapshot
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Topic :: Utilities
24
24
  Requires-Python: >=3.10
25
- Requires-Dist: genson
25
+ Requires-Dist: genschema
26
26
  Requires-Dist: jsonschema
27
27
  Requires-Dist: jsonschema-diff
28
28
  Requires-Dist: pathvalidate
@@ -0,0 +1,12 @@
1
+ pytest_jsonschema_snapshot/__init__.py,sha256=9iA0WeeAywctgadSMJXWRwTkwUnidnkw11x_OjlZxTM,385
2
+ pytest_jsonschema_snapshot/core.py,sha256=4zcUHHzPNEusPDHCCXSO7mAMS8mGygtnKaRwqSmQizA,13626
3
+ pytest_jsonschema_snapshot/plugin.py,sha256=nvAfxtLSX_B5FzaWu7DfsiWRxFjxDvnQNNOhkRrRnbw,8677
4
+ pytest_jsonschema_snapshot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ pytest_jsonschema_snapshot/stats.py,sha256=BfhfMoSkRq6Q8BwhVfrpcFl5TP9OzpgpLwnKf1Kslkw,9593
6
+ pytest_jsonschema_snapshot/tools/__init__.py,sha256=WeD2EVrQpKIoFW1s43QAqsJmartqZ3Irwckt814P1bs,59
7
+ pytest_jsonschema_snapshot/tools/name_maker.py,sha256=tqss8NCGSo2aQX_-RkCJzy3NJx_TDA-xrn8qsblecf0,5799
8
+ pytest_jsonschema_snapshot-0.2.6.dist-info/METADATA,sha256=ev3dFAmfD66Q7epmri2Nqj1C4VDKpA8yFhEk15yemGY,7798
9
+ pytest_jsonschema_snapshot-0.2.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ pytest_jsonschema_snapshot-0.2.6.dist-info/entry_points.txt,sha256=eJ1x4TMmhcc8YtM7IoCsUJO4-rLeTrGy8tPgkrojjKs,58
11
+ pytest_jsonschema_snapshot-0.2.6.dist-info/licenses/LICENSE,sha256=1HRFdSzlJ4BtHv6U7tZun3iCArjbCnm5NUowE9hZpNs,1071
12
+ pytest_jsonschema_snapshot-0.2.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,3 +0,0 @@
1
- from .to_schema_converter import JsonToSchemaConverter
2
-
3
- __all__ = ["JsonToSchemaConverter"]
@@ -1,52 +0,0 @@
1
- import re
2
- from typing import Optional
3
-
4
-
5
- class FormatDetector:
6
- """Class for detecting string formats"""
7
-
8
- # Regular expressions for various formats
9
- EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
10
- UUID_PATTERN = re.compile(
11
- r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
12
- re.I,
13
- )
14
- DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
15
- DATETIME_PATTERN = re.compile(
16
- r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$"
17
- )
18
- URI_PATTERN = re.compile(r"^https?://[^\s/$.?#].[^\s]*$", re.I)
19
- IPV4_PATTERN = re.compile(
20
- r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
21
- r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
22
- )
23
-
24
- @classmethod
25
- def detect_format(cls, value: str) -> Optional[str]:
26
- """
27
- Detects the format of a string.
28
-
29
- Args:
30
- value: The string to analyze
31
-
32
- Returns:
33
- The name of the format or None if the format is not defined
34
- """
35
- if not isinstance(value, str) or not value:
36
- return None
37
-
38
- # Check formats from more specific to less specific
39
- if cls.EMAIL_PATTERN.match(value):
40
- return "email"
41
- elif cls.UUID_PATTERN.match(value):
42
- return "uuid"
43
- elif cls.DATETIME_PATTERN.match(value):
44
- return "date-time"
45
- elif cls.DATE_PATTERN.match(value):
46
- return "date"
47
- elif cls.URI_PATTERN.match(value):
48
- return "uri"
49
- elif cls.IPV4_PATTERN.match(value):
50
- return "ipv4"
51
-
52
- return None
@@ -1,100 +0,0 @@
1
- """Json → Schema with optional format handling.
2
-
3
- `format_mode` options
4
- ---------------------
5
- * ``"on"`` – detect formats and let validators assert them (default).
6
- * ``"off"`` – ignore formats entirely.
7
- * ``"safe"`` – keep the annotations but embed a ``$vocabulary`` block that
8
- **disables** the draft‑2020‑12 *format‑assertion* vocabulary.
9
- This makes every ``format`` purely informational, regardless
10
- of validator settings.
11
- """
12
-
13
- from typing import Any, Dict, Literal
14
-
15
- from genson import SchemaBuilder # type: ignore[import-untyped]
16
-
17
- from .format_detector import FormatDetector
18
-
19
- _FormatMode = Literal["on", "off", "safe"]
20
-
21
-
22
- class JsonToSchemaConverter(SchemaBuilder):
23
- """A thin wrapper around :class:`genson.SchemaBuilder`."""
24
-
25
- # ------------------------------------------------------------------
26
- # Construction
27
- # ------------------------------------------------------------------
28
- def __init__(
29
- self,
30
- schema_uri: str = "https://json-schema.org/draft/2020-12/schema",
31
- *,
32
- format_mode: _FormatMode = "on",
33
- ):
34
- super().__init__(schema_uri) if schema_uri else super().__init__()
35
- if format_mode not in {"on", "off", "safe"}:
36
- raise ValueError("format_mode must be 'on', 'off', or 'safe'.")
37
- self._format_mode: _FormatMode = format_mode
38
- self._format_cache: Dict[str, set[str]] = {}
39
-
40
- # ------------------------------------------------------------------
41
- # Public API (overrides)
42
- # ------------------------------------------------------------------
43
- def add_object(self, obj: Any, path: str = "root") -> None:
44
- super().add_object(obj)
45
- if self._format_mode != "off":
46
- self._collect_formats(obj, path)
47
-
48
- def to_schema(self) -> Dict[str, Any]:
49
- schema = dict(super().to_schema()) # shallow‑copy
50
-
51
- if self._format_mode != "off":
52
- self._inject_formats(schema, "root")
53
-
54
- if self._format_mode == "safe":
55
- schema.setdefault(
56
- "$vocabulary",
57
- {
58
- "https://json-schema.org/draft/2020-12/vocab/core": True,
59
- "https://json-schema.org/draft/2020-12/vocab/applicator": True,
60
- "https://json-schema.org/draft/2020-12/vocab/format-annotation": True,
61
- "https://json-schema.org/draft/2020-12/vocab/format-assertion": False,
62
- },
63
- )
64
-
65
- return schema
66
-
67
- # ------------------------------------------------------------------
68
- # Internals
69
- # ------------------------------------------------------------------
70
- def _collect_formats(self, obj: Any, path: str) -> None:
71
- if isinstance(obj, str):
72
- fmt = FormatDetector.detect_format(obj)
73
- if fmt:
74
- self._format_cache.setdefault(path, set()).add(fmt)
75
- elif isinstance(obj, dict):
76
- for k, v in obj.items():
77
- self._collect_formats(v, f"{path}.{k}")
78
- elif isinstance(obj, (list, tuple)):
79
- for i, item in enumerate(obj):
80
- self._collect_formats(item, f"{path}[{i}]")
81
-
82
- def _inject_formats(self, schema: Dict[str, Any], path: str) -> None:
83
- t = schema.get("type")
84
- if t == "string":
85
- fmts = self._format_cache.get(path)
86
- if fmts and len(fmts) == 1:
87
- schema["format"] = next(iter(fmts))
88
- elif t == "object" and "properties" in schema:
89
- for name, subschema in schema["properties"].items():
90
- self._inject_formats(subschema, f"{path}.{name}")
91
- elif t == "array" and "items" in schema:
92
- items_schema = schema["items"]
93
- if isinstance(items_schema, dict):
94
- self._inject_formats(items_schema, f"{path}[0]")
95
- else:
96
- for idx, subschema in enumerate(items_schema):
97
- self._inject_formats(subschema, f"{path}[{idx}]")
98
- elif "anyOf" in schema:
99
- for subschema in schema["anyOf"]:
100
- self._inject_formats(subschema, path)
@@ -1,15 +0,0 @@
1
- pytest_jsonschema_snapshot/__init__.py,sha256=rUBgKVh7nCLEwfO0XZS-TewnM5eTXpBEI7NaC5p4qLg,385
2
- pytest_jsonschema_snapshot/core.py,sha256=CoL_W-u6o3N7XDwv-MbePJiZaGX0LtGS6BLbj9MHROU,11995
3
- pytest_jsonschema_snapshot/plugin.py,sha256=nvAfxtLSX_B5FzaWu7DfsiWRxFjxDvnQNNOhkRrRnbw,8677
4
- pytest_jsonschema_snapshot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- pytest_jsonschema_snapshot/stats.py,sha256=BfhfMoSkRq6Q8BwhVfrpcFl5TP9OzpgpLwnKf1Kslkw,9593
6
- pytest_jsonschema_snapshot/tools/__init__.py,sha256=WMS6PdgMABBfTRhPGuoUOXB-R2PcqcadwH8pG1C6MFU,132
7
- pytest_jsonschema_snapshot/tools/name_maker.py,sha256=tqss8NCGSo2aQX_-RkCJzy3NJx_TDA-xrn8qsblecf0,5799
8
- pytest_jsonschema_snapshot/tools/genson_addon/__init__.py,sha256=nANkqHTaWTZPwBDztsnQvObHUZLSeHenJS--oWfep8c,92
9
- pytest_jsonschema_snapshot/tools/genson_addon/format_detector.py,sha256=Wc5pB_xstyr4OtjwJ2qqmV62xET63cN7Nb0gxkrYyW0,1636
10
- pytest_jsonschema_snapshot/tools/genson_addon/to_schema_converter.py,sha256=UdQIkZhMrTJNHwI1B1dv3aEwx41B1B_lLyr4KWiUpNY,4168
11
- pytest_jsonschema_snapshot-0.2.4.dist-info/METADATA,sha256=iiGeO3N83BH1griTvads-RUawss1rw_9NIlFEdK6mkM,7795
12
- pytest_jsonschema_snapshot-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- pytest_jsonschema_snapshot-0.2.4.dist-info/entry_points.txt,sha256=eJ1x4TMmhcc8YtM7IoCsUJO4-rLeTrGy8tPgkrojjKs,58
14
- pytest_jsonschema_snapshot-0.2.4.dist-info/licenses/LICENSE,sha256=1HRFdSzlJ4BtHv6U7tZun3iCArjbCnm5NUowE9hZpNs,1071
15
- pytest_jsonschema_snapshot-0.2.4.dist-info/RECORD,,