porringer 0.2.1.dev0__tar.gz → 0.2.1.dev6__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 (152) hide show
  1. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/PKG-INFO +1 -1
  2. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/api.py +1 -1
  3. porringer-0.2.1.dev6/porringer/backend/backend.py +153 -0
  4. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/builder.py +70 -0
  5. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/command/sync.py +456 -87
  6. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/resolver.py +4 -1
  7. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/plugin.py +3 -1
  8. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/self.py +1 -1
  9. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/sync.py +23 -2
  10. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/core/plugin_schema/environment.py +151 -29
  11. porringer-0.2.1.dev6/porringer/core/plugin_schema/project_environment.py +195 -0
  12. porringer-0.2.1.dev6/porringer/core/plugin_schema/runtime.py +85 -0
  13. porringer-0.2.1.dev6/porringer/core/schema.py +298 -0
  14. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/apt/plugin.py +18 -5
  15. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/brew/plugin.py +20 -5
  16. porringer-0.2.1.dev6/porringer/plugin/bun/__init__.py +1 -0
  17. porringer-0.2.1.dev6/porringer/plugin/bun/plugin.py +139 -0
  18. porringer-0.2.1.dev6/porringer/plugin/bun_project/__init__.py +1 -0
  19. porringer-0.2.1.dev6/porringer/plugin/bun_project/plugin.py +41 -0
  20. porringer-0.2.1.dev6/porringer/plugin/deno/__init__.py +1 -0
  21. porringer-0.2.1.dev6/porringer/plugin/deno/plugin.py +156 -0
  22. porringer-0.2.1.dev6/porringer/plugin/deno_project/__init__.py +1 -0
  23. porringer-0.2.1.dev6/porringer/plugin/deno_project/plugin.py +69 -0
  24. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/npm/plugin.py +44 -10
  25. porringer-0.2.1.dev6/porringer/plugin/npm_project/__init__.py +1 -0
  26. porringer-0.2.1.dev6/porringer/plugin/npm_project/plugin.py +39 -0
  27. porringer-0.2.1.dev6/porringer/plugin/pdm/__init__.py +1 -0
  28. porringer-0.2.1.dev6/porringer/plugin/pdm/plugin.py +37 -0
  29. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/pim/plugin.py +75 -9
  30. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/pip/plugin.py +137 -38
  31. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/pipx/plugin.py +37 -8
  32. porringer-0.2.1.dev6/porringer/plugin/pnpm/__init__.py +1 -0
  33. porringer-0.2.1.dev6/porringer/plugin/pnpm/plugin.py +167 -0
  34. porringer-0.2.1.dev6/porringer/plugin/pnpm_project/__init__.py +1 -0
  35. porringer-0.2.1.dev6/porringer/plugin/pnpm_project/plugin.py +66 -0
  36. porringer-0.2.1.dev6/porringer/plugin/poetry/__init__.py +1 -0
  37. porringer-0.2.1.dev6/porringer/plugin/poetry/plugin.py +78 -0
  38. porringer-0.2.1.dev6/porringer/plugin/pyenv/__init__.py +1 -0
  39. porringer-0.2.1.dev6/porringer/plugin/pyenv/plugin.py +309 -0
  40. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/uv/plugin.py +43 -23
  41. porringer-0.2.1.dev6/porringer/plugin/uv_project/__init__.py +1 -0
  42. porringer-0.2.1.dev6/porringer/plugin/uv_project/plugin.py +39 -0
  43. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/winget/plugin.py +15 -13
  44. porringer-0.2.1.dev6/porringer/plugin/yarn_project/__init__.py +1 -0
  45. porringer-0.2.1.dev6/porringer/plugin/yarn_project/plugin.py +71 -0
  46. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/schema.py +88 -21
  47. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/test/mock/environment.py +2 -4
  48. porringer-0.2.1.dev6/porringer/test/mock/project_environment.py +29 -0
  49. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/test/pytest/shared.py +29 -13
  50. porringer-0.2.1.dev6/porringer/test/pytest/tests.py +100 -0
  51. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/test/pytest/variants.py +17 -0
  52. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/pyproject.toml +20 -1
  53. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/test_example_presence.py +2 -2
  54. porringer-0.2.1.dev6/tests/unit/plugins/apt/__init__.py +1 -0
  55. porringer-0.2.1.dev6/tests/unit/plugins/apt/test_environment.py +20 -0
  56. porringer-0.2.1.dev6/tests/unit/plugins/brew/__init__.py +1 -0
  57. porringer-0.2.1.dev6/tests/unit/plugins/brew/test_environment.py +20 -0
  58. porringer-0.2.1.dev6/tests/unit/plugins/bun/__init__.py +1 -0
  59. porringer-0.2.1.dev6/tests/unit/plugins/bun/test_environment.py +20 -0
  60. porringer-0.2.1.dev6/tests/unit/plugins/bun_project/__init__.py +1 -0
  61. porringer-0.2.1.dev6/tests/unit/plugins/bun_project/test_environment.py +16 -0
  62. porringer-0.2.1.dev6/tests/unit/plugins/deno/__init__.py +1 -0
  63. porringer-0.2.1.dev6/tests/unit/plugins/deno/test_environment.py +20 -0
  64. porringer-0.2.1.dev6/tests/unit/plugins/deno_project/__init__.py +1 -0
  65. porringer-0.2.1.dev6/tests/unit/plugins/deno_project/test_environment.py +16 -0
  66. porringer-0.2.1.dev6/tests/unit/plugins/npm/__init__.py +1 -0
  67. porringer-0.2.1.dev6/tests/unit/plugins/npm/test_environment.py +20 -0
  68. porringer-0.2.1.dev6/tests/unit/plugins/npm_project/__init__.py +1 -0
  69. porringer-0.2.1.dev6/tests/unit/plugins/npm_project/test_environment.py +16 -0
  70. porringer-0.2.1.dev6/tests/unit/plugins/pdm/__init__.py +1 -0
  71. porringer-0.2.1.dev6/tests/unit/plugins/pdm/test_environment.py +16 -0
  72. porringer-0.2.1.dev6/tests/unit/plugins/pim/__init__.py +1 -0
  73. porringer-0.2.1.dev6/tests/unit/plugins/pim/test_environment.py +97 -0
  74. porringer-0.2.1.dev6/tests/unit/plugins/pip/test_environment.py +234 -0
  75. porringer-0.2.1.dev6/tests/unit/plugins/pipx/__init__.py +1 -0
  76. porringer-0.2.1.dev6/tests/unit/plugins/pipx/test_environment.py +20 -0
  77. porringer-0.2.1.dev6/tests/unit/plugins/pnpm/__init__.py +1 -0
  78. porringer-0.2.1.dev6/tests/unit/plugins/pnpm/test_environment.py +20 -0
  79. porringer-0.2.1.dev6/tests/unit/plugins/pnpm_project/__init__.py +1 -0
  80. porringer-0.2.1.dev6/tests/unit/plugins/pnpm_project/test_environment.py +16 -0
  81. porringer-0.2.1.dev6/tests/unit/plugins/poetry/__init__.py +1 -0
  82. porringer-0.2.1.dev6/tests/unit/plugins/poetry/test_environment.py +16 -0
  83. porringer-0.2.1.dev6/tests/unit/plugins/pyenv/__init__.py +1 -0
  84. porringer-0.2.1.dev6/tests/unit/plugins/pyenv/test_environment.py +135 -0
  85. porringer-0.2.1.dev6/tests/unit/plugins/uv/__init__.py +1 -0
  86. porringer-0.2.1.dev6/tests/unit/plugins/uv/test_environment.py +20 -0
  87. porringer-0.2.1.dev6/tests/unit/plugins/uv_project/__init__.py +1 -0
  88. porringer-0.2.1.dev6/tests/unit/plugins/uv_project/test_environment.py +16 -0
  89. porringer-0.2.1.dev6/tests/unit/plugins/yarn_project/__init__.py +1 -0
  90. porringer-0.2.1.dev6/tests/unit/plugins/yarn_project/test_environment.py +16 -0
  91. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_command_plugin.py +6 -0
  92. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_command_self.py +3 -3
  93. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_manifest.py +85 -73
  94. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_package_ref.py +76 -4
  95. porringer-0.2.1.dev6/tests/unit/test_project_directory.py +240 -0
  96. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_sub_action_progress.py +4 -1
  97. porringer-0.2.1.dev0/porringer/backend/backend.py +0 -142
  98. porringer-0.2.1.dev0/porringer/core/schema.py +0 -150
  99. porringer-0.2.1.dev0/porringer/test/pytest/tests.py +0 -31
  100. porringer-0.2.1.dev0/tests/unit/plugins/pip/test_environment.py +0 -182
  101. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/LICENSE.md +0 -0
  102. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/README.md +0 -0
  103. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/__init__.py +0 -0
  104. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/__init__.py +0 -0
  105. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/cache.py +0 -0
  106. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/command/__init__.py +0 -0
  107. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/command/plugin.py +0 -0
  108. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/command/self.py +0 -0
  109. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/backend/schema.py +0 -0
  110. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/__init__.py +0 -0
  111. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/__init__.py +0 -0
  112. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/cache.py +0 -0
  113. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/check.py +0 -0
  114. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/command/download.py +0 -0
  115. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/entry.py +0 -0
  116. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/console/schema.py +0 -0
  117. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/core/__init__.py +0 -0
  118. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/core/plugin_schema/__init__.py +0 -0
  119. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/__init__.py +0 -0
  120. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/apt/__init__.py +0 -0
  121. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/brew/__init__.py +0 -0
  122. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/npm/__init__.py +0 -0
  123. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/pim/__init__.py +0 -0
  124. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/pip/__init__.py +0 -0
  125. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/pipx/__init__.py +0 -0
  126. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/uv/__init__.py +0 -0
  127. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/plugin/winget/__init__.py +0 -0
  128. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/py.typed +0 -0
  129. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/test/mock/__init__.py +0 -0
  130. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/test/pytest/__init__.py +0 -0
  131. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/test/pytest/plugin.py +0 -0
  132. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/utility/__init__.py +0 -0
  133. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/utility/download.py +0 -0
  134. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/utility/exception.py +0 -0
  135. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/utility/py.typed +0 -0
  136. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/porringer/utility/utility.py +0 -0
  137. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/__init__.py +0 -0
  138. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/conftest.py +0 -0
  139. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/__init__.py +0 -0
  140. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/__init__.py +0 -0
  141. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/pip/__init__.py +0 -0
  142. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/pip/test_environment.py +0 -0
  143. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/pipx/__init__.py +0 -0
  144. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/pipx/test_environment.py +0 -0
  145. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/winget/__init__.py +0 -0
  146. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/integration/plugins/winget/test_environment.py +0 -0
  147. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/__init__.py +0 -0
  148. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/plugins/__init__.py +0 -0
  149. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/plugins/pip/__init__.py +0 -0
  150. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_cache.py +0 -0
  151. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_check.py +0 -0
  152. {porringer-0.2.1.dev0 → porringer-0.2.1.dev6}/tests/unit/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: porringer
