Skip to content

Commit 32375d3

Browse files
authored
Fix typing issues and add pyright to CI (#3011)
* Fix possible unbound vars in background tasks * Move class Config after fields * Fix unbound vars in app.py * Assert provisioning gateways have compute * Assert region set for ssh instances * Assert region service configurations * Fix logging type annotations * Fix redundant add_project_members line * Fix Lockset annotations * Assert configuration types in job configurators * Fix unbound vars in process_running_jobs * Check backend is not None * Assert service and jpd in register_service * Assert jpd in container_ssh_tunnel * Assert jpd in ServerProxyRepo * Fix configuration.model type annotation * Assert conf.replicas * Fix abstract AsyncGenerator def * Fix ProbeConfig type annotations * Fix max_duration and stop_duration type annotations * Fix idle_duration type annotations * Fix volumes and files type annotations * Fix retry.duration type annotation * Do not define Storage implementations when deps missing * Fix gateway domain None * Overload get_backend_config * Do not define LogStorage implementations when deps missing * Fix filelog typing * Use async_sessionmaker * Assert proxy_jump.ssh_key * Ignore type errors from deps * Forbid entrypoint for dev-environment Fixes #3002 * Fix vscode and cursor __init__ annotations * Pass probe_spec.body as content * Assert gateway configuration.name * Fix unbound next_token * Cast path to str * Fix unbound success var * Fix typing * Add pyright config * Run pyright in CI * Run pyright as part of tests * Ignore type for entry_points * Fix _detect_vscode_version * Remove run_name assert * Fix add_extra_schema_types for one $ref * Replace ConfigurationWith extensions with mixins * Fix unbound spec_json * Remove huggingface api * Type check plugins * Fix BaseApplyConfigurator generics * Type check core/services * Fix services.gpus typing * Document pyright in Contributing
1 parent f76cda6 commit 32375d3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1127
-935
lines changed

.github/workflows/build-artifacts.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ jobs:
7373
python-version: ${{ matrix.python-version }}
7474
- name: Install dependencies
7575
run: uv sync --all-extras
76+
- name: Run pyright
77+
uses: jakebailey/pyright-action@v2
78+
with:
79+
pylance-version: latest-release
7680
- name: Download frontend build
7781
uses: actions/download-artifact@v4
7882
with:

contributing/DEVELOPMENT.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,26 @@ uv sync --all-extras
2525

2626
Alternatively, if you want to manage virtual environments by yourself, you can install `dstack` into the activated virtual environment with `uv sync --all-extras --active`.
2727

28-
## 4. (Recommended) Install pre-commits:
28+
## 4. (Recommended) Install pre-commit hooks:
29+
30+
Code formatting and linting can be done automatically on each commit with `pre-commit` hooks:
2931

3032
```shell
3133
uv run pre-commit install
3234
```
3335

34-
## 5. Frontend
36+
## 5. (Recommended) Use pyright:
37+
38+
The CI runs `pyright` for type checking `dstack` Python code.
39+
So we recommend you configure your IDE to use `pyright`/`pylance` with `standard` type checking mode.
40+
41+
You can also install `pyright` and run it from the CLI:
42+
43+
```shell
44+
uv tool install pyright
45+
pyright -p .
46+
```
47+
48+
## 6. Frontend
3549

3650
See [FRONTEND.md](FRONTEND.md) for the details on how to build and develop the frontend.

pyproject.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ pattern = '<picture>\s*|<source[^>]*>\s*|\s*</picture>|<video[^>]*>\s*|</video>\
7878
replacement = ''
7979
ignore-case = true
8080

81+
[tool.pyright]
82+
include = [
83+
"src/dstack/plugins",
84+
"src/dstack/_internal/server",
85+
"src/dstack/_internal/core/services",
86+
"src/dstack/_internal/cli/services/configurators",
87+
]
88+
ignore = [
89+
"src/dstack/_internal/server/migrations/versions",
90+
]
91+
8192
[dependency-groups]
8293
dev = [
8394
"httpx>=0.28.1",

src/dstack/_internal/cli/commands/offer.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
from typing import List
44

55
from dstack._internal.cli.commands import APIBaseCommand
6-
from dstack._internal.cli.services.configurators.run import BaseRunConfigurator
6+
from dstack._internal.cli.services.args import cpu_spec, disk_spec, gpu_spec
7+
from dstack._internal.cli.services.configurators.run import (
8+
BaseRunConfigurator,
9+
)
10+
from dstack._internal.cli.services.profile import register_profile_args
711
from dstack._internal.cli.utils.common import console
812
from dstack._internal.cli.utils.gpu import print_gpu_json, print_gpu_table
913
from dstack._internal.cli.utils.run import print_offers_json, print_run_plan
@@ -18,11 +22,8 @@ class OfferConfigurator(BaseRunConfigurator):
1822
TYPE = ApplyConfigurationType.TASK
1923

2024
@classmethod
21-
def register_args(
22-
cls,
23-
parser: argparse.ArgumentParser,
24-
):
25-
super().register_args(parser, default_max_offers=50)
25+
def register_args(cls, parser: argparse.ArgumentParser):
26+
configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
2627
parser.add_argument(
2728
"--group-by",
2829
action="append",
@@ -33,6 +34,43 @@ def register_args(
3334
"Can be repeated or comma-separated (e.g. [code]--group-by gpu,backend[/code])."
3435
),
3536
)
37+
configuration_group.add_argument(
38+
"-n",
39+
"--name",
40+
dest="run_name",
41+
help="The name of the run. If not specified, a random name is assigned",
42+
)
43+
configuration_group.add_argument(
44+
"--max-offers",
45+
help="Number of offers to show in the run plan",
46+
type=int,
47+
default=50,
48+
)
49+
cls.register_env_args(configuration_group)
50+
configuration_group.add_argument(
51+
"--cpu",
52+
type=cpu_spec,
53+
help="Request CPU for the run. "
54+
"The format is [code]ARCH[/]:[code]COUNT[/] (all parts are optional)",
55+
dest="cpu_spec",
56+
metavar="SPEC",
57+
)
58+
configuration_group.add_argument(
59+
"--gpu",
60+
type=gpu_spec,
61+
help="Request GPU for the run. "
62+
"The format is [code]NAME[/]:[code]COUNT[/]:[code]MEMORY[/] (all parts are optional)",
63+
dest="gpu_spec",
64+
metavar="SPEC",
65+
)
66+
configuration_group.add_argument(
67+
"--disk",
68+
type=disk_spec,
69+
help="Request the size range of disk for the run. Example [code]--disk 100GB..[/].",
70+
metavar="RANGE",
71+
dest="disk_spec",
72+
)
73+
register_profile_args(parser)
3674

3775

3876
class OfferCommand(APIBaseCommand):
@@ -117,7 +155,7 @@ def _process_group_by_args(self, group_by_args: List[str]) -> List[str]:
117155

118156
return processed
119157

120-
def _list_gpus(self, args: List[str], run_spec: RunSpec) -> List[GpuGroup]:
158+
def _list_gpus(self, args: argparse.Namespace, run_spec: RunSpec) -> List[GpuGroup]:
121159
group_by = [g for g in args.group_by if g != "gpu"] or None
122160
return self.api.client.gpus.list_gpus(
123161
self.api.project,

src/dstack/_internal/cli/services/configurators/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
APPLY_STDIN_NAME = "-"
2525

2626

27-
apply_configurators_mapping: Dict[ApplyConfigurationType, Type[BaseApplyConfigurator]] = {
27+
apply_configurators_mapping: Dict[
28+
ApplyConfigurationType, Type[BaseApplyConfigurator[AnyApplyConfiguration]]
29+
] = {
2830
cls.TYPE: cls
2931
for cls in [
3032
DevEnvironmentConfigurator,
@@ -47,7 +49,9 @@
4749
}
4850

4951

50-
def get_apply_configurator_class(configurator_type: str) -> Type[BaseApplyConfigurator]:
52+
def get_apply_configurator_class(
53+
configurator_type: str,
54+
) -> Type[BaseApplyConfigurator[AnyApplyConfiguration]]:
5155
return apply_configurators_mapping[ApplyConfigurationType(configurator_type)]
5256

5357

src/dstack/_internal/cli/services/configurators/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import argparse
22
import os
33
from abc import ABC, abstractmethod
4-
from typing import List, Optional, Union, cast
4+
from typing import Generic, List, Optional, TypeVar, Union, cast
55

66
from dstack._internal.cli.services.args import env_var
77
from dstack._internal.core.errors import ConfigurationError
@@ -15,8 +15,10 @@
1515

1616
ArgsParser = Union[argparse._ArgumentGroup, argparse.ArgumentParser]
1717

18+
ApplyConfigurationT = TypeVar("ApplyConfigurationT", bound=AnyApplyConfiguration)
1819

19-
class BaseApplyConfigurator(ABC):
20+
21+
class BaseApplyConfigurator(ABC, Generic[ApplyConfigurationT]):
2022
TYPE: ApplyConfigurationType
2123

2224
def __init__(self, api_client: Client):
@@ -25,7 +27,7 @@ def __init__(self, api_client: Client):
2527
@abstractmethod
2628
def apply_configuration(
2729
self,
28-
conf: AnyApplyConfiguration,
30+
conf: ApplyConfigurationT,
2931
configuration_path: str,
3032
command_args: argparse.Namespace,
3133
configurator_args: argparse.Namespace,
@@ -48,7 +50,7 @@ def apply_configuration(
4850
@abstractmethod
4951
def delete_configuration(
5052
self,
51-
conf: AnyApplyConfiguration,
53+
conf: ApplyConfigurationT,
5254
configuration_path: str,
5355
command_args: argparse.Namespace,
5456
):

src/dstack/_internal/cli/services/configurators/fleet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
logger = get_logger(__name__)
4747

4848

49-
class FleetConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
49+
class FleetConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator[FleetConfiguration]):
5050
TYPE: ApplyConfigurationType = ApplyConfigurationType.FLEET
5151

5252
def apply_configuration(

src/dstack/_internal/cli/services/configurators/gateway.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from dstack.api._public import Client
2828

2929

30-
class GatewayConfigurator(BaseApplyConfigurator):
30+
class GatewayConfigurator(BaseApplyConfigurator[GatewayConfiguration]):
3131
TYPE: ApplyConfigurationType = ApplyConfigurationType.GATEWAY
3232

3333
def apply_configuration(

src/dstack/_internal/cli/services/configurators/run.py

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
import time
55
from pathlib import Path
6-
from typing import Dict, List, Optional, Set
6+
from typing import Dict, List, Optional, Set, TypeVar
77

88
import gpuhunt
99
from pydantic import parse_obj_as
@@ -33,8 +33,7 @@
3333
from dstack._internal.core.models.configurations import (
3434
AnyRunConfiguration,
3535
ApplyConfigurationType,
36-
BaseRunConfiguration,
37-
BaseRunConfigurationWithPorts,
36+
ConfigurationWithPortsParams,
3837
DevEnvironmentConfiguration,
3938
PortMapping,
4039
RunConfigurationType,
@@ -63,13 +62,18 @@
6362

6463
logger = get_logger(__name__)
6564

65+
RunConfigurationT = TypeVar("RunConfigurationT", bound=AnyRunConfiguration)
6666

67-
class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
67+
68+
class BaseRunConfigurator(
69+
ApplyEnvVarsConfiguratorMixin,
70+
BaseApplyConfigurator[RunConfigurationT],
71+
):
6872
TYPE: ApplyConfigurationType
6973

7074
def apply_configuration(
7175
self,
72-
conf: BaseRunConfiguration,
76+
conf: RunConfigurationT,
7377
configuration_path: str,
7478
command_args: argparse.Namespace,
7579
configurator_args: argparse.Namespace,
@@ -267,7 +271,7 @@ def apply_configuration(
267271

268272
def delete_configuration(
269273
self,
270-
conf: AnyRunConfiguration,
274+
conf: RunConfigurationT,
271275
configuration_path: str,
272276
command_args: argparse.Namespace,
273277
):
@@ -293,7 +297,7 @@ def delete_configuration(
293297
console.print(f"Run [code]{conf.name}[/] deleted")
294298

295299
@classmethod
296-
def register_args(cls, parser: argparse.ArgumentParser, default_max_offers: int = 3):
300+
def register_args(cls, parser: argparse.ArgumentParser):
297301
configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
298302
configuration_group.add_argument(
299303
"-n",
@@ -305,7 +309,7 @@ def register_args(cls, parser: argparse.ArgumentParser, default_max_offers: int
305309
"--max-offers",
306310
help="Number of offers to show in the run plan",
307311
type=int,
308-
default=default_max_offers,
312+
default=3,
309313
)
310314
cls.register_env_args(configuration_group)
311315
configuration_group.add_argument(
@@ -333,7 +337,7 @@ def register_args(cls, parser: argparse.ArgumentParser, default_max_offers: int
333337
)
334338
register_profile_args(parser)
335339

336-
def apply_args(self, conf: BaseRunConfiguration, args: argparse.Namespace, unknown: List[str]):
340+
def apply_args(self, conf: RunConfigurationT, args: argparse.Namespace, unknown: List[str]):
337341
apply_profile_args(args, conf)
338342
if args.run_name:
339343
conf.name = args.run_name
@@ -357,7 +361,7 @@ def interpolate_run_args(self, value: List[str], unknown):
357361
except InterpolatorError as e:
358362
raise ConfigurationError(e.args[0])
359363

360-
def interpolate_env(self, conf: BaseRunConfiguration):
364+
def interpolate_env(self, conf: RunConfigurationT):
361365
env_dict = conf.env.as_dict()
362366
interpolator = VariablesInterpolator({"env": env_dict}, skip=["secrets"])
363367
try:
@@ -377,7 +381,7 @@ def interpolate_env(self, conf: BaseRunConfiguration):
377381
except InterpolatorError as e:
378382
raise ConfigurationError(e.args[0])
379383

380-
def validate_gpu_vendor_and_image(self, conf: BaseRunConfiguration) -> None:
384+
def validate_gpu_vendor_and_image(self, conf: RunConfigurationT) -> None:
381385
"""
382386
Infers and sets `resources.gpu.vendor` if not set, requires `image` if the vendor is AMD.
383387
"""
@@ -438,7 +442,7 @@ def validate_gpu_vendor_and_image(self, conf: BaseRunConfiguration) -> None:
438442
"`image` is required if `resources.gpu.vendor` is `tenstorrent`"
439443
)
440444

441-
def validate_cpu_arch_and_image(self, conf: BaseRunConfiguration) -> None:
445+
def validate_cpu_arch_and_image(self, conf: RunConfigurationT) -> None:
442446
"""
443447
Infers `resources.cpu.arch` if not set, requires `image` if the architecture is ARM.
444448
"""
@@ -462,10 +466,9 @@ def validate_cpu_arch_and_image(self, conf: BaseRunConfiguration) -> None:
462466
raise ConfigurationError("`image` is required if `resources.cpu.arch` is `arm`")
463467

464468

465-
class RunWithPortsConfigurator(BaseRunConfigurator):
469+
class RunWithPortsConfiguratorMixin:
466470
@classmethod
467-
def register_args(cls, parser: argparse.ArgumentParser):
468-
super().register_args(parser)
471+
def register_ports_args(cls, parser: argparse.ArgumentParser):
469472
parser.add_argument(
470473
"-p",
471474
"--port",
@@ -482,29 +485,42 @@ def register_args(cls, parser: argparse.ArgumentParser):
482485
metavar="HOST",
483486
)
484487

485-
def apply_args(
486-
self, conf: BaseRunConfigurationWithPorts, args: argparse.Namespace, unknown: List[str]
488+
def apply_ports_args(
489+
self,
490+
conf: ConfigurationWithPortsParams,
491+
args: argparse.Namespace,
487492
):
488-
super().apply_args(conf, args, unknown)
489493
if args.ports:
490494
conf.ports = list(_merge_ports(conf.ports, args.ports).values())
491495

492496

493-
class TaskConfigurator(RunWithPortsConfigurator):
497+
class TaskConfigurator(RunWithPortsConfiguratorMixin, BaseRunConfigurator):
494498
TYPE = ApplyConfigurationType.TASK
495499

500+
@classmethod
501+
def register_args(cls, parser: argparse.ArgumentParser):
502+
super().register_args(parser)
503+
cls.register_ports_args(parser)
504+
496505
def apply_args(self, conf: TaskConfiguration, args: argparse.Namespace, unknown: List[str]):
497506
super().apply_args(conf, args, unknown)
507+
self.apply_ports_args(conf, args)
498508
self.interpolate_run_args(conf.commands, unknown)
499509

500510

501-
class DevEnvironmentConfigurator(RunWithPortsConfigurator):
511+
class DevEnvironmentConfigurator(RunWithPortsConfiguratorMixin, BaseRunConfigurator):
502512
TYPE = ApplyConfigurationType.DEV_ENVIRONMENT
503513

514+
@classmethod
515+
def register_args(cls, parser: argparse.ArgumentParser):
516+
super().register_args(parser)
517+
cls.register_ports_args(parser)
518+
504519
def apply_args(
505520
self, conf: DevEnvironmentConfiguration, args: argparse.Namespace, unknown: List[str]
506521
):
507522
super().apply_args(conf, args, unknown)
523+
self.apply_ports_args(conf, args)
508524
if conf.ide == "vscode" and conf.version is None:
509525
conf.version = _detect_vscode_version()
510526
if conf.version is None:
@@ -674,6 +690,8 @@ def render_run_spec_diff(old_spec: RunSpec, new_spec: RunSpec) -> Optional[str]:
674690
if type(old_spec.profile) is not type(new_spec.profile):
675691
item = NestedListItem("Profile")
676692
else:
693+
assert old_spec.profile is not None
694+
assert new_spec.profile is not None
677695
item = NestedListItem(
678696
"Profile properties:",
679697
children=[

src/dstack/_internal/cli/services/configurators/volume.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from dstack.api._public import Client
2727

2828

29-
class VolumeConfigurator(BaseApplyConfigurator):
29+
class VolumeConfigurator(BaseApplyConfigurator[VolumeConfiguration]):
3030
TYPE: ApplyConfigurationType = ApplyConfigurationType.VOLUME
3131

3232
def apply_configuration(

0 commit comments

Comments
 (0)