diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11c94d10..b05de88d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,10 +23,10 @@ jobs: TOX_PARALLEL_NO_SPINNER: 1 steps: - - name: Switch to using Python 3.9 by default - uses: actions/setup-python@v4 + - name: Switch to using Python 3.12 by default + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.12" - name: Install tox run: python3 -m pip install --user "tox>=4.0.0" - name: Check out src from Git diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 6079cddc..f56f2663 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -24,6 +24,7 @@ jobs: with: min_python: "3.9" max_python: "3.12" + default_python: "3.10" other_names: | lint docs @@ -32,7 +33,10 @@ jobs: py39-ansible213 py39-ansible214 py39-ansible215 - py311-devel + py310-ansible215 + py311-ansible215 + py312-ansible216 + py312-devel smoke platforms: linux,macos macos: minmax @@ -53,7 +57,7 @@ jobs: fetch-depth: 0 # needed by setuptools-scm - name: Set up Python ${{ matrix.python_version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 645ea7d4..14f411dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,19 +9,20 @@ ci: for more information, see https://pre-commit.ci skip: # https://github.com/pre-commit-ci/issues/issues/55 + - ccv - pip-compile # No docker on pre-commit.ci - validate-config-in-container default_language_version: # Needed in order to make pip-compile output predictable. - python: python3.9 + python: python3.10 exclude: | (?x)^( test/assets/.* )$ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.0.291" + rev: "v0.1.9" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -35,7 +36,7 @@ repos: - prettier-plugin-toml - prettier-plugin-sort-json - repo: https://github.com/pre-commit/pre-commit-hooks.git - rev: v4.4.0 + rev: v4.5.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace @@ -51,23 +52,23 @@ repos: - id: debug-statements language_version: python3 - repo: https://github.com/codespell-project/codespell - rev: v2.2.5 + rev: v2.2.6 hooks: - id: codespell - repo: https://github.com/adrienverge/yamllint.git - rev: v1.32.0 + rev: v1.33.0 hooks: - id: yamllint files: \.(yaml|yml)$ types: [file, yaml] entry: yamllint --strict - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.12.1 hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.8.0 hooks: - id: mypy # empty args needed in order to match mypy cli behavior @@ -84,7 +85,7 @@ repos: - types-pkg_resources - types-jsonschema>=4.4.9 - repo: https://github.com/pycqa/pylint - rev: v3.0.0b0 + rev: v3.0.3 hooks: - id: pylint additional_dependencies: @@ -117,4 +118,10 @@ repos: rev: v1.2.0 hooks: - id: validate-config-in-container + name: packit alias: packit + - repo: https://github.com/mashi/codecov-validator + rev: "1.0.1" + hooks: + - id: ccv + name: codecov diff --git a/.vscode/settings.json b/.vscode/settings.json index d9cc730e..990033df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,9 +4,9 @@ }, "[python]": { "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.fixAll.ruff": false, - "source.organizeImports": false + "source.fixAll": "explicit", + "source.fixAll.ruff": "never", + "source.organizeImports": "never" } }, "editor.formatOnSave": true, diff --git a/codecov.yml b/codecov.yml index 9eca5046..0ba95168 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,6 +3,4 @@ codecov: comment: false coverage: status: - patch: false - project: - threshold: 0.5% + patch: true # we want github annotations diff --git a/requirements.txt b/requirements.txt index 37e64478..6610cc5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --extra=docs --extra=test --output-file=requirements.txt --strip-extras --unsafe-package=ansible-core --unsafe-package=resolvelib --unsafe-package=typing_extensions pyproject.toml @@ -68,7 +68,7 @@ defusedxml==0.7.1 # via # cairosvg # mkdocs-ansible -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 # via pytest ghp-import==2.1.0 # via @@ -87,14 +87,7 @@ idna==3.4 # mkdocs-ansible # requests importlib-metadata==6.8.0 - # via - # build - # markdown - # mkdocs - # mkdocs-ansible - # mkdocstrings -importlib-resources==5.0.7 - # via ansible-core + # via mkdocs-ansible iniconfig==2.0.0 # via pytest jinja2==3.1.2 @@ -147,7 +140,9 @@ mkdocs==1.5.3 # mkdocs-monorepo-plugin # mkdocstrings mkdocs-ansible==0.2.0 - # via ansible-compat (pyproject.toml) + # via + # ansible-compat (pyproject.toml) + # mkdocs-ansible mkdocs-autorefs==0.5.0 # via # mkdocs-ansible @@ -300,12 +295,10 @@ tomli==2.0.1 # pip-tools # pyproject-hooks # pytest -typing-extensions==4.8.0 ; python_version < "3.10" +typing-extensions==4.8.0 # via - # ansible-compat (pyproject.toml) # black # mkdocs-ansible - # mkdocstrings urllib3==2.0.5 # via # mkdocs-ansible diff --git a/src/ansible_compat/config.py b/src/ansible_compat/config.py index 45300d39..a0b41b75 100644 --- a/src/ansible_compat/config.py +++ b/src/ansible_compat/config.py @@ -216,6 +216,11 @@ class AnsibleConfig(UserDict[str, object]): # pylint: disable=too-many-ancestor default_private_role_vars: bool = False default_remote_port: str | None = None default_remote_user: str | None = None + # https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths + default_collections_path: list[str] = [ + "~/.ansible/collections", + "/usr/share/ansible/collections", + ] default_roles_path: list[str] = [ "~/.ansible/roles", "/usr/share/ansible/roles", diff --git a/src/ansible_compat/runtime.py b/src/ansible_compat/runtime.py index badb449b..ad811324 100644 --- a/src/ansible_compat/runtime.py +++ b/src/ansible_compat/runtime.py @@ -10,7 +10,6 @@ import shutil import subprocess import sys -import tempfile import warnings from collections import OrderedDict from dataclasses import dataclass, field @@ -457,7 +456,7 @@ def install_collection( if isinstance(collection, Path): collection = str(collection) # As ansible-galaxy install is not able to automatically determine - # if the range requires a pre-release, we need to manuall add the --pre + # if the range requires a pre-release, we need to manually add the --pre # flag when needed. matches = version_re.search(collection) @@ -477,13 +476,13 @@ def install_collection( cmd.append(f"{collection}") _logger.info("Running from %s : %s", Path.cwd(), " ".join(cmd)) - run = self.run( + process = self.run( cmd, retry=True, env={**self.environ, ansible_collections_path(): ":".join(cpaths)}, ) - if run.returncode != 0: - msg = f"Command returned {run.returncode} code:\n{run.stdout}\n{run.stderr}" + if process.returncode != 0: + msg = f"Command returned {process.returncode} code:\n{process.stdout}\n{process.stderr}" _logger.error(msg) raise InvalidPrerequisiteError(msg) @@ -493,30 +492,7 @@ def install_collection_from_disk( destination: Path | None = None, ) -> None: """Build and install collection from a given disk path.""" - if not self.version_in_range(upper="2.11"): - self.install_collection(path, destination=destination, force=True) - return - # older versions of ansible able unable to install without building - with tempfile.TemporaryDirectory() as tmp_dir: - cmd = [ - "ansible-galaxy", - "collection", - "build", - "--output-path", - str(tmp_dir), - str(path), - ] - _logger.info("Running %s", " ".join(cmd)) - run = self.run(cmd, retry=False) - if run.returncode != 0: - _logger.error(run.stdout) - raise AnsibleCommandError(run) - for archive_file in os.listdir(tmp_dir): - self.install_collection( - str(Path(tmp_dir) / archive_file), - destination=destination, - force=True, - ) + self.install_collection(path, destination=destination, force=True) # pylint: disable=too-many-branches def install_requirements( # noqa: C901 @@ -664,14 +640,14 @@ def prepare_environment( # noqa: C901 destination=destination, ) - if Path("galaxy.yml").exists(): + if (self.project_dir / "galaxy.yml").exists(): if destination: # while function can return None, that would not break the logic colpath = Path( - f"{destination}/ansible_collections/{colpath_from_path(Path.cwd())}", + f"{destination}/ansible_collections/{colpath_from_path(self.project_dir)}", ) if colpath.is_symlink(): - if os.path.realpath(colpath) == Path.cwd(): + if os.path.realpath(colpath) == str(Path.cwd()): _logger.warning( "Found symlinked collection, skipping its installation.", ) @@ -791,7 +767,7 @@ def _prepare_ansible_paths(self) -> None: msg = "Unexpected ansible configuration" raise RuntimeError(msg) from exc - alterations_list = [ + alterations_list: list[tuple[list[str], str, bool]] = [ (library_paths, "plugins/modules", True), (roles_path, "roles", True), ] @@ -812,12 +788,12 @@ def _prepare_ansible_paths(self) -> None: if must_be_present: continue path.mkdir(parents=True, exist_ok=True) - if path not in path_list: + if str(path) not in path_list: path_list.insert(0, str(path)) if library_paths != self.config.DEFAULT_MODULE_PATH: self._update_env("ANSIBLE_LIBRARY", library_paths) - if collections_path != self.config.collections_paths: + if collections_path != self.config.default_collections_path: self._update_env(ansible_collections_path(), collections_path) if roles_path != self.config.default_roles_path: self._update_env("ANSIBLE_ROLES_PATH", roles_path) @@ -960,7 +936,10 @@ def _get_galaxy_role_ns(galaxy_infos: dict[str, Any]) -> str: def _get_galaxy_role_name(galaxy_infos: dict[str, Any]) -> str: """Compute role name from meta/main.yml.""" - return galaxy_infos.get("role_name", "") + result = galaxy_infos.get("role_name", "") + if not isinstance(result, str): + return "" + return result def search_galaxy_paths(search_dir: Path) -> list[str]: diff --git a/test/collections/acme.minimal/galaxy.yml b/test/collections/acme.minimal/galaxy.yml new file mode 100644 index 00000000..a15e4184 --- /dev/null +++ b/test/collections/acme.minimal/galaxy.yml @@ -0,0 +1,30 @@ +name: minimal +namespace: acme +version: 1.0.0 +readme: README.md +authors: + - Red Hat +description: Sample collection to use with molecule +build_ignore: + - "*.egg-info" + - .DS_Store + - .eggs + - .gitignore + - .mypy_cache + - .pytest_cache + - .stestr + - .stestr.conf + - .tox + - .vscode + - MANIFEST.in + - build + - dist + - doc + - report.html + - setup.cfg + - setup.py + - "tests/unit/*.*" + - README.rst + - tox.ini + +license_file: LICENSE diff --git a/test/test_runtime.py b/test/test_runtime.py index 2b7de935..2af343d6 100644 --- a/test/test_runtime.py +++ b/test/test_runtime.py @@ -24,6 +24,7 @@ from ansible_compat.runtime import ( CompletedProcess, Runtime, + _get_galaxy_role_name, is_url, search_galaxy_paths, ) @@ -861,3 +862,32 @@ def test_galaxy_path(path: str, result: list[str]) -> None: def test_is_url(name: str, result: bool) -> None: """Checks functionality of is_url.""" assert is_url(name) == result + + +def test_prepare_environment_repair_broken_symlink( + caplog: pytest.LogCaptureFixture, +) -> None: + """Ensure we can deal with broken symlinks in collections.""" + caplog.set_level(logging.INFO) + project_dir = Path(__file__).parent / "collections" / "acme.minimal" + runtime = Runtime(isolated=True, project_dir=project_dir) + assert runtime.cache_dir + acme = runtime.cache_dir / "collections" / "ansible_collections" / "acme" + acme.mkdir(parents=True, exist_ok=True) + goodies = acme / "minimal" + rmtree(goodies, ignore_errors=True) + goodies.unlink(missing_ok=True) + goodies.symlink_to("/invalid/destination") + runtime.prepare_environment(install_local=True) + assert any( + msg.startswith("Collection is symlinked, but not pointing to") + for msg in caplog.messages + ) + + +def test_get_galaxy_role_name_invalid() -> None: + """Verifies that function returns empty string on invalid input.""" + galaxy_infos = { + "role_name": False, # <-- invalid data, should be string + } + assert _get_galaxy_role_name(galaxy_infos) == "" diff --git a/tox.ini b/tox.ini index e6c42071..88ce66a1 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,22 @@ envlist = lint pkg docs - # matrix assumed current (implicit) is 2.13: - py{39,310,311}{,-devel,-ansible212,-ansible213,-ansible214,-ansible215} + py + py-devel + py39-ansible212 + py39-ansible213 + py39-ansible214 + py39-ansible215 + py310-ansible212 + py310-ansible213 + py310-ansible214 + py310-ansible215 + py311-ansible212 + py311-ansible213 + py311-ansible214 + py311-ansible215 + py312-ansible216 + isolated_build = true skip_missing_interpreters = True requires = @@ -14,18 +28,20 @@ requires = [testenv] description = - Run the tests with {basepython} + Run the tests devel: ansible devel branch ansible212: ansible-core 2.12 ansible213: ansible-core 2.13 ansible214: ansible-core 2.14 ansible215: ansible-core 2.15 + ansible216: ansible-core 2.16 deps = ansible212: ansible-core>=2.12,<2.13 ansible213: ansible-core>=2.13,<2.14 ansible214: ansible-core>=2.14,<2.15 ansible215: ansible-core>=2.15,<2.16 + ansible216: ansible-core>=2.16,<2.17 devel: ansible-core @ git+https://github.com/ansible/ansible.git@c5d18c39d81e2b3b10856b2fb76747230e4fac4a # GPLv3+ # avoid installing ansible-core on -devel envs: @@ -71,7 +87,7 @@ setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 PIP_CONSTRAINT = {toxinidir}/requirements.txt PRE_COMMIT_COLOR = always - PYTEST_REQPASS = 91 + PYTEST_REQPASS = 93 FORCE_COLOR = 1 allowlist_externals = ansible @@ -83,7 +99,7 @@ package = editable [testenv:lint] description = Run all linters # locked basepython is needed because to keep constrains.txt predictable -basepython = python3.9 +basepython = python3.10 deps = pre-commit>=2.6.0 skip_install = true