Repository: RealOrangeOne/seahash-py (language: python)

Issue:
`SeaHash` is not compatible with `hashlib.file_digest`
The following snippet:
```python
    with open("file.txt", "rb") as f:
        s = hashlib.file_digest(f, lambda: seahash.SeaHash())
```
fails with the following error:
```
>           digestobj.update(view[:size])
E           TypeError: argument 'obj': 'memoryview' object cannot be converted to 'PyBytes'

C:\Python312\Lib\hashlib.py:236: TypeError
```


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 24da6d8..f96d71a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -199,6 +199,7 @@ name = "seahash-py"
 version = "0.2.0"
 dependencies = [
  "pyo3",
+ "pyo3-build-config",
  "seahash",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 1f27fd8..aed08c2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,3 +12,7 @@ crate-type = ["cdylib"]
 [dependencies]
 pyo3 = { version = "0.20.3", features = ["extension-module"] }
 seahash = "4.1.0"
+
+[build-dependencies]
+pyo3-build-config = { version = "0.20.3", features = ["resolve-config"] }
+
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..0475124
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+    pyo3_build_config::use_pyo3_cfgs();
+}
diff --git a/pyproject.toml b/pyproject.toml
index 94cdea8..db786ac 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["maturin>=1.4,<1.5"]
+requires = ["maturin>=1.5,<1.6"]
 build-backend = "maturin"
 
 [project]
@@ -32,5 +32,5 @@ Issues = "https://github.com/RealOrangeOne/seahash-py/issues"
 [project.optional-dependencies]
 dev = [
     "pytest==8.0.2",
-    "ruff==0.2.2"
+    "ruff==0.3.0"
 ]
diff --git a/seahash.pyi b/seahash.pyi
index 667206c..329fa50 100644
--- a/seahash.pyi
+++ b/seahash.pyi
@@ -5,8 +5,8 @@ class SeaHash:
     digest_size: int
     block_size: int
 
-    def __init__(self, string: bytes = b""): ...
-    def update(self, obj: bytes) -> None: ...
+    def __init__(self, data: bytes = b""): ...
+    def update(self, obj: bytes | memoryview) -> None: ...
     def digest(self) -> bytes: ...
     def intdigest(self) -> int: ...
     def hexdigest(self) -> str: ...
diff --git a/src/lib.rs b/src/lib.rs
index b50844e..3d89ab7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,10 @@
 use pyo3::prelude::*;
+
 mod inner {
+    use std::hash::Hasher;
+
+    #[cfg(Py_3_11)]
+    use pyo3::buffer::PyBuffer;
     use pyo3::prelude::*;
     use pyo3::types::PyBytes;
 
@@ -15,9 +20,10 @@ mod inner {
         py.allow_threads(|| seahash::hash_seeded(buf, a, b, c, d))
     }
 
+    #[derive(Copy, Clone)]
     #[pyclass]
     pub(crate) struct SeaHash {
-        inner: Vec<u8>,
+        inner: seahash::SeaHasher,
     }
 
     #[pymethods]
@@ -31,31 +37,44 @@ mod inner {
         const BLOCK_SIZE: u8 = 8;
 
         #[new]
-        #[pyo3(signature = (string = Vec::new()))]
-        fn new(string: Vec<u8>) -> Self {
-            SeaHash { inner: string }
+        #[pyo3(signature = (data = Vec::new()))]
+        fn new(data: Vec<u8>) -> Self {
+            let mut inner = seahash::SeaHasher::new();
+            inner.write(data.as_slice());
+            SeaHash { inner }
+        }
+
+        #[cfg(not(Py_3_11))]
+        pub fn update(&mut self, py: Python, obj: &[u8]) {
+            py.allow_threads(|| self.inner.write(obj))
         }
 
-        pub fn update(&mut self, obj: &[u8]) {
-            self.inner.extend_from_slice(obj)
+        #[cfg(Py_3_11)]
+        pub fn update(&mut self, py: Python, obj: &PyAny) -> PyResult<()> {
+            if let Ok(buf) = obj.extract() {
+                py.allow_threads(|| self.inner.write(buf));
+            } else {
+                let vec = PyBuffer::get(obj)?.to_vec(py)?;
+                py.allow_threads(|| self.inner.write(vec.as_slice()));
+            }
+            Ok(())
         }
 
         pub fn digest(&self, py: Python) -> PyObject {
-            PyBytes::new(py, &self.intdigest(py).to_ne_bytes()).into()
+            PyBytes::new(py, &self.intdigest().to_ne_bytes()).into()
         }
 
-        pub fn intdigest(&self, py: Python) -> u64 {
-            hash(py, &self.inner)
+        pub fn intdigest(&self) -> u64 {
+            self.inner.finish()
         }
 
-        pub fn hexdigest(&self, py: Python) -> String {
-            format!("{:x}", self.intdigest(py))
+        pub fn hexdigest(&self) -> String {
+            format!("{:x}", self.intdigest())
         }
 
         pub fn copy(&self) -> Self {
-            Self {
-                inner: self.inner.clone(),
-            }
+            // Self derives Copy
+            *self
         }
     }
 }


Test patch:
diff --git a/tests/test_seahash.py b/tests/test_seahash.py
index c714a4d..fbe5459 100644
--- a/tests/test_seahash.py
+++ b/tests/test_seahash.py
@@ -1,7 +1,9 @@
 from importlib.metadata import version
 
 import sys
+import hashlib
 
+import pytest
 import seahash
 
 
@@ -29,6 +31,21 @@ def test_hashlib_compatible():
     assert s.intdigest() == int.from_bytes(s.digest(), sys.byteorder)
 
 
+@pytest.mark.skipif(
+    sys.version_info < (3, 11),
+    reason="requires hashlib.file_digest which is new in python3.11",
+)
+def test_hashlib_file_digest_compatible(tmp_path):
+    p = tmp_path / "hello.txt"
+    content = b"Hash me!"
+    p.write_bytes(content)
+    with p.open("rb") as f:
+        s = hashlib.file_digest(f, lambda: seahash.SeaHash())
+    assert s.intdigest() == seahash.hash(content)
+    assert s.digest() == b"a\xe8\x98S^\xb0\x8e="
+    assert s.hexdigest() == "3d8eb05e5398e861"
+
+
 def test_initial_state():
     assert seahash.SeaHash(b"123").intdigest() == seahash.hash(b"123")