Skip to content

Commit

Permalink
Change cache_dir location to prefer venv and project_directory
Browse files Browse the repository at this point in the history
  • Loading branch information
ssbarnea committed Jan 8, 2025
1 parent a9387df commit 98425dc
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 52 deletions.
9 changes: 9 additions & 0 deletions .taplo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[formatting]
# compatibility between toml-sort-fix pre-commit hook and panekj.even-betterer-toml extension
align_comments = false
array_trailing_comma = false
compact_arrays = true
compact_entries = false
compact_inline_tables = true
inline_table_expand = false
reorder_keys = true
13 changes: 10 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
Expand All @@ -14,8 +17,6 @@
"editor.formatOnSave": true
},
"editor.formatOnSave": true,
"evenBetterToml.formatter.alignComments": false,
"evenBetterToml.formatter.allowedBlankLines": 2,
"files.exclude": {
"*.egg-info": true,
".pytest_cache": true,
Expand All @@ -37,8 +38,14 @@
"python.testing.pytestArgs": ["tests"],
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"sortLines.filterBlankLines": true,
"yaml.completion": true,
"yaml.customTags": ["!encrypted/pkcs1-oaep scalar", "!vault scalar"],
"yaml.format.enable": false,
"yaml.validate": true
"yaml.validate": true,
"evenBetterToml.formatter.alignComments": false,
"evenBetterToml.formatter.arrayTrailingComma": true,
"[toml]": {
"editor.defaultFormatter": "panekj.even-betterer-toml"
}
}
19 changes: 17 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,16 @@ known-first-party = ["ansible_compat"]
known-third-party = ["packaging"]

[tool.ruff.lint.per-file-ignores]
"test/**/*.py" = ["DOC402", "DOC501", "SLF001", "S101", "S404", "FBT001", "PLC2701"]
"test/**/*.py" = [
"DOC402",
"DOC501",
"FBT001",
"PLC2701",
"PLR0917",
"S101",
"S404",
"SLF001"
]

[tool.ruff.lint.pydocstyle]
convention = "google"
Expand Down Expand Up @@ -430,4 +439,10 @@ sort_table_keys = true
[tool.uv.pip]
annotation-style = "line"
custom-compile-command = "tox run deps"
no-emit-package = ["ansible-core", "pip", "resolvelib", "typing_extensions", "uv"]
no-emit-package = [
"ansible-core",
"pip",
"resolvelib",
"typing_extensions",
"uv"
]
37 changes: 22 additions & 15 deletions src/ansible_compat/prerun.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
"""Utilities for configuring ansible runtime environment."""

import hashlib
import os
from pathlib import Path


def get_cache_dir(project_dir: Path) -> Path:
"""Compute cache directory to be used based on project path."""
# we only use the basename instead of the full path in order to ensure that
# we would use the same key regardless the location of the user home
# directory or where the project is clones (as long the project folder uses
# the same name).
basename = project_dir.resolve().name.encode(encoding="utf-8")
# 6 chars of entropy should be enough
cache_key = hashlib.sha256(basename).hexdigest()[:6]
cache_dir = (
Path(os.getenv("XDG_CACHE_HOME", "~/.cache")).expanduser()
/ "ansible-compat"
/ cache_key
)
def get_cache_dir(project_dir: Path, *, isolated: bool = True) -> Path:
"""Compute cache directory to be used based on project path.
Args:
project_dir: Path to the project directory.
isolated: Whether to use isolated cache directory.
Returns:
Cache directory path.
"""
if "VIRTUAL_ENV" in os.environ:
cache_dir = Path(os.environ["VIRTUAL_ENV"]) / ".ansible"
elif isolated:
cache_dir = project_dir / ".ansible"

Check warning on line 20 in src/ansible_compat/prerun.py

View check run for this annotation

Codecov / codecov/patch

src/ansible_compat/prerun.py#L20

Added line #L20 was not covered by tests
else:
cache_dir = Path(os.environ.get("ANSIBLE_HOME", "~/.ansible")).expanduser()

# Ensure basic folder structure exists so `ansible-galaxy list` does not
# fail with: None of the provided paths were usable. Please specify a valid path with
for name in ("roles", "collections"):
(cache_dir / name).mkdir(parents=True, exist_ok=True)

