htmlcmp 1.0.17__tar.gz → 1.1.0__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.
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/PKG-INFO +1 -1
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/pyproject.toml +1 -1
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp/compare_output_server.py +287 -69
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp.egg-info/PKG-INFO +1 -1
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/README.md +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/setup.cfg +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp/__init__.py +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp/common.py +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp/compare_output.py +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp/html_render_diff.py +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp/tidy_output.py +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp.egg-info/SOURCES.txt +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp.egg-info/dependency_links.txt +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp.egg-info/entry_points.txt +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp.egg-info/requires.txt +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/src/htmlcmp.egg-info/top_level.txt +0 -0
- {htmlcmp-1.0.17 → htmlcmp-1.1.0}/tests/test_html_render_diff.py +0 -0
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
import argparse
|
|
7
7
|
import logging
|
|
8
8
|
import threading
|
|
9
|
+
import functools
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from concurrent.futures import ThreadPoolExecutor
|
|
11
12
|
|
|
@@ -30,6 +31,32 @@ class Config:
|
|
|
30
31
|
thread_local = threading.local()
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
def result_symbol(result: str):
|
|
35
|
+
if not isinstance(result, str):
|
|
36
|
+
raise TypeError("Result must be of type str")
|
|
37
|
+
|
|
38
|
+
if result == "pending":
|
|
39
|
+
return "🔄"
|
|
40
|
+
if result == "same":
|
|
41
|
+
return "✔"
|
|
42
|
+
if result == "different":
|
|
43
|
+
return "❌"
|
|
44
|
+
return "⛔"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def result_css(result: str):
|
|
48
|
+
if not isinstance(result, str):
|
|
49
|
+
raise TypeError("Result must be of type str")
|
|
50
|
+
|
|
51
|
+
if result == "pending":
|
|
52
|
+
return "color:blue;"
|
|
53
|
+
if result == "same":
|
|
54
|
+
return "color:green;"
|
|
55
|
+
if result == "different":
|
|
56
|
+
return "color:orange;"
|
|
57
|
+
return "color:red;"
|
|
58
|
+
|
|
59
|
+
|
|
33
60
|
class Observer:
|
|
34
61
|
def __init__(self):
|
|
35
62
|
class Handler(watchdog.events.FileSystemEventHandler):
|
|
@@ -40,7 +67,7 @@ class Observer:
|
|
|
40
67
|
event_type = event.event_type
|
|
41
68
|
src_path = Path(event.src_path)
|
|
42
69
|
|
|
43
|
-
logger.
|
|
70
|
+
logger.debug(f"Watchdog event: {event_type} {src_path}")
|
|
44
71
|
|
|
45
72
|
if event_type not in ["moved", "deleted", "created", "modified"]:
|
|
46
73
|
return
|
|
@@ -64,7 +91,7 @@ class Observer:
|
|
|
64
91
|
self._observer.start()
|
|
65
92
|
|
|
66
93
|
def init_compare(a: Path, b: Path):
|
|
67
|
-
logger.
|
|
94
|
+
logger.debug(f"Initial compare: {a} vs {b}")
|
|
68
95
|
|
|
69
96
|
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
70
97
|
raise TypeError("Paths must be of type Path")
|
|
@@ -158,37 +185,58 @@ class Comparator:
|
|
|
158
185
|
|
|
159
186
|
if path in self._result:
|
|
160
187
|
return self._result[path]
|
|
161
|
-
return "unknown"
|
|
162
|
-
|
|
163
|
-
def result_symbol(self, path: Path):
|
|
164
|
-
logger.debug(f"Getting comparison result symbol for path: {path}")
|
|
165
188
|
|
|
166
|
-
if
|
|
167
|
-
|
|
189
|
+
if (Config.path_a / path).is_dir():
|
|
190
|
+
a = Config.path_a / path
|
|
191
|
+
b = Config.path_b / path
|
|
168
192
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return "🔄"
|
|
172
|
-
if result == "same":
|
|
173
|
-
return "✔"
|
|
174
|
-
if result == "different":
|
|
175
|
-
return "❌"
|
|
176
|
-
return "⛔"
|
|
193
|
+
left = sorted(p.name for p in a.iterdir())
|
|
194
|
+
right = sorted(p.name for p in b.iterdir())
|
|
177
195
|
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
left_relevant = sorted(
|
|
197
|
+
[
|
|
198
|
+
name
|
|
199
|
+
for name in left
|
|
200
|
+
if (a / name).is_dir()
|
|
201
|
+
or ((a / name).is_file() and comparable_file(a / name))
|
|
202
|
+
]
|
|
203
|
+
)
|
|
204
|
+
right_relevant = sorted(
|
|
205
|
+
[
|
|
206
|
+
name
|
|
207
|
+
for name in right
|
|
208
|
+
if (b / name).is_dir()
|
|
209
|
+
or ((b / name).is_file() and comparable_file(b / name))
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
common = [name for name in left_relevant if name in right_relevant]
|
|
214
|
+
left_missing = [
|
|
215
|
+
name for name in right_relevant if name not in left_relevant
|
|
216
|
+
]
|
|
217
|
+
right_missing = [
|
|
218
|
+
name for name in left_relevant if name not in right_relevant
|
|
219
|
+
]
|
|
180
220
|
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
return functools.reduce(
|
|
222
|
+
lambda a, b: (
|
|
223
|
+
"pending"
|
|
224
|
+
if "pending" in (a, b)
|
|
225
|
+
else ("different" if "different" in (a, b) else "same")
|
|
226
|
+
),
|
|
227
|
+
[self.result(path / name) for name in common]
|
|
228
|
+
+ [
|
|
229
|
+
(
|
|
230
|
+
"different"
|
|
231
|
+
if len(left_missing) + len(right_missing) > 0
|
|
232
|
+
else "same"
|
|
233
|
+
)
|
|
234
|
+
],
|
|
235
|
+
"same",
|
|
236
|
+
)
|
|
183
237
|
|
|
184
|
-
result
|
|
185
|
-
|
|
186
|
-
return "color:blue;"
|
|
187
|
-
if result == "same":
|
|
188
|
-
return "color:green;"
|
|
189
|
-
if result == "different":
|
|
190
|
-
return "color:orange;"
|
|
191
|
-
return "color:red;"
|
|
238
|
+
logger.warning(f"No comparison result for path: {path}")
|
|
239
|
+
return "unknown"
|
|
192
240
|
|
|
193
241
|
|
|
194
242
|
app = Flask("compare")
|
|
@@ -198,7 +246,66 @@ app = Flask("compare")
|
|
|
198
246
|
def root():
|
|
199
247
|
logger.debug("Generating root directory listing")
|
|
200
248
|
|
|
201
|
-
|
|
249
|
+
current_entry_id = 0
|
|
250
|
+
|
|
251
|
+
def next_entry_id():
|
|
252
|
+
nonlocal current_entry_id
|
|
253
|
+
entry_id = current_entry_id
|
|
254
|
+
current_entry_id += 1
|
|
255
|
+
return entry_id
|
|
256
|
+
|
|
257
|
+
def generate_entry(
|
|
258
|
+
entry_id: int,
|
|
259
|
+
parrent_id: int | None,
|
|
260
|
+
depth: int,
|
|
261
|
+
is_directory: bool,
|
|
262
|
+
is_comparable: bool,
|
|
263
|
+
path: Path,
|
|
264
|
+
name: str,
|
|
265
|
+
message: str = "",
|
|
266
|
+
cmp_result: str = None,
|
|
267
|
+
) -> str:
|
|
268
|
+
result = ""
|
|
269
|
+
|
|
270
|
+
if cmp_result is None and Config.comparator is not None:
|
|
271
|
+
cmp_result = Config.comparator.result(path)
|
|
272
|
+
is_hidden = (
|
|
273
|
+
Config.comparator is not None
|
|
274
|
+
and Config.comparator.result(path.parent) == "same"
|
|
275
|
+
)
|
|
276
|
+
is_collapsed = cmp_result == "same"
|
|
277
|
+
|
|
278
|
+
hidden_str = "hidden" if is_hidden else ""
|
|
279
|
+
result += f'<tr data-entry-id="{entry_id}" data-parent-id="{"" if parrent_id is None else parrent_id}" data-depth="{depth}" {hidden_str}>'
|
|
280
|
+
|
|
281
|
+
if is_directory:
|
|
282
|
+
result += f'<td><button class="toggle">{"▶" if is_collapsed else "▼"}</button></td>'
|
|
283
|
+
else:
|
|
284
|
+
result += "<td></td>"
|
|
285
|
+
|
|
286
|
+
if is_comparable:
|
|
287
|
+
main = f'<a href="/compare/{path}">{name}</a>'
|
|
288
|
+
else:
|
|
289
|
+
main = f"{name}"
|
|
290
|
+
if cmp_result is not None:
|
|
291
|
+
status = result_symbol(cmp_result)
|
|
292
|
+
style = result_css(cmp_result)
|
|
293
|
+
result += f'<td style="{style}">{status}</td>'
|
|
294
|
+
else:
|
|
295
|
+
style = ""
|
|
296
|
+
result += f"<td></td>"
|
|
297
|
+
result += f'<td class="main" style="{style}">{main}</td>'
|
|
298
|
+
result += f"<td>{message}</td>"
|
|
299
|
+
|
|
300
|
+
result += "</tr>"
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
def generate_tree(
|
|
305
|
+
a: Path, b: Path, name: str, parrent_id: int | None, depth: int
|
|
306
|
+
) -> str:
|
|
307
|
+
result = ""
|
|
308
|
+
|
|
202
309
|
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
203
310
|
raise TypeError("Paths must be of type Path")
|
|
204
311
|
if not a.is_dir() or not b.is_dir():
|
|
@@ -206,6 +313,17 @@ def root():
|
|
|
206
313
|
|
|
207
314
|
common_path = a.relative_to(Config.path_a)
|
|
208
315
|
|
|
316
|
+
directory_id = next_entry_id()
|
|
317
|
+
result += generate_entry(
|
|
318
|
+
directory_id,
|
|
319
|
+
parrent_id,
|
|
320
|
+
depth,
|
|
321
|
+
True,
|
|
322
|
+
False,
|
|
323
|
+
common_path,
|
|
324
|
+
name + "/",
|
|
325
|
+
)
|
|
326
|
+
|
|
209
327
|
left = sorted(p.name for p in a.iterdir())
|
|
210
328
|
right = sorted(p.name for p in b.iterdir())
|
|
211
329
|
|
|
@@ -230,48 +348,148 @@ def root():
|
|
|
230
348
|
common_files = [name for name in left_files if name in right_files]
|
|
231
349
|
common_dirs = [name for name in left_dirs if name in right_dirs]
|
|
232
350
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
result +=
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
351
|
+
left_files_missing = [name for name in right_files if name not in left_files]
|
|
352
|
+
right_files_missing = [name for name in left_files if name not in right_files]
|
|
353
|
+
left_dirs_missing = [name for name in right_dirs if name not in left_dirs]
|
|
354
|
+
right_dirs_missing = [name for name in left_dirs if name not in right_dirs]
|
|
355
|
+
for name in left_files_missing:
|
|
356
|
+
result += generate_entry(
|
|
357
|
+
next_entry_id(),
|
|
358
|
+
directory_id,
|
|
359
|
+
depth + 1,
|
|
360
|
+
False,
|
|
361
|
+
False,
|
|
362
|
+
common_path / name,
|
|
363
|
+
name,
|
|
364
|
+
"file missing in A",
|
|
365
|
+
cmp_result="different",
|
|
366
|
+
)
|
|
367
|
+
for name in right_files_missing:
|
|
368
|
+
result += generate_entry(
|
|
369
|
+
next_entry_id(),
|
|
370
|
+
directory_id,
|
|
371
|
+
depth + 1,
|
|
372
|
+
False,
|
|
373
|
+
False,
|
|
374
|
+
common_path / name,
|
|
375
|
+
name,
|
|
376
|
+
"file missing in B",
|
|
377
|
+
cmp_result="different",
|
|
378
|
+
)
|
|
379
|
+
for name in left_dirs_missing:
|
|
380
|
+
result += generate_entry(
|
|
381
|
+
next_entry_id(),
|
|
382
|
+
directory_id,
|
|
383
|
+
depth + 1,
|
|
384
|
+
False,
|
|
385
|
+
False,
|
|
386
|
+
common_path / name,
|
|
387
|
+
name + "/",
|
|
388
|
+
"dir missing in A",
|
|
389
|
+
cmp_result="different",
|
|
390
|
+
)
|
|
391
|
+
for name in right_dirs_missing:
|
|
392
|
+
result += generate_entry(
|
|
393
|
+
next_entry_id(),
|
|
394
|
+
directory_id,
|
|
395
|
+
depth + 1,
|
|
396
|
+
False,
|
|
397
|
+
False,
|
|
398
|
+
common_path / name,
|
|
399
|
+
name + "/",
|
|
400
|
+
"dir missing in B",
|
|
401
|
+
cmp_result="different",
|
|
402
|
+
)
|
|
255
403
|
|
|
256
404
|
for name in common_files:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
405
|
+
result += generate_entry(
|
|
406
|
+
next_entry_id(),
|
|
407
|
+
directory_id,
|
|
408
|
+
depth + 1,
|
|
409
|
+
False,
|
|
410
|
+
True,
|
|
411
|
+
common_path / name,
|
|
412
|
+
name,
|
|
413
|
+
"",
|
|
414
|
+
)
|
|
263
415
|
|
|
264
416
|
for name in common_dirs:
|
|
265
|
-
result +=
|
|
266
|
-
result += print_tree(a / name, b / name)
|
|
267
|
-
result += "</li>"
|
|
417
|
+
result += generate_tree(a / name, b / name, name, directory_id, depth + 1)
|
|
268
418
|
|
|
269
|
-
result += "</ul>"
|
|
270
419
|
return result
|
|
271
420
|
|
|
272
|
-
result = ""
|
|
273
|
-
|
|
274
|
-
|
|
421
|
+
result = """<!DOCTYPE html>
|
|
422
|
+
<html>
|
|
423
|
+
<head>
|
|
424
|
+
<style>
|
|
425
|
+
tr {
|
|
426
|
+
--depth: attr(data-depth number);
|
|
427
|
+
}
|
|
428
|
+
.main {
|
|
429
|
+
padding-left: calc(1.0rem * var(--depth));
|
|
430
|
+
}
|
|
431
|
+
</style>
|
|
432
|
+
</head>
|
|
433
|
+
<body>
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
result += "<p>"
|
|
437
|
+
result += "comparing<br>"
|
|
438
|
+
result += f"A: {Config.path_a}<br>"
|
|
439
|
+
result += f"B: {Config.path_b}"
|
|
440
|
+
result += "</p>"
|
|
441
|
+
|
|
442
|
+
result += "<p>"
|
|
443
|
+
result += '<button onclick="toggleAll(true)">Expand All</button>'
|
|
444
|
+
result += '<button onclick="toggleAll(false)">Collapse All</button>'
|
|
445
|
+
result += "</p>"
|
|
446
|
+
|
|
447
|
+
result += "<table>"
|
|
448
|
+
result += "<thead>"
|
|
449
|
+
result += "<tr><td></td><td></td><td>Name</td><td>Message</td></tr>"
|
|
450
|
+
result += "</thead>"
|
|
451
|
+
result += "<tbody>"
|
|
452
|
+
result += generate_tree(Config.path_a, Config.path_b, "", None, 0)
|
|
453
|
+
result += "</tbody>"
|
|
454
|
+
result += "</table>"
|
|
455
|
+
|
|
456
|
+
result += """
|
|
457
|
+
<script>
|
|
458
|
+
document.addEventListener("click", e => {
|
|
459
|
+
if (!e.target.classList.contains("toggle")) return;
|
|
460
|
+
|
|
461
|
+
const row = e.target.closest("tr");
|
|
462
|
+
const id = row.dataset.entryId;
|
|
463
|
+
const expanded = e.target.textContent === "▼";
|
|
464
|
+
|
|
465
|
+
e.target.textContent = expanded ? "▶" : "▼";
|
|
466
|
+
|
|
467
|
+
toggleChildren(id, !expanded);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
function toggleChildren(parentId, show) {
|
|
471
|
+
document.querySelectorAll(`tr[data-parent-id="${parentId}"]`)
|
|
472
|
+
.forEach(child => {
|
|
473
|
+
child.hidden = !show;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function toggleAll(show) {
|
|
478
|
+
document.querySelectorAll("tr")
|
|
479
|
+
.forEach(row => {
|
|
480
|
+
const isRoot = !row.dataset.parentId;
|
|
481
|
+
row.hidden = !isRoot && !show;
|
|
482
|
+
const toggle = row.querySelector(".toggle");
|
|
483
|
+
if (toggle) {
|
|
484
|
+
toggle.textContent = show ? "▼" : "▶";
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
</script>
|
|
489
|
+
</body>
|
|
490
|
+
</html>
|
|
491
|
+
"""
|
|
492
|
+
|
|
275
493
|
return result
|
|
276
494
|
|
|
277
495
|
|
|
@@ -313,7 +531,7 @@ iframe_b.contentWindow.addEventListener('scroll', function(event) {{
|
|
|
313
531
|
}});
|
|
314
532
|
</script>
|
|
315
533
|
</body>
|
|
316
|
-
</html>
|
|
534
|
+
</html>
|
|
317
535
|
"""
|
|
318
536
|
|
|
319
537
|
|
|
@@ -350,13 +568,13 @@ def file(variant: str, path: str):
|
|
|
350
568
|
|
|
351
569
|
def setup_logging(verbosity: int):
|
|
352
570
|
if verbosity >= 3:
|
|
353
|
-
level = logging.VERBOSE
|
|
354
|
-
elif verbosity == 2:
|
|
355
571
|
level = logging.DEBUG
|
|
356
|
-
elif verbosity ==
|
|
572
|
+
elif verbosity == 2:
|
|
357
573
|
level = logging.INFO
|
|
358
|
-
|
|
574
|
+
elif verbosity == 1:
|
|
359
575
|
level = logging.WARNING
|
|
576
|
+
else:
|
|
577
|
+
level = logging.ERROR
|
|
360
578
|
|
|
361
579
|
formatter = logging.Formatter(
|
|
362
580
|
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|