htmlcmp 1.0.10__tar.gz → 1.0.12__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.10 → htmlcmp-1.0.12}/PKG-INFO +1 -1
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/pyproject.toml +4 -1
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp/compare_output.py +48 -7
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp/compare_output_server.py +56 -16
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp/html_render_diff.py +38 -7
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp/tidy_output.py +44 -5
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp.egg-info/PKG-INFO +1 -1
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp.egg-info/SOURCES.txt +2 -1
- htmlcmp-1.0.12/tests/test_html_render_diff.py +53 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/README.md +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/setup.cfg +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp/__init__.py +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp/common.py +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp.egg-info/dependency_links.txt +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp.egg-info/entry_points.txt +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp.egg-info/requires.txt +0 -0
- {htmlcmp-1.0.10 → htmlcmp-1.0.12}/src/htmlcmp.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "htmlcmp"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.12"
|
|
4
4
|
description = "Compare HTML files by rendered output"
|
|
5
5
|
classifiers = []
|
|
6
6
|
authors = [
|
|
@@ -32,6 +32,9 @@ download = "https://pypi.org/project/pyodr/#files"
|
|
|
32
32
|
tracker = "https://github.com/opendocument-app/compare-html/issues"
|
|
33
33
|
"release notes" = "https://github.com/opendocument-app/compare-html/releases"
|
|
34
34
|
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
pythonpath = ["src"]
|
|
37
|
+
|
|
35
38
|
[build-system]
|
|
36
39
|
requires = [
|
|
37
40
|
"setuptools",
|
|
@@ -17,18 +17,33 @@ class Config:
|
|
|
17
17
|
thread_local = threading.local()
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def parse_json(path):
|
|
20
|
+
def parse_json(path: Path) -> dict:
|
|
21
|
+
if not isinstance(path, Path):
|
|
22
|
+
raise TypeError(f"Expected Path, got {type(path)}")
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
25
|
+
|
|
21
26
|
with open(path) as f:
|
|
22
27
|
return json.load(f)
|
|
23
28
|
|
|
24
29
|
|
|
25
|
-
def compare_json(a, b):
|
|
30
|
+
def compare_json(a: Path, b: Path) -> bool:
|
|
31
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
32
|
+
raise TypeError("Both arguments must be of type Path")
|
|
33
|
+
if not a.is_file() or not b.is_file():
|
|
34
|
+
raise FileNotFoundError("Both arguments must be files")
|
|
35
|
+
|
|
26
36
|
json_a = json.dumps(parse_json(a), sort_keys=True)
|
|
27
37
|
json_b = json.dumps(parse_json(b), sort_keys=True)
|
|
28
38
|
return json_a == json_b
|
|
29
39
|
|
|
30
40
|
|
|
31
|
-
def compare_html(a, b, browser=None, diff_output=None):
|
|
41
|
+
def compare_html(a: Path, b: Path, browser=None, diff_output: Path = None) -> bool:
|
|
42
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
43
|
+
raise TypeError("Both arguments must be of type Path")
|
|
44
|
+
if not a.is_file() or not b.is_file():
|
|
45
|
+
raise FileNotFoundError("Both arguments must be files")
|
|
46
|
+
|
|
32
47
|
if browser is None:
|
|
33
48
|
browser = get_browser()
|
|
34
49
|
diff, (image_a, image_b) = html_render_diff(a, b, browser=browser)
|
|
@@ -41,7 +56,12 @@ def compare_html(a, b, browser=None, diff_output=None):
|
|
|
41
56
|
return result
|
|
42
57
|
|
|
43
58
|
|
|
44
|
-
def compare_files(a, b, **kwargs):
|
|
59
|
+
def compare_files(a: Path, b: Path, **kwargs) -> bool:
|
|
60
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
61
|
+
raise TypeError("Both arguments must be of type Path")
|
|
62
|
+
if not a.is_file() or not b.is_file():
|
|
63
|
+
raise FileNotFoundError("Both arguments must be files")
|
|
64
|
+
|
|
45
65
|
if filecmp.cmp(a, b):
|
|
46
66
|
return True
|
|
47
67
|
if a.suffix == ".json":
|
|
@@ -50,7 +70,12 @@ def compare_files(a, b, **kwargs):
|
|
|
50
70
|
return compare_html(a, b, **kwargs)
|
|
51
71
|
|
|
52
72
|
|
|
53
|
-
def comparable_file(path):
|
|
73
|
+
def comparable_file(path: Path) -> bool:
|
|
74
|
+
if not isinstance(path, Path):
|
|
75
|
+
raise TypeError(f"Expected Path, got {type(path)}")
|
|
76
|
+
if not path.is_file():
|
|
77
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
78
|
+
|
|
54
79
|
if path.suffix == ".json":
|
|
55
80
|
return True
|
|
56
81
|
if path.suffix == ".html":
|
|
@@ -58,7 +83,14 @@ def comparable_file(path):
|
|
|
58
83
|
return False
|
|
59
84
|
|
|
60
85
|
|
|
61
|
-
def submit_compare_dirs(
|
|
86
|
+
def submit_compare_dirs(
|
|
87
|
+
a: Path, b: Path, executor, diff_output: Path = None, **kwargs
|
|
88
|
+
) -> dict[str, list[Path]]:
|
|
89
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
90
|
+
raise TypeError("Both arguments must be of type Path")
|
|
91
|
+
if not a.is_dir() or not b.is_dir():
|
|
92
|
+
raise FileNotFoundError("Both arguments must be directories")
|
|
93
|
+
|
|
62
94
|
results = {
|
|
63
95
|
"common_dirs": [],
|
|
64
96
|
"common_files": [],
|
|
@@ -123,7 +155,16 @@ def submit_compare_dirs(a, b, executor, diff_output=None, **kwargs):
|
|
|
123
155
|
return results
|
|
124
156
|
|
|
125
157
|
|
|
126
|
-
def print_results(
|
|
158
|
+
def print_results(
|
|
159
|
+
results: dict[str, list[Path]], a: Path, b: Path, level: int = 0, prefix: str = ""
|
|
160
|
+
) -> dict[str, list[Path]]:
|
|
161
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
162
|
+
raise TypeError("Both arguments must be of type Path")
|
|
163
|
+
if not a.is_dir() or not b.is_dir():
|
|
164
|
+
raise FileNotFoundError("Both arguments must be directories")
|
|
165
|
+
if not isinstance(results, dict):
|
|
166
|
+
raise TypeError("Results must be a dictionary")
|
|
167
|
+
|
|
127
168
|
prefix_file = prefix + "├── "
|
|
128
169
|
if level == 0:
|
|
129
170
|
print(f"compare dir {a} with {b}")
|
|
@@ -8,18 +8,18 @@ import io
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from concurrent.futures import ThreadPoolExecutor
|
|
10
10
|
|
|
11
|
-
from compare_output import comparable_file, compare_files
|
|
12
11
|
from flask import Flask, send_from_directory, send_file
|
|
13
12
|
import watchdog.observers
|
|
14
13
|
import watchdog.events
|
|
15
14
|
|
|
15
|
+
from compare_output import comparable_file, compare_files
|
|
16
16
|
from html_render_diff import get_browser, html_render_diff
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class Config:
|
|
20
|
-
path_a = None
|
|
21
|
-
path_b = None
|
|
22
|
-
driver = None
|
|
20
|
+
path_a: Path = None
|
|
21
|
+
path_b: Path = None
|
|
22
|
+
driver: str = None
|
|
23
23
|
observer = None
|
|
24
24
|
comparator = None
|
|
25
25
|
browser = None
|
|
@@ -46,7 +46,12 @@ class Observer:
|
|
|
46
46
|
def start(self):
|
|
47
47
|
self._observer.start()
|
|
48
48
|
|
|
49
|
-
def init_compare(a, b):
|
|
49
|
+
def init_compare(a: Path, b: Path):
|
|
50
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
51
|
+
raise TypeError("Paths must be of type Path")
|
|
52
|
+
if not a.is_dir() or not b.is_dir():
|
|
53
|
+
raise ValueError("Both paths must be directories")
|
|
54
|
+
|
|
50
55
|
common_path = a / Config.path_a
|
|
51
56
|
|
|
52
57
|
left = sorted(p.name for p in a.iterdir())
|
|
@@ -70,7 +75,7 @@ class Observer:
|
|
|
70
75
|
|
|
71
76
|
|
|
72
77
|
class Comparator:
|
|
73
|
-
def __init__(self, max_workers):
|
|
78
|
+
def __init__(self, max_workers: int):
|
|
74
79
|
def initializer():
|
|
75
80
|
browser = getattr(Config.thread_local, "browser", None)
|
|
76
81
|
if browser is None:
|
|
@@ -83,7 +88,10 @@ class Comparator:
|
|
|
83
88
|
self._result = {}
|
|
84
89
|
self._future = {}
|
|
85
90
|
|
|
86
|
-
def submit(self, path):
|
|
91
|
+
def submit(self, path: Path):
|
|
92
|
+
if not isinstance(path, Path):
|
|
93
|
+
raise TypeError("Path must be of type Path")
|
|
94
|
+
|
|
87
95
|
if path in self._future:
|
|
88
96
|
try:
|
|
89
97
|
self._future[path].cancel()
|
|
@@ -95,7 +103,14 @@ class Comparator:
|
|
|
95
103
|
self._result[path] = "pending"
|
|
96
104
|
self._future[path] = self._executor.submit(self.compare, path)
|
|
97
105
|
|
|
98
|
-
def compare(self, path):
|
|
106
|
+
def compare(self, path: Path):
|
|
107
|
+
if not isinstance(path, Path):
|
|
108
|
+
raise TypeError("Path must be of type Path")
|
|
109
|
+
if not path.is_file():
|
|
110
|
+
raise ValueError("Path must be a file")
|
|
111
|
+
if path not in self._future:
|
|
112
|
+
raise RuntimeError("Path not submitted for comparison")
|
|
113
|
+
|
|
99
114
|
browser = getattr(Config.thread_local, "browser", None)
|
|
100
115
|
result = compare_files(
|
|
101
116
|
Config.path_a / path,
|
|
@@ -105,12 +120,18 @@ class Comparator:
|
|
|
105
120
|
self._result[path] = "same" if result else "different"
|
|
106
121
|
self._future.pop(path)
|
|
107
122
|
|
|
108
|
-
def result(self, path):
|
|
123
|
+
def result(self, path: Path):
|
|
124
|
+
if not isinstance(path, Path):
|
|
125
|
+
raise TypeError("Path must be of type Path")
|
|
126
|
+
|
|
109
127
|
if path in self._result:
|
|
110
128
|
return self._result[path]
|
|
111
129
|
return "unknown"
|
|
112
130
|
|
|
113
|
-
def result_symbol(self, path):
|
|
131
|
+
def result_symbol(self, path: Path):
|
|
132
|
+
if not isinstance(path, Path):
|
|
133
|
+
raise TypeError("Path must be of type Path")
|
|
134
|
+
|
|
114
135
|
result = self.result(path)
|
|
115
136
|
if result == "pending":
|
|
116
137
|
return "🔄"
|
|
@@ -120,7 +141,10 @@ class Comparator:
|
|
|
120
141
|
return "❌"
|
|
121
142
|
return "⛔"
|
|
122
143
|
|
|
123
|
-
def result_css(self, path):
|
|
144
|
+
def result_css(self, path: Path):
|
|
145
|
+
if not isinstance(path, Path):
|
|
146
|
+
raise TypeError("Path must be of type Path")
|
|
147
|
+
|
|
124
148
|
result = self.result(path)
|
|
125
149
|
if result == "pending":
|
|
126
150
|
return "color:blue;"
|
|
@@ -136,7 +160,12 @@ app = Flask("compare")
|
|
|
136
160
|
|
|
137
161
|
@app.route("/")
|
|
138
162
|
def root():
|
|
139
|
-
def print_tree(a, b):
|
|
163
|
+
def print_tree(a: Path, b: Path):
|
|
164
|
+
if not isinstance(a, Path) or not isinstance(b, Path):
|
|
165
|
+
raise TypeError("Paths must be of type Path")
|
|
166
|
+
if not a.is_dir() or not b.is_dir():
|
|
167
|
+
raise ValueError("Both paths must be directories")
|
|
168
|
+
|
|
140
169
|
common_path = a / Config.path_a
|
|
141
170
|
|
|
142
171
|
left = sorted(p.name for p in a.iterdir())
|
|
@@ -209,7 +238,10 @@ def root():
|
|
|
209
238
|
|
|
210
239
|
|
|
211
240
|
@app.route("/compare/<path:path>")
|
|
212
|
-
def compare(path):
|
|
241
|
+
def compare(path: str):
|
|
242
|
+
if not isinstance(path, str):
|
|
243
|
+
raise TypeError("Path must be a string")
|
|
244
|
+
|
|
213
245
|
return f"""<!DOCTYPE html>
|
|
214
246
|
<html>
|
|
215
247
|
<head>
|
|
@@ -219,7 +251,7 @@ html,body {{height:100%;margin:0;}}
|
|
|
219
251
|
</head>
|
|
220
252
|
<body style="display:flex;flex-flow:row;">
|
|
221
253
|
<div style="display:flex;flex:1;flex-flow:column;margin:5px;">
|
|
222
|
-
<a href="/file/a/{path}">{Config.path_a /path}</a>
|
|
254
|
+
<a href="/file/a/{path}">{Config.path_a / path}</a>
|
|
223
255
|
<iframe id="a" src="/file/a/{path}" title="a" frameborder="0" align="left" style="flex:1;"></iframe>
|
|
224
256
|
</div>
|
|
225
257
|
<div style="display:flex;flex:0 0 50px;flex-flow:column;">
|
|
@@ -246,7 +278,10 @@ iframe_b.contentWindow.addEventListener('scroll', function(event) {{
|
|
|
246
278
|
|
|
247
279
|
|
|
248
280
|
@app.route("/image_diff/<path:path>")
|
|
249
|
-
def image_diff(path):
|
|
281
|
+
def image_diff(path: str):
|
|
282
|
+
if not isinstance(path, str):
|
|
283
|
+
raise TypeError("Path must be a string")
|
|
284
|
+
|
|
250
285
|
diff, _ = html_render_diff(
|
|
251
286
|
Config.path_a / path,
|
|
252
287
|
Config.path_b / path,
|
|
@@ -259,7 +294,12 @@ def image_diff(path):
|
|
|
259
294
|
|
|
260
295
|
|
|
261
296
|
@app.route("/file/<variant>/<path:path>")
|
|
262
|
-
def file(variant, path):
|
|
297
|
+
def file(variant: str, path: str):
|
|
298
|
+
if not isinstance(variant, str) or not isinstance(path, str):
|
|
299
|
+
raise TypeError("Variant and path must be strings")
|
|
300
|
+
if variant not in ["a", "b"]:
|
|
301
|
+
raise ValueError("Variant must be 'a' or 'b'")
|
|
302
|
+
|
|
263
303
|
variant_root = Config.path_a if variant == "a" else Config.path_b
|
|
264
304
|
return send_from_directory(variant_root, path)
|
|
265
305
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
|
+
import shutil
|
|
4
5
|
import sys
|
|
5
6
|
import argparse
|
|
6
7
|
import io
|
|
@@ -14,13 +15,23 @@ from selenium.webdriver.support import expected_conditions
|
|
|
14
15
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def to_url(path):
|
|
18
|
-
if path
|
|
19
|
-
|
|
18
|
+
def to_url(path: str | Path) -> str:
|
|
19
|
+
if not isinstance(path, (str, Path)):
|
|
20
|
+
raise TypeError(f"Expected str or Path, got {type(path)}")
|
|
21
|
+
|
|
22
|
+
if isinstance(path, Path):
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
25
|
+
return path.resolve().as_uri()
|
|
20
26
|
return path
|
|
21
27
|
|
|
22
28
|
|
|
23
|
-
def screenshot(browser, url):
|
|
29
|
+
def screenshot(browser: webdriver.Remote, url: str) -> Image.Image:
|
|
30
|
+
if not isinstance(url, str):
|
|
31
|
+
raise TypeError(f"Expected str, got {type(url)}")
|
|
32
|
+
if not isinstance(browser, webdriver.Remote):
|
|
33
|
+
raise TypeError(f"Expected webdriver.Remote, got {type(browser)}")
|
|
34
|
+
|
|
24
35
|
browser.get(url)
|
|
25
36
|
|
|
26
37
|
target_find_by = By.TAG_NAME
|
|
@@ -49,22 +60,42 @@ def screenshot(browser, url):
|
|
|
49
60
|
return Image.open(io.BytesIO(png))
|
|
50
61
|
|
|
51
62
|
|
|
52
|
-
def get_browser(
|
|
63
|
+
def get_browser(
|
|
64
|
+
driver: str, max_width: int = 1000, max_height: int = 10000
|
|
65
|
+
) -> webdriver.Remote:
|
|
66
|
+
if not isinstance(driver, str):
|
|
67
|
+
raise TypeError(f"Expected str, got {type(driver)}")
|
|
68
|
+
if not isinstance(max_width, int) or not isinstance(max_height, int):
|
|
69
|
+
raise TypeError(
|
|
70
|
+
f"Expected int for max_width and max_height, got {type(max_width)} and {type(max_height)}"
|
|
71
|
+
)
|
|
72
|
+
|
|
53
73
|
if driver == "phantomjs":
|
|
74
|
+
if shutil.which("phantomjs") is None:
|
|
75
|
+
raise EnvironmentError("PhantomJS is not installed or not found in PATH")
|
|
54
76
|
browser = webdriver.PhantomJS()
|
|
55
77
|
elif driver == "firefox":
|
|
56
78
|
options = webdriver.FirefoxOptions()
|
|
57
79
|
options.add_argument("--headless")
|
|
58
80
|
browser = webdriver.Firefox(options=options)
|
|
59
|
-
|
|
81
|
+
elif driver == "chrome":
|
|
60
82
|
options = webdriver.ChromeOptions()
|
|
61
83
|
options.add_argument("--headless=new")
|
|
62
84
|
browser = webdriver.Chrome(options=options)
|
|
85
|
+
else:
|
|
86
|
+
raise ValueError(f"Unsupported driver: {driver}")
|
|
63
87
|
browser.set_window_size(max_width, max_height)
|
|
64
88
|
return browser
|
|
65
89
|
|
|
66
90
|
|
|
67
|
-
def html_render_diff(
|
|
91
|
+
def html_render_diff(
|
|
92
|
+
a: str | Path, b: str | Path, browser: webdriver.Remote
|
|
93
|
+
) -> tuple[Image.Image, tuple[Image.Image, Image.Image]]:
|
|
94
|
+
if not isinstance(a, (str, Path)) or not isinstance(b, (str, Path)):
|
|
95
|
+
raise TypeError("Both a and b must be str or Path instances")
|
|
96
|
+
if not isinstance(browser, webdriver.Remote):
|
|
97
|
+
raise TypeError(f"Expected webdriver.Remote, got {type(browser)}")
|
|
98
|
+
|
|
68
99
|
image_a = screenshot(browser, to_url(a))
|
|
69
100
|
image_b = screenshot(browser, to_url(b))
|
|
70
101
|
|
|
@@ -10,7 +10,12 @@ from pathlib import Path
|
|
|
10
10
|
from htmlcmp.common import bcolors
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def tidy_json(path):
|
|
13
|
+
def tidy_json(path: Path) -> int:
|
|
14
|
+
if not isinstance(path, Path):
|
|
15
|
+
raise TypeError("path must be a Path object")
|
|
16
|
+
if not path.is_file():
|
|
17
|
+
raise FileNotFoundError(f"{path} is not a file")
|
|
18
|
+
|
|
14
19
|
try:
|
|
15
20
|
with open(path, "r") as f:
|
|
16
21
|
json.load(f)
|
|
@@ -19,7 +24,16 @@ def tidy_json(path):
|
|
|
19
24
|
return 1
|
|
20
25
|
|
|
21
26
|
|
|
22
|
-
def tidy_html(path, html_tidy_config=None):
|
|
27
|
+
def tidy_html(path: Path, html_tidy_config: Path = None) -> int:
|
|
28
|
+
if not isinstance(path, Path):
|
|
29
|
+
raise TypeError("path must be a Path object")
|
|
30
|
+
if not path.is_file():
|
|
31
|
+
raise FileNotFoundError(f"{path} is not a file")
|
|
32
|
+
if html_tidy_config is not None and not isinstance(html_tidy_config, Path):
|
|
33
|
+
raise TypeError("html_tidy_config must be a Path object or None")
|
|
34
|
+
if html_tidy_config is not None and not html_tidy_config.is_file():
|
|
35
|
+
raise FileNotFoundError(f"{html_tidy_config} is not a file")
|
|
36
|
+
|
|
23
37
|
cmd = ["tidy"]
|
|
24
38
|
if html_tidy_config:
|
|
25
39
|
cmd.extend(["-config", str(html_tidy_config.resolve())])
|
|
@@ -34,14 +48,24 @@ def tidy_html(path, html_tidy_config=None):
|
|
|
34
48
|
return 0
|
|
35
49
|
|
|
36
50
|
|
|
37
|
-
def tidy_file(path, html_tidy_config=None):
|
|
51
|
+
def tidy_file(path: Path, html_tidy_config: Path = None) -> int:
|
|
52
|
+
if not isinstance(path, Path):
|
|
53
|
+
raise TypeError("path must be a Path object")
|
|
54
|
+
if not path.is_file():
|
|
55
|
+
raise FileNotFoundError(f"{path} is not a file")
|
|
56
|
+
|
|
38
57
|
if path.suffix == ".json":
|
|
39
58
|
return tidy_json(path)
|
|
40
59
|
elif path.suffix == ".html":
|
|
41
60
|
return tidy_html(path, html_tidy_config=html_tidy_config)
|
|
42
61
|
|
|
43
62
|
|
|
44
|
-
def tidyable_file(path):
|
|
63
|
+
def tidyable_file(path: Path) -> bool:
|
|
64
|
+
if not isinstance(path, Path):
|
|
65
|
+
raise TypeError("path must be a Path object")
|
|
66
|
+
if not path.is_file():
|
|
67
|
+
raise FileNotFoundError(f"{path} is not a file")
|
|
68
|
+
|
|
45
69
|
if path.suffix == ".json":
|
|
46
70
|
return True
|
|
47
71
|
if path.suffix == ".html":
|
|
@@ -49,7 +73,22 @@ def tidyable_file(path):
|
|
|
49
73
|
return False
|
|
50
74
|
|
|
51
75
|
|
|
52
|
-
def tidy_dir(
|
|
76
|
+
def tidy_dir(
|
|
77
|
+
path: Path, level: int = 0, prefix: str = "", html_tidy_config: Path = None
|
|
78
|
+
) -> dict[str, list[Path]]:
|
|
79
|
+
if not isinstance(path, Path):
|
|
80
|
+
raise TypeError("path must be a Path object")
|
|
81
|
+
if not path.is_dir():
|
|
82
|
+
raise NotADirectoryError(f"{path} is not a directory")
|
|
83
|
+
if not isinstance(level, int) or level < 0:
|
|
84
|
+
raise ValueError("level must be a non-negative integer")
|
|
85
|
+
if not isinstance(prefix, str):
|
|
86
|
+
raise TypeError("prefix must be a string")
|
|
87
|
+
if html_tidy_config is not None and not isinstance(html_tidy_config, Path):
|
|
88
|
+
raise TypeError("html_tidy_config must be a Path object or None")
|
|
89
|
+
if html_tidy_config is not None and not html_tidy_config.is_file():
|
|
90
|
+
raise FileNotFoundError(f"{html_tidy_config} is not a file")
|
|
91
|
+
|
|
53
92
|
prefix_file = prefix + "├── "
|
|
54
93
|
if level == 0:
|
|
55
94
|
print(f"tidy dir {path}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
from htmlcmp.html_render_diff import to_url, get_browser, html_render_diff
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_to_url():
|
|
9
|
+
with pytest.raises(TypeError):
|
|
10
|
+
to_url(None)
|
|
11
|
+
with pytest.raises(TypeError):
|
|
12
|
+
to_url(1)
|
|
13
|
+
with pytest.raises(FileNotFoundError):
|
|
14
|
+
to_url(Path(__file__).parent)
|
|
15
|
+
assert to_url("file:///path/to/file") == "file:///path/to/file"
|
|
16
|
+
assert to_url("https://example.com") == "https://example.com"
|
|
17
|
+
assert to_url(Path(__file__))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_browser():
|
|
21
|
+
with pytest.raises(ValueError):
|
|
22
|
+
get_browser("unsupported_driver")
|
|
23
|
+
|
|
24
|
+
# Test with Chrome
|
|
25
|
+
browser = get_browser("chrome")
|
|
26
|
+
assert browser.name == "chrome"
|
|
27
|
+
browser.quit()
|
|
28
|
+
|
|
29
|
+
# Test with Firefox
|
|
30
|
+
browser = get_browser("firefox")
|
|
31
|
+
assert browser.name == "firefox"
|
|
32
|
+
browser.quit()
|
|
33
|
+
|
|
34
|
+
# Test with PhantomJS
|
|
35
|
+
if shutil.which("phantomjs") is not None:
|
|
36
|
+
browser = get_browser("phantomjs")
|
|
37
|
+
assert browser.name == "phantomjs"
|
|
38
|
+
browser.quit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_html_render_diff():
|
|
42
|
+
test1 = Path(__file__).parent / "test1.html"
|
|
43
|
+
test2 = Path(__file__).parent / "test2.html"
|
|
44
|
+
|
|
45
|
+
with pytest.raises(TypeError):
|
|
46
|
+
html_render_diff(None, None, None)
|
|
47
|
+
with pytest.raises(TypeError):
|
|
48
|
+
html_render_diff(str(test1), str(test2), None)
|
|
49
|
+
|
|
50
|
+
diff, _ = html_render_diff(test1, test1, get_browser("firefox"))
|
|
51
|
+
assert diff.getbbox() is None
|
|
52
|
+
diff, _ = html_render_diff(test1, test2, get_browser("firefox"))
|
|
53
|
+
assert diff.getbbox() is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|