pytest-jsonschema-snapshot 0.2.0__tar.gz → 0.2.1__tar.gz

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 (76) hide show
  1. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/PKG-INFO +3 -3
  2. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/README.md +2 -2
  3. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/docs/source/basic/quick_start.rst +12 -1
  4. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/__init__.py +1 -1
  5. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/core.py +41 -9
  6. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/plugin.py +65 -5
  7. pytest_jsonschema_snapshot-0.2.1/pytest_jsonschema_snapshot/tools/genson_addon/to_schema_converter.py +100 -0
  8. pytest_jsonschema_snapshot-0.2.1/tests/tools/genson_addon/test_format_safe.py +82 -0
  9. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/TestDataClass.get_data.first.schema.json +1 -1
  10. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/TestDataClass.get_data.schema.json +1 -1
  11. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/array_formats_test.schema.json +1 -1
  12. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/date_test.schema.json +1 -1
  13. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/datetime_test.schema.json +1 -1
  14. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/email_test.schema.json +1 -1
  15. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/get_data.first.schema.json +1 -1
  16. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/get_data.schema.json +1 -1
  17. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/ipv4_test.schema.json +1 -1
  18. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/mixed_formats_test.schema.json +1 -1
  19. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/regular_strings_test.schema.json +1 -1
  20. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/some_schema.schema.json +1 -1
  21. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/strict_email_validation_test.schema.json +1 -1
  22. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/uri_test.schema.json +1 -1
  23. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/uuid_test.schema.json +1 -1
  24. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/test_multiple_schema_creation.py +4 -2
  25. pytest_jsonschema_snapshot-0.2.0/pytest_jsonschema_snapshot/tools/genson_addon/to_schema_converter.py +0 -119
  26. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.flake8 +0 -0
  27. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  28. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  29. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/ISSUE_TEMPLATE/documentation_issue.yml +0 -0
  30. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/PULL_REQUEST_TEMPLATE/general.md +0 -0
  32. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/actions/build-docs/action.yml +0 -0
  33. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/workflows/release.yml +0 -0
  34. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/workflows/reusable-test.yml +0 -0
  35. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.github/workflows/test.yml +0 -0
  36. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/.gitignore +0 -0
  37. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/LICENSE +0 -0
  38. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/Makefile +0 -0
  39. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/assets/logo.png +0 -0
  40. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/docs/source/_static/logo_day.png +0 -0
  41. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/docs/source/_static/logo_night.png +0 -0
  42. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/docs/source/conf.py +0 -0
  43. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/docs/source/index.rst +0 -0
  44. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pyproject.toml +0 -0
  45. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/py.typed +0 -0
  46. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/stats.py +0 -0
  47. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/tools/__init__.py +0 -0
  48. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/tools/genson_addon/__init__.py +0 -0
  49. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/tools/genson_addon/format_detector.py +0 -0
  50. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/pytest_jsonschema_snapshot/tools/name_maker.py +0 -0
  51. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/reinstall_plugin.sh +0 -0
  52. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/requirements.txt +0 -0
  53. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/test_stats.py +0 -0
  54. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/tools/test_name_maker.py +0 -0
  55. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/TestDataClass.get_data.first.json +0 -0
  56. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/TestDataClass.get_data.json +0 -0
  57. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/array_formats_test.json +0 -0
  58. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/date_test.json +0 -0
  59. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/datetime_test.json +0 -0
  60. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/email_test.json +0 -0
  61. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/get_data.first.json +0 -0
  62. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/get_data.json +0 -0
  63. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/ipv4_test.json +0 -0
  64. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/mixed_formats_test.json +0 -0
  65. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/multi_schema_one.json +0 -0
  66. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/multi_schema_three.json +0 -0
  67. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/multi_schema_two.json +0 -0
  68. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/regular_strings_test.json +0 -0
  69. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/some_schema.json +0 -0
  70. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/strict_email_validation_test.json +0 -0
  71. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/uri_test.json +0 -0
  72. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/__snapshots__/uuid_test.json +0 -0
  73. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/test_base.py +0 -0
  74. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/test_base_name_callable.py +0 -0
  75. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/test_format_detection.py +0 -0
  76. {pytest_jsonschema_snapshot-0.2.0 → pytest_jsonschema_snapshot-0.2.1}/tests/usage/test_format_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-jsonschema-snapshot
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -117,12 +117,12 @@ pip install pytest-jsonschema-snapshot
117
117
  )
