|
7 | 7 | from contextlib import contextmanager |
8 | 8 | from dataclasses import dataclass |
9 | 9 | from graphlib import TopologicalSorter |
| 10 | +from itertools import chain |
10 | 11 | from pathlib import Path |
11 | 12 | from types import MappingProxyType |
12 | 13 | from typing import TYPE_CHECKING, NoReturn, TypeVar, cast |
|
22 | 23 | set_qpy_environment, |
23 | 24 | ) |
24 | 25 | from questionpy_common.manifest import PackageType |
| 26 | +from questionpy_server.dependencies import SolutionAndLocation, StaticDependencySolution |
25 | 27 | from questionpy_server.worker.runtime.connection import WorkerToServerConnection |
26 | 28 | from questionpy_server.worker.runtime.messages import ( |
27 | 29 | CreateQuestionFromOptions, |
@@ -87,12 +89,14 @@ def register_on_request_callback(self, callback: OnRequestCallback) -> None: |
87 | 89 | type OnMessageCallback[M: MessageToWorker] = Callable[[M], MessageToServer] |
88 | 90 |
|
89 | 91 |
|
90 | | -def _linearize_packages( |
91 | | - packages: Mapping[PackageNamespaceAndShortName, ImportablePackage], |
| 92 | +def _linearize_dependencies( |
| 93 | + solutions: Mapping[PackageNamespaceAndShortName, SolutionAndLocation], |
92 | 94 | ) -> Sequence[PackageNamespaceAndShortName]: |
93 | 95 | sorter = TopologicalSorter[PackageNamespaceAndShortName]() |
94 | | - for nssn, package in packages.items(): |
95 | | - sorter.add(nssn, *package.dependencies.keys()) |
| 96 | + |
| 97 | + for nssn, (solution, _) in solutions.items(): |
| 98 | + dep_nssns = [PackageNamespaceAndShortName(dep.namespace, dep.short_name) for dep in solution.dependencies.qpy] |
| 99 | + sorter.add(nssn, *dep_nssns) |
96 | 100 |
|
97 | 101 | return tuple(sorter.static_order()) |
98 | 102 |
|
@@ -159,54 +163,48 @@ def _open_package(location: PackageLocation, worker_home: Path) -> ImportablePac |
159 | 163 | # This is a separate method to allow it to be mocked separately. |
160 | 164 | return open_qpy_package(location, worker_home) |
161 | 165 |
|
162 | | - def _open_packages_recursively( |
163 | | - self, |
164 | | - msg: LoadQPyPackage, |
165 | | - package_location: PackageLocation, |
166 | | - stack: tuple[PackageNamespaceAndShortName, ...] = (), |
167 | | - ) -> tuple[PackageNamespaceAndShortName, ImportablePackage]: |
| 166 | + def on_msg_load_qpy_package(self, msg: LoadQPyPackage) -> MessageToServer: |
168 | 167 | if not self._env or not self._worker_home: |
169 | 168 | self._raise_not_initialized(msg) |
170 | 169 |
|
171 | | - package = self._open_package(package_location, self._worker_home) |
172 | | - nssn = PackageNamespaceAndShortName(package.manifest.namespace, package.manifest.short_name) |
173 | | - |
174 | | - if nssn in stack and self._packages[nssn].manifest.version == package.manifest.version: |
175 | | - raise CircularDependencyError(nssn, stack) |
176 | | - |
177 | | - if nssn in self._packages: |
178 | | - # For now, we don't support two packages using the same static dependency, even if they would use the same |
179 | | - # version. Supporting the latter case would require us to either trust or check that both dependency's |
180 | | - # content is identical. |
181 | | - err_msg = f"Package '{nssn}' is already loaded. Dependency stack: {stack}" |
182 | | - raise DependencyError(err_msg, stack) |
| 170 | + root_package = self._open_package(msg.location, self._worker_home) |
| 171 | + root_nssn = root_package.manifest.nssn |
| 172 | + self._packages[root_nssn] = root_package |
183 | 173 |
|
184 | | - self._packages[nssn] = package |
| 174 | + linearized = _linearize_dependencies(msg.dependencies) |
185 | 175 |
|
186 | | - new_stack = (*stack, nssn) |
| 176 | + for nssn in reversed(linearized): |
| 177 | + solution, package_location = msg.dependencies[nssn] |
| 178 | + if isinstance(solution, StaticDependencySolution): |
| 179 | + owner = self._packages.get(solution.owner) |
| 180 | + if not owner: |
| 181 | + # Since we open packages in reverse topological order, this shouldn't happen. |
| 182 | + # (Unless the tree passed to us by the server contains errors.) |
| 183 | + err_msg = f"Cannot open static dependency '{nssn}' before owner '{solution.owner}'." |
| 184 | + raise RuntimeError(err_msg) |
187 | 185 |
|
188 | | - if len(stack) >= MAX_QPY_DEPENDENCY_LEVELS and package.manifest.dependencies.qpy: |
189 | | - raise TooDeeplyNestedDependencyError(new_stack) |
| 186 | + package_location = owner.resolve_static_dependency(nssn) |
190 | 187 |
|
191 | | - for dep_location in package.resolve_static_dependencies(): |
192 | | - dep_nssn, dep_package = self._open_packages_recursively(msg, dep_location, new_stack) |
193 | | - package.dependencies[dep_nssn] = dep_package |
194 | | - |
195 | | - return nssn, package |
196 | | - |
197 | | - def on_msg_load_qpy_package(self, msg: LoadQPyPackage) -> MessageToServer: |
198 | | - if not self._env or not self._worker_home: |
199 | | - self._raise_not_initialized(msg) |
200 | | - |
201 | | - root_nssn, root_package = self._open_packages_recursively(msg, msg.location, ()) |
| 188 | + # MyPy doesn't narrow the type properly. |
| 189 | + self._packages[nssn] = self._open_package(cast("PackageLocation", package_location), self._worker_home) |
202 | 190 |
|
203 | 191 | if msg.main: |
204 | 192 | self._env = dataclasses.replace(self._env, _main_package=root_package) |
205 | 193 | set_qpy_environment(self._env) |
206 | 194 |
|
207 | | - linearized = _linearize_packages(self._packages) |
208 | | - for nssn in linearized: |
| 195 | + for nssn in chain(linearized, (root_nssn,)): |
209 | 196 | package = self._packages[nssn] |
| 197 | + |
| 198 | + # Make the package's dependencies accessible to the package. |
| 199 | + for dep in package.manifest.dependencies.qpy: |
| 200 | + dep_nssn = PackageNamespaceAndShortName(dep.namespace, dep.short_name) |
| 201 | + dep_package = self._packages.get(dep_nssn) |
| 202 | + if not dep_package: |
| 203 | + err_msg = f"Unfulfilled dependency of '{nssn}': '{dep_nssn}'" |
| 204 | + raise RuntimeError(err_msg) |
| 205 | + |
| 206 | + package.dependencies[dep_nssn] = dep_package |
| 207 | + |
210 | 208 | if package.state < PackageState.LOADED: |
211 | 209 | package.load() |
212 | 210 |
|
|
0 commit comments