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
1 change: 1 addition & 0 deletions docs/AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Authors
* Oliver Beckstein (`webpage-ob <https://becksteinlab.physics.asu.edu>`_, `github-ob <https://github.com/orbeckst>`_)
* Lily Wang (`github-lw <https://github.com/lilyminium>`_)
* Henrik Jäger (`github-hj <https://github.com/hejamu>`_)
* Shreejan Dolai (`github-sd <https://github.com/spyke7>`_)
6 changes: 6 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changelog
=========

Unreleased
----------

* Added argcomplete for tab completion
* Updated docs

v0.1.33 (2025-10-20)
------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/src/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
installation
philosophy
usage
tab-completion
contributing
api
changelog
Expand Down
119 changes: 119 additions & 0 deletions docs/src/tab-completion.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
==============
Tab-Completion
==============

``mdacli`` includes built-in support for command-line tab-completion

Activation
==========

The activation method depends on your shell:

Bash
----

**Temporary (current session only)**::

eval "$(register-python-argcomplete mda)"

**Permanent (recommended)**

Add to your ``~/.bashrc``::

echo 'eval "$(register-python-argcomplete mda)"' >> ~/.bashrc
source ~/.bashrc

Zsh
---

Add to your ``~/.zshrc``::

autoload -U bashcompinit
bashcompinit
eval "$(register-python-argcomplete mda)"

Then reload::

source ~/.zshrc

Fish
----

Generate the completion file::

register-python-argcomplete --shell fish mda > ~/.config/fish/completions/mda.fish

Restart your Fish shell or run::

source ~/.config/fish/config.fish

Tcsh
----

Add to your shell startup file::

eval `register-python-argcomplete --shell tcsh mda`

Usage Examples
==============

Once enabled, tab-completion works for:

**Module names**::

mda <TAB>
# Shows: AlignTraj, AverageStructure, Contacts, DensityAnalysis, ...

**Partial module names**::

mda RM<TAB>
# Shows: RMSD, RMSF

**Options and flags**::

mda RMSD -<TAB>
# Shows: -s, -f, -atomgroup, -b, -e, -dt, -v, --debug, --version, ...

**Case insensitive**::

mda rmsd<TAB> # Also works
mda RmSd<TAB> # Also works

Troubleshooting
===============

Tab-completion not working
--------------------------

1. **Verify argcomplete is installed**::

python -c "import argcomplete; print(argcomplete.__version__)"

2. **Check if activation command was added**::

grep "register-python-argcomplete mda" ~/.bashrc

3. **Reload your shell**::

source ~/.bashrc # or restart terminal

4. **Test basic completion**::

mda <TAB>

Still not working
-----------------

- Make sure you've restarted your terminal or sourced the configuration file
- For Zsh, ensure ``bashcompinit`` is loaded before argcomplete
- Check that ``mda`` is in your PATH: ``which mda``
- Try running the registration command manually in your current shell

Global activation (for all Python scripts)
-------------------------------------------

To enable argcomplete for all Python scripts at once::

activate-global-python-argcomplete

