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.
Files changed (223) hide show
  1. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/PKG-INFO +3 -3
  2. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/execution.py +8 -0
  3. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/package.py +98 -7
  4. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/tool_based.py +14 -0
  5. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pip/plugin.py +34 -3
  6. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/tests.py +73 -2
  7. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/pyproject.toml +8 -7
  8. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/conftest.py +29 -0
  9. porringer-0.2.1.dev89/tests/integration/test_bare_environment.py +49 -0
  10. porringer-0.2.1.dev89/tests/unit/plugins/pip/conftest.py +7 -0
  11. porringer-0.2.1.dev89/tests/unit/plugins/pip/test_auxiliary_tools.py +96 -0
  12. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_backend_resolver.py +2 -2
  13. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_wsl.py +231 -2
  14. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/LICENSE.md +0 -0
  15. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/README.md +0 -0
  16. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/__init__.py +0 -0
  17. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/api.py +0 -0
  18. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/__init__.py +0 -0
  19. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/backend.py +0 -0
  20. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/builder.py +0 -0
  21. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/cache.py +0 -0
  22. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/__init__.py +0 -0
  23. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/__init__.py +0 -0
  24. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/action_builder.py +0 -0
  25. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/discovery.py +0 -0
  26. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/phase.py +0 -0
  27. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/presence.py +0 -0
  28. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/resolution.py +0 -0
  29. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/core/wsl_overlay.py +0 -0
  30. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/manifest.py +0 -0
  31. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/plugin.py +0 -0
  32. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/self.py +0 -0
  33. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/command/sync.py +0 -0
  34. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/resolver.py +0 -0
  35. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/backend/schema.py +0 -0
  36. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/__init__.py +0 -0
  37. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/__init__.py +0 -0
  38. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/cache.py +0 -0
  39. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/check.py +0 -0
  40. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/download.py +0 -0
  41. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/package.py +0 -0
  42. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/plugin.py +0 -0
  43. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/schema.py +0 -0
  44. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/self.py +0 -0
  45. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/command/sync.py +0 -0
  46. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/entry.py +0 -0
  47. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/console/schema.py +0 -0
  48. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/__init__.py +0 -0
  49. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/path.py +0 -0
  50. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/__init__.py +0 -0
  51. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/environment.py +0 -0
  52. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/manifest.py +0 -0
  53. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/plugin_manager.py +0 -0
  54. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/project_environment.py +0 -0
  55. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/python_environment.py +0 -0
  56. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/runtime.py +0 -0
  57. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/plugin_schema/scm.py +0 -0
  58. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/schema.py +0 -0
  59. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/core/transport.py +0 -0
  60. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/__init__.py +0 -0
  61. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/apt/__init__.py +0 -0
  62. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/apt/plugin.py +0 -0
  63. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/brew/__init__.py +0 -0
  64. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/brew/plugin.py +0 -0
  65. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun/__init__.py +0 -0
  66. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun/plugin.py +0 -0
  67. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun_project/__init__.py +0 -0
  68. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/bun_project/plugin.py +0 -0
  69. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno/__init__.py +0 -0
  70. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno/plugin.py +0 -0
  71. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno_project/__init__.py +0 -0
  72. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/deno_project/plugin.py +0 -0
  73. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/git/__init__.py +0 -0
  74. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/git/plugin.py +0 -0
  75. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm/__init__.py +0 -0
  76. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm/plugin.py +0 -0
  77. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm_project/__init__.py +0 -0
  78. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/npm_project/plugin.py +0 -0
  79. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pdm/__init__.py +0 -0
  80. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pdm/plugin.py +0 -0
  81. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pim/__init__.py +0 -0
  82. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pim/plugin.py +0 -0
  83. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pip/__init__.py +0 -0
  84. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pipx/__init__.py +0 -0
  85. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pipx/plugin.py +0 -0
  86. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm/__init__.py +0 -0
  87. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm/plugin.py +0 -0
  88. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm_project/__init__.py +0 -0
  89. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pnpm_project/plugin.py +0 -0
  90. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/poetry/__init__.py +0 -0
  91. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/poetry/plugin.py +0 -0
  92. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pyenv/__init__.py +0 -0
  93. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/pyenv/plugin.py +0 -0
  94. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv/__init__.py +0 -0
  95. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv/plugin.py +0 -0
  96. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv_project/__init__.py +0 -0
  97. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/uv_project/plugin.py +0 -0
  98. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/winget/__init__.py +0 -0
  99. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/winget/plugin.py +0 -0
  100. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/wsl/__init__.py +0 -0
  101. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/wsl/transport.py +0 -0
  102. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/wsl/utility.py +0 -0
  103. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/yarn_project/__init__.py +0 -0
  104. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/plugin/yarn_project/plugin.py +0 -0
  105. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/py.typed +0 -0
  106. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/__init__.py +0 -0
  107. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/cache.py +0 -0
  108. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/check.py +0 -0
  109. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/config.py +0 -0
  110. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/download.py +0 -0
  111. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/execution.py +0 -0
  112. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/manifest.py +0 -0
  113. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/plugin.py +0 -0
  114. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/schema/progress.py +0 -0
  115. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/__init__.py +0 -0
  116. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/environment.py +0 -0
  117. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/plugin_manager.py +0 -0
  118. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/project_environment.py +0 -0
  119. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/scm.py +0 -0
  120. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/mock/subprocess.py +0 -0
  121. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/__init__.py +0 -0
  122. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/plugin.py +0 -0
  123. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/shared.py +0 -0
  124. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/test/pytest/variants.py +0 -0
  125. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/__init__.py +0 -0
  126. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/download.py +0 -0
  127. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/exception.py +0 -0
  128. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/py.typed +0 -0
  129. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/porringer/utility/utility.py +0 -0
  130. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/__init__.py +0 -0
  131. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/__init__.py +0 -0
  132. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/api.py +0 -0
  133. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/http.py +0 -0
  134. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/manifests.py +0 -0
  135. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/mock_plugins.py +0 -0
  136. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/fixtures/packages.py +0 -0
  137. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/__init__.py +0 -0
  138. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/__init__.py +0 -0
  139. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/git/__init__.py +0 -0
  140. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/git/test_scm.py +0 -0
  141. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pip/__init__.py +0 -0
  142. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pip/test_environment.py +0 -0
  143. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pipx/__init__.py +0 -0
  144. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/pipx/test_environment.py +0 -0
  145. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/winget/__init__.py +0 -0
  146. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/plugins/winget/test_environment.py +0 -0
  147. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_bootstrap_presence.py +0 -0
  148. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_example_bootstrap.py +0 -0
  149. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_example_presence.py +0 -0
  150. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/integration/test_frozen_app_presence.py +0 -0
  151. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/__init__.py +0 -0
  152. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/__init__.py +0 -0
  153. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/apt/__init__.py +0 -0
  154. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/apt/test_environment.py +0 -0
  155. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/brew/__init__.py +0 -0
  156. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/brew/test_environment.py +0 -0
  157. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun/__init__.py +0 -0
  158. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun/test_environment.py +0 -0
  159. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun_project/__init__.py +0 -0
  160. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/bun_project/test_environment.py +0 -0
  161. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno/__init__.py +0 -0
  162. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno/test_environment.py +0 -0
  163. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno_project/__init__.py +0 -0
  164. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/deno_project/test_environment.py +0 -0
  165. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/git/__init__.py +0 -0
  166. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/git/test_clone_detection.py +0 -0
  167. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/git/test_scm.py +0 -0
  168. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm/__init__.py +0 -0
  169. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm/test_environment.py +0 -0
  170. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm_project/__init__.py +0 -0
  171. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/npm_project/test_environment.py +0 -0
  172. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pdm/__init__.py +0 -0
  173. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pdm/test_environment.py +0 -0
  174. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pim/__init__.py +0 -0
  175. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pim/test_environment.py +0 -0
  176. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pip/__init__.py +0 -0
  177. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pip/test_environment.py +0 -0
  178. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pipx/__init__.py +0 -0
  179. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pipx/test_environment.py +0 -0
  180. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm/__init__.py +0 -0
  181. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm/test_environment.py +0 -0
  182. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm_project/__init__.py +0 -0
  183. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pnpm_project/test_environment.py +0 -0
  184. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/poetry/__init__.py +0 -0
  185. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/poetry/test_environment.py +0 -0
  186. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pyenv/__init__.py +0 -0
  187. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/pyenv/test_environment.py +0 -0
  188. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv/__init__.py +0 -0
  189. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv/test_environment.py +0 -0
  190. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv_project/__init__.py +0 -0
  191. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/uv_project/test_environment.py +0 -0
  192. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/yarn_project/__init__.py +0 -0
  193. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/plugins/yarn_project/test_environment.py +0 -0
  194. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_bootstrap_cross_platform.py +0 -0
  195. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_cache.py +0 -0
  196. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_check.py +0 -0
  197. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_check_updates.py +0 -0
  198. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_cli.py +0 -0
  199. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_command_plugin.py +0 -0
  200. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_command_self.py +0 -0
  201. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_concurrency_and_client.py +0 -0
  202. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_deferred_resolution.py +0 -0
  203. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_event_loop_safety.py +0 -0
  204. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_extras_introspection.py +0 -0
  205. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_frozen_app_detection.py +0 -0
  206. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_frozen_presence_resolution.py +0 -0
  207. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_manifest.py +0 -0
  208. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_package_filtering.py +0 -0
  209. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_package_ref.py +0 -0
  210. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_package_relation.py +0 -0
  211. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_path_sync.py +0 -0
  212. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_plugin_filtering.py +0 -0
  213. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_plugin_manager.py +0 -0
  214. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_presence_detection.py +0 -0
  215. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_project_directory.py +0 -0
  216. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_project_root.py +0 -0
  217. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_runtime_context_seeding.py +0 -0
  218. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_runtime_propagation.py +0 -0
  219. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_runtime_updates.py +0 -0
  220. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_sub_action_progress.py +0 -0
  221. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_uninstall.py +0 -0
  222. {porringer-0.2.1.dev87 → porringer-0.2.1.dev89}/tests/unit/test_update_detection.py +0 -0
  223. {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.dev87
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.4
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.3
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 plugins.environments.items():
675
+ for name, env in environments.items():
587
676
  if params.plugins and name not in params.plugins:
588
677
  continue
589
678
 
590
- plugin_type = type(env)
591
- if not plugin_type.is_supported() or not env.query_availability(runtime_context):
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(env, check_params, runtime_context)
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
- return await self._install_simple(args, params.package, logger)
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
- return await self._install_with_progress(args, params, logger)
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.4",
13
+ "platformdirs>=4.9.6",
14
14
  "userpath>=1.9.2",
15
15
  "packaging>=26.0",
16
- "aiohttp>=3.13.3",
16
+ "aiohttp>=3.13.5",
17
17
  ]
18
- version = "0.2.1.dev87"
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.7",
60
- "pyrefly>=0.57.1",
59
+ "ruff>=0.15.10",
60
+ "pyrefly>=0.60.2",
61
61
  ]
62
62
  test = [
63
- "pytest>=9.0.2",
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.28",
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
@@ -0,0 +1,7 @@
1
+ """Shared fixtures for pip plugin tests."""
2
+
3
+ from packaging.version import Version
4
+
5
+ from porringer.core.schema import Distribution, PluginParameters
6
+
7
+ _MOCK_PARAMS = PluginParameters(distribution=Distribution(version=Version('0.0.0')))