Skip to content

Commit

Permalink
Add Platform.normalize_path to ensure standard path strings
Browse files Browse the repository at this point in the history
`WinPlatform.normalize_path` ensures that drive letters are always capitalized.

This fixes issues with windows drive letters not being consistently saved
in the habcache. When platform_path_map is not used on a habcache'd path
the cache would not find and load the cached value.
  • Loading branch information
MHendricks committed Nov 16, 2024
1 parent 6d3b79d commit a429d01
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 2 deletions.
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

0 comments on commit a429d01

Please sign in to comment.