Skip to content

Commit

Permalink
Start of a next-status command
Browse files Browse the repository at this point in the history
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
mih committed Jan 12, 2024
1 parent 7eadb4b commit 66203ab
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 0 deletions.
4 changes: 4 additions & 0 deletions datalad_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
'datalad_next.commands.ls_file_collection', 'LsFileCollection',
'ls-file-collection',
),
(
'datalad_next.commands.status', 'Status',
'next-status', 'next_status',
),
]
)

Expand Down
1 change: 1 addition & 0 deletions datalad_next/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CommandResult
CommandResultStatus
status.StatusResult
"""
from __future__ import annotations

Expand Down
212 changes: 212 additions & 0 deletions datalad_next/commands/status.py
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 ''),
)
1 change: 1 addition & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ High-level API commands
credentials
download
ls_file_collection
next_status
tree
1 change: 1 addition & 0 deletions docs/source/cmd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 66203ab

Please sign in to comment.