118
118
  ```
119
119
 
120
- 2. On first run, generate schemas with the `--schema-update` flag
120
+ 2. On first run, generate schemas with the `--schema-update` or `--schema-reset` (what is the difference? see the documentation) flag
121
121
  ```bash
122
122
  pytest --schema-update --save-original
123
123
  ```
124
124
 
125
- **--save-original**: save the original data on which the validation was performed. Saving occurs when `--schema-update`, if you run the schema update without this attribute, the old original data will be deleted without saving new ones.
125
+ **--save-original**: save the original data on which the validation was performed. Saving occurs when `--schema-update` or `--schema-reset`, if you run the schema update without this attribute, the old original data will be deleted without saving new ones.
126
126
 
127
127
  3. On subsequent runs, tests will validate data against saved schemas
128
128
  ```bash
@@ -72,12 +72,12 @@ pip install pytest-jsonschema-snapshot
72
72
  )
73
73
  ```
74
74
 
75
- 2. On first run, generate schemas with the `--schema-update` flag
75
+ 2. On first run, generate schemas with the `--schema-update` or `--schema-reset` (what is the difference? see the documentation) flag
76
76
  ```bash
77
77
  pytest --schema-update --save-original
78
78
  ```
79
79
 
80
- **--save-original**: save the original data on which the validation was performed. Saving occurs when `--schema-update`, if you run the schema update without this attribute, the old original data will be deleted without saving new ones.
80
+ **--save-original**: save the original data on which the validation was performed. Saving occurs when `--schema-update` or `--schema-reset`, if you run the schema update without this attribute, the old original data will be deleted without saving new ones.
81
81
 
82
82
  3. On subsequent runs, tests will validate data against saved schemas
83
83
  ```bash
@@ -19,6 +19,7 @@ Choose the option that best suits your project:
19
19
  [pytest]
20
20
  jsss_dir = __snapshots__ # where to put the schemas
21
21
  jsss_callable_regex = {class_method=.} # rule for the callable part
22
+ jsss_format_mode = on
22
23
 
23
24
  .. code-block:: python
24
25
 
@@ -26,6 +27,7 @@ Choose the option that best suits your project:
26
27
  [tool.pytest.ini_options]
27
28
  jsss_dir = "__snapshots__"
28
29
  jsss_callable_regex = "{class_method=.}"
30
+ jsss_format_mode = "on"
29
31
 
30
32
  .. code-block:: ini
31
33
 
@@ -33,9 +35,11 @@ Choose the option that best suits your project:
33
35
  [tool:pytest]
34
36
  jsss_dir = __snapshots__
35
37
  jsss_callable_regex = {class_method=.}
38
+ jsss_format_mode = on
36
39
 
37
40
  * **jsss_dir**: the name of the folder where the library will save the schemas/originals *(always in the same directory as the test that called it)*.
38
41
  * **jsss_callable_regex**: the rule for interpreting the callable name.
42
+ * **jsss_format_mode**: "on" (annotate and validate), "safe" (annotate), "off" (disable).
39
43
 
40
44
  Next, you can use the fixture in your tests:
41
45
  --------------------------------------------
@@ -84,8 +88,15 @@ Run
84
88
 
85
89
  * **--jsss-debug**: by default, the library hides its part of the call stack when raising. This is convenient for debugging your tests, but if the problem is in PJSSS itself - you can pass.
86
90
  * **--save-original**: save the original data on which the validation was performed. Saving occurs when `--schema-update`, if you run the schema update without this attribute, the old original data will be deleted without saving new ones.
87
- * **--schema-update**: update the schema - without this attribute, the library will only report changes in the schemas and fail the tests, with it, the tests (and the originals, if requested) will be updated.
88
91
 
92
+ Modes of operation
93
+ * **--schema-update**: when updating **merges** two schemas. Works by principle: "what is valid for the old one, is valid for the new one, and vice versa"
94
+ * **--schema-reset**: when updating **replaces** the old schema with the new one.
95
+
96
+ Disabling update mechanisms
97
+ * **--without-delete**: disables deletion of old schemas
98
+ * **--without-update**: disables updating of existing schemas
99
+ * **--without-add**: disables adding new schemas
89
100
 