return cache_dir
4 changes: 2 additions & 2 deletions src/ansible_compat/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ def __init__(
if "PYTHONWARNINGS" not in self.environ: # pragma: no cover
self.environ["PYTHONWARNINGS"] = "ignore:Blowfish has been deprecated"

if isolated:
self.cache_dir = get_cache_dir(self.project_dir)
self.cache_dir = get_cache_dir(self.project_dir, isolated=self.isolated)

self.config = AnsibleConfig(cache_dir=self.cache_dir)

# Add the sys.path to the collection paths if not isolated
Expand Down
17 changes: 17 additions & 0 deletions test/test_prerun.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""Tests for ansible_compat.prerun module."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch

from ansible_compat.prerun import get_cache_dir

Expand All @@ -10,3 +16,14 @@ def test_get_cache_dir_relative() -> None:
relative_path = Path()
abs_path = relative_path.resolve()
assert get_cache_dir(relative_path) == get_cache_dir(abs_path)


def test_get_cache_dir_no_isolation_no_venv(monkeypatch: MonkeyPatch) -> None:
"""Test behaviors of get_cache_dir.
Args:
monkeypatch: Pytest fixture for monkeypatching
"""
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
monkeypatch.delenv("ANSIBLE_HOME", raising=False)
assert get_cache_dir(Path(), isolated=False) == Path("~/.ansible").expanduser()
10 changes: 7 additions & 3 deletions test/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,10 +654,9 @@ def test_upgrade_collection(runtime_tmp: Runtime) -> None:
runtime_tmp.require_collection("community.molecule", "0.1.0")


def test_require_collection_no_cache_dir() -> None:
def test_require_collection_not_isolated() -> None:
"""Check require_collection without a cache directory."""
runtime = Runtime()
assert not runtime.cache_dir
runtime = Runtime(isolated=False)
runtime.require_collection("community.molecule", "0.1.0", install=True)


Expand Down Expand Up @@ -1024,6 +1023,11 @@ def test_runtime_has_playbook() -> None:
"""Tests has_playbook method."""
runtime = Runtime(require_module=True)

runtime.prepare_environment(
required_collections={"community.molecule": "0.1.0"},
install_local=True,
)

assert not runtime.has_playbook("this-does-not-exist.yml")
# call twice to ensure cache is used:
assert not runtime.has_playbook("this-does-not-exist.yml")
Expand Down
45 changes: 18 additions & 27 deletions test/test_runtime_scan_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import json
import textwrap
from dataclasses import dataclass, fields
from pathlib import Path

import pytest
Expand All @@ -19,26 +18,11 @@
V2_COLLECTION_FULL_NAME = f"{V2_COLLECTION_NAMESPACE}.{V2_COLLECTION_NAME}"


@dataclass
class ScanSysPath:
"""Parameters for scan tests."""

scan: bool
raises_not_found: bool

def __str__(self) -> str:
"""Return a string representation of the object."""
parts = [
f"{field.name}{str(getattr(self, field.name))[0]}" for field in fields(self)
]
return "-".join(parts)


@pytest.mark.parametrize(
("param"),
("scan", "raises_not_found"),
(
ScanSysPath(scan=False, raises_not_found=True),
ScanSysPath(scan=True, raises_not_found=False),
pytest.param(False, True, id="0"),
pytest.param(True, False, id="1"),
),
ids=str,
)
Expand All @@ -47,16 +31,23 @@ def test_scan_sys_path(
monkeypatch: MonkeyPatch,
runtime_tmp: Runtime,
tmp_path: Path,
param: ScanSysPath,
scan: bool,
raises_not_found: bool,
) -> None:
"""Confirm sys path is scanned for collections.
:param venv_module: Fixture for a virtual environment
:param monkeypatch: Fixture for monkeypatching
:param runtime_tmp: Fixture for a Runtime object
:param tmp_dir: Fixture for a temporary directory
:param param: The parameters for the test
Args:
venv_module: Fixture for a virtual environment
monkeypatch: Fixture for monkeypatching
runtime_tmp: Fixture for a Runtime object
tmp_path: Fixture for a temporary directory
scan: Whether to scan the sys path
raises_not_found: Whether the collection is expected to be found
"""
# Isolated the test from the others, so ansible will not find collections
# that might be installed by other tests.
monkeypatch.setenv("VIRTUAL_ENV", venv_module.project.as_posix())
monkeypatch.setenv("ANSIBLE_HOME", tmp_path.as_posix())
first_site_package_dir = venv_module.site_package_dirs()[0]

installed_to = (
Expand All @@ -76,7 +67,7 @@ def test_scan_sys_path(
# Confirm the collection is installed
assert installed_to.exists()
# Set the sys scan path environment variable
monkeypatch.setenv("ANSIBLE_COLLECTIONS_SCAN_SYS_PATH", str(param.scan))
monkeypatch.setenv("ANSIBLE_COLLECTIONS_SCAN_SYS_PATH", str(scan))
# Set the ansible collections paths to avoid bleed from other tests
monkeypatch.setenv("ANSIBLE_COLLECTIONS_PATH", str(tmp_path))

Expand All @@ -91,7 +82,7 @@ def test_scan_sys_path(
)

proc = venv_module.python_script_run(script)
if param.raises_not_found:
if raises_not_found:
assert proc.returncode != 0, (proc.stdout, proc.stderr)
assert "InvalidPrerequisiteError" in proc.stderr
assert "'community.molecule' not found" in proc.stderr
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ passenv =
LANG
LC_*
setenv =
ANSIBLE_HOME = {envdir}/.ansible
ANSIBLE_DEVEL_WARNING='false'
COVERAGE_FILE = {env:COVERAGE_FILE:{envdir}/.coverage.{envname}}
COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
Expand Down

0 comments on commit 98425dc

Please sign in to comment.