Skip to content

New wrapped CLI using click - #4961

Open
Crivella wants to merge 40 commits into
easybuilders:developfrom
Crivella:feature-click_cli
Open

New wrapped CLI using click#4961
Crivella wants to merge 40 commits into
easybuilders:developfrom
Crivella:feature-click_cli

Conversation

@Crivella

@Crivella Crivella commented Jul 17, 2025

Copy link
Copy Markdown
Contributor

Enabling

Using pip and optional python dependencies:

pip install easybuild-framework[eb_click]

How it works

  • Extract all the parameters from EasyBuildOptions and use them to generate a CLI with click
  • Uses click + rich_click to have a fancier looking CLI
  • click is only used for the nicer help and auto completion, but the parameters are still passed as is to the normal parser

Autocomplete

From click docs add the following to the .bashrc or venv activation script.

eval "$(_EB_COMPLETE=bash_source eb)"

Alternatively see aiida-core EC to activate it as a module

Advantages

  • Nicer looking
  • Automatic integration and autocomplete with bash zsh fish shells

TODO

  • Implement/convert more types for better autocompletion

Maybe

  • Add autocomplete for path options (do not think there is a way that they are flagged as such in EB right now)
  • Better integrate logging with rich (moved in favor of Add rich colors to print_msg #5125)
  • Rewrite the CLI logic to use click natively and divide the commands in subgroups to have an easier to read help (Discussion moved to Modern CLI for easybuild #5122)

How would this look like

eb --help image
Error image
Normal build run image
Opt in/out

the new behavior will only be turned on if click is installed, otherwise the old one will be used.
image

@boegel boegel added this to the 5.x milestone Jul 30, 2025
@boegel

boegel commented Jul 30, 2025

Copy link
Copy Markdown
Member

This looks kind of nice, but I'm not a big fan of introducing a required 3rd party dependency.

Is there any way we can support this opt-in, for example by defining an environment variable like $EB_CLICK_CLI?

@Crivella

Copy link
Copy Markdown
Contributor Author

We could make use of
https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies

probably not only for this but also for all the optional dependencies we specify in
https://docs.easybuild.io/installation/#optional_python_packages

Then we add a check on top of eb2 to try and import the required packages and print an error if they are not there suggesting to install the optional dependencies (if we keep eb2 and eb separate, otherwise we could just fallback to the normal eb behavior if they are not there and potentially also allow to disable the behavior with the env variable)

@Crivella Crivella changed the title WIP click CLI wrapper around normal CLI New experimental eb2 CLI using click Jul 31, 2025
@Crivella
Crivella marked this pull request as ready for review July 31, 2025 13:05
@Crivella

Copy link
Copy Markdown
Contributor Author

@boegel

So for now the eb2 CLI is separate from the existing eb one.
I've added an optional set of install dependencies so that if one does pip install easybuild-framework[eb2] it also install the stuff required (and optional) for the new CLI.
The CLI will still be present if the dependencies are not there but will print an informative message instead of actually running

try:
    import click as original_click
except ImportError:
    def eb():
        """Placeholder function to inform the user that `click` is required."""
        print('Using `eb2` requires `click` to be installed.')
        print('Either use `eb` or install `click` with `pip install click`.')
        print('`eb2` also uses `rich` and `rich_click` as optional dependencies for enhanced CLI experience.')
        print('Exiting...')
        sys.exit(0)

The approach of using an ENV variable i think make sense if we decide to merge eb with eb2 to only have one CLI with both behaviors, but it might be difficult to reconcile the autocomplete as they come from the environment

@Crivella
Crivella force-pushed the feature-click_cli branch 2 times, most recently from a07c32e to 681e781 Compare February 9, 2026 18:15
@Crivella
Crivella force-pushed the feature-click_cli branch from b8c5791 to 0267a3c Compare April 9, 2026 11:01
@boegel

boegel commented Apr 9, 2026

Copy link
Copy Markdown
Member

@Crivella FYI, I've cancelled CI workflows here to give precedence to #5166

@Crivella

Copy link
Copy Markdown
Contributor Author

@boegel

  1. Done
  2. Added it manually, it was not being picked up as in the normal parser it was not added as an EasybuildOption through add_group_parser.
  3. Added the EB_CLI_CLICK env variable as suggested. Now the question is whether we want to hard fail if click is not available with EB_CLI_CLICK=1 or revert to the old CLI like we do know (or a middle ground where we add a warning but still revert to the old CLI)
  4. I would say it both puts in place the machinery to replace the current wrapper click CLI with a new independent one and also starts people to get used to the new auto-complete and colored CLI. The way we want to phase it out in favor of the new one will depend on how we decide to also play it with the old CLI (eg both in place with a deprecation warning).

Also modified the end2end tests to do

  • Everything they already did
  • Install deps for venv + create a virtualenv
  • Install the checkout branch + the easyblock and configs downloaded for the previous test
  • run the same tests + also a grep to check that rich_click is being used in eb --help
  • One thing is whether we want to do the actual builds of bzip2 in temporary dirs so we rebuild everything the second time or just run with a --rebuild

Comment thread easybuild/cli/__init__.py Outdated
except ImportError:
def eb(*args, **kwargs):
"""Placeholder function to inform the user that `click` is required."""
main_with_hooks()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I doesn't make much sense to me to specify that a Click-based CLI should be used, to then fall back to a non-Click CLI when click is not found...

If click is not found, then we should fail with an error (and print a hint about unsetting $EB_CLI_CLICK to opt-out of using a Click-based CLI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread easybuild/cli/__init__.py
#
# You should have received a copy of the GNU General Public License
# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>.
# #

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Crivella please add a docstring with some info on what this provides, add yourself as author

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread easybuild/cli/__init__.py Outdated
])

from .options import EasyBuildCliOption, EasyconfigParam
from easybuild.tools.version import this_is_easybuild

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not do these imports on top?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The second one can be outside, the first i would leave inside so that the only check we need w.r.t. click is in this file.
Originally it would cause the fallback to the normal CLI to not work (since something was still trying to import click) now it would cause an ModuleNotFound error before our new raise that replaced the CLI fallback

decl = f"--{self.name}/--disable-{self.name}"
elif isinstance(self.default, (list, tuple)):
kwargs['multiple'] = True
kwargs['type'] = click.STRING

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add else to avoid that we don't handle unknown option types?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Had to check since it has been a while, added a print to check what this would hit:

Warning: Cannot infer type for option bwrap-installpath with default value /home/crivella/.local/easybuild/bwrap, treating as string
Warning: Cannot infer type for option module-naming-scheme with default value EasyBuildMNS, treating as string
Warning: Cannot infer type for option packagepath with default value /home/crivella/.local/easybuild/packages, treating as string
Warning: Cannot infer type for option subdir-data with default value data, treating as string
Warning: Cannot infer type for option subdir-modules with default value modules, treating as string
Warning: Cannot infer type for option subdir-software with default value software, treating as string
Warning: Cannot infer type for option subdir-user-modules with default value None, treating as string
Warning: Cannot infer type for option suffix-modules-path with default value all, treating as string
Warning: Cannot infer type for option container-image-name with default value None, treating as string
Warning: Cannot infer type for option container-tmpdir with default value None, treating as string
Warning: Cannot infer type for option create-index with default value None, treating as string
Warning: Cannot infer type for option dump-test-report with default value test_report.md, treating as string
Warning: Cannot infer type for option review-pr-filter with default value None, treating as string
Warning: Cannot infer type for option test-report-env-filter with default value None, treating as string
Warning: Cannot infer type for option dep-graph with default value None, treating as string
Warning: Cannot infer type for option search with default value None, treating as string
Warning: Cannot infer type for option search-filename with default value None, treating as string
Warning: Cannot infer type for option search-short with default value None, treating as string
Warning: Cannot infer type for option easystack with default value None, treating as string
Warning: Cannot infer type for option job-backend-config with default value None, treating as string
Warning: Cannot infer type for option job-output-dir with default value /home/crivella, treating as string
Warning: Cannot infer type for option job-target-resource with default value None, treating as string
Warning: Cannot infer type for option backup-modules with default value None, treating as string
Warning: Cannot infer type for option check-ebroot-env-vars with default value warn, treating as string
Warning: Cannot infer type for option deprecated with default value None, treating as string
Warning: Cannot infer type for option detect-loaded-modules with default value warn, treating as string
Warning: Cannot infer type for option easyblock with default value None, treating as string
Warning: Cannot infer type for option env-for-shebang with default value /usr/bin/env, treating as string
Warning: Cannot infer type for option group with default value None, treating as string
Warning: Cannot infer type for option http-header-fields-urlpat with default value None, treating as string
Warning: Cannot infer type for option minimal-build-env with default value CC:gcc,CXX:g++, treating as string
Warning: Cannot infer type for option module-cache-suffix with default value None, treating as string
Warning: Cannot infer type for option mpi-cmd-template with default value None, treating as string
Warning: Cannot infer type for option optarch with default value None, treating as string
Warning: Cannot infer type for option rpath-override-dirs with default value None, treating as string
Warning: Cannot infer type for option software-commit with default value None, treating as string
Warning: Cannot infer type for option sysroot with default value None, treating as string
Warning: Cannot infer type for option umask with default value None, treating as string
Warning: Cannot infer type for option zip-logs with default value gzip, treating as string
Warning: Cannot infer type for option package-tool with default value fpm, treating as string
Warning: Cannot infer type for option package-tool-options with default value , treating as string
Warning: Cannot infer type for option package-type with default value rpm, treating as string
Warning: Cannot infer type for option package-release with default value 1, treating as string
Warning: Cannot infer type for option aggregate-regtest with default value None, treating as string
Warning: Cannot infer type for option regtest-output-dir with default value None, treating as string
Warning: Cannot infer type for option amend with default value None, treating as string
Warning: Cannot infer type for option software-name with default value None, treating as string
Warning: Cannot infer type for option software-version with default value None, treating as string
Warning: Cannot infer type for option toolchain-name with default value None, treating as string
Warning: Cannot infer type for option toolchain-version with default value None, treating as string
Warning: Cannot infer type for option try-amend with default value None, treating as string
Warning: Cannot infer type for option try-software-name with default value None, treating as string
Warning: Cannot infer type for option try-software-version with default value None, treating as string
Warning: Cannot infer type for option try-toolchain-name with default value None, treating as string
Warning: Cannot infer type for option try-toolchain-version with default value None, treating as string
Warning: Cannot infer type for option unittest-file with default value None, treating as string

this are all variables with no specific type.
When we do not hit any of those if/else type will be left as None and click will treat the argument as a string eg:
image

This only come into play for the auto-completion (nothing will be suggested), not sure if it is better to enforce click.STRING as a type, but it is standard to not specify a type with click

meta: Dict = None
lst: List = None

def __post_init__(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please add missing docstring

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

return res


class DelimitedString(click.ParamType):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

missing docstrings in methods below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

extracter = OptionExtracter(go_args=[])


class DelimitedPathList(click.Path):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

missing docstrings in methods below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

'buildpath',
'containerpath',
'installpath',
'sourcepath',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

bwrap_installpath missing?

packagepath, repositorypath too?

Why not keep these alphabetically sorted?

Also, how will we keep this in sync as new options get added?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is for convenience to allow the autocompletion to treat something that should be a string for optparse as a path to a file/directory.

Also, how will we keep this in sync as new options get added?

Right now there is no easy way to maintain this but adding stuff manually.
With a proper click CLI we will use the correct type from the outset

Why not keep these alphabetically sorted?

I think i tried to divide them logically while i was manually adding them but fine also having them alphabetically sorted

@Crivella Crivella Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also repositorypath is flagged as strlist and passes a list as a default so it is not compatible with click.Path the way we are setting it now

'repositorypath': (("Repository path, used by repository "
"(is passed as list of arguments to create the repository instance). "
"For more info, use --avail-repositories."),
'strlist', 'store', self.default_repositorypath),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants