Skip to content

Commit d7f422c

Browse files
committed
wip
1 parent 4875bd3 commit d7f422c

File tree

16 files changed

+502
-34
lines changed

16 files changed

+502
-34
lines changed

questionpy_common/manifest.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
from keyword import iskeyword, issoftkeyword
88
from typing import Annotated, Literal, NewType
99

10-
from pydantic import AfterValidator, BaseModel, ByteSize, PositiveInt, StringConstraints, conset, field_validator
10+
from pydantic import (
11+
AfterValidator,
12+
BaseModel,
13+
ByteSize,
14+
PositiveInt,
15+
StringConstraints,
16+
conset,
17+
field_validator,
18+
)
1119
from pydantic.fields import Field
1220

1321
from questionpy_common import PackageNamespaceAndShortName
@@ -137,8 +145,13 @@ class PackageFile(BaseModel):
137145

138146

139147
class DistStaticQPyDependency(BaseModel):
140-
dir_name: str
141-
"""Name (without `dist/dependencies/qpy/`) of the directory the dependency package contents reside in."""
148+
namespace: Annotated[str, AfterValidator(ensure_is_valid_name)]
149+
short_name: Annotated[str, AfterValidator(ensure_is_valid_name)]
150+
version: Annotated[str, Field(pattern=RE_SEMVER)]
151+
152+
dependencies: "DistDependencies"
153+
"""Transitive dependencies of this dependency."""
154+
142155
hash: str
143156
"""Hash of the ZIP package whose contents lie in `dir_name`."""
144157

@@ -158,9 +171,6 @@ class AbstractDynamicQPyDependency(BaseModel, ABC):
158171
version: QPyDependencyVersionSpecifier | None = None
159172
include_prereleases: bool = False
160173

161-
def to_specifier_str(self) -> str:
162-
return f"@{self.namespace}/{self.short_name}{" " + str(self.version) if self.version else ""}"
163-
164174

165175
class DistDynamicQPyDependency(AbstractDynamicQPyDependency):
166176
locked: LockedDependencyInfo | None = None

questionpy_common/version_specifiers.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from collections.abc import Iterable
23
from dataclasses import dataclass
34
from typing import Any, Literal, Protocol, Self
45

@@ -81,7 +82,7 @@ def from_string(cls, string: str) -> Self:
8182
operator = next(filter(string.startswith, _OPERATORS), None)
8283
if operator:
8384
version_string = string.removeprefix(operator).lstrip()
84-
if not _SEMVER_PATTERN.match(string):
85+
if not _SEMVER_PATTERN.match(version_string):
8586
msg = f"Comparison version '{version_string}' of clause '{string}' does not conform to SemVer."
8687
raise ValueError(msg)
8788

@@ -91,7 +92,7 @@ def from_string(cls, string: str) -> Self:
9192
if not _SEMVER_PATTERN.match(string):
9293
msg = (
9394
f"Version specifier clause '{string}' does not start with a valid operator and isn't a "
94-
f"version itself. Valid operators are {', '.join(_OPERATORS)}."
95+
f"version itself. Valid operators are {", ".join(_OPERATORS)}."
9596
)
9697
raise ValueError(msg)
9798

@@ -103,10 +104,18 @@ def from_string(cls, string: str) -> Self:
103104
def __str__(self) -> str:
104105
return f"{self.operator} {self.operand}"
105106

106-
clauses: tuple[Clause, ...]
107+
# Dict because we want to preserve order (for readability) but not compare order or allow dupes.
108+
_clauses: dict[Clause, None]
109+
110+
def __init__(self, clauses: Iterable[Clause]) -> None:
111+
super().__setattr__("_clauses", dict.fromkeys(clauses))
112+
113+
@property
114+
def clauses(self) -> tuple[Clause, ...]:
115+
return tuple(self._clauses)
107116

108117
def __str__(self) -> str:
109-
return ", ".join(map(str, self.clauses))
118+
return ", ".join(map(str, self._clauses))
110119

111120
@classmethod
112121
def from_string(cls, string: str) -> Self:
@@ -116,7 +125,7 @@ def from_string(cls, string: str) -> Self:
116125

117126
def allows(self, version: VersionProtocol) -> bool:
118127
"""Checks if _all_ clauses allow the given version."""
119-
return all(clause.allows(version) for clause in self.clauses)
128+
return all(clause.allows(version) for clause in self._clauses)
120129

121130
@classmethod
122131
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:

questionpy_server/dependency_management/__init__.py

Whitespace-only changes.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from questionpy_common import PackageNamespaceAndShortName
2+
from questionpy_common.version_specifiers import QPyDependencyVersionSpecifier
3+
from questionpy_server.collector import PackageCollection
4+
from questionpy_server.dependency_management._dependency_tree import DependencySolution, resolve_dependency_tree
5+
from questionpy_server.dependency_management._dynamic_dependencies import (
6+
AvailablePackageVersion,
7+
DynamicDependencyResolver,
8+
NoPackageMatchingVersionSpecError,
9+
)
10+
from questionpy_server.package import Package
11+
from questionpy_server.utils.manifest import ComparableManifest
12+
from questionpy_server.worker import PackageLocation
13+
14+
15+
class PackageCollectionAvailableVersion(AvailablePackageVersion):
16+
def __init__(self, package: Package) -> None:
17+
self._package = package
18+
19+
@property
20+
def manifest(self) -> ComparableManifest:
21+
return self._package.manifest
22+
23+
@property
24+
def hash(self) -> str:
25+
return self._package.hash
26+
27+
async def get_package_location(self) -> PackageLocation:
28+
return await self._package.get_zip_package_location()
29+
30+
31+
class _PackageCollectionDependencyResolver(DynamicDependencyResolver[PackageCollectionAvailableVersion]):
32+
def __init__(self, package_collection: PackageCollection) -> None:
33+
self._package_collection = package_collection
34+
35+
async def resolve(
36+
self,
37+
nssn: PackageNamespaceAndShortName,
38+
version_spec: QPyDependencyVersionSpecifier | None,
39+
*,
40+
include_prereleases: bool,
41+
) -> PackageCollectionAvailableVersion:
42+
versions = self._package_collection.get_by_identifier(str(nssn))
43+
44+
allowed_versions = {
45+
version: package
46+
for version, package in versions.items()
47+
if (include_prereleases or version.prerelease is None)
48+
and (version_spec is None or version_spec.allows(version))
49+
}
50+
51+
if not allowed_versions:
52+
raise NoPackageMatchingVersionSpecError(
53+
nssn, version_spec, tuple(versions.keys()), include_prereleases=include_prereleases
54+
)
55+
56+
_, latest_package = max(allowed_versions.items(), key=lambda tup: tup[0])
57+
58+
return PackageCollectionAvailableVersion(latest_package)
59+
60+
61+
async def resolve_dependencies_for_execution(
62+
package_collection: PackageCollection, package: Package
63+
) -> dict[PackageNamespaceAndShortName, DependencySolution]:
64+
return await resolve_dependency_tree(package.manifest, _PackageCollectionDependencyResolver(package_collection))

0 commit comments

Comments
 (0)