porringer 0.2.1.dev50__tar.gz → 0.2.1.dev52__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 (189) hide show
  1. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/PKG-INFO +2 -2
  2. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/core/action_builder.py +5 -1
  3. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/core/execution.py +103 -120
  4. porringer-0.2.1.dev52/porringer/backend/command/core/phase.py +247 -0
  5. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/sync.py +133 -160
  6. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/scm.py +47 -2
  7. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/git/plugin.py +39 -5
  8. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/__init__.py +4 -0
  9. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/execution.py +34 -0
  10. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/mock/scm.py +9 -3
  11. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/pyproject.toml +6 -14
  12. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/conftest.py +13 -0
  13. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/test_example_bootstrap.py +2 -1
  14. porringer-0.2.1.dev52/tests/integration/test_example_presence.py +39 -0
  15. porringer-0.2.1.dev52/tests/unit/plugins/git/test_clone_detection.py +146 -0
  16. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pim/test_environment.py +24 -8
  17. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pyenv/test_environment.py +38 -13
  18. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_backend_resolver.py +15 -0
  19. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_cache.py +2 -1
  20. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_check_updates.py +30 -2
  21. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_manifest.py +10 -5
  22. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_plugin_manager.py +22 -17
  23. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_project_directory.py +5 -3
  24. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_runtime_propagation.py +60 -32
  25. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_update_detection.py +15 -1
  26. porringer-0.2.1.dev50/tests/integration/test_example_presence.py +0 -71
  27. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/LICENSE.md +0 -0
  28. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/README.md +0 -0
  29. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/__init__.py +0 -0
  30. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/api.py +0 -0
  31. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/__init__.py +0 -0
  32. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/backend.py +0 -0
  33. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/builder.py +0 -0
  34. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/cache.py +0 -0
  35. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/__init__.py +0 -0
  36. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/core/__init__.py +0 -0
  37. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/core/discovery.py +0 -0
  38. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/core/presence.py +0 -0
  39. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/core/resolution.py +0 -0
  40. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/manifest.py +0 -0
  41. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/plugin.py +0 -0
  42. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/self.py +0 -0
  43. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/command/sync.py +0 -0
  44. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/resolver.py +0 -0
  45. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/backend/schema.py +0 -0
  46. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/__init__.py +0 -0
  47. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/__init__.py +0 -0
  48. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/cache.py +0 -0
  49. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/check.py +0 -0
  50. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/download.py +0 -0
  51. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/plugin.py +0 -0
  52. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/command/self.py +0 -0
  53. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/entry.py +0 -0
  54. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/console/schema.py +0 -0
  55. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/__init__.py +0 -0
  56. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/__init__.py +0 -0
  57. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/environment.py +0 -0
  58. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/manifest.py +0 -0
  59. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/plugin_manager.py +0 -0
  60. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/project_environment.py +0 -0
  61. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/python_environment.py +0 -0
  62. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/runtime.py +0 -0
  63. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/plugin_schema/tool_based.py +0 -0
  64. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/core/schema.py +0 -0
  65. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/__init__.py +0 -0
  66. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/apt/__init__.py +0 -0
  67. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/apt/plugin.py +0 -0
  68. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/brew/__init__.py +0 -0
  69. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/brew/plugin.py +0 -0
  70. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/bun/__init__.py +0 -0
  71. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/bun/plugin.py +0 -0
  72. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/bun_project/__init__.py +0 -0
  73. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/bun_project/plugin.py +0 -0
  74. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/deno/__init__.py +0 -0
  75. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/deno/plugin.py +0 -0
  76. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/deno_project/__init__.py +0 -0
  77. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/deno_project/plugin.py +0 -0
  78. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/git/__init__.py +0 -0
  79. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/npm/__init__.py +0 -0
  80. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/npm/plugin.py +0 -0
  81. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/npm_project/__init__.py +0 -0
  82. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/npm_project/plugin.py +0 -0
  83. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pdm/__init__.py +0 -0
  84. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pdm/plugin.py +0 -0
  85. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pim/__init__.py +0 -0
  86. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pim/plugin.py +0 -0
  87. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pip/__init__.py +0 -0
  88. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pip/plugin.py +0 -0
  89. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pipx/__init__.py +0 -0
  90. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pipx/plugin.py +0 -0
  91. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pnpm/__init__.py +0 -0
  92. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pnpm/plugin.py +0 -0
  93. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pnpm_project/__init__.py +0 -0
  94. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pnpm_project/plugin.py +0 -0
  95. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/poetry/__init__.py +0 -0
  96. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/poetry/plugin.py +0 -0
  97. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pyenv/__init__.py +0 -0
  98. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/pyenv/plugin.py +0 -0
  99. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/uv/__init__.py +0 -0
  100. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/uv/plugin.py +0 -0
  101. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/uv_project/__init__.py +0 -0
  102. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/uv_project/plugin.py +0 -0
  103. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/winget/__init__.py +0 -0
  104. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/winget/plugin.py +0 -0
  105. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/yarn_project/__init__.py +0 -0
  106. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/plugin/yarn_project/plugin.py +0 -0
  107. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/py.typed +0 -0
  108. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/cache.py +0 -0
  109. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/check.py +0 -0
  110. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/config.py +0 -0
  111. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/download.py +0 -0
  112. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/manifest.py +0 -0
  113. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/plugin.py +0 -0
  114. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/schema/progress.py +0 -0
  115. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/mock/__init__.py +0 -0
  116. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/mock/environment.py +0 -0
  117. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/mock/plugin_manager.py +0 -0
  118. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/mock/project_environment.py +0 -0
  119. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/pytest/__init__.py +0 -0
  120. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/pytest/plugin.py +0 -0
  121. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/pytest/shared.py +0 -0
  122. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/pytest/tests.py +0 -0
  123. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/test/pytest/variants.py +0 -0
  124. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/utility/__init__.py +0 -0
  125. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/utility/download.py +0 -0
  126. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/utility/exception.py +0 -0
  127. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/utility/py.typed +0 -0
  128. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/porringer/utility/utility.py +0 -0
  129. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/__init__.py +0 -0
  130. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/__init__.py +0 -0
  131. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/__init__.py +0 -0
  132. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/git/__init__.py +0 -0
  133. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/git/test_scm.py +0 -0
  134. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/pip/__init__.py +0 -0
  135. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/pip/test_environment.py +0 -0
  136. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/pipx/__init__.py +0 -0
  137. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/pipx/test_environment.py +0 -0
  138. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/winget/__init__.py +0 -0
  139. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/plugins/winget/test_environment.py +0 -0
  140. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/integration/test_bootstrap_presence.py +0 -0
  141. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/__init__.py +0 -0
  142. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/__init__.py +0 -0
  143. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/apt/__init__.py +0 -0
  144. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/apt/test_environment.py +0 -0
  145. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/brew/__init__.py +0 -0
  146. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/brew/test_environment.py +0 -0
  147. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/bun/__init__.py +0 -0
  148. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/bun/test_environment.py +0 -0
  149. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/bun_project/__init__.py +0 -0
  150. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/bun_project/test_environment.py +0 -0
  151. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/deno/__init__.py +0 -0
  152. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/deno/test_environment.py +0 -0
  153. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/deno_project/__init__.py +0 -0
  154. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/deno_project/test_environment.py +0 -0
  155. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/git/__init__.py +0 -0
  156. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/git/test_scm.py +0 -0
  157. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/npm/__init__.py +0 -0
  158. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/npm/test_environment.py +0 -0
  159. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/npm_project/__init__.py +0 -0
  160. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/npm_project/test_environment.py +0 -0
  161. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pdm/__init__.py +0 -0
  162. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pdm/test_environment.py +0 -0
  163. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pim/__init__.py +0 -0
  164. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pip/__init__.py +0 -0
  165. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pip/test_environment.py +0 -0
  166. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pipx/__init__.py +0 -0
  167. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pipx/test_environment.py +0 -0
  168. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pnpm/__init__.py +0 -0
  169. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pnpm/test_environment.py +0 -0
  170. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pnpm_project/__init__.py +0 -0
  171. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pnpm_project/test_environment.py +0 -0
  172. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/poetry/__init__.py +0 -0
  173. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/poetry/test_environment.py +0 -0
  174. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/pyenv/__init__.py +0 -0
  175. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/uv/__init__.py +0 -0
  176. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/uv/test_environment.py +0 -0
  177. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/uv_project/__init__.py +0 -0
  178. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/uv_project/test_environment.py +0 -0
  179. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/yarn_project/__init__.py +0 -0
  180. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/plugins/yarn_project/test_environment.py +0 -0
  181. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_bootstrap_cross_platform.py +0 -0
  182. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_check.py +0 -0
  183. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_cli.py +0 -0
  184. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_command_plugin.py +0 -0
  185. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_command_self.py +0 -0
  186. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_package_ref.py +0 -0
  187. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_plugin_filtering.py +0 -0
  188. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_project_root.py +0 -0
  189. {porringer-0.2.1.dev50 → porringer-0.2.1.dev52}/tests/unit/test_sub_action_progress.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: porringer
