Skip to content
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

Add Platform.normalize_path to ensure standard path strings #111

Merged
merged 1 commit into from
Nov 16, 2024
Merged
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
2 changes: 2 additions & 0 deletions hab/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ def platform_path_key(self, path, platform=None):
path = PureWindowsPath(path)
else:
path = PurePosixPath(path)
# Ensure any path normalization is applied
path = utils.Platform.normalize_path(path)

platform = utils.Platform.name()
mappings = self.get("platform_path_maps", {})
Expand Down
21 changes: 19 additions & 2 deletions hab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,8 @@ def expand_paths(cls, paths):
a list containing Path objects.
"""
if isinstance(paths, str):
return [Path(p) for p in paths.split(cls.pathsep())]
return [Path(p) for p in paths]
return [cls.normalize_path(Path(p)) for p in paths.split(cls.pathsep())]
return [cls.normalize_path(Path(p)) for p in paths]

@staticmethod
def get_platform(name=None):
Expand All @@ -491,6 +491,11 @@ def name(cls):
"""The hab name for this platform."""
return cls._name

@classmethod
def normalize_path(cls, path):
"""Returns the provided `pathlib.Path` with any normalization required."""
return path

@classmethod
def path_split(cls, path, pathsep=None):
"""Split a string by pathsep unless that path is a single windows path.
Expand Down Expand Up @@ -570,6 +575,18 @@ def collapse_paths(cls, paths, ext=None, key=None):
paths = [str(p) for p in paths]
return cls.pathsep(ext=ext, key=key).join(paths)

@classmethod
def normalize_path(cls, path):
"""Returns the provided `pathlib.Path` with any normalization required.

This ensures that the drive letter is resolved consistently to uppercase.
"""
if not path.is_absolute():
return path
parts = path.parts
cls = type(path)
return cls(parts[0].upper()).joinpath(*parts[1:])

@classmethod
def pathsep(cls, ext=None, key=None):
"""The path separator used by this platform."""
Expand Down
43 changes: 43 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pathlib
import sys
from collections import OrderedDict
from pathlib import Path
Expand Down Expand Up @@ -46,6 +47,16 @@ def test_environment_variables(config_root, helpers, monkeypatch):
assert resolver_env.config_paths == config_paths_env
assert resolver_env.distro_paths == distro_paths_env

# Test that normalize_path is called by expand_paths. WinPlatform will
# ensure that the drive letter is capitalized.
# Force the test to use a Windows pathlib class so this test works on linux
monkeypatch.setattr(utils, "Path", pathlib.PureWindowsPath)
win_paths = utils.WinPlatform.expand_paths([r"c:\distro", "d:\\", r"z:\path", ""])
assert win_paths[0].as_posix().startswith("C:")
assert win_paths[1].as_posix().startswith("D:")
assert win_paths[2].as_posix().startswith("Z:")
assert win_paths[3].as_posix() == "."


def test_config(resolver):
"""Spot check a few of the parsed configs to ensure config works."""
Expand Down Expand Up @@ -816,6 +827,38 @@ def test_pathsep(self):
assert utils.WinPlatform.pathsep(ext=".sh") == ";"
assert utils.WinPlatform.pathsep(ext=".sh", key="PATH") == ":"

@pytest.mark.parametrize(
"cls",
(
pathlib.Path,
pathlib.PurePath,
pathlib.PurePosixPath,
pathlib.PureWindowsPath,
),
)
@pytest.mark.parametrize("platform", utils.BasePlatform.__subclasses__())
def test_normalize_path_preserves_cls(self, cls, platform):
path = cls()
result = utils.LinuxPlatform.normalize_path(path)
# The class of the result is preserved
assert isinstance(result, cls)
# For an empty path, the same path is always returned
assert result == path

@pytest.mark.parametrize(
"path,check",
(
# the drive letter is always upper-cased if specified
(r"c:\temp", "C:/temp"),
(r"C:\temp", "C:/temp"),
(r"z:\subfolder", "Z:/subfolder"),
(r"relative\path", "relative/path"),
),
)
def test_normalize_path_windows(self, path, check):
result = utils.WinPlatform.normalize_path(pathlib.PureWindowsPath(path))
assert result.as_posix() == check


def test_cygpath():
# Check space handling
Expand Down
5 changes: 5 additions & 0 deletions tests/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,11 @@ def test_win(self, monkeypatch, config_root):
out = site.platform_path_key(r"c:\host\root\extra", platform="windows")
assert out.as_posix() == "{host-root}/extra"

# Test that normalize_path was called for unmodified paths and
# capitalized the drive letter
out = site.platform_path_key(r"z:\root\path", platform="windows")
assert out.as_posix() == r"Z:/root/path"

def test_unset_variables(self, config_root):
"""Don't modify variables that are not specified in platform_path_map"""
site = Site([config_root / "site_main.json"])
Expand Down