Skip to content

Commit 653baa2

Browse files
committed
Tasks can contain when deactivated commands.
As long as a `dev-cmd` invocation retains at least 1 command to run, the run proceeds and just executes fewer commands under the selected Python.
1 parent 496dec5 commit 653baa2

File tree

6 files changed

+80
-34
lines changed

6 files changed

+80
-34
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 0.28.0
4+
5+
As long as there is at least one command left to execute, tasks and any groups they contain now
6+
support having individual commands deactivated by `when` environment markers. This allows running
7+
tasks under multiple different Pythons and just running a subset of commands instead of failing when
8+
not all commands are available to execute under the selected Python.
9+
310
## 0.27.0
411

512
The command python value can now be parametrized allowing for tox-style ad-hoc Python specification.

dev_cmd/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2024 John Sirois.
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
__version__ = "0.27.0"
4+
__version__ = "0.28.0"

dev_cmd/invoke.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from asyncio.tasks import Task as AsyncTask
1313
from contextlib import asynccontextmanager
1414
from dataclasses import dataclass, field
15-
from typing import Any, AsyncIterator, Container, Mapping
15+
from typing import Any, AsyncIterator, Container, Iterator, Mapping
1616

1717
from dev_cmd import color
1818
from dev_cmd.color import USE_COLOR
@@ -65,14 +65,15 @@ def create(
6565
f"{accepts_extra_args.name!r} already does."
6666
)
6767
accepts_extra_args = step.base or step
68-
elif command := step.accepts_extra_args(skips):
69-
if accepts_extra_args and accepts_extra_args not in (command.base, command):
70-
raise InvalidModelError(
71-
f"The task {step.name!r} invokes command {command.name!r} which accepts extra "
72-
f"args, but only one command can accept extra args per invocation and command "
73-
f"{accepts_extra_args.name!r} already does."
74-
)
75-
accepts_extra_args = command.base or command
68+
elif commands := tuple(step.accepts_extra_args(skips)):
69+
for command in commands:
70+
if accepts_extra_args and accepts_extra_args not in (command.base, command):
71+
raise InvalidModelError(
72+
f"The task {step.name!r} invokes command {command.name!r} which accepts extra "
73+
f"args, but only one command can accept extra args per invocation and command "
74+
f"{accepts_extra_args.name!r} already does."
75+
)
76+
accepts_extra_args = command.base or command
7677

7778
return cls(
7879
steps=tuple(step for step in steps if step.name not in skips),
@@ -95,6 +96,16 @@ def create(
9596
console: Console
9697
_in_flight_processes: dict[Process, Command] = field(default_factory=dict, init=False)
9798

99+
def iter_commands(self) -> Iterator[Command]:
100+
for step in self.steps:
101+
if step.name in self.skips:
102+
continue
103+
elif isinstance(step, Command):
104+
yield step
105+
else:
106+
for command in step.iter_commands(self.skips):
107+
yield command
108+
98109
@asynccontextmanager
99110
async def _guarded_ctrl_c(self) -> AsyncIterator[None]:
100111
try:

dev_cmd/model.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from dataclasses import dataclass, field
99
from enum import Enum
1010
from pathlib import PurePath
11-
from typing import Any, Container, Mapping, MutableMapping
11+
from typing import Any, Container, Iterator, Mapping, MutableMapping
1212

1313
from packaging.markers import Marker
1414

@@ -60,14 +60,14 @@ class Command:
6060
class Group:
6161
members: tuple[Command | Task | Group, ...]
6262

63-
def accepts_extra_args(self, skips: Container[str]) -> Command | None:
63+
def iter_commands(self, skips: Container[str]) -> Iterator[Command]:
6464
for member in self.members:
6565
if isinstance(member, Command):
66-
if member.accepts_extra_args and member.name not in skips:
67-
return member
68-
elif command := member.accepts_extra_args(skips):
69-
return command
70-
return None
66+
if member.name not in skips:
67+
yield member
68+
else:
69+
for command in member.iter_commands(skips):
70+
yield command
7171

7272

7373
@dataclass(frozen=True)
@@ -78,10 +78,15 @@ class Task:
7878
description: str | None = None
7979
when: Marker | None = None
8080

81-
def accepts_extra_args(self, skips: Container[str] = ()) -> Command | None:
82-
if self.name in skips:
83-
return None
84-
return self.steps.accepts_extra_args(skips)
81+
def accepts_extra_args(self, skips: Container[str] = ()) -> Iterator[Command]:
82+
for command in self.iter_commands(skips):
83+
if command.accepts_extra_args:
84+
yield command
85+
86+
def iter_commands(self, skips: Container[str]) -> Iterator[Command]:
87+
if self.name not in skips:
88+
for command in self.steps.iter_commands(skips):
89+
yield command
8590

8691

8792
class ExitStyle(Enum):

dev_cmd/parse.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import itertools
88
import os
99
from collections import defaultdict
10+
from dataclasses import dataclass
1011
from pathlib import Path
1112
from typing import Any, Container, Dict, Iterable, Iterator, List, Mapping, Set, cast
1213

@@ -79,12 +80,17 @@ def _parse_when(data: dict[str, Any], table_path: str) -> Marker | None:
7980
)
8081

8182

83+
@dataclass(frozen=True)
84+
class DeactivatedCommand:
85+
name: str
86+
87+
8288
def _parse_commands(
8389
commands: dict[str, Any] | None,
8490
required_steps: dict[str, list[tuple[Factor, ...]]],
8591
project_dir: Path,
8692
marker_environment: dict[str, str] | None,
87-
) -> Iterator[Command]:
93+
) -> Iterator[Command | DeactivatedCommand]:
8894
if not commands:
8995
raise InvalidModelError(
9096
"There must be at least one entry in the [tool.dev-cmd.commands] table to run "
@@ -280,8 +286,10 @@ def substitute(text: str) -> str:
280286
python=substituted_python,
281287
)
282288

