forked from datalad/datalad-next
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This implementation is the first to emit `CommandResult` type result items, ie. dataclass instances rather than result dicts. It also uses uniform parameter validation, enabling substantially simplified implementation (e.g., of the result renderer). The user-facing appearance remains (largely?) the same. TODO more detailed analysis The command options `untracked` and `recursive` now both take (optional) qualifiying values, but also work with any value specification at the CLI. Closes datalad#586 (eventually)
- Loading branch information
Showing
5 changed files
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
CommandResult | ||
CommandResultStatus | ||
status.StatusResult | ||
""" | ||
from __future__ import annotations | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
""" | ||
""" | ||
from __future__ import annotations | ||
|
||
__docformat__ = 'restructuredtext' | ||
|
||
from dataclasses import dataclass | ||
from enum import Enum | ||
from logging import getLogger | ||
from pathlib import Path | ||
from typing import Generator | ||
|
||
from datalad_next.commands import ( | ||
CommandResult, | ||
CommandResultStatus, | ||
EnsureCommandParameterization, | ||
ValidatedInterface, | ||
Parameter, | ||
build_doc, | ||
datasetmethod, | ||
eval_results, | ||
) | ||
from datalad_next.constraints import ( | ||
EnsureChoice, | ||
WithDescription, | ||
) | ||
from datalad_next.constraints.dataset import EnsureDataset | ||
|
||
from datalad_next.iter_collections.gitdiff import GitDiffStatus | ||
from datalad_next.iter_collections.gitstatus import ( | ||
GitTreeItemType, | ||
iter_gitstatus, | ||
) | ||
from datalad_next.uis import ( | ||
ui_switcher as ui, | ||
ansi_colors as ac, | ||
) | ||
|
||
lgr = getLogger('datalad.core.local.status') | ||
|
||
|
||
# TODO Could be `StrEnum`, came with PY3.11 | ||
class StatusState(Enum): | ||
"""Enumeration of possible states of a status command result | ||
The "state" is the condition of the dataset item being reported | ||
on. | ||
""" | ||
clean = 'clean' | ||
added = 'added' | ||
modified = 'modified' | ||
deleted = 'deleted' | ||
untracked = 'untracked' | ||
unknown = 'unknown' | ||
|
||
|
||
STATE_COLOR_MAP = { | ||
StatusState.added: ac.GREEN, | ||
StatusState.modified: ac.RED, | ||
StatusState.deleted: ac.RED, | ||
StatusState.untracked: ac.RED, | ||
StatusState.unknown: ac.YELLOW, | ||
} | ||
|
||
|
||
diffstatus2resultstate_map = { | ||
GitDiffStatus.addition: StatusState.added, | ||
GitDiffStatus.copy: StatusState.added, | ||
GitDiffStatus.deletion: StatusState.deleted, | ||
GitDiffStatus.modification: StatusState.modified, | ||
GitDiffStatus.rename: StatusState.added, | ||
GitDiffStatus.typechange: StatusState.modified, | ||
GitDiffStatus.unmerged: StatusState.unknown, | ||
GitDiffStatus.unknown: StatusState.unknown, | ||
GitDiffStatus.other: StatusState.untracked, | ||
} | ||
|
||
|
||
# see base class decorator comment for why this is commented out | ||
#@dataclass(kw_only=True) | ||
@dataclass | ||
class StatusResult(CommandResult): | ||
# TODO any of the following property are not actually optional | ||
# we only have to declare them such for limitations of dataclasses | ||
# prior PY3.10 (see kw_only command in base class | ||
|
||
diff_state: GitDiffStatus | None = None | ||
"""The ``status`` of the underlying ``GitDiffItem``. It is named | ||
"_state" to emphasize the conceptual similarity with the legacy | ||
property 'state' | ||
""" | ||
type: GitTreeItemType | None = None | ||
"""The ``type`` of the underlying ``GitDiffItem``.""" | ||
prev_type: GitTreeItemType | None = None | ||
"""The ``prev_type`` of the underlying ``GitDiffItem``.""" | ||
|
||
@property | ||
def state(self) -> StatusState: | ||
"""A (more or less legacy) simplified representation of the subject | ||
state. For a more accurate classification use the ``diff_status`` | ||
property. | ||
""" | ||
return diffstatus2resultstate_map[self.diff_state] | ||
|
||
@property | ||
def type_src(self) -> str | None: | ||
"""Backward-compatibility adaptor""" | ||
return self.prev_type | ||
|
||
|
||
@build_doc | ||
class Status(ValidatedInterface): | ||
"""The is a previous of an upcoming command implementation to replace | ||
the DataLad ``status`` command. | ||
For now expect anything here to change again. | ||
""" | ||
# Interface.validate_args() will inspect this dict for the presence of a | ||
# validator for particular parameters | ||
_validator_ = EnsureCommandParameterization( | ||
param_constraints=dict( | ||
# if given, it must also exist | ||
dataset=EnsureDataset(installed=True), | ||
untracked=EnsureChoice( | ||
'no', 'whole-dir', 'no-empty-dir', 'normal', 'all'), | ||
), | ||
validate_defaults=('dataset',), | ||
) | ||
|
||
# this is largely here for documentation and CLI parser building | ||
_params_ = dict( | ||
dataset=Parameter( | ||
args=("-d", "--dataset"), | ||
doc="""Dataset to be used as a configuration source. Beyond | ||
reading configuration items, this command does not interact with | ||
the dataset."""), | ||
untracked=Parameter( | ||
args=('--untracked',), | ||
nargs='?', | ||
const='no-empty-dir', | ||
doc="""If and how untracked content is reported when comparing | ||
a revision to the state of the working tree. 'no': no untracked | ||
content is reported; 'normal': untracked files and entire | ||
untracked directories are reported as such; 'all': report | ||
individual files even in fully untracked directories."""), | ||
recursive=Parameter( | ||
args=('-r', '--recursive'), | ||
nargs='?', | ||
const='datasets', | ||
doc="some"), | ||
) | ||
|
||
_examples_ = [ | ||
] | ||
|
||
@staticmethod | ||
@datasetmethod(name="next_status") | ||
@eval_results | ||
def __call__( | ||
# TODO later | ||
#path=None, | ||
*, | ||
dataset=None, | ||
# TODO later | ||
#annex=None, | ||
untracked='whole-dir', | ||
recursive='repository', | ||
# TODO unclear if needed | ||
#eval_subdataset_state='full', | ||
) -> Generator[StatusResult, None, None] | list[StatusResult]: | ||
ds = dataset.ds | ||
rootpath = Path.cwd() if dataset.original is None else ds.pathobj | ||
|
||
for item in iter_gitstatus( | ||
path=rootpath, | ||
untracked=untracked, | ||
recursive=recursive, | ||
): | ||
yield StatusResult( | ||
action='status', | ||
status=CommandResultStatus.ok, | ||
path=rootpath / (item.path or item.prev_path), | ||
type=item.gittype, | ||
prev_type=item.prev_gittype, | ||
diff_state=item.status, | ||
refds=ds, | ||
logger=lgr, | ||
) | ||
|
||
def custom_result_renderer(res, **kwargs): | ||
# we are guaranteed to have dataset-arg info through uniform | ||
# parameter validation | ||
dsarg = kwargs['dataset'] | ||
rootpath = Path.cwd() if dsarg.original is None else dsarg.ds.pathobj | ||
# because we can always determine the root path of the command | ||
# execution environment, we can report meaningful relative paths | ||
# unconditionally | ||
path = res.path.relative_to(rootpath) | ||
# collapse item type information across current and previous states | ||
type_ = res.type or res.prev_type | ||
type_str = '' if type_ is None else type_.value | ||
max_len = len('untracked') | ||
# message format is same as for previous command implementation | ||
ui.message(u'{fill}{state}: {path}{type_}'.format( | ||
fill=' ' * max(0, max_len - len(res.state.value)), | ||
state=ac.color_word( | ||
res.state.value, | ||
STATE_COLOR_MAP.get(res.state)), | ||
path=path, | ||
type_=' ({})'.format(ac.color_word(type_str, ac.MAGENTA)) | ||
if type_str else ''), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,4 +9,5 @@ High-level API commands | |
credentials | ||
download | ||
ls_file_collection | ||
next_status | ||
tree |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters