Skip to content

Commit 5eb088b

Browse files
committed
rejig kernel api
1 parent f17f028 commit 5eb088b

File tree

2 files changed

+77
-42
lines changed

2 files changed

+77
-42
lines changed

pytest_mpl/kernels.py

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,25 @@
22
This module contains the supported hashing kernel implementations.
33
44
"""
5-
from PIL import Image
6-
from abc import ABC, abstractmethod
75
import hashlib
6+
from abc import ABC, abstractmethod
7+
88
import imagehash
9+
from PIL import Image
10+
11+
#: The default hamming distance bit tolerance for "similar" imagehash hashes.
12+
DEFAULT_HAMMING_TOLERANCE = 2
913

14+
#: The default imagehash hash size (N), resulting in a hash of N**2 bits.
15+
DEFAULT_HASH_SIZE = 16
1016

17+
#: Registered kernel names.
1118
KERNEL_SHA256 = "sha256"
1219
KERNEL_PHASH = "phash"
1320

1421
__all__ = [
22+
"DEFAULT_HAMMING_TOLERANCE",
23+
"DEFAULT_HASH_SIZE",
1524
"KERNEL_PHASH",
1625
"KERNEL_SHA256",
1726
"KernelPHash",
@@ -22,13 +31,37 @@
2231

2332
class Kernel(ABC):
2433
"""
25-
Kernel abstract base class (ABC) which defines a common kernel API.
34+
Kernel abstract base class (ABC) which defines a simple common kernel API.
2635
2736
"""
2837

2938
def __init__(self, plugin):
39+
# Containment (read-only) of the plugin allows the kernel to cherry-pick state that it requires
3040
self._plugin = plugin
3141

42+
@abstractmethod
43+
def equivalent_hash(self, actual, expected, marker=None):
44+
"""
45+
Determine whether the kernel considers the provided actual and
46+
expected hashes as similar.
47+
48+
Parameters
49+
----------
50+
actual : str
51+
The hash of the test image.
52+
expected : str
53+
The hash of the baseline image.
54+
marker : pytest.Mark
55+
The test marker, which may contain kwarg options to be
56+
applied to the equivalence test.
57+
58+
Returns
59+
-------
60+
bool
61+
Whether the actual and expected hashes are deemed similar.
62+
63+
"""
64+
3265
@abstractmethod
3366
def generate_hash(self, buffer):
3467
"""
@@ -47,28 +80,22 @@ def generate_hash(self, buffer):
4780
4881
"""
4982

50-
@abstractmethod
51-
def equivalent_hash(self, actual, expected, marker=None):
83+
def update_status(self, message):
5284
"""
53-
Determine whether the kernel considers the provided actual and
54-
expected hashes as similar.
85+
Append the kernel status message to the provided message.
5586
5687
Parameters
5788
----------
58-
actual : str
59-
The hash of the test image.
60-
expected : str
61-
The hash of the baseline image.
62-
marker : pytest.Mark
63-
The test marker, which may contain kwarg options to be
64-
applied to the equivalence test.
89+
message : str
90+
The existing status message.
6591
6692
Returns
6793
-------
68-
bool
69-
Whether the actual and expected hashes are deemed similar.
94+
str
95+
The updated status message.
7096
7197
"""
98+
return message
7299

73100
def update_summary(self, summary):
74101
"""
@@ -81,7 +108,7 @@ def update_summary(self, summary):
81108
Returns
82109
-------
83110
dict
84-
The image comparison summary.
111+
The updated image comparison summary.
85112
86113
"""
87114
return summary
@@ -104,23 +131,34 @@ def __init__(self, plugin):
104131
# py.test marker kwarg
105132
self.option = "hamming_tolerance"
106133
# value may be overridden by py.test marker kwarg
107-
self.hamming_tolerance = self._plugin.hamming_tolerance
134+
self.hamming_tolerance = self._plugin.hamming_tolerance or DEFAULT_HAMMING_TOLERANCE
108135
# keep state of hash hamming distance (whole number) result
109136
self.hamming_distance = None
110-
111-
def generate_hash(self, buffer):
112-
buffer.seek(0)
113-
data = Image.open(buffer)
114-
phash = imagehash.phash(data, hash_size=self.hash_size)
115-
return str(phash)
137+
# keep state of equivalence result
138+
self.equivalent = None
116139

117140
def equivalent_hash(self, actual, expected, marker=None):
118141
if marker:
119142
self.hamming_tolerance = int(marker.kwargs.get(self.option))
120143
actual = imagehash.hex_to_hash(actual)
121144
expected = imagehash.hex_to_hash(expected)
122145
self.hamming_distance = actual - expected
123-
return self.hamming_distance <= self.hamming_tolerance
146+
self.equivalent = self.hamming_distance <= self.hamming_tolerance
147+
return self.equivalent
148+
149+
def generate_hash(self, buffer):
150+
buffer.seek(0)
151+
data = Image.open(buffer)
152+
phash = imagehash.phash(data, hash_size=self.hash_size)
153+
return str(phash)
154+
155+
def update_status(self, message):
156+
result = str() if message is None else str(message)
157+
if self.equivalent is False:
158+
msg = (f"Hash hamming distance of {self.hamming_distance} bits > "
159+
f"hamming tolerance of {self.hamming_tolerance} bits.")
160+
result = f"{message} {msg}" if len(result) else msg
161+
return result
124162

125163
def update_summary(self, summary):
126164
summary["hamming_distance"] = self.hamming_distance
@@ -136,16 +174,16 @@ class KernelSHA256(Kernel):
136174

137175
name = KERNEL_SHA256
138176

177+
def equivalent_hash(self, actual, expected, marker=None):
178+
return actual == expected
179+
139180
def generate_hash(self, buffer):
140181
buffer.seek(0)
141182
data = buffer.read()
142183
hasher = hashlib.sha256()
143184
hasher.update(data)
144185
return hasher.hexdigest()
145186

146-
def equivalent_hash(self, actual, expected, marker=None):
147-
return actual == expected
148-
149187

150188
#: Registry of available hashing kernel factories.
151189
kernel_factory = {

pytest_mpl/plugin.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
import pytest
4545

46-
from .kernels import KERNEL_SHA256, kernel_factory
46+
from .kernels import DEFAULT_HAMMING_TOLERANCE, DEFAULT_HASH_SIZE, KERNEL_SHA256, kernel_factory
4747
from .summary.html import generate_summary_basic_html, generate_summary_html
4848

4949
SUPPORTED_FORMATS = {'html', 'json', 'basic-html'}
@@ -57,12 +57,6 @@
5757
#: The default matplotlib backend.
5858
DEFAULT_BACKEND = "agg"
5959

60-
#: The default hamming distance bit tolerance for "similar" imagehash hashes.
61-
DEFAULT_HAMMING_TOLERANCE = 2
62-
63-
#: The default imagehash hash size (N), resulting in a hash of N**2 bits.
64-
DEFAULT_HASH_SIZE = 16
65-
6660
#: The default algorithm to use for image hashing.
6761
DEFAULT_KERNEL = KERNEL_SHA256
6862

@@ -307,7 +301,7 @@ def __init__(self,
307301
DEFAULT_HAMMING_TOLERANCE)
308302
self.hamming_tolerance = hamming_tolerance
309303

310-
# Configure the hashing kernel.
304+
# Configure the hashing kernel - must be done *after* kernel options.
311305
option = "mpl-kernel"
312306
kernel = config.getoption(f"--{option}") or config.getini(option)
313307
if kernel:
@@ -318,7 +312,7 @@ def __init__(self,
318312
kernel = requested
319313
else:
320314
kernel = DEFAULT_KERNEL
321-
print(f"{kernel_factory=}")
315+
# Create the kernel.
322316
self.kernel = kernel_factory[kernel](self)
323317

324318
# Generate the containing dir for all test results
@@ -599,8 +593,10 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
599593
if baseline_hash is None: # hash-missing
600594
summary['status'] = 'failed'
601595
summary['hash_status'] = 'missing'
602-
summary['status_msg'] = (f"Hash for test '{hash_name}' not found in {hash_library_filename}. "
603-
f"Generated hash is {test_hash}.")
596+
msg = (f'Hash for test {hash_name!r} not found in '
597+
f'{hash_library_filename!r}. Generated hash is '
598+
f'{test_hash!r}.')
599+
summary['status_msg'] = msg
604600
elif self.kernel.equivalent_hash(test_hash, baseline_hash): # hash-match
605601
hash_comparison_pass = True
606602
summary['status'] = 'passed'
@@ -610,9 +606,10 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
610606
else: # hash-diff
611607
summary['status'] = 'failed'
612608
summary['hash_status'] = 'diff'
613-
summary['status_msg'] = (f"Hash {test_hash} doesn't match hash "
614-
f"{baseline_hash} in library "
615-
f"{hash_library_filename} for test {hash_name}.")
609+
msg = (f"Test hash {test_hash!r} doesn't match baseline hash "
610+
f'{baseline_hash!r} in library {str(hash_library_filename)!r} '
611+
f'for test {hash_name!r}.')
612+
summary['status_msg'] = self.kernel.update_status(msg)
616613
self.kernel.update_summary(summary)
617614

618615
# Save the figure for later summary (will be removed later if not needed)

0 commit comments

Comments
 (0)