From 66203ab97dd6c9b788a7b365377db73d8c91e0a0 Mon Sep 17 00:00:00 2001 From: Michael Hanke Date: Thu, 11 Jan 2024 18:01:13 +0100 Subject: [PATCH] Start of a `next-status` command 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 #586 (eventually) --- datalad_next/__init__.py | 4 + datalad_next/commands/__init__.py | 1 + datalad_next/commands/status.py | 212 ++++++++++++++++++++++++++++++ docs/source/api.rst | 1 + docs/source/cmd.rst | 1 + 5 files changed, 219 insertions(+) create mode 100644 datalad_next/commands/status.py diff --git a/datalad_next/__init__.py b/datalad_next/__init__.py index 116df896c..86b879839 100644 --- a/datalad_next/__init__.py +++ b/datalad_next/__init__.py @@ -43,6 +43,10 @@ 'datalad_next.commands.ls_file_collection', 'LsFileCollection', 'ls-file-collection', ), + ( + 'datalad_next.commands.status', 'Status', + 'next-status', 'next_status', + ), ] ) diff --git a/datalad_next/commands/__init__.py b/datalad_next/commands/__init__.py index 36de84564..c7cfe99b5 100644 --- a/datalad_next/commands/__init__.py +++ b/datalad_next/commands/__init__.py @@ -14,6 +14,7 @@ CommandResult CommandResultStatus + status.StatusResult """ from __future__ import annotations diff --git a/datalad_next/commands/status.py b/datalad_next/commands/status.py new file mode 100644 index 000000000..ee07d526c --- /dev/null +++ b/datalad_next/commands/status.py @@ -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 ''), + ) diff --git a/docs/source/api.rst b/docs/source/api.rst index bd4fa567c..f75f34a2b 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -9,4 +9,5 @@ High-level API commands credentials download ls_file_collection + next_status tree diff --git a/docs/source/cmd.rst b/docs/source/cmd.rst index ce3d3a4c3..3bae9c82c 100644 --- a/docs/source/cmd.rst +++ b/docs/source/cmd.rst @@ -8,4 +8,5 @@ Command line reference generated/man/datalad-credentials generated/man/datalad-download generated/man/datalad-ls-file-collection + generated/man/datalad-next-status generated/man/datalad-tree