htmlcmp 1.0.11__tar.gz → 1.0.13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlcmp
3
- Version: 1.0.11
3
+ Version: 1.0.13
4
4
  Summary: Compare HTML files by rendered output
5
5
  Author: Andreas Stefl
6
6
  Maintainer-email: Andreas Stefl <stefl.andreas@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "htmlcmp"
3
- version = "1.0.11"
3
+ version = "1.0.13"
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(a, b, executor, diff_output=None, **kwargs):
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(results, a, b, level=0, prefix=""):
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
 
16
- from html_render_diff import get_browser, html_render_diff
15
+ from htmlcmp.compare_output import comparable_file, compare_files
16
+ from htmlcmp.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.is_file():
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}")
19
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(driver="firefox", max_width=1000, max_height=10000):
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
- else: # chrome or unknown
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(a, b, browser):
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(path, level=0, prefix="", html_tidy_config=None):
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}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlcmp
3
- Version: 1.0.11
3
+ Version: 1.0.13
4
4
  Summary: Compare HTML files by rendered output
5
5
  Author: Andreas Stefl
6
6
  Maintainer-email: Andreas Stefl <stefl.andreas@gmail.com>
@@ -11,4 +11,5 @@ src/htmlcmp.egg-info/SOURCES.txt
11
11
  src/htmlcmp.egg-info/dependency_links.txt
12
12
  src/htmlcmp.egg-info/entry_points.txt
13
13
  src/htmlcmp.egg-info/requires.txt
14
- src/htmlcmp.egg-info/top_level.txt
14
+ src/htmlcmp.egg-info/top_level.txt
15
+ tests/test_html_render_diff.py
@@ -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