3
- Version: 0.2.1.dev0
3
+ Version: 0.2.1.dev6
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: MIT
6
6
  Project-URL: homepage, https://github.com/synodic/porringer
@@ -34,5 +34,5 @@ class API:
34
34
  self.cache = DirectoryCacheManager(self.configuration.data_directory)
35
35
 
36
36
  self.plugin = PluginCommands()
37
- self.porringer = SelfCommands()
37
+ self.updates = SelfCommands()
38
38
  self.sync = SyncCommands(self.cache)
@@ -0,0 +1,153 @@
1
+ """Backend resolution for mapping (kind, ecosystem) pairs to installer plugins.
2
+
3
+ The ``BackendResolver`` determines which plugin should handle each
4
+ ``(PluginKind, ecosystem)`` pair declared in a manifest. For example,
5
+ ``(PACKAGE, "python")`` might resolve to ``uv`` or ``pip`` depending
6
+ on availability and user preferences.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from collections.abc import Mapping
13
+
14
+ from porringer.core.plugin_schema.environment import Environment
15
+ from porringer.core.plugin_schema.project_environment import ProjectEnvironment
16
+ from porringer.core.schema import PluginKind
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Type alias for any plugin that participates in backend resolution.
21
+ BackendPlugin = Environment | ProjectEnvironment
22
+
23
+
24
+ class BackendResolver:
25
+ """Maps ``(PluginKind, ecosystem)`` pairs to the best available plugin.
26
+
27
+ Construction requires two inputs:
28
+
29
+ * *environments* – all instantiated :class:`Environment` plugins
30
+ keyed by their canonical name.
31
+ * *preferences* – an optional dict coming from the manifest's
32
+ ``preferences`` field (e.g. ``{"python": "uv"}``).
33
+
34
+ Optionally accepts *project_environments* for project-scoped plugins.
35
+
36
+ Resolution algorithm per ``(kind, ecosystem)`` pair:
37
+
38
+ 1. If the user gave an explicit **preference** for the ecosystem and
39
+ the named plugin is available, use it.
40
+ 2. Otherwise sort all registered candidates by
41
+ :meth:`~Plugin.default_priority` ascending and pick the first one
42
+ whose ``is_available()`` returns ``True``.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ environments: Mapping[str, Environment],
48
+ preferences: Mapping[str, str] | None = None,
49
+ project_environments: Mapping[str, ProjectEnvironment] | None = None,
50
+ ) -> None:
51
+ self._environments = environments
52
+ self._project_environments: Mapping[str, ProjectEnvironment] = project_environments or {}
53
+ self._preferences = preferences or {}
54
+
55
+ # Merged view for indexing and availability checks
56
+ self._all_plugins: dict[str, BackendPlugin] = dict(environments)
57
+ self._all_plugins.update(self._project_environments)
58
+
59
+ # Index: (kind, ecosystem) -> [plugin_name, ...]
60
+ self._backend_plugins: dict[tuple[PluginKind, str], list[str]] = {}
61
+ for name, plugin in self._all_plugins.items():
62
+ ecosystem = type(plugin).ecosystem()
63
+ if ecosystem is not None:
64
+ kind = type(plugin).plugin_kind()
65
+ key = (kind, ecosystem)
66
+ self._backend_plugins.setdefault(key, []).append(name)
67
+
68
+ # Resolve once and cache
69
+ self._resolved: dict[tuple[PluginKind, str], str | None] = {}
70
+ for key in self._backend_plugins:
71
+ self._resolved[key] = self._resolve(key)
72
+
73
+ # ------------------------------------------------------------------
74
+ # Public API
75
+ # ------------------------------------------------------------------
76
+
77
+ def resolve(self, kind: PluginKind, ecosystem: str) -> str | None:
78
+ """Return the chosen plugin name for *(kind, ecosystem)*, or ``None``."""
79
+ key = (kind, ecosystem)
80
+ if key not in self._resolved:
81
+ logger.warning("No plugins registered for (%s, '%s')", kind.value, ecosystem)
82
+ return None
83
+ return self._resolved[key]
84
+
85
+ def validator_for(self, kind: PluginKind, ecosystem: str) -> str | None:
86
+ """Return the ``package_name_validator()`` tag for the resolved plugin.
87
+
88
+ Returns ``None`` when no plugin is resolved or the plugin
89
+ declares no validator.
90
+ """
91
+ name = self.resolve(kind, ecosystem)
92
+ if name is None:
93
+ return None
94
+ plugin = self._all_plugins.get(name)
95
+ if plugin is None:
96
+ return None
97
+ return type(plugin).package_name_validator()
98
+
99
+ # ------------------------------------------------------------------
100
+ # Internal
101
+ # ------------------------------------------------------------------
102
+
103
+ def _resolve(self, key: tuple[PluginKind, str]) -> str | None:
104
+ """Pick the best plugin for *(kind, ecosystem)*.
105
+
106
+ 1. Explicit preference (if available).
107
+ 2. Sort candidates by ``default_priority()`` ascending, pick first available.
108
+ """
109
+ kind, ecosystem = key
110
+ candidates = self._backend_plugins.get(key, [])
111
+ if not candidates:
112
+ return None
113
+
114
+ # 1. Explicit preference
115
+ if ecosystem in self._preferences:
116
+ preferred = self._preferences[ecosystem]
117
+ if preferred in candidates and self._is_available(preferred):
118
+ return preferred
119
+ logger.warning(
120
+ "Preferred plugin '%s' for (%s, '%s') is not available; falling back",
121
+ preferred,
122
+ kind.value,
123
+ ecosystem,
124
+ )
125
+
126
+ # 2. Sort by default_priority ascending
127
+ def _priority(name: str) -> int:
128
+ plugin = self._all_plugins.get(name)
129
+ if plugin is None:
130
+ return 9999
131
+ try:
132
+ return type(plugin).default_priority()
133
+ except Exception:
134
+ return 9999
135
+
136
+ sorted_candidates = sorted(candidates, key=_priority)
137
+ for name in sorted_candidates:
138
+ if self._is_available(name):
139
+ return name
140
+
141
+ logger.warning("No available plugin for (%s, '%s')", kind.value, ecosystem)
142
+ return None
143
+
144
+ def _is_available(self, plugin_name: str) -> bool:
145
+ """Check if *plugin_name* reports itself as available."""
146
+ plugin = self._all_plugins.get(plugin_name)
147
+ if plugin is None:
148
+ return False
149
+ try:
150
+ return type(plugin).is_available()
151
+ except Exception:
152
+ logger.debug("is_available() failed for plugin '%s'", plugin_name, exc_info=True)
153
+ return False
@@ -6,6 +6,7 @@ from importlib import metadata
6
6
  from packaging.version import Version
