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.
@@ -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
+ [![PyPI Version](https://img.shields.io/pypi/v/rpytest.svg)](https://pypi.org/project/rpytest/)
40
+ [![License](https://img.shields.io/pypi/l/rpytest.svg)](https://github.com/neul-labs/rpytest/blob/main/LICENSE)
41
+ [![Python Versions](https://img.shields.io/pypi/pyversions/rpytest.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ rpytest = rpytest_cli:main
3
+
4
+ [pytest11]
5
+ rpytest = rpytest_cli.plugin
@@ -0,0 +1 @@
1
+ rpytest_cli
@@ -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()
@@ -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 []