90
101
  .. code-block:: console
91
102
 
@@ -8,5 +8,5 @@ pytest-typed-schema-shot
8
8
 
9
9
  from .core import SchemaShot
10
10
 
11
- __version__ = "0.2.0"
11
+ __version__ = "0.2.1"
12
12
  __all__ = ["SchemaShot"]
@@ -25,7 +25,10 @@ class SchemaShot:
25
25
  root_dir: Path,
26
26
  differ: "JsonSchemaDiff",
27
27
  callable_regex: str = "{class_method=.}",
28
+ format_mode: str = "on",
28
29
  update_mode: bool = False,
30
+ reset_mode: bool = False,
31
+ update_actions: dict[str, bool] = {},
29
32
  save_original: bool = False,
30
33
  debug_mode: bool = False,
31
34
  snapshot_dir_name: str = "__snapshots__",
@@ -41,7 +44,11 @@ class SchemaShot:
41
44
  self.root_dir: Path = root_dir
42
45
  self.differ: "JsonSchemaDiff" = differ
43
46
  self.callable_regex: str = callable_regex
47
+ self.format_mode: str = format_mode
48
+ # self.examples_limit: int = examples_limit
44
49
  self.update_mode: bool = update_mode
50
+ self.reset_mode: bool = reset_mode
51
+ self.update_actions: dict[str, bool] = update_actions
45
52
  self.save_original: bool = save_original
46
53
  self.debug_mode: bool = debug_mode
47
54
  self.snapshot_dir: Path = root_dir / snapshot_dir_name
@@ -104,7 +111,9 @@ class SchemaShot:
104
111
  available_to_create = not json_path.exists() or status is None
105
112
  available_to_update = status is True
106
113
 
107
- if available_to_create or available_to_update:
114
+ if (available_to_create and self.update_actions.get("add")) or (
115
+ available_to_update and self.update_actions.get("update")
116
+ ):
108
117
  with open(json_path, "w", encoding="utf-8") as f:
109
118
  json.dump(data, f, indent=2, ensure_ascii=False)
110
119
 
@@ -114,7 +123,7 @@ class SchemaShot:
114
123
  GLOBAL_STATS.add_updated(json_name)
115
124
  else:
116
125
  raise ValueError(f"Unexpected status: {status}")
117
- elif json_path.exists():
126
+ elif json_path.exists() and self.update_actions.get("delete"):
118
127
  # удаляем
119
128
  json_path.unlink()
120
129
  GLOBAL_STATS.add_deleted(json_name)
@@ -135,13 +144,15 @@ class SchemaShot:
135
144
 
136
145
  real_name = self._process_name(name)
137
146
 
138
- builder = JsonToSchemaConverter()
147
+ builder = JsonToSchemaConverter(
148
+ format_mode=self.format_mode # type: ignore[arg-type]
149
+ ) # , examples=self.examples_limit)
139
150
  builder.add_object(data)
140
151
  current_schema = builder.to_schema()
141
152
 
142
153
  real_name, status = self._base_match(data, current_schema, real_name)
143
154
 
144
- if self.update_mode:
155
+ if self.update_mode or self.reset_mode:
145
156
  self._save_process_original(real_name=real_name, status=status, data=data)
146
157
 
147
158
  return status
@@ -199,11 +210,15 @@ class SchemaShot:
199
210
 
200
211
  # --- когда схемы ещё нет ---
201
212
  if not schema_exists_before:
202
- if not self.update_mode:
213
+ if not self.update_mode and not self.reset_mode:
203
214
  raise pytest.fail.Exception(
204
215
  f"Schema `{name}` not found."
205
216
  "Run the test with the --schema-update option to create it."
206
217
  )
218
+ elif not self.update_actions.get("add"):
219
+ raise pytest.fail.Exception(
220
+ f"Schema `{name}` not found and adding new schemas is disabled."
221
+ )
207
222
 
208
223
  with open(schema_path, "w", encoding="utf-8") as f:
