diff --git a/libp2p/utils/paths.py b/libp2p/utils/paths.py new file mode 100644 index 000000000..6b725042c --- /dev/null +++ b/libp2p/utils/paths.py @@ -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 \ No newline at end of file diff --git a/scripts/release/test_package.py b/scripts/release/test_package.py index 2f23898ea..139275e29 100644 --- a/scripts/release/test_package.py +++ b/scripts/release/test_package.py @@ -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 @@ -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, ) @@ -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.") diff --git a/tests/utils/interop/envs.py b/tests/utils/interop/envs.py index 23d9f27a6..c2c8a0a18 100644 --- a/tests/utils/interop/envs.py +++ b/tests/utils/interop/envs.py @@ -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") diff --git a/tests/utils/test_logging.py b/tests/utils/test_logging.py index 603af5e1c..8a280e614 100644 --- a/tests/utils/test_logging.py +++ b/tests/utils/test_logging.py @@ -20,6 +20,7 @@ log_queue, setup_logging, ) +from libp2p.utils.paths import get_log_file_path def _reset_logging(): @@ -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() diff --git a/tests/utils/test_paths.py b/tests/utils/test_paths.py new file mode 100644 index 000000000..e23c10360 --- /dev/null +++ b/tests/utils/test_paths.py @@ -0,0 +1,203 @@ +""" +Tests for cross-platform path handling utilities. +""" + +import os +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from libp2p.utils.paths import ( + get_binary_path, + get_log_file_path, + get_platform_specific_path, + get_temp_dir, + get_venv_activate_script, + get_venv_pip, + get_venv_python, + is_unix_like, + is_windows, +) + + +class TestPathUtilities: + """Test the cross-platform path handling utilities.""" + + def test_get_temp_dir(self): + """Test that get_temp_dir returns a valid temporary directory.""" + temp_dir = get_temp_dir() + assert isinstance(temp_dir, Path) + assert temp_dir.exists() + assert temp_dir.is_dir() + + def test_get_log_file_path(self): + """Test that get_log_file_path creates proper log file paths.""" + timestamp = "20240101_120000" + log_path = get_log_file_path(timestamp) + + assert isinstance(log_path, Path) + assert log_path.name == f"{timestamp}_py-libp2p.log" + assert log_path.parent == get_temp_dir() + + def test_get_venv_python_windows(self): + """Test virtual environment Python path on Windows.""" + with patch('os.name', 'nt'): + venv_path = Path("test_venv") + python_path = get_venv_python(venv_path) + expected_path = venv_path / "Scripts" / "python.exe" + assert python_path == expected_path + + def test_get_venv_python_unix(self): + """Test virtual environment Python path on Unix-like systems.""" + with patch('os.name', 'posix'): + venv_path = Path("test_venv") + python_path = get_venv_python(venv_path) + expected_path = venv_path / "bin" / "python" + assert python_path == expected_path + + def test_get_venv_pip_windows(self): + """Test virtual environment pip path on Windows.""" + with patch('os.name', 'nt'): + venv_path = Path("test_venv") + pip_path = get_venv_pip(venv_path) + expected_path = venv_path / "Scripts" / "pip.exe" + assert pip_path == expected_path + + def test_get_venv_pip_unix(self): + """Test virtual environment pip path on Unix-like systems.""" + with patch('os.name', 'posix'): + venv_path = Path("test_venv") + pip_path = get_venv_pip(venv_path) + expected_path = venv_path / "bin" / "pip" + assert pip_path == expected_path + + def test_get_venv_activate_script_windows(self): + """Test virtual environment activation script path on Windows.""" + with patch('os.name', 'nt'): + venv_path = Path("test_venv") + activate_path = get_venv_activate_script(venv_path) + expected_path = venv_path / "Scripts" / "activate.bat" + assert activate_path == expected_path + + def test_get_venv_activate_script_unix(self): + """Test virtual environment activation script path on Unix-like systems.""" + with patch('os.name', 'posix'): + venv_path = Path("test_venv") + activate_path = get_venv_activate_script(venv_path) + expected_path = venv_path / "bin" / "activate" + assert activate_path == expected_path + + def test_get_binary_path_with_env_var(self): + """Test getting binary path with environment variable set.""" + with patch.dict(os.environ, {'TEST_BIN': '/usr/local'}): + binary_path = get_binary_path("TEST_BIN", "test-binary") + expected_path = Path("/usr/local/bin/test-binary") + if os.name == 'nt': + expected_path = Path("/usr/local/bin/test-binary.exe") + assert binary_path == expected_path + + def test_get_binary_path_with_default(self): + """Test getting binary path with default fallback.""" + with patch.dict(os.environ, {}, clear=True): + default_path = Path("/default/bin") + binary_path = get_binary_path("MISSING_VAR", "test-binary", default_path) + expected_path = default_path / "bin" / "test-binary" + if os.name == 'nt': + expected_path = default_path / "bin" / "test-binary.exe" + assert binary_path == expected_path + + def test_get_binary_path_no_env_no_default(self): + """Test that get_binary_path raises KeyError when env var is missing and no default.""" + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(KeyError, match="Environment variable MISSING_VAR is not set"): + get_binary_path("MISSING_VAR", "test-binary") + + def test_is_windows(self): + """Test Windows platform detection.""" + with patch('os.name', 'nt'): + assert is_windows() is True + with patch('os.name', 'posix'): + assert is_windows() is False + + def test_is_unix_like(self): + """Test Unix-like platform detection.""" + with patch('os.name', 'posix'): + assert is_unix_like() is True + with patch('os.name', 'nt'): + assert is_unix_like() is False + + def test_get_platform_specific_path_windows_executable(self): + """Test platform-specific path with Windows executable.""" + with patch('os.name', 'nt'): + base_path = Path("/usr/local") + path = get_platform_specific_path(base_path, "bin", "python") + expected_path = base_path / "bin" / "python.exe" + assert path == expected_path + + def test_get_platform_specific_path_unix_executable(self): + """Test platform-specific path with Unix executable.""" + with patch('os.name', 'posix'): + base_path = Path("/usr/local") + path = get_platform_specific_path(base_path, "bin", "python") + expected_path = base_path / "bin" / "python" + assert path == expected_path + + def test_get_platform_specific_path_non_executable(self): + """Test platform-specific path with non-executable file.""" + base_path = Path("/usr/local") + path = get_platform_specific_path(base_path, "config", "settings.json") + expected_path = base_path / "config" / "settings.json" + assert path == expected_path + + +class TestPathUtilitiesIntegration: + """Integration tests for path utilities.""" + + def test_log_file_creation_integration(self): + """Test that log file path points to a writable location.""" + timestamp = "test_20240101_120000" + log_path = get_log_file_path(timestamp) + + # Ensure the parent directory exists and is writable + assert log_path.parent.exists() + assert log_path.parent.is_dir() + + # Test that we can write to the location + try: + log_path.write_text("test content") + assert log_path.exists() + assert log_path.read_text() == "test content" + finally: + # Clean up + if log_path.exists(): + log_path.unlink() + + def test_temp_dir_integration(self): + """Test that temp directory is actually usable.""" + temp_dir = get_temp_dir() + + # Test creating a file in the temp directory + test_file = temp_dir / "test_file.txt" + try: + test_file.write_text("test content") + assert test_file.exists() + assert test_file.read_text() == "test content" + finally: + # Clean up + if test_file.exists(): + test_file.unlink() + + def test_binary_path_integration(self): + """Test binary path resolution with real environment.""" + # Test with a common environment variable + try: + # Try to get PATH environment variable + path_binary = get_binary_path("PATH", "python", Path("/usr/bin")) + assert isinstance(path_binary, Path) + except KeyError: + # If PATH is not set, test with a default + path_binary = get_binary_path("PATH", "python", Path("/usr/bin")) + assert isinstance(path_binary, Path) + assert path_binary.parent == Path("/usr/bin/bin") \ No newline at end of file