283-
if not when or when.evaluate(marker_environment):
284-
final_name = f"{name}{factors_suffix}"
289+
final_name = f"{name}{factors_suffix}"
290+
if when and not when.evaluate(marker_environment):
291+
yield DeactivatedCommand(final_name)
292+
else:
285293
previous_original_name = seen_commands.get(final_name)
286294
if previous_original_name and previous_original_name != original_name:
287295
raise InvalidModelError(
@@ -312,14 +320,17 @@ def _parse_group(
312320
group: list[Any],
313321
all_task_names: Container[str],
314322
tasks_defined_so_far: Mapping[str, Task],
315-
commands: Mapping[str, Command],
323+
commands: Mapping[str, Command | DeactivatedCommand],
316324
) -> Group:
317325
members: list[Command | Task | Group] = []
318326
for index, member in enumerate(group):
319327
if isinstance(member, str):
320328
for item in expand(member):
329+
command = commands.get(item)
330+
if isinstance(command, DeactivatedCommand):
331+
continue
321332
try:
322-
members.append(commands.get(item) or tasks_defined_so_far[item])
333+
members.append(command or tasks_defined_so_far[item])
323334
except KeyError:
324335
if item in all_task_names:
325336
raise InvalidModelError(
@@ -362,7 +373,7 @@ def _parse_group(
362373

363374
def _parse_tasks(
364375
tasks: dict[str, Any] | None,
365-
commands: Mapping[str, Command],
376+
commands: Mapping[str, Command | DeactivatedCommand],
366377
marker_environment: dict[str, str] | None,
367378
) -> Iterator[Task]:
368379
if not tasks:
@@ -454,11 +465,13 @@ def _parse_tasks(
454465

455466

456467
def _parse_default(
457-
default: Any, commands: Mapping[str, Command], tasks: Mapping[str, Task]
468+
default: Any, commands: Mapping[str, Command | DeactivatedCommand], tasks: Mapping[str, Task]
458469
) -> Command | Task | None:
459470
if default is None:
460471
if len(commands) == 1:
461-
return next(iter(commands.values()))
472+
only_command = next(iter(commands.values()))
473+
if isinstance(only_command, Command):
474+
return only_command
462475
return None
463476

464477
if not isinstance(default, str):
@@ -467,8 +480,11 @@ def _parse_default(
467480
f"{type(default)}."
468481
)
469482

483+
if selected_task := tasks.get(default):
484+
return selected_task
470485
try:
471-
return tasks.get(default) or commands[default]
486+
selected_command = commands[default]
487+
return selected_command if isinstance(selected_command, Command) else None
472488
except KeyError:
473489
raise InvalidModelError(
474490
os.linesep.join(
@@ -875,7 +891,7 @@ def pop_dict(key: str, *, path: str) -> dict[str, Any] | None:
875891
)
876892
break
877893

878-
commands = {
894+
commands: dict[str, Command | DeactivatedCommand] = {
879895
cmd.name: cmd
880896
for cmd in _parse_commands(
881897
commands_data,
@@ -904,7 +920,7 @@ def pop_dict(key: str, *, path: str) -> dict[str, Any] | None:
904920
)
905921

906922
return Configuration(
907-
commands=tuple(commands.values()),
923+
commands=tuple(cmd for cmd in commands.values() if isinstance(cmd, Command)),
908924
tasks=tuple(tasks.values()),
909925
default=default,
910926
exit_style=exit_style,

dev_cmd/run.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ def _run(
174174
f"arguments: {shlex.join(extra_args)}"
175175
)
176176

177+
if not tuple(invocation.iter_commands()):
178+
raise InvalidArgumentError(
179+
"All steps in this invocation of `dev-cmd` were deactivated by `when` markers leaving "
180+
"nothing to run."
181+
)
177182
invocation = dataclasses.replace(
178183
invocation, venvs=_ensure_venvs(invocation.steps, config.pythons)
179184
)
@@ -435,8 +440,10 @@ def _list(
435440
rendered_task_name = color.color(task.name, fg="magenta", style="bold")
436441
if config.default == task:
437442
rendered_task_name = f"* {rendered_task_name}"
438-
if extra_args_cmd := task.accepts_extra_args():
439-
extra_args_help = color.magenta(f" (-- extra {extra_args_cmd.args[0]} args ...)")
443+
if extra_args_cmds := tuple(task.accepts_extra_args()):
444+
extra_args_help = color.magenta(
445+
f" (-- extra {extra_args_cmds[0].args[0]} args ...)"
446+
)
440447
rendered_task_name = f"{rendered_task_name}{extra_args_help}"
441448
if task.description:
442449
console.print(f"{rendered_task_name}: ")

0 commit comments

Comments
 (0)