209
224
  json.dump(current_schema, f, indent=2, ensure_ascii=False)
@@ -221,13 +236,30 @@ class SchemaShot:
221
236
  if existing_schema != current_schema: # есть отличия
222
237
  differences = self.differ.compare(dict(existing_schema), current_schema).render()
223
238
 
224
- if self.update_mode:
239
+ if (self.update_mode or self.reset_mode) and self.update_actions.get("update"):
225
240
  GLOBAL_STATS.add_updated(schema_path.name, differences)
226
241
 
227
242
  # обновляем файл
228
- with open(schema_path, "w", encoding="utf-8") as f:
229
- json.dump(current_schema, f, indent=2, ensure_ascii=False)
230
- self.logger.warning(f"Schema `{name}` updated.\n\n{differences}")
243
+ if self.reset_mode and not self.update_mode:
244
+ with open(schema_path, "w", encoding="utf-8") as f:
245
+ json.dump(current_schema, f, indent=2, ensure_ascii=False)
246
+ self.logger.warning(f"Schema `{name}` updated (reset).\n\n{differences}")
247
+ elif self.update_mode and not self.reset_mode:
248
+ builder = JsonToSchemaConverter(
249
+ format_mode=self.format_mode # type: ignore[arg-type]
250
+ ) # , examples=self.examples_limit)
251
+ builder.add_schema(existing_schema)
252
+ builder.add_schema(current_schema)
253
+ merged_schema = builder.to_schema()
254
+
255
+ with open(schema_path, "w", encoding="utf-8") as f:
256
+ json.dump(merged_schema, f, indent=2, ensure_ascii=False)
257
+
258
+ self.logger.warning(f"Schema `{name}` updated (update).\n\n{differences}")
259
+ else: # both update_mode and reset_mode are True
260
+ raise ValueError(
261
+ "Both update_mode and reset_mode cannot be True at the same time."
262
+ )
231
263
  schema_updated = True
232
264
  elif data is not None:
233
265
  GLOBAL_STATS.add_uncommitted(schema_path.name, differences)
@@ -22,7 +22,16 @@ def pytest_addoption(parser: pytest.Parser) -> None:
22
22
  parser.addoption(
23
23
  "--schema-update",
24
24
  action="store_true",
25
- help="Update or create JSON Schema files based on current data",
25
+ help=(
26
+ "Augmenting mode for updating schemas. "
27
+ "If something is valid for the old schema, then it is valid "
28
+ "for the new one (and vice versa)."
29
+ ),
30
+ )
31
+ parser.addoption(
32
+ "--schema-reset",
33
+ action="store_true",
34
+ help="New schema does not take into account the old one during update.",
26
35
  )
27
36
  parser.addoption(
28
37
  "--save-original",
@@ -35,6 +44,22 @@ def pytest_addoption(parser: pytest.Parser) -> None:
35
44
  help="Show internal exception stack (stops hiding them)",
36
45
  )
37
46
 
47
+ parser.addoption(
48
+ "--without-delete",
49
+ action="store_true",
50
+ help="Disable deleting unused schemas",
51
+ )
52
+ parser.addoption(
53
+ "--without-update",
54
+ action="store_true",
55
+ help="Disable updating schemas",
56
+ )
57
+ parser.addoption(
58
+ "--without-add",
59
+ action="store_true",
60
+ help="Disable adding new schemas",
61
+ )
62
+
38
63
  parser.addini(
39
64
  "jsss_dir",
40
65
  default="__snapshots__",
@@ -45,6 +70,11 @@ def pytest_addoption(parser: pytest.Parser) -> None:
45
70
  default="{class_method=.}",
46
71
  help="Regex for saving callable part of path",
47
72
  )
73
+ parser.addini(
74
+ "jsss_format_mode",
75
+ default="on",
76
+ help="Format mode: 'on' (annotate and validate), 'safe' (annotate), 'off' (disable)",
77
+ )
48
78
 
49
79
 
50
80
  @pytest.fixture(scope="function")
