Skip to content

Commit

Permalink
Maintain consistent data paths for case insensitive file systems
Browse files Browse the repository at this point in the history
  • Loading branch information
ofek committed Apr 6, 2024
1 parent f2017b6 commit 64ed72f
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/history/hatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

***Fixed:***

- Maintain consistent data paths for case insensitive file systems
- When projects derive dependencies from metadata hooks, there is now by default a status indicator for when the hooks are executed for better responsiveness
- Properly support projects with a `pyproject.toml` file but no `project` table e.g. applications
- Fix dependency inheritance for the template of the `types` environment for new projects
Expand Down
5 changes: 1 addition & 4 deletions src/hatch/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,7 @@ def _metadata_file(self, environment: EnvironmentInterface) -> Path:

@cached_property
def _storage_dir(self) -> Path:
from base64 import urlsafe_b64encode
from hashlib import sha256

return self.__data_dir / urlsafe_b64encode(sha256(str(self.__project_path).encode()).digest())[:8].decode()
return self.__data_dir / self.__project_path.id[:8]


def is_isolated_environment(env_name: str, config: dict[str, Any]) -> bool:
Expand Down
6 changes: 1 addition & 5 deletions src/hatch/cli/fmt/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ def get_default_args(self) -> list[str]:

@cached_property
def internal_config_file(self) -> Path:
from base64 import urlsafe_b64encode
from hashlib import sha256

project_id = urlsafe_b64encode(sha256(str(self.env.root).encode()).digest())[:8].decode()
return self.env.isolated_data_directory / '.config' / project_id / 'ruff_defaults.toml'
return self.env.isolated_data_directory / '.config' / self.env.root.id[:8] / 'ruff_defaults.toml'

def construct_config_file(self, *, preview: bool) -> str:
lines = [
Expand Down
6 changes: 1 addition & 5 deletions src/hatch/cli/test/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ def user_config_path(self) -> Path:

@cached_property
def internal_config_path(self) -> Path:
from base64 import urlsafe_b64encode
from hashlib import sha256

project_id = urlsafe_b64encode(sha256(str(self.project_root).encode()).digest())[:8].decode()
return self.data_dir / 'coverage' / project_id / self.user_config_path.name
return self.data_dir / 'coverage' / self.project_root.id[:8] / self.user_config_path.name

def write_config_file(self) -> None:
self.internal_config_path.parent.ensure_dir_exists()
Expand Down
5 changes: 1 addition & 4 deletions src/hatch/env/virtual.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

import os
import sys
from base64 import urlsafe_b64encode
from contextlib import contextmanager, suppress
from functools import cached_property
from hashlib import sha256
from os.path import isabs
from typing import TYPE_CHECKING, Callable

Expand All @@ -31,8 +29,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Always compute the isolated app path for build environments
hashed_root = sha256(str(self.root).encode('utf-8')).digest()
checksum = urlsafe_b64encode(hashed_root).decode('utf-8')[:8]
checksum = self.root.id[:8]

project_name = self.metadata.name if 'project' in self.metadata.config else f'{checksum}-unmanaged'
venv_name = project_name if self.name == 'default' else self.name
Expand Down
13 changes: 13 additions & 0 deletions src/hatch/utils/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pathlib
import sys
from contextlib import contextmanager, suppress
from functools import cached_property
from typing import TYPE_CHECKING, Any, Generator

from hatch.utils.structures import EnvVars
Expand Down Expand Up @@ -31,6 +32,18 @@ def disk_sync(fd: FileDescriptorLike) -> None:


class Path(_PathBase):
@cached_property
def id(self) -> str:
from base64 import urlsafe_b64encode
from hashlib import sha256

path = str(self)
if sys.platform == 'win32' or sys.platform == 'darwin':
path = path.casefold()

digest = sha256(path.encode('utf-8')).digest()
return urlsafe_b64encode(digest).decode('utf-8')

def ensure_dir_exists(self) -> None:
self.mkdir(parents=True, exist_ok=True)

Expand Down
37 changes: 37 additions & 0 deletions tests/cli/env/test_find.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import os
import sys

import pytest

from hatch.project.core import Project
from hatchling.utils.constants import DEFAULT_CONFIG_FILE
Expand Down Expand Up @@ -198,3 +201,37 @@ def test_plugin_dependencies_unmet(hatch, helpers, temp_dir_data, config_file, m
"""
)
helpers.assert_plugin_installation(mock_plugin_installation, [dependency])


@pytest.mark.skipif(sys.platform == 'win32' or sys.platform == 'darwin', reason='Case sensitive file system required')
def test_case_sensitivity(hatch, temp_dir_data):
from hatch.utils.fs import Path

project_name = 'My.App'

with temp_dir_data.as_cwd():
result = hatch('new', project_name)

assert result.exit_code == 0, result.output

project_path = temp_dir_data / 'my-app'

with project_path.as_cwd():
result = hatch('env', 'find')

assert result.exit_code == 0, result.output
path_default = result.output.strip()

with Path(str(project_path).upper()).as_cwd():
result = hatch('env', 'find')

assert result.exit_code == 0, result.output
path_upper = result.output.strip()

with Path(str(project_path).lower()).as_cwd():
result = hatch('env', 'find')

assert result.exit_code == 0, result.output
path_lower = result.output.strip()

assert path_default == path_upper == path_lower

0 comments on commit 64ed72f

Please sign in to comment.