Skip to content

Commit dfb269e

Browse files
Prepare package for PyPI: fix versioning, lazy imports, isolated tests
- Single source of truth for version: pyproject.toml is authoritative, importlib.metadata reads it at runtime, _FALLBACK_VERSION as fallback - Expose __version__ in pyfuse.__init__ - Make RedisBackend import lazy in remote.py (consistent with other backends) - Remove eager backend imports from backends/__init__.py - Lower requires-python from >=3.13 to >=3.10 (no 3.13-specific features) - Add test_packaging.py: version consistency + isolated venv install tests - Update README badge and classifiers Agent-Logs-Url: https://github.com/codeSamuraii/pyfuse/sessions/92c23804-a854-46b3-978f-2599f056b232 Co-authored-by: codeSamuraii <17270548+codeSamuraii@users.noreply.github.com>
1 parent 66a3760 commit dfb269e

7 files changed

Lines changed: 186 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Run any Python function on a remote worker — with zero setup.**
44

5-
[![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue)](https://www.python.org/downloads/)
5+
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
66
[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-green)](LICENSE)
77
[![Typed](https://img.shields.io/badge/typing-strict%20mypy-blue)](https://mypy-lang.org/)
88

pyfuse/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44

55
from pyfuse.core.errors import DependencyError, Error, RemoteError, TaskCancelled, TaskStalled, WorkerError
6+
from pyfuse.core.version import _VERSION
67
from pyfuse.core.models import FunctionNode, ImportInfo
78
from pyfuse.core.progress import ProgressInfo
89
from pyfuse.core.progress import progress as progress
@@ -65,7 +66,10 @@ def pack(func: Callable[..., object], *args: Any, **kwargs: Any) -> Task:
6566
)
6667

6768

69+
__version__: str = _VERSION
70+
6871
__all__ = [
72+
"__version__",
6973
# Primary API
7074
"trace",
7175
"connect",

pyfuse/core/version.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
_VERSION = "0.4.0"
1+
from importlib.metadata import version as _pkg_version
2+
3+
_FALLBACK_VERSION = "0.4.0"
4+
5+
try:
6+
_VERSION: str = _pkg_version("pyfuse")
7+
except Exception:
8+
# Not installed as a package (e.g. running from source checkout).
9+
_VERSION = _FALLBACK_VERSION

pyfuse/worker/backends/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
from pyfuse.worker.backends.base import Backend
2-
from pyfuse.worker.backends.local import LocalBackend
3-
from pyfuse.worker.backends.redis import RedisBackend

pyfuse/worker/remote.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from pyfuse.core.task import Task
1818
from pyfuse.core.version import _VERSION
1919
from pyfuse.worker.backends.base import Backend
20-
from pyfuse.worker.backends.redis import RedisBackend
2120
from pyfuse.worker.result import Result, ResultEnvelope
2221

2322
if TYPE_CHECKING:
@@ -48,6 +47,8 @@ def _create_backend(url: str, **kwargs: Any) -> Backend:
4847
"""Create a backend instance from a URL."""
4948
scheme = url.split("://", 1)[0].lower()
5049
if scheme in ("redis", "rediss"):
50+
from pyfuse.worker.backends.redis import RedisBackend
51+
5152
return RedisBackend(url, **kwargs)
5253
if scheme == "local":
5354
from pyfuse.worker.backends.local import LocalBackend

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ authors = [
77
{name = "Rémi Héneault", email = "remi@heneault.fr"}
88
]
99
license = {text = "AGPL-3.0-only"}
10-
requires-python = ">=3.13"
10+
requires-python = ">=3.10"
1111
dependencies = []
1212
classifiers = [
1313
"Development Status :: 3 - Alpha",
1414
"Intended Audience :: Developers",
1515
"License :: OSI Approved :: GNU Affero General Public License v3",
1616
"Programming Language :: Python :: 3",
17+
"Programming Language :: Python :: 3.10",
18+
"Programming Language :: Python :: 3.11",
19+
"Programming Language :: Python :: 3.12",
1720
"Programming Language :: Python :: 3.13",
1821
"Topic :: Software Development :: Libraries",
1922
"Topic :: System :: Distributed Computing",

tests/test_packaging.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""Tests for PyPI packaging: version consistency and isolated installation."""
2+
from __future__ import annotations
3+
4+
import subprocess
5+
import sys
6+
import venv
7+
from pathlib import Path
8+
9+
import pytest
10+
11+
pytestmark = pytest.mark.slow
12+
13+
14+
def _project_root() -> Path:
15+
"""Return the repository root (contains pyproject.toml)."""
16+
root = Path(__file__).resolve().parent.parent
17+
assert (root / "pyproject.toml").exists()
18+
return root
19+
20+
21+
class TestVersionConsistency:
22+
"""Ensure the package exposes a consistent version."""
23+
24+
def test_version_is_string(self) -> None:
25+
from pyfuse.core.version import _VERSION
26+
27+
assert isinstance(_VERSION, str)
28+
assert _VERSION # non-empty
29+
30+
def test_init_exports_version(self) -> None:
31+
import pyfuse
32+
33+
assert hasattr(pyfuse, "__version__")
34+
assert isinstance(pyfuse.__version__, str)
35+
assert pyfuse.__version__ # non-empty
36+
37+
def test_pyproject_matches_fallback(self) -> None:
38+
"""The fallback version in version.py must match pyproject.toml."""
39+
try:
40+
import tomllib
41+
except ModuleNotFoundError:
42+
import tomli as tomllib # type: ignore[no-redef]
43+
44+
from pyfuse.core.version import _FALLBACK_VERSION
45+
46+
pyproject = _project_root() / "pyproject.toml"
47+
with open(pyproject, "rb") as f:
48+
data = tomllib.load(f)
49+
assert data["project"]["version"] == _FALLBACK_VERSION
50+
51+
52+
class TestIsolatedInstallation:
53+
"""Install pyfuse in a temporary venv and verify it works."""
54+
55+
def test_install_and_import_no_extras(self, tmp_path: Path) -> None:
56+
"""Package installs and imports without optional dependencies."""
57+
venv_dir = tmp_path / "venv"
58+
venv.create(str(venv_dir), with_pip=True)
59+
60+
if sys.platform == "win32":
61+
python = str(venv_dir / "Scripts" / "python.exe")
62+
else:
63+
python = str(venv_dir / "bin" / "python")
64+
65+
root = _project_root()
66+
67+
# Install the package (no extras)
68+
result = subprocess.run(
69+
[python, "-m", "pip", "install", str(root), "--quiet"],
70+
capture_output=True,
71+
text=True,
72+
timeout=120,
73+
)
74+
assert result.returncode == 0, f"pip install failed:\n{result.stderr}"
75+
76+
# Verify import works
77+
result = subprocess.run(
78+
[
79+
python, "-c",
80+
"import pyfuse; "
81+
"print(pyfuse.__version__); "
82+
"print(pyfuse.serialize); "
83+
"print(pyfuse.trace); "
84+
"print(pyfuse.get_graph()); "
85+
],
86+
capture_output=True,
87+
text=True,
88+
timeout=30,
89+
)
90+
assert result.returncode == 0, (
91+
f"Import failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
92+
)
93+
94+
lines = result.stdout.strip().splitlines()
95+
# First line is the version
96+
assert lines[0] # non-empty version string
97+
98+
def test_install_version_matches(self, tmp_path: Path) -> None:
99+
"""Installed package version matches pyproject.toml."""
100+
try:
101+
import tomllib
102+
except ModuleNotFoundError:
103+
import tomli as tomllib # type: ignore[no-redef]
104+
105+
venv_dir = tmp_path / "venv"
106+
venv.create(str(venv_dir), with_pip=True)
107+
108+
if sys.platform == "win32":
109+
python = str(venv_dir / "Scripts" / "python.exe")
110+
else:
111+
python = str(venv_dir / "bin" / "python")
112+
113+
root = _project_root()
114+
115+
pyproject = root / "pyproject.toml"
116+
with open(pyproject, "rb") as f:
117+
expected_version = tomllib.load(f)["project"]["version"]
118+
119+
# Install
120+
result = subprocess.run(
121+
[python, "-m", "pip", "install", str(root), "--quiet"],
122+
capture_output=True,
123+
text=True,
124+
timeout=120,
125+
)
126+
assert result.returncode == 0, f"pip install failed:\n{result.stderr}"
127+
128+
# Check version
129+
result = subprocess.run(
130+
[python, "-c", "import pyfuse; print(pyfuse.__version__)"],
131+
capture_output=True,
132+
text=True,
133+
timeout=30,
134+
)
135+
assert result.returncode == 0
136+
assert result.stdout.strip() == expected_version
137+
138+
def test_cli_entrypoint(self, tmp_path: Path) -> None:
139+
"""The ``pyfuse`` CLI entry point is installed and functional."""
140+
venv_dir = tmp_path / "venv"
141+
venv.create(str(venv_dir), with_pip=True)
142+
143+
if sys.platform == "win32":
144+
python = str(venv_dir / "Scripts" / "python.exe")
145+
else:
146+
python = str(venv_dir / "bin" / "python")
147+
148+
root = _project_root()
149+
150+
result = subprocess.run(
151+
[python, "-m", "pip", "install", str(root), "--quiet"],
152+
capture_output=True,
153+
text=True,
154+
timeout=120,
155+
)
156+
assert result.returncode == 0, f"pip install failed:\n{result.stderr}"
157+
158+
# Run ``pyfuse info`` via the entry point
159+
result = subprocess.run(
160+
[python, "-m", "pyfuse", "info"],
161+
capture_output=True,
162+
text=True,
163+
timeout=30,
164+
)
165+
assert result.returncode == 0
166+
assert "pyfuse" in result.stdout

0 commit comments

Comments
 (0)