Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Queries the Zenodo record of every dataset in the manifest and rewrites the regi
fwl-io list
```

Lists every dataset from all installed manifests with its key and consumers. Datasets without a committed registry are flagged `[NO REGISTRY]`. Providers whose manifest fails to load are reported on stderr and the exit status is 1.
Lists every dataset from all installed manifests with its key and consumers. When a manifest gives a dataset a human-readable `name` that differs from its key, that label is printed on the next line below the key. Datasets without a committed registry are flagged `[NO REGISTRY]`. Providers whose manifest fails to load are reported on stderr and the exit status is 1.

## fwl-io fetch

Expand Down
5 changes: 5 additions & 0 deletions src/fwl_io/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ def _cmd_list(args: argparse.Namespace) -> int:
'' if ds.registry_path and ds.registry_path.is_file() else ' [NO REGISTRY]'
)
print(f' {ds.key:50s} required_by: {consumers}{registry_note}')
if ds.name != ds.key:
# A name is a single-line label; drop control characters so a
# manifest cannot inject extra lines into the listing.
label = ''.join(c for c in ds.name if c.isprintable())
print(f' {label}')
for provider, message in sorted(errors.items()):
print(f'[{provider}] FAILED TO LOAD: {message}', file=sys.stderr)
return 1 if errors else 0
Expand Down
78 changes: 78 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,84 @@ def broken():
assert 'badmodel' in captured.err and 'FAILED TO LOAD' in captured.err


@pytest.mark.unit
def test_list_shows_declared_name_but_not_the_key_fallback(tmp_path, capsys, monkeypatch):
"""A declared human-readable name is printed; an undeclared one is not repeated."""
manifest = tmp_path / 'manifest.toml'
manifest.write_text(
'[labelled]\n'
'name = "Wolf & Bower (2018) MgSiO3 equation of state"\n'
'zenodo = "10.5281/zenodo.1"\n'
'[unlabelled]\n'
'zenodo = "10.5281/zenodo.2"\n'
)

class _EP:
def __init__(self, name, target):
self.name = name
self._target = target

def load(self):
return self._target

monkeypatch.setattr(
'fwl_io.manifest.entry_points',
lambda group: [_EP('demo', lambda: manifest)],
)

code = main(['list'])
out = capsys.readouterr().out
lines = out.splitlines()
name = 'Wolf & Bower (2018) MgSiO3 equation of state'
assert code == 0
# The declared name prints on the line immediately below its key, not merely
# somewhere in the output: a print-order swap has to fail this.
key_line = next(i for i, line in enumerate(lines) if line.strip().startswith('labelled'))
assert lines[key_line + 1].strip() == name
# The undeclared dataset falls back to its key, so its key line is not
# followed by a repeat of the key.
fallback_line = next(i for i, line in enumerate(lines) if line.strip().startswith('unlabelled'))
following = lines[fallback_line + 1] if fallback_line + 1 < len(lines) else ''
assert following.strip() != 'unlabelled'
assert out.count('unlabelled') == 1


@pytest.mark.unit
def test_list_strips_control_characters_from_a_declared_name(tmp_path, capsys, monkeypatch):
"""A name carrying a newline cannot inject an extra line into the listing."""
manifest = tmp_path / 'manifest.toml'
manifest.write_text(
'[evil]\n'
'name = "harmless\\n forged.key required_by: victim [NO REGISTRY]"\n'
'zenodo = "10.5281/zenodo.1"\n'
)

class _EP:
def __init__(self, name, target):
self.name = name
self._target = target

def load(self):
return self._target

monkeypatch.setattr(
'fwl_io.manifest.entry_points',
lambda group: [_EP('demo', lambda: manifest)],
)

code = main(['list'])
out = capsys.readouterr().out
lines = out.splitlines()
assert code == 0
# The embedded newline must not produce a fourth line that reads like a
# second dataset: provider header, key line, one name line, nothing else.
assert len(lines) == 3
assert lines[0] == '[demo]'
assert lines[1].startswith(' evil')
assert lines[2].startswith(' harmless')
assert not any(line.startswith(' forged') for line in lines)


@pytest.mark.unit
def test_fetch_unknown_module_exits_nonzero(capsys, monkeypatch):
"""Asking for a model no manifest declares is an error, not an empty success."""
Expand Down
Loading