Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: python -m pip install -U pip 'setuptools>=45' pytest
run: python -m pip install -U pip 'setuptools>=45' pytest pyright

- name: Run tests
run: |
python setup.py build_ext --inplace
python -m unittest discover -vv tests
./bench.sh

12 changes: 0 additions & 12 deletions Pipfile

This file was deleted.

84 changes: 0 additions & 84 deletions Pipfile.lock

This file was deleted.

22 changes: 0 additions & 22 deletions mypy.ini

This file was deleted.

18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@ build-frontend = "build"
[[tool.cibuildwheel.overrides]]
select = "*-ios*"
build-frontend = "build"

[tool.mypy]
warn_unused_configs = true
disallow_any_generics = true
disallow_subclassing_any = true
disallow_untyped_calls = true
# disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
implicit_reexport = false
strict_equality = true
pretty = true
error_summary = false
81 changes: 81 additions & 0 deletions tests/test_stubs_pyright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Validate the .pyi type stubs using pyright."""

import subprocess
import tempfile
import unittest
from pathlib import Path


class TestStubsPyright(unittest.TestCase):
def _run_pyright(self, source: str) -> subprocess.CompletedProcess:
"""Run pyright on a temporary file with the given source.

The file is placed under the repo root so pyright can discover the
xxhash package and its .pyi stubs via pyproject.toml.
"""
repo_root = Path(__file__).resolve().parent.parent
with tempfile.NamedTemporaryFile(
mode="w",
suffix=".py",
prefix="__pyright_check_",
dir=repo_root,
delete=False,
) as f:
f.write(source)
tmp_path = f.name

try:
return subprocess.run(
["pyright", "--project", str(repo_root), tmp_path],
capture_output=True,
text=True,
cwd=repo_root,
timeout=30,
)
Comment on lines +27 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test will fail with a FileNotFoundError if the pyright executable is not installed in the environment. It is better to catch this exception and skip the test gracefully, especially for local development environments where pyright might not be globally available.

Suggested change
try:
return subprocess.run(
["pyright", "--project", str(repo_root), tmp_path],
capture_output=True,
text=True,
cwd=repo_root,
timeout=30,
)
try:
return subprocess.run(
["pyright", "--project", str(repo_root), tmp_path],
capture_output=True,
text=True,
cwd=repo_root,
timeout=30,
)
except FileNotFoundError:
self.skipTest("pyright executable not found")

finally:
Path(tmp_path).unlink(missing_ok=True)

def test_valid_buffer_types(self):
"""Valid buffer types should type-check without errors."""
code = """\
import xxhash

h1 = xxhash.xxh32(b"hello")
h1.update(b"world")
xxhash.xxh32_digest(b"hello")

h2 = xxhash.xxh32(bytearray(b"hello"))
h2.update(bytearray(b"world"))

h3 = xxhash.xxh32(memoryview(b"hello"))
h3.update(memoryview(b"world"))