This requires root/admin privileges and will enable completion system-wide.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ classifiers = [
dependencies = [
"MDAnalysis>=2.10.0",
"threadpoolctl",
"argcomplete",
]

[project.urls]
Expand Down
1 change: 1 addition & 0 deletions src/mdacli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
#
# Copyright (c) 2021 Authors and contributors
#
Expand Down
5 changes: 4 additions & 1 deletion src/mdacli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import traceback
import warnings

import argcomplete
from MDAnalysis.analysis.base import AnalysisBase
from threadpoolctl import threadpool_limits

Expand Down Expand Up @@ -89,6 +90,9 @@ def cli(

ap = init_base_argparse(name=name, version=version, description=description)

setup_clients(ap, title=f"{name} Analysis Modules", members=modules)
argcomplete.autocomplete(ap)

if len(sys.argv) < 2:
ap.error("A subcommand is required.")

Expand All @@ -97,7 +101,6 @@ def cli(
# i.e. for `mda RMSD` only the RMSD client should be build.
# 2. for something like `mdacli -h` We do not have to build every
# sub parser in complete detail.
setup_clients(ap, title=f"{name} Analysis Modules", members=modules)

args = ap.parse_args()

Expand Down
56 changes: 53 additions & 3 deletions src/mdacli/libcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,54 @@ def add_output_group(analysis_class_parser):
)


def create_extension_completer(extension_list):
"""Create a completer function for MDAnalysis file formats.

Parameters
----------
extension_list : list of str
List of file extensions (e.g., ['pdb', 'gro', 'psf'])

Returns
-------
function
Completer function for argcomplete that only shows files
with valid MDAnalysis extensions.
"""

def completer(prefix, parsed_args, **kwargs): # noqa: ARG001
"""Complete only files with specified extensions."""
valid_exts = tuple(f".{ext.lower()}" for ext in extension_list)

prefix_path = Path(prefix)

# Determine directory and file prefix
if prefix_path.parent.name:
directory = prefix_path.parent
file_prefix = prefix_path.name
else:
directory = Path()
file_prefix = prefix

matches = []
try:
for item in directory.iterdir():
item_name = item.name
# Checking if item matches prefix and is a file with valid extension
if (
item_name.startswith(file_prefix)
and item.is_file()
and item_name.lower().endswith(valid_exts)
):
matches.append(str(item) if directory != Path() else item_name)
except (OSError, PermissionError):
pass

return matches

return completer


def add_cli_universe(parser, name=""):
"""Add universe parameters to an given argparse.ArgumentParser.

Expand All @@ -255,13 +303,13 @@ def add_cli_universe(parser, name=""):
Parameters
----------
analysis_class_parser : argparse.ArgumentParser
The ArgumentsParser instance to which the run grorup is added
The ArgumentsParser instance to which the run group is added
name : str
suffix for the argument names
"""
name = f"_{name}" if name else ""

parser.add_argument(
topology_arg = parser.add_argument(
f"-s{name}",
dest=f"topology{name}",
type=str,
Expand All @@ -270,6 +318,7 @@ def add_cli_universe(parser, name=""):
", ".join(mda._PARSERS.keys())
),
)
topology_arg.completer = create_extension_completer(mda._PARSERS)

parser.add_argument(
f"-top{name}",
Expand All @@ -289,7 +338,7 @@ def add_cli_universe(parser, name=""):
"(currently only LAMMPS parser). E.g. atom_style='id type x y z'.",
)

parser.add_argument(
trajectory_arg = parser.add_argument(
f"-f{name}",
dest=f"coordinates{name}",
type=str,
Expand All @@ -299,6 +348,7 @@ def add_cli_universe(parser, name=""):
"The FORMATs {} are implemented in MDAnalysis."
"".format(", ".join(mda._READERS.keys())),
)
trajectory_arg.completer = create_extension_completer(mda._READERS)

parser.add_argument(
f"-traj{name}",
Expand Down
78 changes: 78 additions & 0 deletions tests/test_cli.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the tests you added didn't really fail even when before the completion was not working we should maybe think about more robust test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous one was checking for setup
Setup was perfect before and after also
PYTHON_ARGCOMPLETE_OK was the main thing
It was not related with test cases, but crucial for bash completion

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import subprocess
import sys
from contextlib import suppress
from pathlib import Path

import pytest
Expand Down Expand Up @@ -47,6 +48,83 @@ def test_case_insensitive_with_flags(args):
subprocess.check_call(["mda", "--debug", args, "-h"])


def test_subparser_setup_for_tab_completion():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice test. But I think we should also have a test that the argcompletion is really setup. Maybe check the argcomplete repo how they do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"""Test that subparsers are correctly set up for tab-completion.

This verifies that RMSF and RMSD modules are registered as subcommands,
which is what argcomplete needs for tab-completion to work.
"""
from MDAnalysis.analysis.base import AnalysisBase

from src.mdacli.libcli import find_cls_members, init_base_argparse, setup_clients

modules = find_cls_members(AnalysisBase, ["MDAnalysis.analysis.rms"])

parser = init_base_argparse(
name="MDAnalysis", version="0.1.0", description="Test CLI"
)

setup_clients(parser, title="MDAnalysis Analysis Modules", members=modules)

subparser_action = [
a for a in parser._subparsers._group_actions if hasattr(a, "choices")
][0]

choices = list(subparser_action.choices.keys())
assert "RMSF" in choices
assert "RMSD" in choices


def test_argcomplete_working():
"""Test that argcomplete is properly registered and working."""
import argparse
from unittest.mock import patch

from MDAnalysis.analysis import __all__

import src.mdacli
from src.mdacli.cli import cli

skip_mods = [
"AnalysisFromFunction",
"HydrogenBondAnalysis",
"WaterBridgeAnalysis",
"Contacts",
"PersistenceLength",
"InterRDF_s",
]

with (
patch("src.mdacli.cli.argcomplete.autocomplete") as mock_autocomplete,
patch("sys.argv", ["mda", "--help"]),
suppress(SystemExit),
):
cli(
name="MDAnalysis",
module_list=[f"MDAnalysis.analysis.{m}" for m in __all__],
version=src.mdacli.__version__,
description="Test",
skip_modules=skip_mods,
ignore_warnings=True,
)

# Verify that argcomplete.autocomplete was called
assert mock_autocomplete.called, "argcomplete.autocomplete() was not called"

# Verify it was called with an ArgumentParser instance
call_args = mock_autocomplete.call_args
assert call_args is not None, (
"argcomplete.autocomplete() was called with no arguments"
)

parser_arg = call_args[0][0]
msg = (
"argcomplete.autocomplete() should be called with ArgumentParser, "
f"got {type(parser_arg)}"
)
assert isinstance(parser_arg, argparse.ArgumentParser), msg


def test_running_analysis(tmpdir):
"""Test running a complete analysis."""
with tmpdir.as_cwd():
Expand Down
3 changes: 1 addition & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ setenv =
PYTHONUNBUFFERED=yes
usedevelop = true
deps =
MDAnalysis>=2.1.0
MDAnalysisTests>=2.1.0
MDAnalysisTests>=2.10.0
coverage[toml]
pytest
pytest-cov
Expand Down