Skip to content

fix: standardize cross-platform path handling in test utilities #792

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
154 changes: 154 additions & 0 deletions libp2p/utils/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
Cross-platform path handling utilities for py-libp2p.

This module provides platform-agnostic functions for handling paths,
temporary directories, virtual environments, and binary paths.
"""

import os
import tempfile
from pathlib import Path
from typing import Optional


def get_temp_dir() -> Path:
"""
Get the platform-appropriate temporary directory.

Returns:
Path: Path to the system's temporary directory
"""
return Path(tempfile.gettempdir())


def get_log_file_path(timestamp: str) -> Path:
"""
Get a platform-agnostic log file path.

Args:
timestamp: Timestamp string for the log file name

Returns:
Path: Path to the log file in the system's temp directory
"""
temp_dir = get_temp_dir()
return temp_dir / f"{timestamp}_py-libp2p.log"


def get_venv_python(venv_path: Path) -> Path:
"""
Get the Python executable path for a virtual environment.

Args:
venv_path: Path to the virtual environment

Returns:
Path: Path to the Python executable in the virtual environment
"""
if os.name == 'nt': # Windows
return venv_path / "Scripts" / "python.exe"
return venv_path / "bin" / "python"


def get_venv_pip(venv_path: Path) -> Path:
"""
Get the pip executable path for a virtual environment.

Args:
venv_path: Path to the virtual environment

Returns:
Path: Path to the pip executable in the virtual environment
"""
if os.name == 'nt': # Windows
return venv_path / "Scripts" / "pip.exe"
return venv_path / "bin" / "pip"


def get_binary_path(env_var: str, binary_name: str, default_path: Optional[Path] = None) -> Path:
"""
Get a binary path from an environment variable with platform-specific handling.

Args:
env_var: Environment variable name containing the base path
binary_name: Name of the binary (without extension)
default_path: Optional default path if environment variable is not set

Returns:
Path: Path to the binary

Raises:
KeyError: If env_var is not set and no default_path is provided
"""
base_path_str = os.environ.get(env_var)
if base_path_str is None:
if default_path is None:
raise KeyError(f"Environment variable {env_var} is not set")
base_path = default_path
else:
base_path = Path(base_path_str)

# Add .exe extension on Windows
if os.name == 'nt':
binary_name = f"{binary_name}.exe"

return base_path / "bin" / binary_name


def get_venv_activate_script(venv_path: Path) -> Path:
"""
Get the virtual environment activation script path.

Args:
venv_path: Path to the virtual environment

Returns:
Path: Path to the activation script
"""
if os.name == 'nt': # Windows
return venv_path / "Scripts" / "activate.bat"
return venv_path / "bin" / "activate"


def is_windows() -> bool:
"""
Check if the current platform is Windows.

Returns:
bool: True if running on Windows, False otherwise
"""
return os.name == 'nt'


def is_unix_like() -> bool:
"""
Check if the current platform is Unix-like (Linux, macOS, etc.).

Returns:
bool: True if running on a Unix-like system, False otherwise
"""
return os.name != 'nt'


def get_platform_specific_path(base_path: Path, *components: str) -> Path:
"""
Build a platform-specific path from components.

Args:
base_path: Base path to start from
*components: Path components to join

Returns:
Path: Platform-specific path
"""
path = base_path
for component in components:
path = path / component

# Add .exe extension for executables on Windows if not already present
if is_windows() and path.suffix == '' and 'bin' in str(path):
# Check if this looks like an executable path
if any(executable in str(path) for executable in ['python', 'pip', 'go', 'node']):
path = path.with_suffix('.exe')

return path
12 changes: 9 additions & 3 deletions scripts/release/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
)
import venv

from libp2p.utils.paths import get_venv_pip, get_venv_python, get_venv_activate_script


def create_venv(parent_path: Path) -> Path:
venv_path = parent_path / "package-smoke-test"
venv.create(venv_path, with_pip=True)
subprocess.run(
[venv_path / "bin" / "pip", "install", "-U", "pip", "setuptools"], check=True
[get_venv_pip(venv_path), "install", "-U", "pip", "setuptools"], check=True
)
return venv_path

Expand All @@ -31,7 +33,7 @@ def find_wheel(project_path: Path) -> Path:

def install_wheel(venv_path: Path, wheel_path: Path) -> None:
subprocess.run(
[venv_path / "bin" / "pip", "install", f"{wheel_path}"],
[get_venv_pip(venv_path), "install", f"{wheel_path}"],
check=True,
)

Expand All @@ -42,7 +44,11 @@ def test_install_local_wheel() -> None:
wheel_path = find_wheel(Path("."))
install_wheel(venv_path, wheel_path)
print("Installed", wheel_path.absolute(), "to", venv_path)
print(f"Activate with `source {venv_path}/bin/activate`")
activate_script = get_venv_activate_script(venv_path)
if activate_script.suffix == '.bat':
print(f"Activate with `{activate_script}`")
else:
print(f"Activate with `source {activate_script}`")
input("Press enter when the test has completed. The directory will be deleted.")


Expand Down
12 changes: 11 additions & 1 deletion tests/utils/interop/envs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import os
import pathlib

GO_BIN_PATH = pathlib.Path(os.environ["GOPATH"]) / "bin"
from libp2p.utils.paths import get_binary_path

# Use the new cross-platform binary path function with fallback
try:
GO_BIN_PATH = get_binary_path("GOPATH", "go")
except KeyError:
# Fallback to default Go installation path if GOPATH is not set
if os.name == 'nt': # Windows
GO_BIN_PATH = pathlib.Path("C:/Go/bin")
else: # Unix-like
GO_BIN_PATH = pathlib.Path("/usr/local/go/bin")
6 changes: 2 additions & 4 deletions tests/utils/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
log_queue,
setup_logging,
)
from libp2p.utils.paths import get_log_file_path


def _reset_logging():
Expand Down Expand Up @@ -190,10 +191,7 @@ async def test_default_log_file(clean_env):
mock_datetime.now.return_value.strftime.return_value = "20240101_120000"

# Remove the log file if it exists
if os.name == "nt": # Windows
log_file = Path("C:/Windows/Temp/20240101_120000_py-libp2p.log")
else: # Unix-like
log_file = Path("/tmp/20240101_120000_py-libp2p.log")
log_file = get_log_file_path("20240101_120000")
log_file.unlink(missing_ok=True)

setup_logging()
Expand Down
Loading
Loading