h4 = xxhash.xxh32()
h4.update(b"test")
"""
result = self._run_pyright(code)
if result.returncode != 0:
self.fail(
f"pyright reported errors for valid buffer types:\n"
f"{result.stdout}\n{result.stderr}"
)

def test_str_is_rejected(self):
"""str should be rejected (not a buffer type)."""
code = """\
import xxhash
xxhash.xxh32("hello")
"""
result = self._run_pyright(code)
if result.returncode == 0:
self.fail("pyright did not reject str argument")

def test_int_is_rejected(self):
"""int should be rejected (not a buffer type)."""
code = """\
import xxhash
xxhash.xxh32(42)
"""
result = self._run_pyright(code)
if result.returncode == 0:
self.fail("pyright did not reject int argument")
3 changes: 0 additions & 3 deletions typecheck.sh

This file was deleted.

63 changes: 46 additions & 17 deletions xxhash/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
import array
from typing import Union
from typing_extensions import final, Buffer
from typing import Protocol, final

# __buffer__ protocol makes this redundant on python 3.12+
Buffer.register(array.ArrayType)
_InputType = Union[str, Buffer]
class _Buffer(Protocol):
"""Objects that support the buffer protocol (PEP 688)."""
def __buffer__(self, flags: int, /) -> memoryview: ...

_DataType = _Buffer
Comment on lines +1 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a custom Protocol to define the buffer protocol is problematic for compatibility with Python versions prior to 3.12. In those versions, built-in types like bytes, bytearray, and memoryview do not explicitly define the __buffer__ method in their stubs, which will cause type checkers like mypy to report errors when these types are passed where _Buffer is expected.

It is recommended to use typing_extensions.Buffer (which backports collections.abc.Buffer from PEP 688). Type checkers have special-case handling for this type to ensure it correctly matches all buffer-providing types across all supported Python versions.

from typing import final
from typing_extensions import Buffer

_DataType = Buffer

Comment on lines +1 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The custom _Buffer protocol defined here is based on PEP 688, which was introduced in Python 3.12. On older Python versions (like 3.9, which this project appears to support), built-in types like bytes, bytearray, and memoryview do not have a Python-accessible __buffer__ method, so they will not match this protocol in type checkers like mypy or pyright unless special-cased.

Furthermore, array.array (which was explicitly supported in the previous version of the stubs) also lacks this method. Using typing_extensions.Buffer is the recommended way to represent the buffer protocol across all supported Python versions, as it is specifically recognized by type checkers to include all buffer-like types (including C-level implementations).

Suggested change
from typing import Protocol, final
# __buffer__ protocol makes this redundant on python 3.12+
Buffer.register(array.ArrayType)
_InputType = Union[str, Buffer]
class _Buffer(Protocol):
"""Objects that support the buffer protocol (PEP 688)."""
def __buffer__(self, flags: int, /) -> memoryview: ...
_DataType = _Buffer
import array
from typing import final
from typing_extensions import Buffer
# __buffer__ protocol makes this redundant on python 3.12+
Buffer.register(array.ArrayType)
_DataType = Buffer

Comment on lines +1 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a custom Protocol with __buffer__ to represent the buffer protocol is based on PEP 688, which is fully supported in Python 3.12+. For older Python versions, built-in types like bytes, bytearray, and array.array do not have a __buffer__ method in their type stubs within typeshed. This can lead to false-positive type errors in type checkers (like mypy or pyright) when they are configured to target Python versions prior to 3.12.

Since this project appears to support Python 3.9+, it is recommended to use typing_extensions.Buffer. This is the standard, backported way to represent the buffer protocol across all supported Python versions and is correctly recognized by modern type checkers for all built-in buffer types.

from typing import final
from typing_extensions import Buffer

_DataType = Buffer


VERSION: str
XXHASH_VERSION: str
#: Deprecated, will be removed in the next major release
VERSION_TUPLE: tuple[int, ...]

algorithms_available: set[str]
algorithms_guaranteed: set[str]

__all__: list[str] = [
"xxh32",
"xxh32_digest",
"xxh32_intdigest",
"xxh32_hexdigest",
"xxh64",
"xxh64_digest",
"xxh64_intdigest",
"xxh64_hexdigest",
"xxh3_64",
"xxh3_64_digest",
"xxh3_64_intdigest",
"xxh3_64_hexdigest",
"xxh3_128",
"xxh3_128_digest",
"xxh3_128_intdigest",
"xxh3_128_hexdigest",
"xxh128",
"xxh128_digest",
"xxh128_intdigest",
"xxh128_hexdigest",
"VERSION",
"VERSION_TUPLE",
"XXHASH_VERSION",
"algorithms_available",
"algorithms_guaranteed",
]

class _Hasher:
def __init__(self, input: _InputType = ..., seed: int = ...) -> None: ...
def update(self, input: _InputType) -> None: ...
def __init__(self, data: _DataType = ..., seed: int = ...) -> None: ...
def update(self, data: _DataType) -> None: ...
Comment on lines +46 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The xxhash C extension methods do not support keyword arguments. Adding the positional-only marker / ensures that type checkers will correctly flag invalid usage like xxh32(data=b'...'), which currently results in a TypeError at runtime.

Suggested change
def __init__(self, data: _DataType = ..., seed: int = ...) -> None: ...
def update(self, data: _DataType) -> None: ...
def __init__(self, data: _DataType = ..., seed: int = ..., /) -> None: ...
def update(self, data: _DataType, /) -> None: ...

def digest(self) -> bytes: ...
def hexdigest(self) -> str: ...
def intdigest(self) -> int: ...
Expand Down Expand Up @@ -44,17 +73,17 @@ class xxh3_128(_Hasher): ...
xxh64 = xxh3_64
xxh128 = xxh3_128

def xxh32_digest(args: _InputType, seed: int = ...) -> bytes: ...
def xxh32_hexdigest(args: _InputType, seed: int = ...) -> str: ...
def xxh32_intdigest(args: _InputType, seed: int = ...) -> int: ...
def xxh32_digest(data: _DataType, seed: int = ...) -> bytes: ...
def xxh32_hexdigest(data: _DataType, seed: int = ...) -> str: ...
def xxh32_intdigest(data: _DataType, seed: int = ...) -> int: ...
Comment on lines +76 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These module-level functions are positional-only at runtime. Adding the / marker improves the accuracy of the stubs and prevents users from attempting to use keyword arguments that are not supported by the underlying C implementation.

Suggested change
def xxh32_digest(data: _DataType, seed: int = ...) -> bytes: ...
def xxh32_hexdigest(data: _DataType, seed: int = ...) -> str: ...
def xxh32_intdigest(data: _DataType, seed: int = ...) -> int: ...
def xxh32_digest(data: _DataType, seed: int = ..., /) -> bytes: ...
def xxh32_hexdigest(data: _DataType, seed: int = ..., /) -> str: ...
def xxh32_intdigest(data: _DataType, seed: int = ..., /) -> int: ...


def xxh3_64_digest(args: _InputType, seed: int = ...) -> bytes: ...
def xxh3_64_hexdigest(args: _InputType, seed: int = ...) -> str: ...
def xxh3_64_intdigest(args: _InputType, seed: int = ...) -> int: ...
def xxh3_64_digest(data: _DataType, seed: int = ...) -> bytes: ...
def xxh3_64_hexdigest(data: _DataType, seed: int = ...) -> str: ...
def xxh3_64_intdigest(data: _DataType, seed: int = ...) -> int: ...
Comment on lines +80 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These functions are positional-only at runtime. Adding the / marker ensures type-level correctness.

Suggested change
def xxh3_64_digest(data: _DataType, seed: int = ...) -> bytes: ...
def xxh3_64_hexdigest(data: _DataType, seed: int = ...) -> str: ...
def xxh3_64_intdigest(data: _DataType, seed: int = ...) -> int: ...
def xxh3_64_digest(data: _DataType, seed: int = ..., /) -> bytes: ...
def xxh3_64_hexdigest(data: _DataType, seed: int = ..., /) -> str: ...
def xxh3_64_intdigest(data: _DataType, seed: int = ..., /) -> int: ...


def xxh3_128_digest(args: _InputType, seed: int = ...) -> bytes: ...
def xxh3_128_hexdigest(args: _InputType, seed: int = ...) -> str: ...
def xxh3_128_intdigest(args: _InputType, seed: int = ...) -> int: ...
def xxh3_128_digest(data: _DataType, seed: int = ...) -> bytes: ...
def xxh3_128_hexdigest(data: _DataType, seed: int = ...) -> str: ...
def xxh3_128_intdigest(data: _DataType, seed: int = ...) -> int: ...
Comment on lines +84 to +86
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These functions are positional-only at runtime. Adding the / marker ensures type-level correctness.

Suggested change
def xxh3_128_digest(data: _DataType, seed: int = ...) -> bytes: ...
def xxh3_128_hexdigest(data: _DataType, seed: int = ...) -> str: ...
def xxh3_128_intdigest(data: _DataType, seed: int = ...) -> int: ...
def xxh3_128_digest(data: _DataType, seed: int = ..., /) -> bytes: ...
def xxh3_128_hexdigest(data: _DataType, seed: int = ..., /) -> str: ...
def xxh3_128_intdigest(data: _DataType, seed: int = ..., /) -> int: ...


xxh64_digest = xxh3_64_digest
xxh64_hexdigest = xxh3_64_hexdigest
Expand Down
2 changes: 1 addition & 1 deletion xxhash/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION = "3.8.0.dev6"
VERSION = "3.8.0.dev7"
#: Deprecated, will be removed in the next major release
VERSION_TUPLE = (3, 8, 0)
Loading