porringer 0.2.1.dev87__tar.gz → 0.2.1.dev89__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/PKG-INFO +3 -3
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/execution.py +8 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/package.py +98 -7
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/tool_based.py +14 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pip/plugin.py +34 -3
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/tests.py +73 -2
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/pyproject.toml +8 -7
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/conftest.py +29 -0
- porringer-0.2.1.dev89/tests/integration/test_bare_environment.py +49 -0
- porringer-0.2.1.dev89/tests/unit/plugins/pip/conftest.py +7 -0
- porringer-0.2.1.dev89/tests/unit/plugins/pip/test_auxiliary_tools.py +96 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_backend_resolver.py +2 -2
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_wsl.py +231 -2
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/LICENSE.md +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/README.md +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/api.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/backend.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/builder.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/cache.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/action_builder.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/discovery.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/phase.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/presence.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/resolution.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/wsl_overlay.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/manifest.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/self.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/sync.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/resolver.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/schema.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/cache.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/check.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/download.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/package.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/schema.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/self.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/sync.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/entry.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/schema.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/path.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/manifest.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/plugin_manager.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/project_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/python_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/runtime.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/scm.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/schema.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/transport.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/apt/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/apt/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/brew/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/brew/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun_project/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno_project/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/git/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/git/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm_project/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pdm/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pdm/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pim/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pim/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pip/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pipx/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pipx/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm_project/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/poetry/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/poetry/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pyenv/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pyenv/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv_project/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/winget/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/winget/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/wsl/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/wsl/transport.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/wsl/utility.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/yarn_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/yarn_project/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/py.typed +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/cache.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/check.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/config.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/download.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/execution.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/manifest.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/progress.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/plugin_manager.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/project_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/scm.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/subprocess.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/shared.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/variants.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/download.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/exception.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/py.typed +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/utility.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/api.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/http.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/manifests.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/mock_plugins.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/packages.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/git/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/git/test_scm.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pip/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pip/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pipx/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pipx/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/winget/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/winget/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_bootstrap_presence.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_example_bootstrap.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_example_presence.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_frozen_app_presence.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/apt/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/apt/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/brew/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/brew/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun_project/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno_project/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/git/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/git/test_clone_detection.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/git/test_scm.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm_project/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pdm/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pdm/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pim/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pim/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pip/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pip/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pipx/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pipx/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm_project/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/poetry/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/poetry/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pyenv/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pyenv/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv_project/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/yarn_project/__init__.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/yarn_project/test_environment.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_bootstrap_cross_platform.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_cache.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_check.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_check_updates.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_cli.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_command_plugin.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_command_self.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_concurrency_and_client.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_deferred_resolution.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_event_loop_safety.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_extras_introspection.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_frozen_app_detection.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_frozen_presence_resolution.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_manifest.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_package_filtering.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_package_ref.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_package_relation.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_path_sync.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_plugin_filtering.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_plugin_manager.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_presence_detection.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_project_directory.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_project_root.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_runtime_context_seeding.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_runtime_propagation.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_runtime_updates.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_sub_action_progress.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_uninstall.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_update_detection.py +0 -0
- {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: porringer
|
|
3
|
-
Version: 0.2.1.
|
|
3
|
+
Version: 0.2.1.dev89
|
|
4
4
|
Author-Email: Synodic Software <contact@synodic.software>
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: homepage, https://github.com/synodic/porringer
|
|
@@ -8,10 +8,10 @@ Project-URL: repository, https://github.com/synodic/porringer
|
|
|
8
8
|
Requires-Python: >=3.14
|
|
9
9
|
Requires-Dist: typer[all]>=0.24.1
|
|
10
10
|
Requires-Dist: pydantic>=2.12.5
|
|
11
|
-
Requires-Dist: platformdirs>=4.9.
|
|
11
|
+
Requires-Dist: platformdirs>=4.9.6
|
|
12
12
|
Requires-Dist: userpath>=1.9.2
|
|
13
13
|
Requires-Dist: packaging>=26.0
|
|
14
|
-
Requires-Dist: aiohttp>=3.13.
|
|
14
|
+
Requires-Dist: aiohttp>=3.13.5
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
@@ -688,6 +688,14 @@ async def execute_uninstall(
|
|
|
688
688
|
if package_cache is not None:
|
|
689
689
|
ctx = replace(ctx, package_cache=package_cache)
|
|
690
690
|
|
|
691
|
+
# --- Per-distro WSL runtime context -----------------------------------
|
|
692
|
+
if (
|
|
693
|
+
action.distro is not None
|
|
694
|
+
and ctx.wsl_runtime_contexts
|
|
695
|
+
and (wsl_ctx := ctx.wsl_runtime_contexts.get(action.distro)) is not None
|
|
696
|
+
):
|
|
697
|
+
ctx = replace(ctx, runtime_context=wsl_ctx)
|
|
698
|
+
|
|
691
699
|
resolved = await resolve_uninstall_operation(
|
|
692
700
|
action,
|
|
693
701
|
environments,
|
|
@@ -14,7 +14,7 @@ import builtins
|
|
|
14
14
|
import logging
|
|
15
15
|
from collections.abc import Awaitable, Callable
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import Any, cast
|
|
18
18
|
|
|
19
19
|
from packaging.utils import canonicalize_name
|
|
20
20
|
from packaging.version import Version
|
|
@@ -30,8 +30,9 @@ from porringer.backend.command.core.resolution import (
|
|
|
30
30
|
resolve_uninstall_operation,
|
|
31
31
|
resolved_to_result,
|
|
32
32
|
)
|
|
33
|
+
from porringer.backend.command.core.wsl_overlay import overlay_wsl_plugin, wsl_transport_for
|
|
33
34
|
from porringer.core.plugin_schema.environment import CheckUpdatesParameters, Environment
|
|
34
|
-
from porringer.core.plugin_schema.runtime import RuntimeConsumer, RuntimeContext
|
|
35
|
+
from porringer.core.plugin_schema.runtime import RuntimeConsumer, RuntimeContext, RuntimeProvider
|
|
35
36
|
from porringer.core.schema import Package, PackageRef
|
|
36
37
|
from porringer.schema import (
|
|
37
38
|
CheckParameters,
|
|
@@ -74,6 +75,7 @@ async def _imperative_action(
|
|
|
74
75
|
dry_run: bool,
|
|
75
76
|
resolve_fn: _ResolveFn,
|
|
76
77
|
execute_fn: _ExecuteFn,
|
|
78
|
+
distro: str | None = None,
|
|
77
79
|
) -> SetupActionResult:
|
|
78
80
|
"""Shared skeleton for imperative package operations.
|
|
79
81
|
|
|
@@ -114,10 +116,19 @@ async def _imperative_action(
|
|
|
114
116
|
installer=plugin_name,
|
|
115
117
|
package=package,
|
|
116
118
|
runtime_tag=runtime_tag,
|
|
119
|
+
distro=distro,
|
|
117
120
|
)
|
|
118
121
|
|
|
119
122
|
ctx = ResolutionContext(runtime_context=runtime_context)
|
|
120
123
|
|
|
124
|
+
if distro is not None:
|
|
125
|
+
wsl_ctx = await _resolve_wsl_runtime_context(environments, distro)
|
|
126
|
+
if wsl_ctx is not None:
|
|
127
|
+
ctx = ResolutionContext(
|
|
128
|
+
runtime_context=runtime_context,
|
|
129
|
+
wsl_runtime_contexts={distro: wsl_ctx},
|
|
130
|
+
)
|
|
131
|
+
|
|
121
132
|
if dry_run:
|
|
122
133
|
resolved = await resolve_fn(action, environments, ctx)
|
|
123
134
|
return resolved_to_result(resolved)
|
|
@@ -147,6 +158,42 @@ async def _discover_with_runtime(runtime_context: RuntimeContext | None) -> Disc
|
|
|
147
158
|
_discover_environments = discover_environments
|
|
148
159
|
|
|
149
160
|
|
|
161
|
+
async def _resolve_wsl_runtime_context(
|
|
162
|
+
environments: dict[str, Environment],
|
|
163
|
+
distro: str,
|
|
164
|
+
) -> RuntimeContext | None:
|
|
165
|
+
"""Resolve the interpreter path inside a WSL distro.
|
|
166
|
+
|
|
167
|
+
Scans *environments* for :class:`RuntimeProvider` plugins, wraps
|
|
168
|
+
each with a :class:`WslTransport`, and attempts to resolve the
|
|
169
|
+
default executable inside the target distro.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A :class:`RuntimeContext` mapping the provider's kind to the
|
|
173
|
+
resolved executable, or ``None`` when no provider succeeds.
|
|
174
|
+
"""
|
|
175
|
+
transport = wsl_transport_for(distro)
|
|
176
|
+
if transport is None:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
for env in environments.values():
|
|
180
|
+
if not isinstance(env, RuntimeProvider):
|
|
181
|
+
continue
|
|
182
|
+
wsl_env = cast(RuntimeProvider, env.with_transport(transport))
|
|
183
|
+
kind = cast(type[RuntimeProvider], type(env)).provided_runtime_kind()
|
|
184
|
+
try:
|
|
185
|
+
tag = await wsl_env.default_tag()
|
|
186
|
+
except Exception:
|
|
187
|
+
continue
|
|
188
|
+
if tag is None:
|
|
189
|
+
continue
|
|
190
|
+
executable = await wsl_env.resolve_executable(tag)
|
|
191
|
+
if executable is not None:
|
|
192
|
+
return RuntimeContext(executables={kind: executable})
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
150
197
|
async def _build_update_infos(
|
|
151
198
|
env: Any,
|
|
152
199
|
check_params: CheckUpdatesParameters,
|
|
@@ -199,6 +246,7 @@ class PackageCommands:
|
|
|
199
246
|
*,
|
|
200
247
|
plugins: DiscoveredPlugins | None = None,
|
|
201
248
|
runtime_context: RuntimeContext | None = None,
|
|
249
|
+
distro: str | None = None,
|
|
202
250
|
) -> builtins.list[Package]:
|
|
203
251
|
"""List packages installed in a plugin's environment.
|
|
204
252
|
|
|
@@ -215,6 +263,9 @@ class PackageCommands:
|
|
|
215
263
|
queries the global / default environment.
|
|
216
264
|
plugins: Pre-discovered plugins.
|
|
217
265
|
runtime_context: Pre-resolved runtime context.
|
|
266
|
+
distro: Target WSL2 distribution name. When set,
|
|
267
|
+
the plugin is wrapped with a :class:`WslTransport`
|
|
268
|
+
so that package queries execute inside the distro.
|
|
218
269
|
|
|
219
270
|
Returns:
|
|
220
271
|
The packages managed by the named plugin.
|
|
@@ -222,7 +273,7 @@ class PackageCommands:
|
|
|
222
273
|
Raises:
|
|
223
274
|
PluginError: If the plugin is not found.
|
|
224
275
|
"""
|
|
225
|
-
logger.debug('Listing packages for plugin: %s', plugin_name)
|
|
276
|
+
logger.debug('Listing packages for plugin: %s (distro=%s)', plugin_name, distro)
|
|
226
277
|
|
|
227
278
|
if plugins is not None:
|
|
228
279
|
environments = plugins.environments
|
|
@@ -239,6 +290,14 @@ class PackageCommands:
|
|
|
239
290
|
available = sorted(environments.keys())
|
|
240
291
|
raise PluginError(f"Plugin '{plugin_name}' not found. Available: {', '.join(available)}")
|
|
241
292
|
|
|
293
|
+
if distro is not None:
|
|
294
|
+
environments = overlay_wsl_plugin(environments, key, distro)
|
|
295
|
+
env = environments[key]
|
|
296
|
+
if isinstance(env, RuntimeConsumer):
|
|
297
|
+
wsl_ctx = await _resolve_wsl_runtime_context(environments, distro)
|
|
298
|
+
if wsl_ctx is not None:
|
|
299
|
+
runtime_context = wsl_ctx
|
|
300
|
+
|
|
242
301
|
if not env.query_availability(runtime_context):
|
|
243
302
|
logger.debug("Plugin '%s' is not available; returning empty package list", plugin_name)
|
|
244
303
|
return []
|
|
@@ -329,6 +388,7 @@ class PackageCommands:
|
|
|
329
388
|
skip_global: bool = False,
|
|
330
389
|
plugins: DiscoveredPlugins | None = None,
|
|
331
390
|
runtime_context: RuntimeContext | None = None,
|
|
391
|
+
distro: str | None = None,
|
|
332
392
|
) -> builtins.list[ScopedPackage]:
|
|
333
393
|
"""List packages across multiple scopes (global + per-directory).
|
|
334
394
|
|
|
@@ -342,6 +402,7 @@ class PackageCommands:
|
|
|
342
402
|
skip_global: When ``True``, skip the global (``project_path=None``) scope.
|
|
343
403
|
plugins: Pre-discovered plugins.
|
|
344
404
|
runtime_context: Pre-resolved runtime context.
|
|
405
|
+
distro: Target WSL2 distribution name.
|
|
345
406
|
|
|
346
407
|
Returns:
|
|
347
408
|
A flat list of scoped packages across all queried scopes.
|
|
@@ -364,6 +425,7 @@ class PackageCommands:
|
|
|
364
425
|
project_path=project_path,
|
|
365
426
|
plugins=plugins,
|
|
366
427
|
runtime_context=runtime_context,
|
|
428
|
+
distro=distro,
|
|
367
429
|
)
|
|
368
430
|
return [
|
|
369
431
|
ScopedPackage(
|
|
@@ -396,6 +458,7 @@ class PackageCommands:
|
|
|
396
458
|
project_path: Path | None = None,
|
|
397
459
|
plugins: DiscoveredPlugins | None = None,
|
|
398
460
|
runtime_context: RuntimeContext | None = None,
|
|
461
|
+
distro: str | None = None,
|
|
399
462
|
) -> tuple[builtins.list[Package], set[str]]:
|
|
400
463
|
"""List installed packages with manifest cross-referencing.
|
|
401
464
|
|
|
@@ -410,6 +473,7 @@ class PackageCommands:
|
|
|
410
473
|
project_path: Path to the project directory.
|
|
411
474
|
plugins: Pre-discovered plugins.
|
|
412
475
|
runtime_context: Pre-resolved runtime context.
|
|
476
|
+
distro: Target WSL2 distribution name.
|
|
413
477
|
|
|
414
478
|
Returns:
|
|
415
479
|
A ``(packages, declared_names)`` tuple. *declared_names*
|
|
@@ -429,6 +493,7 @@ class PackageCommands:
|
|
|
429
493
|
project_path=project_path,
|
|
430
494
|
plugins=plugins,
|
|
431
495
|
runtime_context=runtime_context,
|
|
496
|
+
distro=distro,
|
|
432
497
|
)
|
|
433
498
|
|
|
434
499
|
return installed, declared_names
|
|
@@ -444,6 +509,7 @@ class PackageCommands:
|
|
|
444
509
|
plugins: DiscoveredPlugins | None = None,
|
|
445
510
|
runtime_context: RuntimeContext | None = None,
|
|
446
511
|
dry_run: bool = False,
|
|
512
|
+
distro: str | None = None,
|
|
447
513
|
) -> SetupActionResult:
|
|
448
514
|
"""Install a package if it is not already present.
|
|
449
515
|
|
|
@@ -457,6 +523,8 @@ class PackageCommands:
|
|
|
457
523
|
plugins: Pre-discovered plugins.
|
|
458
524
|
runtime_context: Optional resolved runtime paths.
|
|
459
525
|
dry_run: When ``True``, resolve only.
|
|
526
|
+
distro: Target WSL2 distribution name. When set,
|
|
527
|
+
the action executes inside the named distro.
|
|
460
528
|
|
|
461
529
|
Returns:
|
|
462
530
|
A ``SetupActionResult`` describing the outcome.
|
|
@@ -473,6 +541,7 @@ class PackageCommands:
|
|
|
473
541
|
execute_fn=lambda action, envs, queue, ctx: execute_package(
|
|
474
542
|
action, envs, SyncStrategy.MINIMAL, queue, context=ctx
|
|
475
543
|
),
|
|
544
|
+
distro=distro,
|
|
476
545
|
)
|
|
477
546
|
|
|
478
547
|
@staticmethod
|
|
@@ -484,6 +553,7 @@ class PackageCommands:
|
|
|
484
553
|
plugins: DiscoveredPlugins | None = None,
|
|
485
554
|
runtime_context: RuntimeContext | None = None,
|
|
486
555
|
dry_run: bool = False,
|
|
556
|
+
distro: str | None = None,
|
|
487
557
|
) -> SetupActionResult:
|
|
488
558
|
"""Upgrade (or install) a single package to its latest version.
|
|
489
559
|
|
|
@@ -497,6 +567,8 @@ class PackageCommands:
|
|
|
497
567
|
plugins: Pre-discovered plugins.
|
|
498
568
|
runtime_context: Optional resolved runtime paths.
|
|
499
569
|
dry_run: When ``True``, resolve only.
|
|
570
|
+
distro: Target WSL2 distribution name. When set,
|
|
571
|
+
the action executes inside the named distro.
|
|
500
572
|
|
|
501
573
|
Returns:
|
|
502
574
|
A ``SetupActionResult`` describing the outcome.
|
|
@@ -513,6 +585,7 @@ class PackageCommands:
|
|
|
513
585
|
execute_fn=lambda action, envs, queue, ctx: execute_package(
|
|
514
586
|
action, envs, SyncStrategy.LATEST, queue, context=ctx
|
|
515
587
|
),
|
|
588
|
+
distro=distro,
|
|
516
589
|
)
|
|
517
590
|
|
|
518
591
|
@staticmethod
|
|
@@ -524,6 +597,7 @@ class PackageCommands:
|
|
|
524
597
|
plugins: DiscoveredPlugins | None = None,
|
|
525
598
|
runtime_context: RuntimeContext | None = None,
|
|
526
599
|
dry_run: bool = False,
|
|
600
|
+
distro: str | None = None,
|
|
527
601
|
) -> SetupActionResult:
|
|
528
602
|
"""Uninstall a managed package.
|
|
529
603
|
|
|
@@ -537,6 +611,8 @@ class PackageCommands:
|
|
|
537
611
|
plugins: Pre-discovered plugins.
|
|
538
612
|
runtime_context: Optional resolved runtime paths.
|
|
539
613
|
dry_run: When ``True``, resolve only.
|
|
614
|
+
distro: Target WSL2 distribution name. When set,
|
|
615
|
+
the action executes inside the named distro.
|
|
540
616
|
|
|
541
617
|
Returns:
|
|
542
618
|
A ``SetupActionResult`` describing the outcome.
|
|
@@ -551,6 +627,7 @@ class PackageCommands:
|
|
|
551
627
|
dry_run=dry_run,
|
|
552
628
|
resolve_fn=resolve_uninstall_operation,
|
|
553
629
|
execute_fn=lambda action, envs, queue, ctx: execute_uninstall(action, envs, queue, context=ctx),
|
|
630
|
+
distro=distro,
|
|
554
631
|
)
|
|
555
632
|
|
|
556
633
|
# --- Update checking ---
|
|
@@ -560,6 +637,7 @@ class PackageCommands:
|
|
|
560
637
|
params: CheckParameters | None = None,
|
|
561
638
|
*,
|
|
562
639
|
plugins: DiscoveredPlugins | None = None,
|
|
640
|
+
distro: str | None = None,
|
|
563
641
|
) -> builtins.list[CheckResult]:
|
|
564
642
|
"""Check for package updates across all (or selected) plugins.
|
|
565
643
|
|
|
@@ -567,6 +645,9 @@ class PackageCommands:
|
|
|
567
645
|
params: Optional check parameters (plugin filter,
|
|
568
646
|
pre-release flag).
|
|
569
647
|
plugins: Pre-discovered plugins.
|
|
648
|
+
distro: Target WSL2 distribution name. When set,
|
|
649
|
+
each plugin is wrapped with a :class:`WslTransport`
|
|
650
|
+
so that update checks execute inside the distro.
|
|
570
651
|
|
|
571
652
|
Returns:
|
|
572
653
|
One :class:`CheckResult` per queried plugin.
|
|
@@ -581,14 +662,24 @@ class PackageCommands:
|
|
|
581
662
|
if runtime_context is None:
|
|
582
663
|
runtime_context = await Builder.resolve_runtime_context(plugins.environments)
|
|
583
664
|
|
|
665
|
+
environments: dict[str, Environment] = dict(plugins.environments)
|
|
666
|
+
|
|
667
|
+
transport = wsl_transport_for(distro) if distro is not None else None
|
|
668
|
+
if distro is not None:
|
|
669
|
+
wsl_ctx = await _resolve_wsl_runtime_context(environments, distro)
|
|
670
|
+
if wsl_ctx is not None:
|
|
671
|
+
runtime_context = wsl_ctx
|
|
672
|
+
|
|
584
673
|
results: builtins.list[CheckResult] = []
|
|
585
674
|
|
|
586
|
-
for name, env in
|
|
675
|
+
for name, env in environments.items():
|
|
587
676
|
if params.plugins and name not in params.plugins:
|
|
588
677
|
continue
|
|
589
678
|
|
|
590
|
-
|
|
591
|
-
|
|
679
|
+
target_env = env.with_transport(transport) if transport else env
|
|
680
|
+
|
|
681
|
+
plugin_type = type(target_env)
|
|
682
|
+
if not plugin_type.is_supported() or not target_env.query_availability(runtime_context):
|
|
592
683
|
logger.debug('Skipping unavailable plugin %s for update check', name)
|
|
593
684
|
continue
|
|
594
685
|
|
|
@@ -598,7 +689,7 @@ class PackageCommands:
|
|
|
598
689
|
include_prereleases=params.include_prereleases,
|
|
599
690
|
runtime_context=runtime_context,
|
|
600
691
|
)
|
|
601
|
-
package_infos = await _build_update_infos(
|
|
692
|
+
package_infos = await _build_update_infos(target_env, check_params, runtime_context)
|
|
602
693
|
results.append(CheckResult(plugin=name, packages=package_infos))
|
|
603
694
|
|
|
604
695
|
except (PluginError, UpdateError) as e:
|
|
@@ -19,6 +19,7 @@ import logging
|
|
|
19
19
|
import re
|
|
20
20
|
import shutil
|
|
21
21
|
import subprocess
|
|
22
|
+
from collections.abc import Sequence
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import Any, Self
|
|
24
25
|
|
|
@@ -39,6 +40,19 @@ class ToolBasedPlugin(Plugin):
|
|
|
39
40
|
considered available.
|
|
40
41
|
"""
|
|
41
42
|
|
|
43
|
+
@classmethod
|
|
44
|
+
def auxiliary_tools(cls) -> Sequence[str]:
|
|
45
|
+
"""Return optional CLI tools that may be invoked as post-action side effects.
|
|
46
|
+
|
|
47
|
+
Override to declare tools the plugin calls via ``shutil.which``
|
|
48
|
+
outside its primary ``tool_name()``. The test framework uses
|
|
49
|
+
this to verify that the plugin tolerates each tool being absent.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tool names (empty by default).
|
|
53
|
+
"""
|
|
54
|
+
return ()
|
|
55
|
+
|
|
42
56
|
def with_transport(self, transport: Transport) -> Self:
|
|
43
57
|
"""Create a new instance of this plugin using a different transport."""
|
|
44
58
|
parameters = PluginParameters(distribution=self._distribution, transport=transport)
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
7
|
import shutil
|
|
8
|
-
from collections.abc import Callable
|
|
8
|
+
from collections.abc import Callable, Sequence
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import override
|
|
11
11
|
|
|
@@ -44,6 +44,12 @@ class PIPEnvironment(PythonEnvironment):
|
|
|
44
44
|
the underlying Python installation.
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
|
+
@classmethod
|
|
48
|
+
@override
|
|
49
|
+
def auxiliary_tools(cls) -> Sequence[str]:
|
|
50
|
+
"""Pip may invoke ``pymanager`` to refresh global aliases."""
|
|
51
|
+
return ('pymanager',)
|
|
52
|
+
|
|
47
53
|
@classmethod
|
|
48
54
|
@override
|
|
49
55
|
def tool_name(cls) -> str:
|
|
@@ -127,6 +133,10 @@ class PIPEnvironment(PythonEnvironment):
|
|
|
127
133
|
When a progress_callback is provided, streams stderr line-by-line to
|
|
128
134
|
report download and install phases. Otherwise falls back to the simple
|
|
129
135
|
`run_command` path for zero overhead.
|
|
136
|
+
|
|
137
|
+
On Windows, if the Python Install Manager (pymanager) is available,
|
|
138
|
+
runs ``pymanager install --refresh`` after a successful install to
|
|
139
|
+
regenerate global aliases for newly installed entry points.
|
|
130
140
|
"""
|
|
131
141
|
logger = logging.getLogger('porringer.pip.install')
|
|
132
142
|
args = list(
|
|
@@ -139,9 +149,30 @@ class PIPEnvironment(PythonEnvironment):
|
|
|
139
149
|
|
|
140
150
|
if params.progress_callback is None:
|
|
141
151
|
# Fast path — no streaming needed
|
|
142
|
-
|
|
152
|
+
result = await self._install_simple(args, params.package, logger)
|
|
153
|
+
else:
|
|
154
|
+
result = await self._install_with_progress(args, params, logger)
|
|
143
155
|
|
|
144
|
-
|
|
156
|
+
if not params.dry:
|
|
157
|
+
await self._refresh_pymanager_aliases(logger)
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
async def _refresh_pymanager_aliases(logger: logging.Logger) -> None:
|
|
163
|
+
"""Best-effort refresh of pymanager global aliases after pip install.
|
|
164
|
+
|
|
165
|
+
No-op when pymanager is absent. Failures never block the install.
|
|
166
|
+
"""
|
|
167
|
+
if shutil.which('pymanager') is None:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
result = await run_command(['pymanager', 'install', '--refresh'])
|
|
172
|
+
if result.returncode != 0:
|
|
173
|
+
logger.warning('pymanager install --refresh exited with code %d', result.returncode)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
logger.warning('Failed to refresh pymanager aliases: %s', exc)
|
|
145
176
|
|
|
146
177
|
@staticmethod
|
|
147
178
|
async def _install_simple(args: list[str], package: PackageRef, logger: logging.Logger) -> Package | None:
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
"""Implementation of tests that should be overridden in plugins"""
|
|
2
2
|
|
|
3
3
|
import shutil
|
|
4
|
-
from abc import ABCMeta
|
|
4
|
+
from abc import ABCMeta, abstractmethod
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
from unittest.mock import AsyncMock
|
|
5
8
|
|
|
6
9
|
from packaging.version import Version
|
|
7
10
|
|
|
8
11
|
import pytest
|
|
9
|
-
from porringer.core.plugin_schema.environment import Environment
|
|
12
|
+
from porringer.core.plugin_schema.environment import Environment, PackageParameters
|
|
10
13
|
from porringer.core.plugin_schema.project_environment import ProjectEnvironment
|
|
11
14
|
from porringer.core.plugin_schema.runtime import RuntimeProvider
|
|
12
15
|
from porringer.core.plugin_schema.scm import ScmEnvironment
|
|
16
|
+
from porringer.core.plugin_schema.tool_based import ToolBasedPlugin
|
|
13
17
|
from porringer.core.schema import Distribution, PackageRef, PluginParameters
|
|
14
18
|
from porringer.test.pytest.shared import (
|
|
15
19
|
EnvironmentTests,
|
|
@@ -183,3 +187,70 @@ class RuntimeProviderUnitTests[T: Environment](EnvironmentUnitTests[T], RuntimeP
|
|
|
183
187
|
kind = plugin_type.provided_runtime_kind()
|
|
184
188
|
assert isinstance(kind, str)
|
|
185
189
|
assert len(kind) > 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class InstallContext(NamedTuple):
|
|
193
|
+
"""Context returned by the ``install_context`` fixture."""
|
|
194
|
+
|
|
195
|
+
plugin: Environment
|
|
196
|
+
params: PackageParameters
|
|
197
|
+
mock_run: AsyncMock
|
|
198
|
+
scenario: str
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class AuxiliaryToolTests[T: ToolBasedPlugin](metaclass=ABCMeta):
|
|
202
|
+
"""Mixin that tests auxiliary-tool interactions declared via ``_auxiliary_tools``.
|
|
203
|
+
|
|
204
|
+
Concrete test classes must provide:
|
|
205
|
+
|
|
206
|
+
* ``fixture_install_context`` — a fixture yielding an
|
|
207
|
+
:class:`InstallContext` whose ``scenario`` field indicates which
|
|
208
|
+
auxiliary-tool configuration is active.
|
|
209
|
+
|
|
210
|
+
Expected scenarios (the fixture should be parametrized over these):
|
|
211
|
+
|
|
212
|
+
* ``"tools_absent"`` — ``shutil.which`` returns ``None`` for
|
|
213
|
+
auxiliary tools; ``run_command`` is not expected to be called
|
|
214
|
+
for them.
|
|
215
|
+
* ``"tools_present"`` — ``shutil.which`` returns a fake path;
|
|
216
|
+
``run_command`` mock returns success.
|
|
217
|
+
* ``"tools_fail_exception"`` — ``shutil.which`` returns a fake
|
|
218
|
+
path; ``run_command`` raises ``OSError`` for auxiliary tools.
|
|
219
|
+
* ``"tools_fail_nonzero"`` — ``shutil.which`` returns a fake path;
|
|
220
|
+
``run_command`` returns non-zero for auxiliary tools.
|
|
221
|
+
|
|
222
|
+
The mixin is intentionally separate from ``EnvironmentUnitTests`` so
|
|
223
|
+
that plugins without auxiliary tools don't inherit meaningless tests.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
@abstractmethod
|
|
227
|
+
@pytest.fixture(name='install_context')
|
|
228
|
+
def fixture_install_context(self) -> Generator[InstallContext]:
|
|
229
|
+
"""Yield an ``InstallContext`` with install internals mocked."""
|
|
230
|
+
raise NotImplementedError('Override this fixture')
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
async def test_install_succeeds(install_context: InstallContext) -> None:
|
|
234
|
+
"""install() succeeds regardless of auxiliary tool availability."""
|
|
235
|
+
plugin, params, _mock_run, _scenario = install_context
|
|
236
|
+
result = await plugin.install(params)
|
|
237
|
+
# Install should never fail due to auxiliary tool issues
|
|
238
|
+
assert result is not None
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
async def test_auxiliary_tools_called_only_when_present(
|
|
242
|
+
install_context: InstallContext,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Auxiliary tools are invoked when present, skipped when absent."""
|
|
245
|
+
plugin, params, mock_run, scenario = install_context
|
|
246
|
+
aux = type(plugin).auxiliary_tools()
|
|
247
|
+
if not aux:
|
|
248
|
+
pytest.skip('No auxiliary tools declared')
|
|
249
|
+
|
|
250
|
+
await plugin.install(params)
|
|
251
|
+
|
|
252
|
+
called_args = [str(c) for c in mock_run.call_args_list]
|
|
253
|
+
if scenario == 'tools_absent':
|
|
254
|
+
assert not any(tool in arg for arg in called_args for tool in aux)
|
|
255
|
+
else:
|
|
256
|
+
assert any(tool in arg for arg in called_args for tool in aux)
|
|
@@ -10,12 +10,12 @@ requires-python = ">=3.14"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"typer[all]>=0.24.1",
|
|
12
12
|
"pydantic>=2.12.5",
|
|
13
|
-
"platformdirs>=4.9.
|
|
13
|
+
"platformdirs>=4.9.6",
|
|
14
14
|
"userpath>=1.9.2",
|
|
15
15
|
"packaging>=26.0",
|
|
16
|
-
"aiohttp>=3.13.
|
|
16
|
+
"aiohttp>=3.13.5",
|
|
17
17
|
]
|
|
18
|
-
version = "0.2.1.
|
|
18
|
+
version = "0.2.1.dev89"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "MIT"
|
|
@@ -56,17 +56,17 @@ porringer = "porringer.console.entry:app"
|
|
|
56
56
|
|
|
57
57
|
[dependency-groups]
|
|
58
58
|
lint = [
|
|
59
|
-
"ruff>=0.15.
|
|
60
|
-
"pyrefly>=0.
|
|
59
|
+
"ruff>=0.15.10",
|
|
60
|
+
"pyrefly>=0.60.2",
|
|
61
61
|
]
|
|
62
62
|
test = [
|
|
63
|
-
"pytest>=9.0.
|
|
63
|
+
"pytest>=9.0.3",
|
|
64
64
|
"pytest-cov>=7.1.0",
|
|
65
65
|
"pytest-mock>=3.15.1",
|
|
66
66
|
"pytest-asyncio>=1.3.0",
|
|
67
67
|
]
|
|
68
68
|
docs = [
|
|
69
|
-
"zensical>=0.0.
|
|
69
|
+
"zensical>=0.0.32",
|
|
70
70
|
]
|
|
71
71
|
|
|
72
72
|
[tool.pytest.ini_options]
|
|
@@ -78,6 +78,7 @@ testpaths = [
|
|
|
78
78
|
markers = [
|
|
79
79
|
"fresh_plugins: invalidate the plugin discovery cache before this test",
|
|
80
80
|
"mock_packages: use a cached package list instead of real subprocess calls",
|
|
81
|
+
"bare_environment: simulate a bare system with only the primary tool on PATH",
|
|
81
82
|
]
|
|
82
83
|
|
|
83
84
|
[tool.ruff]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Shared pytest configuration and fixtures."""
|
|
2
2
|
|
|
3
|
+
import shutil
|
|
3
4
|
import sys
|
|
4
5
|
import tempfile
|
|
5
6
|
from collections.abc import Generator
|
|
@@ -55,6 +56,10 @@ def pytest_configure(config: pytest.Config) -> None:
|
|
|
55
56
|
'markers',
|
|
56
57
|
'frozen_app: simulate a frozen (PyInstaller) application environment',
|
|
57
58
|
)
|
|
59
|
+
config.addinivalue_line(
|
|
60
|
+
'markers',
|
|
61
|
+
'bare_environment: simulate a bare system with only the primary tool on PATH',
|
|
62
|
+
)
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
def pytest_runtest_setup(item: pytest.Item) -> None:
|
|
@@ -104,6 +109,30 @@ def frozen_context(*, which_result: str | None = None) -> Generator[None]:
|
|
|
104
109
|
p.stop()
|
|
105
110
|
|
|
106
111
|
|
|
112
|
+
@contextmanager
|
|
113
|
+
def bare_environment_context(*, allowed_tools: set[str] | None = None) -> Generator[None]:
|
|
114
|
+
"""Context manager that hides all CLI tools except *allowed_tools*.
|
|
115
|
+
|
|
116
|
+
Patches ``shutil.which`` globally so that any tool not in
|
|
117
|
+
*allowed_tools* appears absent. This catches undeclared
|
|
118
|
+
auxiliary-tool dependencies.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
allowed_tools: Tool names that should remain discoverable.
|
|
122
|
+
When ``None``, **all** tools are hidden.
|
|
123
|
+
"""
|
|
124
|
+
allowed = allowed_tools or set()
|
|
125
|
+
original_which = shutil.which
|
|
126
|
+
|
|
127
|
+
def _restricted_which(name: str, *args, **kwargs) -> str | None:
|
|
128
|
+
if name in allowed:
|
|
129
|
+
return original_which(name, *args, **kwargs)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
with patch('shutil.which', side_effect=_restricted_which):
|
|
133
|
+
yield
|
|
134
|
+
|
|
135
|
+
|
|
107
136
|
@pytest.fixture(autouse=True)
|
|
108
137
|
def _apply_mock_packages(
|
|
109
138
|
request: pytest.FixtureRequest,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Integration tests that verify plugins tolerate a bare environment.
|
|
2
|
+
|
|
3
|
+
Every available plugin is dry-run installed under a restricted PATH
|
|
4
|
+
where only the plugin's primary tool is discoverable. This catches
|
|
5
|
+
undeclared auxiliary-tool dependencies that would fail on a clean
|
|
6
|
+
machine.
|
|
7
|
+
|
|
8
|
+
Safety guarantees (no system mutation):
|
|
9
|
+
|
|
10
|
+
1. ``dry_run=True`` — the sync engine skips all mutating actions
|
|
11
|
+
(install, clone, post-sync commands). Only read-only subprocess
|
|
12
|
+
calls happen (e.g. ``git rev-parse`` for presence detection,
|
|
13
|
+
``pip list`` for package queries).
|
|
14
|
+
2. ``bare_environment_context`` — restricts ``shutil.which`` so that
|
|
15
|
+
even if a mutating code path were accidentally reached, most tools
|
|
16
|
+
would appear absent and the operation would be skipped or fail
|
|
17
|
+
before spawning a process.
|
|
18
|
+
3. ``session_api`` — uses an isolated temporary directory tree for
|
|
19
|
+
configuration and cache (no writes to user/system porringer dirs).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from porringer.api import API
|
|
27
|
+
from porringer.schema import SetupParameters
|
|
28
|
+
from tests.conftest import bare_environment_context
|
|
29
|
+
|
|
30
|
+
_BOOTSTRAP_DIR = Path(__file__).resolve().parents[2] / 'examples' / 'python-bootstrap'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.bare_environment
|
|
34
|
+
@pytest.mark.fresh_plugins
|
|
35
|
+
class TestBareEnvironmentDryRun:
|
|
36
|
+
"""Dry-run the bootstrap example under a bare PATH."""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
async def test_dry_run_succeeds_under_bare_environment(session_api: API) -> None:
|
|
40
|
+
"""Dry-run with only primary tools on PATH should not crash."""
|
|
41
|
+
setup_params = SetupParameters(paths=_BOOTSTRAP_DIR, dry_run=True)
|
|
42
|
+
|
|
43
|
+
with bare_environment_context(allowed_tools={'pip', 'python', 'git'}):
|
|
44
|
+
results = await session_api.sync.run(setup_params)
|
|
45
|
+
|
|
46
|
+
assert len(results.manifest_results) >= 1
|
|
47
|
+
for manifest in results.manifest_results:
|
|
48
|
+
# Every action should resolve without raising
|
|
49
|
+
assert manifest.actions is not None
|