3
- Version: 0.2.1.dev50
3
+ Version: 0.2.1.dev52
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: MIT
6
6
  Project-URL: homepage, https://github.com/synodic/porringer
7
7
  Project-URL: repository, https://github.com/synodic/porringer
8
8
  Requires-Python: >=3.14
9
- Requires-Dist: typer[all]>=0.24.0
9
+ Requires-Dist: typer[all]>=0.24.1
10
10
  Requires-Dist: pydantic>=2.12.5
11
11
  Requires-Dist: platformdirs>=4.9.2
12
12
  Requires-Dist: userpath>=1.9.2
@@ -224,6 +224,10 @@ def build_actions(
224
224
  for package in packages:
225
225
  if not package.is_applicable():
226
226
  continue
227
+ # Use the manifest description if set, otherwise surface
228
+ # the repo URL as the package description so UI cards
229
+ # show *what* is being cloned.
230
+ scm_description = package.description or str(package.name)
227
231
  actions.append(
228
232
  SetupAction(
229
233
  description=action_description(kind, verb, installer, package=package.name),
@@ -231,7 +235,7 @@ def build_actions(
231
235
  ecosystem=ecosystem,
232
236
  installer=installer,
233
237
  package=package.name,
234
- package_description=package.description,
238
+ package_description=scm_description,
235
239
  )
236
240
  )
237
241
  continue
@@ -30,6 +30,7 @@ from porringer.core.plugin_schema.runtime import RuntimeConsumer, RuntimeProvide
30
30
  from porringer.core.plugin_schema.scm import ScmEnvironment
31
31
  from porringer.core.schema import Package, PluginKind
32
32
  from porringer.schema import (
33
+ CloneStatusKind,
33
34
  ManifestMetadata,
34
35
  ProgressEvent,
35
36
  ProgressEventKind,
@@ -53,9 +54,9 @@ from .action_builder import (
53
54
  from .discovery import (
54
55
  DiscoveredPlugins,
55
56
  discover_all_plugins,
56
- discover_plugins,
57
57
  invalidate_plugin_cache,
58
58
  )
59
+ from .phase import run_phases
59
60
  from .presence import async_dry_run_action
60
61
  from .resolution import (
61
62
  OperationKind,
@@ -73,8 +74,8 @@ class ExecutionState:
73
74
 
74
75
  Owns the discovered plugin dicts, grouped phase actions, and
75
76
  common parameters that every phase needs. Provides
76
- ``phase_transition()`` and ``propagate_runtime()`` as methods
77
- so callers don't need to pass a half-dozen arguments.
77
+ :meth:`refresh_all_plugins` and :meth:`propagate_runtime` as
78
+ methods so callers don't need to pass a half-dozen arguments.
78
79
  """
79
80
 
80
81
  actions: list[SetupAction]
@@ -103,36 +104,23 @@ class ExecutionState:
103
104
  """The sync strategy from the current parameters."""
104
105
  return self.parameters.strategy
105
106
 
106
- # -- phase-transition machinery ------------------------------------
107
+ # -- plugin refresh machinery --------------------------------------
107
108
 
108
- def phase_transition(self) -> None:
109
- """Refresh PATH, re-discover environment plugins, and resolve deferred actions.
109
+ def refresh_all_plugins(self) -> None:
110
+ """Refresh PATH and re-discover all plugin types.
110
111
 
111
- Mutates ``self.environments`` in place and updates
112
- descriptions / CLI commands on any newly-resolved actions.
113
- The cached runtime executable (if any) is automatically
114
- re-propagated to newly-created consumer instances.
112
+ Invalidates the plugin cache, re-discovers environments,
113
+ project environments, and SCM environments, then re-applies
114
+ the cached runtime executable to all new consumer instances.
115
115
  """
116
116
  refresh_path()
117
117
  invalidate_plugin_cache()
118
- self.environments = discover_plugins('environment', Environment, check_dependencies=True)
118
+ plugins = discover_all_plugins()
119
+ self.environments = dict(plugins.environments)
120
+ self.project_environments = dict(plugins.project_environments)
121
+ self.scm_environments = dict(plugins.scm_environments)
119
122
  self._apply_resolved_runtime()
120
123
 
121
- for phase_actions in self.phases.values():
122
- if phase_actions:
123
- _resolve_deferred_actions(phase_actions, self.environments, self.strategy)
124
-
125
- for phase_actions in self.phases.values():
126
- for action in phase_actions:
127
- if action.cli_command is None or action.installer is not None:
128
- action.cli_command = get_cli_command(
129
- action,
130
- self.environments,
131
- self.strategy,
132
- self.project_environments,
133
- self.scm_environments,
134
- )
135
-
136
124
  def propagate_runtime(self) -> None:
137
125
  """Resolve interpreter paths from completed runtime actions and propagate downstream.
138
126
 
@@ -140,8 +128,7 @@ class ExecutionState:
140
128
  actions, resolves its executable, injects the directory onto
141
129
  ``PATH``, and sets ``runtime_executable`` on every matching
142
130
  ``RuntimeConsumer``. The result is cached so that subsequent
143
- calls to :meth:`phase_transition` or
144
- :meth:`refresh_project_environments` can re-apply it to
131
+ calls to :meth:`refresh_all_plugins` can re-apply it to
145
132
  newly-created plugin instances.
146
133
  """
147
134
  result = _propagate_runtime(
@@ -152,17 +139,6 @@ class ExecutionState:
152
139
  if result is not None:
153
140
  self._resolved_runtime = result
154
141
 
155
- def refresh_project_environments(self) -> None:
156
- """Re-discover project-environment plugins and re-propagate the runtime.
157
-
158
- Replaces ``self.project_environments`` with freshly-discovered
159
- instances and re-applies the cached runtime executable so that
160
- new ``RuntimeConsumer`` project environments inherit the
161
- resolved interpreter path.
162
- """
163
- self.project_environments = discover_plugins('project_environment', ProjectEnvironment)
164
- self._apply_resolved_runtime()
165
-
166
142
  def _apply_resolved_runtime(self) -> None:
167
143
  """Re-apply the cached runtime executable to all current consumers.
168
144
 
@@ -195,6 +171,59 @@ class ExecutionState:
195
171
  """Plugin-management context for this execution run."""
196
172
  return PluginContext(project_environments=self.project_environments)
197
173
 
174
+ # -- phase executor delegates --------------------------------------
175
+
176
+ async def run_package_actions(self, actions: list[SetupAction]) -> tuple[list[SetupActionResult], bool]:
177
+ """Execute package/tool/runtime actions.
178
+
179
+ Returns:
180
+ Tuple of (results, should_continue).
181
+ """
182
+ return await execute_package_actions(
183
+ actions,
184
+ self.environments,
185
+ self.parameters,
186
+ self.event_queue,
187
+ self.plugin_context,
188
+ )
189
+
190
+ async def run_project_phase(self, actions: list[SetupAction]) -> list[SetupActionResult]:
191
+ """Execute or skip project sync actions."""
192
+ return await handle_project_phase(actions, self)
193
+
194
+ async def run_scm_actions(self, actions: list[SetupAction]) -> list[SetupActionResult]:
195
+ """Execute SCM clone actions."""
196
+ return await _execute_scm_actions(
197
+ actions,
198
+ self.scm_environments,
199
+ self.fallback_dir,
200
+ self.parameters,
201
+ self.event_queue,
202
+ )
203
+
204
+ async def run_command_actions(self, actions: list[SetupAction]) -> list[SetupActionResult]:
205
+ """Execute post-sync shell commands."""
206
+ return await execute_command_actions(actions, self)
207
+
208
+ def resolve_deferred(self, actions: list[SetupAction]) -> None:
209
+ """Resolve deferred actions and update their CLI commands."""
210
+ _resolve_deferred_actions(
211
+ actions,
212
+ self.environments,
213
+ self.strategy,
214
+ scm_environments=self.scm_environments,
215
+ project_environments=self.project_environments,
216
+ )
217
+ for action in actions:
218
+ if action.cli_command is None or action.installer is not None:
219
+ action.cli_command = get_cli_command(
220
+ action,
221
+ self.environments,
222
+ self.strategy,
223
+ self.project_environments,
224
+ self.scm_environments,
225
+ )
226
+
198
227
  def early_return(self) -> SetupResults:
199
228
  """Create a ``SetupResults`` from the results accumulated so far."""
200
229
  return SetupResults(
@@ -1100,78 +1129,8 @@ async def execute_single(
1100
1129
  )
1101
1130
  )
1102
1131
 
1103
- # --- Phase 1: runtime-provider actions (pim / pyenv) ---------------
1104
- if state.phases[PluginKind.RUNTIME]:
1105
- runtime_results, should_continue = await execute_package_actions(
1106
- state.phases[PluginKind.RUNTIME],
1107
- state.environments,
1108
- state.parameters,
1109
- state.event_queue,
1110
- state.plugin_context,
1111
- )
1112
- state.results.extend(runtime_results)
1113
- if not should_continue:
1114
- return state.early_return()
1115
- state.propagate_runtime()
1116
-
1117
- # Phase transition: re-discover plugins now that the runtime
1118
- # is installed and its directories are on PATH, so that
1119
- # downstream consumers (pip, uv, etc.) become available.
1120
- state.phase_transition()
1121
-
1122
- # --- Phase 2a: package-kind actions (pip, uv, etc.) ---------------
1123
- if state.phases[PluginKind.PACKAGE]:
1124
- package_results, should_continue = await execute_package_actions(
1125
- state.phases[PluginKind.PACKAGE],
1126
- state.environments,
1127
- state.parameters,
1128
- state.event_queue,
1129
- state.plugin_context,
1130
- )
1131
- state.results.extend(package_results)
1132
- if not should_continue:
1133
- return state.early_return()
1134
-
1135
- # --- Phase 2b: tool-kind actions (pipx, etc.) ---------------------
1136
- if state.phases[PluginKind.TOOL]:
1137
- # Phase transition: re-discover plugins so that tools installed
1138
- # in Phase 2a (e.g. pipx via pip) are now available as backends.
1139
- state.phase_transition()
1140
-
1141
- tool_results, should_continue = await execute_package_actions(
1142
- state.phases[PluginKind.TOOL],
1143
- state.environments,
1144
- state.parameters,
1145
- state.event_queue,
1146
- state.plugin_context,
1147
- )
1148
- state.results.extend(tool_results)
1149
- if not should_continue:
1150
- return state.early_return()
1151
-
1152
- # --- Phase 3: project sync ----------------------------------------
1153
- if state.phases[PluginKind.PROJECT]:
1154
- # Re-discover project environments in case tools installed in
1155
- # earlier phases provide new project-environment backends.
1156
- state.refresh_project_environments()
1157
-
1158
- state.results.extend(await handle_project_phase(state.phases[PluginKind.PROJECT], state))
1159
-
1160
- # --- Phase 4: SCM clone -------------------------------------------
1161
- if state.phases[PluginKind.SCM]:
1162
- state.results.extend(
1163
- await _execute_scm_actions(
1164
- state.phases[PluginKind.SCM],
1165
- state.scm_environments,
1166
- state.fallback_dir,
1167
- state.parameters,
1168
- state.event_queue,
1169
- )
1170
- )
1171
-
1172
- # --- Phase 5: post-sync commands ----------------------------------
1173
- if state.phases[None]:
1174
- state.results.extend(await execute_command_actions(state.phases[None], state))
1132
+ # Run all phases via the generalized phase loop.
1133
+ await run_phases(state)
1175
1134
 
1176
1135
  return SetupResults(
1177
1136
  actions=actions,
@@ -1284,6 +1243,8 @@ def _resolve_deferred_actions(
1284
1243
  actions: list[SetupAction],
1285
1244
  environments: dict[str, Environment],
1286
1245
  strategy: SyncStrategy = SyncStrategy.MINIMAL,
1246
+ scm_environments: dict[str, ScmEnvironment] | None = None,
1247
+ project_environments: dict[str, ProjectEnvironment] | None = None,
1287
1248
  ) -> None:
1288
1249
  """Resolve deferred actions whose `installer` is `None`.
1289
1250
 
@@ -1297,12 +1258,19 @@ def _resolve_deferred_actions(
1297
1258
  actions: Mutable list of actions to resolve in-place.
1298
1259
  environments: Freshly-discovered environment plugins.
1299
1260
  strategy: Sync strategy (for description verb).
1261
+ scm_environments: Optional SCM-environment plugins.
1262
+ project_environments: Optional project-environment plugins.
1300
1263
  """
1301
1264
  deferred = [a for a in actions if a.installer is None and a.ecosystem is not None]
1302
1265
  if not deferred:
1303
1266
  return
1304
1267
 
1305
- resolver = BackendResolver(environments)
1268
+ all_plugins: dict[str, Environment | ScmEnvironment | ProjectEnvironment] = {
1269
+ **environments,
1270
+ **(scm_environments or {}),
1271
+ **(project_environments or {}),
1272
+ }
1273
+ resolver = BackendResolver(all_plugins)
1306
1274
  verb = STRATEGY_VERB[strategy]
1307
1275
 
1308
1276
  for action in deferred:
@@ -1646,14 +1614,29 @@ async def _execute_scm_clone(
1646
1614
  destination = working_dir
1647
1615
 
1648
1616
  # Skip if already cloned
1649
- if scm_env.is_cloned(url, destination):
1650
- return SetupActionResult(
1651
- action=action,
1652
- success=True,
1653
- skipped=True,
1654
- skip_reason=SkipReason.ALREADY_INSTALLED,
1655
- message=f"Repository already cloned at '{destination}'",
1656
- )
1617
+ clone_status = scm_env.is_cloned(url, destination)
1618
+
1619
+ match clone_status.kind:
1620
+ case CloneStatusKind.CLONED:
1621
+ actual_url = clone_status.remote_url or url
1622
+ message = f"SCM skip: repo already cloned at '{destination}' (remote: {actual_url})"
1623
+ logger.info(message)
1624
+ return SetupActionResult(
1625
+ action=action,
1626
+ success=True,
1627
+ skipped=True,
1628
+ skip_reason=SkipReason.ALREADY_INSTALLED,
1629
+ message=message,
1630
+ )
1631
+ case CloneStatusKind.MISSING:
1632
+ logger.info("SCM clone needed: .git not found at '%s'", destination)
1633
+ case CloneStatusKind.URL_MISMATCH:
1634
+ logger.info(
1635
+ "SCM clone needed: remote URL mismatch at '%s' (expected '%s', found '%s')",
1636
+ destination,
1637
+ url,
1638
+ clone_status.remote_url,
1639
+ )
1657
1640
 
1658
1641
  if parameters.dry_run:
1659
1642
  # Dry-run — no-op
@@ -0,0 +1,247 @@
1
+ """Generalized phase abstraction for the execution pipeline.
2
+
3
+ Each phase in the setup flow (runtime → packages → tools → project-sync →
4
+ SCM → post-sync commands) is represented by a :class:`Phase` instance that
5
+ encapsulates the *refresh → resolve-deferred → execute → post-hook* cycle.
6
+
7
+ The :func:`run_phases` driver replaces the hand-coded per-phase blocks in
8
+ ``execute_single()`` with a uniform loop.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING, Protocol, override
16
+
17
+ from porringer.core.schema import PluginKind
18
+ from porringer.schema import SetupActionResult
19
+
20
+ if TYPE_CHECKING:
21
+ from .execution import ExecutionState
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Phase result
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class PhaseResult:
33
+ """Outcome of a single phase execution.
34
+
35
+ Attributes:
36
+ results: Per-action results produced by this phase.
37
+ should_continue: ``False`` when fail-fast was triggered and the
38
+ pipeline should abort immediately.
39
+ """
40
+
41
+ results: list[SetupActionResult]
42
+ should_continue: bool = True
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Phase protocol
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class Phase(Protocol):
51
+ """Unified interface for every execution phase.
52
+
53
+ Implementors define *kind* (which action bucket they handle),
54
+ *refresh* (plugin re-discovery), *execute* (the actual work), and
55
+ *post_execute* (hooks that run after a successful phase).
56
+ """
57
+
58
+ @property
59
+ def kind(self) -> PluginKind | None:
60
+ """The ``PluginKind`` this phase processes (``None`` for post-sync commands)."""
61
+ ...
62
+
63
+ def refresh(self, state: ExecutionState) -> None:
64
+ """Re-discover plugins and resolve deferred actions for **this** phase only.
65
+
66
+ Called *before* :meth:`execute`. The default implementation is a no-op.
67
+ """
68
+ ...
69
+
70
+ async def execute(self, state: ExecutionState) -> PhaseResult:
71
+ """Run the phase's actions and return aggregated results.
72
+
73
+ Must return a :class:`PhaseResult`. Set ``should_continue=False``
74
+ to abort the pipeline (fail-fast).
75
+ """
76
+ ...
77
+
78
+ def post_execute(self, state: ExecutionState) -> None:
79
+ """Hook called *after* a successful execution (``should_continue=True``).
80
+
81
+ Typical use: propagate a resolved runtime to downstream consumers.
82
+ The default implementation is a no-op.
83
+ """
84
+ ...
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Concrete phases
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ class _PhaseBase:
93
+ """Shared helpers for concrete phase implementations."""
94
+
95
+ _kind: PluginKind | None
96
+
97
+ @property
98
+ def kind(self) -> PluginKind | None:
99
+ return self._kind
100
+
101
+ # Default no-ops — subclasses override as needed.
102
+ def refresh(self, state: ExecutionState) -> None: # noqa: D102
103
+ pass
104
+
105
+ def post_execute(self, state: ExecutionState) -> None: # noqa: D102
106
+ pass
107
+
108
+
109
+ class RuntimePhase(_PhaseBase):
110
+ """Phase 1: install / resolve language runtimes (pim, pyenv)."""
111
+
112
+ _kind = PluginKind.RUNTIME
113
+
114
+ async def execute(self, state: ExecutionState) -> PhaseResult: # noqa: D102
115
+ results, ok = await state.run_package_actions(state.phases[self._kind])
116
+ return PhaseResult(results=results, should_continue=ok)
117
+
118
+ @override
119
+ def post_execute(self, state: ExecutionState) -> None:
120
+ """Propagate the resolved runtime and trigger a full plugin refresh."""
121
+ state.propagate_runtime()
122
+ state.refresh_all_plugins()
123
+
124
+
125
+ class PackagePhase(_PhaseBase):
126
+ """Phase 2a: install packages into the current environment (pip, uv)."""
127
+
128
+ _kind = PluginKind.PACKAGE
129
+
130
+ async def execute(self, state: ExecutionState) -> PhaseResult: # noqa: D102
131
+ results, ok = await state.run_package_actions(state.phases[self._kind])
132
+ return PhaseResult(results=results, should_continue=ok)
133
+
134
+
135
+ class ToolPhase(_PhaseBase):
136
+ """Phase 2b: install isolated CLI tools (pipx, etc.)."""
137
+
138
+ _kind = PluginKind.TOOL
139
+
140
+ @override
141
+ def refresh(self, state: ExecutionState) -> None:
142
+ """Re-discover plugins so that tools installed in Phase 2a become available."""
143
+ state.refresh_all_plugins()
144
+
145
+ async def execute(self, state: ExecutionState) -> PhaseResult: # noqa: D102
146
+ results, ok = await state.run_package_actions(state.phases[self._kind])
147
+ return PhaseResult(results=results, should_continue=ok)
148
+
149
+
150
+ class ProjectPhase(_PhaseBase):
151
+ """Phase 3: run project-sync (pdm install, uv sync, etc.)."""
152
+
153
+ _kind = PluginKind.PROJECT
154
+
155
+ @override
156
+ def refresh(self, state: ExecutionState) -> None:
157
+ """Re-discover all plugins so that project environments installed in earlier phases are available."""
158
+ state.refresh_all_plugins()
159
+
160
+ async def execute(self, state: ExecutionState) -> PhaseResult: # noqa: D102
161
+ results = await state.run_project_phase(state.phases[self._kind])
162
+ failed = any(not r.success and not r.skipped for r in results)
163
+ ok = not (failed and state.parameters.fail_fast)
164
+ return PhaseResult(results=results, should_continue=ok)
165
+
166
+
167
+ class ScmPhase(_PhaseBase):
168
+ """Phase 4: clone source-control repositories."""
169
+
170
+ _kind = PluginKind.SCM
171
+
172
+ @override
173
+ def refresh(self, state: ExecutionState) -> None:
174
+ """Re-discover plugins so that SCM tools installed in earlier phases are available."""
175
+ state.refresh_all_plugins()
176
+
177
+ async def execute(self, state: ExecutionState) -> PhaseResult: # noqa: D102
178
+ results = await state.run_scm_actions(state.phases[self._kind])
179
+ failed = any(not r.success and not r.skipped for r in results)
180
+ ok = not (failed and state.parameters.fail_fast)
181
+ return PhaseResult(results=results, should_continue=ok)
182
+
183
+
184
+ class CommandPhase(_PhaseBase):
185
+ """Phase 5: run arbitrary post-sync shell commands."""
186
+
187
+ _kind = None
188
+
189
+ async def execute(self, state: ExecutionState) -> PhaseResult: # noqa: D102
190
+ results = await state.run_command_actions(state.phases[self._kind])
191
+ failed = any(not r.success and not r.skipped for r in results)
192
+ ok = not (failed and state.parameters.fail_fast)
193
+ return PhaseResult(results=results, should_continue=ok)
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Canonical phase ordering
198
+ # ---------------------------------------------------------------------------
199
+
200
+ PHASES: list[Phase] = [
201
+ RuntimePhase(),
202
+ PackagePhase(),
203
+ ToolPhase(),
204
+ ProjectPhase(),
205
+ ScmPhase(),
206
+ CommandPhase(),
207
+ ]
208
+ """The ordered list of phases that :func:`run_phases` iterates."""
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Phase loop driver
213
+ # ---------------------------------------------------------------------------
214
+
215
+
216
+ async def run_phases(state: ExecutionState) -> None:
217
+ """Execute all non-empty phases in order.
218
+
219
+ For each phase:
220
+
221
+ 1. **Skip** if the phase bucket is empty.
222
+ 2. **Refresh** — re-discover plugins and resolve deferred actions
223
+ (scoped to this phase only).
224
+ 3. **Execute** — run the phase's actions and collect results.
225
+ 4. **Post-execute** — run post-hooks (e.g. runtime propagation)
226
+ only when the phase completed without aborting the pipeline.
227
+ """
228
+ for phase in PHASES:
229
+ actions = state.phases.get(phase.kind)
230
+ if not actions:
231
+ continue
232
+
233
+ # 1. Refresh plugins
234
+ phase.refresh(state)
235
+
236
+ # 2. Resolve deferred actions for THIS phase only
237
+ state.resolve_deferred(actions)
238
+
239
+ # 3. Execute
240
+ result = await phase.execute(state)
241
+ state.results.extend(result.results)
242
+
243
+ if not result.should_continue:
244
+ return
245
+
246
+ # 4. Post-execute hooks
247
+ phase.post_execute(state)