@@ -56,13 +86,26 @@ def schemashot(request: pytest.FixtureRequest) -> Generator[SchemaShot, None, No
56
86
  # Получаем путь к тестовому файлу
57
87
  test_path = Path(request.node.path if hasattr(request.node, "path") else request.node.fspath)
58
88
  root_dir = test_path.parent
89
+
59
90
  update_mode = bool(request.config.getoption("--schema-update"))
91
+ reset_mode = bool(request.config.getoption("--schema-reset"))
92
+ if update_mode and reset_mode:
93
+ raise ValueError("Options --schema-update and --schema-reset are mutually exclusive.")
94
+
60
95
  save_original = bool(request.config.getoption("--save-original"))
61
96
  debug_mode = bool(request.config.getoption("--jsss-debug"))
62
97
 
98
+ actions = {
99
+ "delete": not request.config.getoption("--without-delete"),
100
+ "update": not request.config.getoption("--without-update"),
101
+ "add": not request.config.getoption("--without-add"),
102
+ }
103
+
63
104
  # Получаем настраиваемую директорию для схем
64
105
  schema_dir_name = str(request.config.getini("jsss_dir"))
65
106
  callable_regex = str(request.config.getini("jsss_callable_regex"))
107
+ format_mode = str(request.config.getini("jsss_format_mode")).lower()
108
+ # examples_limit = int(request.config.getini("jsss_examples_limit"))
66
109
 
67
110
  differ = JsonSchemaDiff(
68
111
  ConfigMaker.make(),
@@ -77,7 +120,11 @@ def schemashot(request: pytest.FixtureRequest) -> Generator[SchemaShot, None, No
77
120
  root_dir,
78
121
  differ,
79
122
  callable_regex,
123
+ format_mode,
124
+ # examples_limit,
80
125
  update_mode,
126
+ reset_mode,
127
+ actions,
81
128
  save_original,
82
129
  debug_mode,
83
130
  schema_dir_name,
@@ -108,11 +155,21 @@ def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter, exitstatu
108
155
  """
109
156
  # Выполняем cleanup перед показом summary
110
157
  if _schema_managers:
111
- update_mode = bool(terminalreporter.config.getoption("--schema-update"))
158
+
159
+ def get_opt(opt: str) -> bool:
160
+ return bool(terminalreporter.config.getoption(opt))
161
+
162
+ update_mode = get_opt("--schema-update")
163
+
164
+ actions = {
165
+ "delete": not get_opt("--without-delete"),
166
+ "update": not get_opt("--without-update"),
167
+ "add": not get_opt("--without-add"),
168
+ }
112
169
 
113
170
  # Вызываем метод очистки неиспользованных схем для каждого экземпляра
114
171
  for _root_dir, manager in _schema_managers.items():
115
- cleanup_unused_schemas(manager, update_mode, GLOBAL_STATS)
172
+ cleanup_unused_schemas(manager, update_mode, actions, GLOBAL_STATS)
116
173
 
117
174
  # Используем новую функцию для вывода статистики
118
175
  update_mode = bool(terminalreporter.config.getoption("--schema-update"))
@@ -120,7 +177,10 @@ def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter, exitstatu
120
177
 
121
178
 
122
179
  def cleanup_unused_schemas(
123
- manager: SchemaShot, update_mode: bool, stats: Optional[SchemaStats] = None
180
+ manager: SchemaShot,
181
+ update_mode: bool,
182
+ actions: dict[str, bool],
183
+ stats: Optional[SchemaStats] = None,
124
184
  ) -> None:
125
185
  """
126
186
  Deletes unused schemas in update mode and collects statistics.
@@ -140,7 +200,7 @@ def cleanup_unused_schemas(
140
200
 
141
201
  for schema_file in all_schemas:
142
202
  if schema_file.name not in manager.used_schemas:
143
- if update_mode:
203
+ if update_mode and actions.get("delete"):
144
204
  try:
145
205
  # Удаляем саму схему
146
206
  schema_file.unlink()
@@ -0,0 +1,100 @@
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)
@@ -0,0 +1,82 @@
1
+ """Pytest suite for ``JsonToSchemaConverter`` using generic ``jsonschema.validate``.
2
+
3
+ We use two JSON samples (``SOURCE`` and ``SOURCE_INVALID``) to check that
4
+ *format* handling behaves as intended under three modes.
5
+
6
+ Expectation matrix
7
+ ================== ================= =========================
8
+ format_mode call to validate Expected result
9
+ ================== ================= =========================
10
+ "on" validate + FC ValidationError raised
11
+ "safe" validate passes OK
12
+ "off" validate passes OK
13
+ ================== ================= =========================
14
+ Where **FC** = ``FormatChecker``.
15
+
16
+ Why this matters?
17
+ -----------------
18
+ *User requested*: «должно проходить на обычном validate». Therefore we
19
+ avoid draft‑specific classes like ``Draft202012Validator`` and rely on
20
+ ``jsonschema.validate`` selecting a validator based on the ``$schema``
21
+ keyword embedded by *genson* (currently `http://json-schema.org/schema#`).
22
+
23
+ In python‑jsonschema, *format* assertions only run when a
24
+ ``FormatChecker`` is provided, regardless of the *format-assertion*
25
+ vocabulary. Hence, supplying ``FormatChecker`` only in **on** mode gives
26
+ us the desired behaviour.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import jsonschema
32
+ import pytest
33
+
34
+ from pytest_jsonschema_snapshot.tools import JsonToSchemaConverter
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Fixtures
38
+ # ---------------------------------------------------------------------------
39
+ SOURCE = {
40
+ "email": "alice@example.com",
41
+ "website": "https://example.com",
42
+ }
43
+
44
+ SOURCE_INVALID = {
45
+ "email": "not-an-email",
46
+ "website": "notaurl",
47
+ }
48
+
49
+ PARAMS = [
50
+ ("on", False, True), # должен упасть (ValidationError)
51
+ ("safe", True, True), # должен пройти
52
+ ("off", True, False), # должен пройти
53
+ ]
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Tests
58
+ # ---------------------------------------------------------------------------
59
+ @pytest.mark.parametrize("mode, should_pass, have_formats", PARAMS)
60
+ def test_format_handling(mode: str, should_pass: bool, have_formats: bool) -> None:
61
+ """Validate *SOURCE_INVALID* against schema generated from *SOURCE*."""
62
+
63
+ # 1. Generate schema
64
+ conv = JsonToSchemaConverter(format_mode=mode)
65
+ conv.add_object(SOURCE)
66
+ schema = conv.to_schema()
67
+
68
+ # 2. Prepare kwargs for jsonschema.validate
69
+ kwargs: dict[str, object] = {}
70
+ if mode == "on":
71
+ kwargs["format_checker"] = jsonschema.FormatChecker()
72
+
73
+ # 3. Validate and assert outcome
74
+ if should_pass:
75
+ jsonschema.validate(SOURCE_INVALID, schema, **kwargs)
76
+ else:
77
+ with pytest.raises(jsonschema.ValidationError):
78
+ jsonschema.validate(SOURCE_INVALID, schema, **kwargs)
79
+
80
+ # 4. Ensure presence/absence of "format" matches the mode
81
+ has_format = schema["properties"]["email"].get("format") is not None
82
+ assert has_format == have_formats
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "обязательная": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "обязательная": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "emails": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "birth_date": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "created_at": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "user_email": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "обязательная": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "обязательная": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "server_ip": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "id": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "name": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "обязательная": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "user_email": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "website": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema#",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "id": {
@@ -5,8 +5,10 @@ import pytest
5
5
 
6
6
  def test_multiple_schema_creation(schemashot, pytestconfig):
7
7
  """Ensure multiple schemas can be created sequentially."""
8
- if not pytestconfig.getoption("--schema-update"):
9
- pytest.skip("requires --schema-update")
8
+ if not pytestconfig.getoption("--schema-reset") and not pytestconfig.getoption(
9
+ "--schema-update"
10
+ ):
11
+ pytest.skip("requires --schema-reset or --schema-update")
10
12
 
11
13
  snapshot_dir = Path(__file__).parent / "__snapshots__"
12
14
  names = ["multi_schema_one", "multi_schema_two", "multi_schema_three"]
@@ -1,119 +0,0 @@
1
- """
2
- Module for advanced JSON Schema generation with format detection support.
3
- """
4
-
5
- from typing import Any, Dict, Optional
6
-
7
- from genson import SchemaBuilder # type: ignore[import-untyped]
8
-
9
- from .format_detector import FormatDetector
10
-
11
-
12
- class FormatAwareString:
13
- """Strategy for strings with format detection"""
14
-
15
- def __init__(self) -> None:
16
- self.formats: set = set()
17
-
18
- def match_schema(self, obj: Any) -> bool:
19
- """Checks if the object matches this strategy"""
20
- return isinstance(obj, str)
21
-
22
- def match_object(self, obj: Any) -> bool:
23
- """Checks if the object matches this strategy"""
24
- return isinstance(obj, str)
25
-
26
- def add_object(self, obj: Any) -> None:
27
- """Adds an object for analysis"""
28
- if isinstance(obj, str):
29
- detected_format = FormatDetector.detect_format(obj)
30
- if detected_format:
31
- self.formats.add(detected_format)
32
-
33
- def to_schema(self) -> Dict[str, Any]:
34
- """Generates a schema for the string"""
35
- schema = {"type": "string"}
36
-
37
- # If all strings have the same format, add it to the schema
38
- if len(self.formats) == 1:
39
- schema["format"] = list(self.formats)[0]
40
-
41
- return schema
42
-
43
-
44
- class JsonToSchemaConverter(SchemaBuilder):
45
- """Extended SchemaBuilder with format detection support"""
46
-
47
- def __init__(self, schema_uri: Optional[str] = None):
48
- if schema_uri:
49
- super().__init__(schema_uri)
50
- else:
51
- super().__init__()
52
- self._format_cache: Dict[str, set] = {}
53
-
54
- def add_object(self, obj: Any, path: str = "root") -> None:
55
- """
56
- Adds an object to the builder with format detection.
57
-
58
- Args:
59
- obj: Object to add
60
- path: Path to the object (for internal use)
61
- """
62
- # Call the parent method first
63
- super().add_object(obj)
64
-
65
- # Then process the formats
66
- self._process_formats(obj, path)
67
-
68
- def _process_formats(self, obj: Any, path: str) -> None:
69
- """Recursively processes the object for format detection"""
70
- if isinstance(obj, str):
71
- # Detect the format of the string
72
- detected_format = FormatDetector.detect_format(obj)
73
- if detected_format:
74
- if path not in self._format_cache:
75
- self._format_cache[path] = set()
76
- self._format_cache[path].add(detected_format)
77
- elif isinstance(obj, dict):
78
- # Recursively process the dictionary
79
- for key, value in obj.items():
80
- self._process_formats(value, f"{path}.{key}")
81
- elif isinstance(obj, (list, tuple)):
82
- # Recursively process the list
83
- for i, item in enumerate(obj):
84
- self._process_formats(item, f"{path}[{i}]")
85
-
86
- def to_schema(self) -> Dict:
87
- """Generates the schema with format detection"""
88
- # Get the base schema
89
- schema = dict(super().to_schema())
90
-
91
- # Add the formats
92
- self._add_formats_to_schema(schema, "root")
93
-
94
- return schema
95
-
96
- def _add_formats_to_schema(self, schema: Dict[str, Any], path: str) -> None:
97
- """Recursively adds formats to the schema"""
98
- if schema.get("type") == "string":
99
- # If there is only one format for this path
100
- if path in self._format_cache and len(self._format_cache[path]) == 1:
101
- schema["format"] = list(self._format_cache[path])[0]
102
-
103
- elif schema.get("type") == "object" and "properties" in schema:
104
- # Recursively process the object properties
105
- for prop_name, prop_schema in schema["properties"].items():
106
- self._add_formats_to_schema(prop_schema, f"{path}.{prop_name}")
107
-
108
- elif schema.get("type") == "array" and "items" in schema:
109
- # Process the array items
110
- if isinstance(schema["items"], dict):
111
- self._add_formats_to_schema(schema["items"], f"{path}[0]")
112
- elif isinstance(schema["items"], list):
113
- for i, item_schema in enumerate(schema["items"]):
114
- self._add_formats_to_schema(item_schema, f"{path}[{i}]")
115
-
116
- elif "anyOf" in schema:
117
- # Process the anyOf schemas
118
- for i, sub_schema in enumerate(schema["anyOf"]):
119
- self._add_formats_to_schema(sub_schema, path)