7
7
 
8
8
  from porringer.core.plugin_schema.environment import Environment
9
+ from porringer.core.plugin_schema.project_environment import ProjectEnvironment
9
10
  from porringer.core.schema import Distribution, PluginDependency, PluginParameters
10
11
  from porringer.schema import PluginInformation
11
12
  from porringer.utility.exception import PluginDependencyError
@@ -163,3 +164,72 @@ class Builder:
163
164
  environments.append(Builder.build_environment(environment_type))
164
165
 
165
166
  return environments
167
+
168
+ @staticmethod
169
+ def find_project_environments() -> list[PluginInformation[ProjectEnvironment]]:
170
+ """Searches for registered project environment plugins.
171
+
172
+ Scans the ``porringer.project_environment`` entry-point group for
173
+ classes that subclass :class:`ProjectEnvironment`.
174
+
175
+ Returns:
176
+ A list of loaded project-environment plugins.
177
+ """
178
+ group_name = 'project_environment'
179
+ plugin_types: list[PluginInformation[ProjectEnvironment]] = []
180
+
181
+ for entry_point in list(metadata.entry_points(group=f'porringer.{group_name}')):
182
+ try:
183
+ loaded_type = entry_point.load()
184
+ except ModuleNotFoundError as e:
185
+ logger.warning(f"Plugin '{entry_point.name}' could not be loaded: {e}. Skipping")
186
+ continue
187
+
188
+ canonicalized = canonicalize_type(loaded_type)
189
+
190
+ if entry_point.dist is None:
191
+ logger.error(f"Plugin '{canonicalized.name}' is not installed. Skipping")
192
+ continue
193
+
194
+ if not issubclass(loaded_type, ProjectEnvironment):
195
+ logger.warning(
196
+ f"Found incompatible plugin. The '{canonicalized.name}' plugin must be an instance"
197
+ f" of '{group_name}'"
198
+ )
199
+ else:
200
+ logger.debug(f'{group_name} plugin found: {canonicalized.name}')
201
+ plugin_types.append(PluginInformation(loaded_type, entry_point.dist))
202
+
203
+ return plugin_types
204
+
205
+ @staticmethod
206
+ def build_project_environment(
207
+ project_environment_type: PluginInformation[ProjectEnvironment],
208
+ ) -> ProjectEnvironment:
209
+ """Constructs a single project environment from input type.
210
+
211
+ Args:
212
+ project_environment_type: The type to construct.
213
+
214
+ Returns:
215
+ The instantiated project environment.
216
+ """
217
+ plugin_version = Version(project_environment_type.distribution.version)
218
+ plugin_distribution = Distribution(version=plugin_version)
219
+ parameters = PluginParameters(distribution=plugin_distribution)
220
+
221
+ return project_environment_type.type(parameters)
222
+
223
+ @staticmethod
224
+ def build_project_environments(
225
+ project_environment_types: list[PluginInformation[ProjectEnvironment]],
226
+ ) -> list[ProjectEnvironment]:
227
+ """Constructs project environments from input types.
228
+
229
+ Args:
230
+ project_environment_types: The types to construct.
231
+
232
+ Returns:
233
+ The instantiated project environments.
234
+ """
235
+ return [Builder.build_project_environment(t) for t in project_environment_types]