Coverage for pytest_jsonschema_snapshot / stats.py: 60%
125 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 20:33 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 20:33 +0000
1"""
2Module for collecting and displaying statistics about schemas.
3"""
5from typing import Dict, Generator, List, Optional
7import pytest
10class SchemaStats:
11 """Class for collecting and displaying statistics about schemas"""
13 def __init__(self) -> None:
14 self.created: List[str] = []
15 self.updated: List[str] = []
16 self.updated_diffs: Dict[str, str] = {} # schema_name -> diff
17 self.uncommitted: List[str] = [] # New category for uncommitted changes
18 self.uncommitted_diffs: Dict[str, str] = {} # schema_name -> diff
19 self.deleted: List[str] = []
20 self.unused: List[str] = []
22 def add_created(self, schema_name: str) -> None:
23 """Adds created schema"""
24 self.created.append(schema_name)
26 def add_updated(self, schema_name: str, diff: Optional[str] = None) -> None:
27 """Adds updated schema"""
28 # Generate diff if both schemas are provided
29 if diff and diff.strip():
30 self.updated.append(schema_name)
31 self.updated_diffs[schema_name] = diff
32 else:
33 # If schemas are not provided, assume it was an update
34 self.updated.append(schema_name)
36 def add_uncommitted(self, schema_name: str, diff: Optional[str] = None) -> None:
37 """Adds schema with uncommitted changes"""
38 # Add only if there are real changes
39 if diff and diff.strip():
40 self.uncommitted.append(schema_name)
41 self.uncommitted_diffs[schema_name] = diff
43 def add_deleted(self, schema_name: str) -> None:
44 """Adds deleted schema"""
45 self.deleted.append(schema_name)
47 def add_unused(self, schema_name: str) -> None:
48 """Adds unused schema"""
49 self.unused.append(schema_name)
51 def has_changes(self) -> bool:
52 """Returns True if any schema has changes"""
53 return bool(self.created or self.updated or self.deleted)
55 def has_any_info(self) -> bool:
56 """Is there any information about schemas"""
57 return bool(self.created or self.updated or self.deleted or self.unused or self.uncommitted)
59 def __str__(self) -> str:
60 parts = []
61 if self.created:
62 parts.append(
63 f"Created schemas ({len(self.created)}): "
64 + ", ".join(f"`{s}`" for s in self.created)
65 )
66 if self.updated:
67 parts.append(
68 f"Updated schemas ({len(self.updated)}): "
69 + ", ".join(f"`{s}`" for s in self.updated)
70 )
71 if self.deleted:
72 parts.append(
73 f"Deleted schemas ({len(self.deleted)}): "
74 + ", ".join(f"`{s}`" for s in self.deleted)
75 )
76 if self.unused:
77 parts.append(
78 f"Unused schemas ({len(self.unused)}): " + ", ".join(f"`{s}`" for s in self.unused)
79 )
81 return "\n".join(parts)
83 def _iter_schemas(self, names: List[str]) -> Generator[tuple[str, Optional[str]], None, None]:
84 """
85 Iterates over schema displays: (display, schema_key)
86 - display: string to display (may have " + original")
87 - schema_key: file name of the schema (<name>.schema.json) to find diffs,
88 or None if it's not a schema.
89 Preserves the original list order: merging happens at .schema.json
90 position; skips .json if paired with schema.
91 """
92 names = list(names) # order matters
93 schema_sfx = ".schema.json"
94 json_sfx = ".json"
96 # sets of bases
97 # bases_with_schema = {n[: -len(schema_sfx)] for n in names if n.endswith(schema_sfx)}
98 bases_with_original = {
99 n[: -len(json_sfx)]
100 for n in names
101 if n.endswith(json_sfx) and not n.endswith(schema_sfx)
102 }
104 for n in names:
105 if n.endswith(schema_sfx):
106 base = n[: -len(schema_sfx)]
107 if base in bases_with_original:
108 yield f"{n} + original", n # display, schema_key
109 else:
110 yield n, n
111 # if .json, skip if paired
112 # if other, yield n, n (but assume all are .json or .schema.json)
114 def _iter_only_originals(self, names: List[str]) -> Generator[str, None, None]:
115 """
116 Iterates over only unpaired .json files, in the order they appear.
117 """
118 names = list(names) # order matters
119 schema_sfx = ".schema.json"
120 json_sfx = ".json"
122 bases_with_schema = {n[: -len(schema_sfx)] for n in names if n.endswith(schema_sfx)}
124 for n in names:
125 if n.endswith(json_sfx) and not n.endswith(schema_sfx):
126 base = n[: -len(json_sfx)]
127 if base not in bases_with_schema:
128 yield n
130 def print_summary(self, terminalreporter: pytest.TerminalReporter, update_mode: bool) -> None:
131 """
132 Prints schema summary to pytest terminal output.
133 Pairs of "<name>.schema.json" + "<name>.json" are merged into one line:
134 "<name>.schema.json + original" (if original is present).
135 Unpaired .json are shown in separate "only originals" sections.
136 """
138 if not self.has_any_info():
139 return
141 terminalreporter.write_sep("=", "Schema Summary")
143 # Created
144 if self.created:
145 schemas = list(self._iter_schemas(self.created))
146 only_originals = list(self._iter_only_originals(self.created))
147 if schemas:
148 terminalreporter.write_line(f"Created schemas ({len(schemas)}):", green=True)
149 for display, _key in schemas:
150 terminalreporter.write_line(f" - {display}", green=True)
151 if only_originals:
152 terminalreporter.write_line(
153 f"Created only originals ({len(only_originals)}):", green=True
154 )
155 for display in only_originals:
156 terminalreporter.write_line(f" - {display}", green=True)
158 # Updated
159 if self.updated:
160 schemas = list(self._iter_schemas(self.updated))
161 only_originals = list(self._iter_only_originals(self.updated))
162 if schemas:
163 terminalreporter.write_line(f"Updated schemas ({len(schemas)}):", yellow=True)
164 for display, key in schemas:
165 terminalreporter.write_line(f" - {display}", yellow=True)
166 # Show diff if available for schema
167 if key and key in self.updated_diffs:
168 diff = self.updated_diffs[key]
169 if diff.strip():
170 terminalreporter.write_line(" Changes:", yellow=True)
171 for line in diff.split("\n"):
172 if line.strip():
173 terminalreporter.write_line(f" {line}")
174 terminalreporter.write_line("") # separation
175 else:
176 terminalreporter.write_line(
177 " (Schema unchanged - no differences detected)", cyan=True
178 )
179 if only_originals:
180 terminalreporter.write_line(
181 f"Updated only originals ({len(only_originals)}):", yellow=True
182 )
183 for display in only_originals:
184 terminalreporter.write_line(f" - {display}", yellow=True)
186 # Uncommitted
187 if self.uncommitted:
188 terminalreporter.write_line(
189 f"Uncommitted minor updates ({len(self.uncommitted)}):", bold=True
190 )
191 for display, key in self._iter_schemas(self.uncommitted): # assuming mostly schemas
192 terminalreporter.write_line(f" - {display}", cyan=True)
193 # Show diff if available
194 if key and key in self.uncommitted_diffs:
195 terminalreporter.write_line(" Detected changes:", cyan=True)
196 for line in self.uncommitted_diffs[key].split("\n"):
197 if line.strip():
198 terminalreporter.write_line(f" {line}")
199 terminalreporter.write_line("") # separation
200 terminalreporter.write_line("Use --schema-update to commit these changes", cyan=True)
202 # Deleted
203 if self.deleted:
204 schemas = list(self._iter_schemas(self.deleted))
205 only_originals = list(self._iter_only_originals(self.deleted))
206 if schemas:
207 terminalreporter.write_line(f"Deleted schemas ({len(schemas)}):", red=True)
208 for display, _key in schemas:
209 terminalreporter.write_line(f" - {display}", red=True)
210 if only_originals:
211 terminalreporter.write_line(
212 f"Deleted only originals ({len(only_originals)}):", red=True
213 )
214 for display in only_originals:
215 terminalreporter.write_line(f" - {display}", red=True)
217 # Unused (only if not update_mode)
218 if self.unused and not update_mode:
219 terminalreporter.write_line(f"Unused schemas ({len(self.unused)}):")
220 for display, _key in self._iter_schemas(self.unused): # assuming schemas
221 terminalreporter.write_line(f" - {display}")
222 terminalreporter.write_line("Use --schema-update to delete unused schemas", yellow=True)
225GLOBAL_STATS = SchemaStats()