rpytest 0.1.2__py3-none-any.whl
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.
- rpytest-0.1.2.dist-info/METADATA +120 -0
- rpytest-0.1.2.dist-info/RECORD +8 -0
- rpytest-0.1.2.dist-info/WHEEL +5 -0
- rpytest-0.1.2.dist-info/entry_points.txt +5 -0
- rpytest-0.1.2.dist-info/top_level.txt +1 -0
- rpytest_cli/__init__.py +96 -0
- rpytest_cli/download.py +155 -0
- rpytest_cli/plugin.py +44 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpytest
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Rust-powered, drop-in replacement for pytest
|
|
5
|
+
Author: rpytest contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/neul-labs/rpytest
|
|
8
|
+
Project-URL: Repository, https://github.com/neul-labs/rpytest
|
|
9
|
+
Project-URL: Issues, https://github.com/neul-labs/rpytest/issues
|
|
10
|
+
Keywords: pytest,testing,rust,performance
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Rust
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
Requires-Dist: pytest>=7.0
|
|
28
|
+
Requires-Dist: msgpack>=1.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
|
|
33
|
+
# rpytest
|
|
34
|
+
|
|
35
|
+
> **Run your pytest suite faster. Change nothing.**
|
|
36
|
+
>
|
|
37
|
+
> A Rust-powered, drop-in replacement for pytest that eliminates startup overhead while keeping your tests, fixtures, and plugins untouched.
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/rpytest/)
|
|
40
|
+
[](https://github.com/neul-labs/rpytest/blob/main/LICENSE)
|
|
41
|
+
[](https://pypi.org/project/rpytest/)
|
|
42
|
+
|
|
43
|
+
## Why rpytest?
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pytest -> 2.91s (480 tests)
|
|
47
|
+
rpytest -> 1.55s (same 480 tests)
|
|
48
|
+
= 1.9x faster
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
rpytest uses a persistent Rust daemon to keep Python warm between runs. No more interpreter startup costs on every invocation.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install rpytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That's it. The correct native binary for your platform (macOS or Linux, Intel or Apple Silicon) is bundled automatically.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
rpytest mirrors pytest's CLI exactly. If you know pytest, you know rpytest.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Run all tests
|
|
67
|
+
rpytest
|
|
68
|
+
|
|
69
|
+
# Run specific tests
|
|
70
|
+
rpytest tests/test_api.py::test_login
|
|
71
|
+
|
|
72
|
+
# Filter by keyword or marker
|
|
73
|
+
rpytest -k "auth" -m "not slow"
|
|
74
|
+
|
|
75
|
+
# Parallel execution — no pytest-xdist needed
|
|
76
|
+
rpytest -n auto
|
|
77
|
+
|
|
78
|
+
# Watch mode for TDD
|
|
79
|
+
rpytest --watch
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Key Features
|
|
83
|
+
|
|
84
|
+
| Feature | pytest | rpytest |
|
|
85
|
+
|---------|--------|---------|
|
|
86
|
+
| Startup time | ~200ms | <10ms |
|
|
87
|
+
| Memory usage | 35.8 MB | 6.2 MB |
|
|
88
|
+
| Parallel workers | pytest-xdist plugin | Built-in `-n` flag |
|
|
89
|
+
| Watch mode | pytest-watch plugin | Built-in `--watch` |
|
|
90
|
+
| Flakiness detection | flaky plugin | Built-in `--reruns` |
|
|
91
|
+
| Sharding | pytest-shard plugin | Built-in `--shard` |
|
|
92
|
+
|
|
93
|
+
- **Full pytest compatibility** — plugins, fixtures, conftest.py, pytest.ini all work unchanged
|
|
94
|
+
- **Built-in parallelism** — `rpytest -n 4` or `rpytest -n auto`
|
|
95
|
+
- **Watch mode** — file changes trigger automatic re-runs of affected tests
|
|
96
|
+
- **Flakiness detection** — `rpytest --reruns 3` auto-retries failed tests
|
|
97
|
+
- **Session fixture reuse** — `rpytest --reuse-fixtures` persists expensive fixtures
|
|
98
|
+
- **CI sharding** — `rpytest --shard 0 --total-shards 4`
|
|
99
|
+
|
|
100
|
+
## Requirements
|
|
101
|
+
|
|
102
|
+
- Python 3.9+
|
|
103
|
+
- pytest 7.0+
|
|
104
|
+
- macOS or Linux
|
|
105
|
+
|
|
106
|
+
## How It Works
|
|
107
|
+
|
|
108
|
+
1. **First run**: Spawns a background daemon that collects your test suite
|
|
109
|
+
2. **Subsequent runs**: Rust CLI filters tests and dispatches to warm Python workers
|
|
110
|
+
3. **Results stream back** in real-time
|
|
111
|
+
|
|
112
|
+
The daemon persists between runs, so TDD loops and CI retries skip all startup work.
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
Full docs at [docs.neullabs.com/rpytest](https://docs.neullabs.com/rpytest)
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
rpytest_cli/__init__.py,sha256=ZsczRmh9lKetjgHObawd8ehAziZS_uSMXONd3D85lTc,2644
|
|
2
|
+
rpytest_cli/download.py,sha256=Ic4arsIMVTvTzEcjQD_KnW6PmIh8Hx0kUKkGf74SK48,4706
|
|
3
|
+
rpytest_cli/plugin.py,sha256=0c41ZHX-DICEfam_OM43tF-PRhE76MP5mSejgDhN_fc,1416
|
|
4
|
+
rpytest-0.1.2.dist-info/METADATA,sha256=P2n9itmiczO3LeQeNIgXXZaKrT4qbx5C4Ahd1lIT88E,3861
|
|
5
|
+
rpytest-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
rpytest-0.1.2.dist-info/entry_points.txt,sha256=Yj4eWrTzPzrY3grPMC1P946qjXkDu9E8sOKmGtYnS70,86
|
|
7
|
+
rpytest-0.1.2.dist-info/top_level.txt,sha256=napzyGUjTnHBVNcwXz0bXwd3_CaY_-td7G6-4g2H_-A,12
|
|
8
|
+
rpytest-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rpytest_cli
|
rpytest_cli/__init__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""rpytest - Rust-powered, drop-in replacement for pytest.
|
|
2
|
+
|
|
3
|
+
This package provides the rpytest CLI tool and pytest plugin.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.2"
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_binary_path() -> Path:
|
|
16
|
+
"""Get the path to the rpytest binary."""
|
|
17
|
+
# Check if we're in development mode (source checkout)
|
|
18
|
+
pkg_dir = Path(__file__).parent
|
|
19
|
+
|
|
20
|
+
# Try package-bundled binary first
|
|
21
|
+
bin_dir = pkg_dir / "bin"
|
|
22
|
+
if bin_dir.exists():
|
|
23
|
+
system = platform.system().lower()
|
|
24
|
+
machine = platform.machine().lower()
|
|
25
|
+
|
|
26
|
+
# Normalize architecture names
|
|
27
|
+
if machine in ("x86_64", "amd64"):
|
|
28
|
+
machine = "x86_64"
|
|
29
|
+
elif machine in ("arm64", "aarch64"):
|
|
30
|
+
machine = "aarch64"
|
|
31
|
+
|
|
32
|
+
if system == "darwin":
|
|
33
|
+
binary_name = f"rpytest-{machine}-apple-darwin"
|
|
34
|
+
elif system == "linux":
|
|
35
|
+
binary_name = f"rpytest-{machine}-unknown-linux-gnu"
|
|
36
|
+
else:
|
|
37
|
+
binary_name = "rpytest"
|
|
38
|
+
|
|
39
|
+
binary_path = bin_dir / binary_name
|
|
40
|
+
if binary_path.exists():
|
|
41
|
+
return binary_path
|
|
42
|
+
|
|
43
|
+
# Try system PATH
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["which", "rpytest"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
check=True
|
|
50
|
+
)
|
|
51
|
+
return Path(result.stdout.strip())
|
|
52
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# Try cargo target directory (development)
|
|
56
|
+
workspace_root = pkg_dir.parent.parent
|
|
57
|
+
for build_type in ("release", "debug"):
|
|
58
|
+
target_binary = workspace_root / "target" / build_type / "rpytest"
|
|
59
|
+
if target_binary.exists():
|
|
60
|
+
return target_binary
|
|
61
|
+
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"rpytest binary not found. Please install via:\n"
|
|
64
|
+
" cargo install --path crates/rpytest\n"
|
|
65
|
+
"or download prebuilt binaries from releases."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
"""Main entry point for rpytest CLI."""
|
|
71
|
+
try:
|
|
72
|
+
binary_path = get_binary_path()
|
|
73
|
+
except RuntimeError as e:
|
|
74
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
# Make sure the binary is executable
|
|
78
|
+
if not os.access(binary_path, os.X_OK):
|
|
79
|
+
os.chmod(binary_path, 0o755)
|
|
80
|
+
|
|
81
|
+
# Execute the Rust binary with all arguments
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
[str(binary_path)] + sys.argv[1:],
|
|
85
|
+
check=False
|
|
86
|
+
)
|
|
87
|
+
sys.exit(result.returncode)
|
|
88
|
+
except KeyboardInterrupt:
|
|
89
|
+
sys.exit(130)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Error executing rpytest: {e}", file=sys.stderr)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
rpytest_cli/download.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Download prebuilt rpytest binaries."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# GitHub release URL pattern
|
|
13
|
+
RELEASE_URL = "https://github.com/neul-labs/rpytest/releases/download"
|
|
14
|
+
VERSION = "0.1.2"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_platform_target() -> str:
|
|
18
|
+
"""Get the target triple for the current platform."""
|
|
19
|
+
system = platform.system().lower()
|
|
20
|
+
machine = platform.machine().lower()
|
|
21
|
+
|
|
22
|
+
# Normalize architecture
|
|
23
|
+
if machine in ("x86_64", "amd64"):
|
|
24
|
+
arch = "x86_64"
|
|
25
|
+
elif machine in ("arm64", "aarch64"):
|
|
26
|
+
arch = "aarch64"
|
|
27
|
+
elif machine in ("i686", "i386"):
|
|
28
|
+
arch = "i686"
|
|
29
|
+
else:
|
|
30
|
+
raise RuntimeError(f"Unsupported architecture: {machine}")
|
|
31
|
+
|
|
32
|
+
# Build target triple
|
|
33
|
+
if system == "darwin":
|
|
34
|
+
return f"{arch}-apple-darwin"
|
|
35
|
+
elif system == "linux":
|
|
36
|
+
# Check for musl vs glibc
|
|
37
|
+
try:
|
|
38
|
+
import subprocess
|
|
39
|
+
result = subprocess.run(["ldd", "--version"], capture_output=True, text=True)
|
|
40
|
+
if "musl" in result.stderr.lower() or "musl" in result.stdout.lower():
|
|
41
|
+
return f"{arch}-unknown-linux-musl"
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
return f"{arch}-unknown-linux-gnu"
|
|
45
|
+
elif system == "windows":
|
|
46
|
+
return f"{arch}-pc-windows-msvc"
|
|
47
|
+
else:
|
|
48
|
+
raise RuntimeError(f"Unsupported platform: {system}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def download_binary(target: str | None = None, output_dir: Path | None = None) -> Path:
|
|
52
|
+
"""Download the rpytest binary for the given target.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
target: Target triple (e.g., "x86_64-unknown-linux-gnu").
|
|
56
|
+
If None, auto-detect the current platform.
|
|
57
|
+
output_dir: Directory to place the binary. If None, uses the package bin/ directory.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Path to the downloaded binary.
|
|
61
|
+
"""
|
|
62
|
+
if target is None:
|
|
63
|
+
target = get_platform_target()
|
|
64
|
+
|
|
65
|
+
if output_dir is None:
|
|
66
|
+
output_dir = Path(__file__).parent / "bin"
|
|
67
|
+
|
|
68
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# Construct download URL
|
|
71
|
+
archive_name = f"rpytest-{VERSION}-{target}.tar.gz"
|
|
72
|
+
url = f"{RELEASE_URL}/v{VERSION}/{archive_name}"
|
|
73
|
+
|
|
74
|
+
print(f"Downloading rpytest {VERSION} for {target}...")
|
|
75
|
+
print(f"URL: {url}")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Download to temp file
|
|
79
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz") as tmp:
|
|
80
|
+
tmp_path = tmp.name
|
|
81
|
+
urllib.request.urlretrieve(url, tmp_path)
|
|
82
|
+
|
|
83
|
+
# Extract binary
|
|
84
|
+
with tarfile.open(tmp_path, "r:gz") as tar:
|
|
85
|
+
# Find the binary in the archive
|
|
86
|
+
for member in tar.getmembers():
|
|
87
|
+
if member.name.endswith("rpytest") or member.name == "rpytest":
|
|
88
|
+
# Extract to output directory
|
|
89
|
+
member.name = f"rpytest-{target}"
|
|
90
|
+
tar.extract(member, output_dir)
|
|
91
|
+
binary_path = output_dir / f"rpytest-{target}"
|
|
92
|
+
os.chmod(binary_path, 0o755)
|
|
93
|
+
print(f"Installed: {binary_path}")
|
|
94
|
+
return binary_path
|
|
95
|
+
|
|
96
|
+
raise RuntimeError("Binary not found in archive")
|
|
97
|
+
|
|
98
|
+
except urllib.error.HTTPError as e:
|
|
99
|
+
if e.code == 404:
|
|
100
|
+
raise RuntimeError(
|
|
101
|
+
f"No prebuilt binary available for {target}.\n"
|
|
102
|
+
f"Please build from source: cargo build --release"
|
|
103
|
+
)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
finally:
|
|
107
|
+
# Clean up temp file
|
|
108
|
+
if 'tmp_path' in locals():
|
|
109
|
+
try:
|
|
110
|
+
os.unlink(tmp_path)
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
"""CLI entry point for downloading binaries."""
|
|
117
|
+
import argparse
|
|
118
|
+
|
|
119
|
+
parser = argparse.ArgumentParser(description="Download rpytest binary")
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--target",
|
|
122
|
+
help="Target triple (auto-detected if not specified)"
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--output-dir",
|
|
126
|
+
type=Path,
|
|
127
|
+
help="Output directory for the binary"
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--list-targets",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="List available targets"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
|
|
137
|
+
if args.list_targets:
|
|
138
|
+
print("Available targets:")
|
|
139
|
+
print(" x86_64-unknown-linux-gnu")
|
|
140
|
+
print(" x86_64-unknown-linux-musl")
|
|
141
|
+
print(" aarch64-unknown-linux-gnu")
|
|
142
|
+
print(" x86_64-apple-darwin")
|
|
143
|
+
print(" aarch64-apple-darwin")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
binary_path = download_binary(args.target, args.output_dir)
|
|
148
|
+
print(f"\nSuccess! Binary installed at: {binary_path}")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|
rpytest_cli/plugin.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""pytest plugin for rpytest integration.
|
|
2
|
+
|
|
3
|
+
This plugin allows pytest to communicate with the rpytest daemon
|
|
4
|
+
for enhanced performance and caching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pytest_configure(config):
|
|
12
|
+
"""Configure pytest to use rpytest if available."""
|
|
13
|
+
# Register custom markers
|
|
14
|
+
config.addinivalue_line(
|
|
15
|
+
"markers", "rpytest_skip: Skip this test when running under rpytest"
|
|
16
|
+
)
|
|
17
|
+
config.addinivalue_line(
|
|
18
|
+
"markers", "rpytest_only: Only run this test when running under rpytest"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pytest_collection_modifyitems(config, items):
|
|
23
|
+
"""Modify collected items based on rpytest markers."""
|
|
24
|
+
running_under_rpytest = os.environ.get("RPYTEST") == "1"
|
|
25
|
+
|
|
26
|
+
skip_rpytest = pytest.mark.skip(reason="Skipped when running under rpytest")
|
|
27
|
+
skip_pytest = pytest.mark.skip(reason="Only runs under rpytest")
|
|
28
|
+
|
|
29
|
+
for item in items:
|
|
30
|
+
if running_under_rpytest:
|
|
31
|
+
# Skip tests marked with rpytest_skip
|
|
32
|
+
if "rpytest_skip" in item.keywords:
|
|
33
|
+
item.add_marker(skip_rpytest)
|
|
34
|
+
else:
|
|
35
|
+
# Skip tests marked with rpytest_only when not using rpytest
|
|
36
|
+
if "rpytest_only" in item.keywords:
|
|
37
|
+
item.add_marker(skip_pytest)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def pytest_report_header(config):
|
|
41
|
+
"""Add rpytest info to the pytest header."""
|
|
42
|
+
if os.environ.get("RPYTEST") == "1":
|
|
43
|
+
return ["rpytest: enabled (daemon mode)"]
|
|
44
|
+
return []
|