Skip to content

Commit c51d6fd

Browse files
committed
wip
1 parent 4875bd3 commit c51d6fd

10 files changed

Lines changed: 51 additions & 26 deletions

File tree

questionpy_common/manifest.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@
33
# (c) Technische Universität Berlin, innoCampus <info@isis.tu-berlin.de>
44

55
from abc import ABC
6+
from collections.abc import MutableMapping
67
from enum import StrEnum
78
from keyword import iskeyword, issoftkeyword
89
from typing import Annotated, Literal, NewType
910

10-
from pydantic import AfterValidator, BaseModel, ByteSize, PositiveInt, StringConstraints, conset, field_validator
11+
from pydantic import (
12+
AfterValidator,
13+
BaseModel,
14+
ByteSize,
15+
PositiveInt,
16+
StringConstraints,
17+
computed_field,
18+
conset,
19+
field_validator,
20+
model_validator,
21+
)
1122
from pydantic.fields import Field
1223

1324
from questionpy_common import PackageNamespaceAndShortName
@@ -135,14 +146,19 @@ class PackageFile(BaseModel):
135146
mime_type: str | None
136147
size: int
137148

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

145160

161+
146162
type DependencyLockStrategy = Literal["required", "preferred-no-downgrade", "preferred-allow-downgrade"]
147163

148164

@@ -158,9 +174,6 @@ class AbstractDynamicQPyDependency(BaseModel, ABC):
158174
version: QPyDependencyVersionSpecifier | None = None
159175
include_prereleases: bool = False
160176

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

165178
class DistDynamicQPyDependency(AbstractDynamicQPyDependency):
166179
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/repository/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
_SCHEME_AND_AUTH_PATTERN = re.compile(r"^https?://(?:[^/@]+@)?")
1717
_FILENAME_SPECIAL_CHARACTERS_PATTERN = re.compile(r"[/\\?%*:|\"<>,;=\s]+")
1818

19+
1920
def _url_to_safe_path_part(url: str) -> str:
2021
return _FILENAME_SPECIAL_CHARACTERS_PATTERN.sub("-", _SCHEME_AND_AUTH_PATTERN.sub("", url))
2122

23+
2224
class Repository:
2325
def __init__(self, url: str, cache: FileCache):
2426
self._url_base = url

questionpy_server/utils/logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
class URLAdapter(logging.LoggerAdapter):
1111
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
1212
if self.extra and "url" in self.extra:
13-
return f"({self.extra['url']}): {msg}", kwargs
13+
return f"({self.extra["url"]}): {msg}", kwargs
1414
return msg, kwargs

questionpy_server/worker/runtime/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from types import MappingProxyType
1212
from typing import TYPE_CHECKING, NoReturn, TypeVar, cast
1313

14+
from questionpy_common import PackageNamespaceAndShortName
1415
from questionpy_common.constants import MAX_QPY_DEPENDENCY_LEVELS
1516
from questionpy_common.environment import (
1617
Environment,
@@ -21,7 +22,6 @@
2122
RequestInfo,
2223
set_qpy_environment,
2324
)
24-
from questionpy_common import PackageNamespaceAndShortName
2525
from questionpy_common.manifest import PackageType
2626
from questionpy_server.worker.runtime.connection import WorkerToServerConnection
2727
from questionpy_server.worker.runtime.messages import (

questionpy_server/worker/runtime/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010

1111
from pydantic import BaseModel, JsonValue
1212

13+
from questionpy_common import PackageNamespaceAndShortName
1314
from questionpy_common.api.attempt import AttemptModel, AttemptScoredModel, AttemptStartedModel
1415
from questionpy_common.api.qtype import InvalidQuestionStateError, OptionsFormValidationError
1516
from questionpy_common.api.question import QuestionModel
1617
from questionpy_common.elements import OptionsFormDefinition
1718
from questionpy_common.environment import PackagePermissions, RequestInfo
18-
from questionpy_common import PackageNamespaceAndShortName
1919
from questionpy_common.error import QPyBaseError
2020
from questionpy_common.manifest import Manifest
2121
from questionpy_server.worker.runtime.package_location import PackageLocation

questionpy_server/worker/runtime/package.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import logging
66
import sys
77
from abc import ABC, abstractmethod
8+
from collections.abc import Iterable
89
from importlib import import_module, resources
910
from importlib.resources.abc import Traversable
1011
from pathlib import Path
1112
from types import ModuleType
1213
from zipfile import ZipFile
1314

15+
from questionpy_common import PackageNamespaceAndShortName
1416
from questionpy_common.api.package import QPyPackageInterface
1517
from questionpy_common.constants import DIST_DIR, MANIFEST_FILENAME
1618
from questionpy_common.environment import (
@@ -20,7 +22,6 @@
2022
PackageNotLoadedError,
2123
PackageState,
2224
)
23-
from questionpy_common import PackageNamespaceAndShortName
2425
from questionpy_common.manifest import DistStaticQPyDependency, Manifest
2526
from questionpy_server.worker.runtime.package_location import (
2627
DirPackageLocation,
@@ -81,7 +82,7 @@ def init(self, env: Environment) -> None:
8182
"""
8283

8384
@abstractmethod
84-
def resolve_static_dependencies(self) -> list[PackageLocation]:
85+
def resolve_static_dependencies(self) -> Iterable[PackageLocation]:
8586
pass
8687

8788

tests/questionpy_common/test_manifest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,6 @@ def test_valid_permissions() -> None:
165165
)
166166

167167
for permission, type_hint in partial.items():
168-
assert type_hint == complete[permission] | None, (
169-
"Custom permissions must be of the same type as the server permissions."
170-
)
168+
assert (
169+
type_hint == complete[permission] | None
170+
), "Custom permissions must be of the same type as the server permissions."

tests/questionpy_server/test_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ def test_multiline_env_var_gets_parsed_correctly(path_with_empty_config_file: Pa
112112

113113

114114
def test_standard_package_permissions_default_main_process_execution_modes_is_valid() -> None:
115-
assert MainProcessExecutionModeValues.issuperset(CompletePackagePermissions().main_process_execution_modes), (
116-
"The default value for 'main_process_execution_modes' is invalid."
117-
)
115+
assert MainProcessExecutionModeValues.issuperset(
116+
CompletePackagePermissions().main_process_execution_modes
117+
), "The default value for 'main_process_execution_modes' is invalid."
118118

119119

120120
def test_standard_package_permissions_can_convert_to_package_permissions() -> None:

tests/questionpy_server/web/routes/test_packages.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ async def add_package_version(server: QPyServer, manifest: ComparableManifest) -
6767
# Assert that the actual package info is a subset of the manifest of the latest package version.
6868
actual_package_info_items = actual_package_info.model_dump().items()
6969
latest_manifest_items = manifests[actual_package_info.namespace][actual_versions[0]].model_dump().items()
70-
assert actual_package_info_items <= latest_manifest_items, (
71-
"Actual package info was not derived from the latest package version."
72-
)
70+
assert (
71+
actual_package_info_items <= latest_manifest_items
72+
), "Actual package info was not derived from the latest package version."
7373

7474
actual_namespaces.append(actual_package_info.namespace)
7575

0 commit comments

Comments
 (0)