-
Notifications
You must be signed in to change notification settings - Fork 39
feat: add .pyi stub validation with stubtest and pyright #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
| 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, | ||
| ) | ||
| 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") | ||
This file was deleted.
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a custom It is recommended to use
Comment on lines
+1
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The custom Furthermore,
Suggested change
Comment on lines
+1
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a custom Since this project appears to support Python 3.9+, it is recommended to use |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| def digest(self) -> bytes: ... | ||||||||||||||||||||||||||||||||||||||
| def hexdigest(self) -> str: ... | ||||||||||||||||||||||||||||||||||||||
| def intdigest(self) -> int: ... | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These module-level functions are positional-only at runtime. Adding the
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These functions are positional-only at runtime. Adding the
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These functions are positional-only at runtime. Adding the
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| xxh64_digest = xxh3_64_digest | ||||||||||||||||||||||||||||||||||||||
| xxh64_hexdigest = xxh3_64_hexdigest | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test will fail with a
FileNotFoundErrorif thepyrightexecutable is not installed in the environment. It is better to catch this exception and skip the test gracefully, especially for local development environments wherepyrightmight not be globally available.