Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate anaconda-client to become a plugin of anaconda-cli-base #715

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ed98cb9
Remove anaconda entrypoint
mattkram Jul 8, 2024
a9c1c29
Add a plugin to anaconda-cli-base
mattkram Jul 8, 2024
eff478f
Register main CLI as anaconda_cli.main plugin
mattkram Jul 9, 2024
1a4cbb0
Don't mount login or logout at the top level if using the typer app
mattkram Jul 10, 2024
758a258
Remove "anaconda_cli.main" plugin
mattkram Jul 10, 2024
1926636
Add descriptive docstring
mattkram Jul 10, 2024
0358443
Move and rename some helper functions
mattkram Jul 10, 2024
14e37cd
Tidy
mattkram Jul 10, 2024
e865a81
Add a docstring
mattkram Jul 10, 2024
0e6dac7
Refactor subcommand mounting into helper function
mattkram Jul 10, 2024
9c16f0d
In-line call to _get_help_text
mattkram Jul 10, 2024
e53eb68
Rename constant
mattkram Jul 10, 2024
40231ef
Make deprecated text red and bold
mattkram Jul 10, 2024
b8c7e1d
Fix type annotation
mattkram Jul 10, 2024
37c88f6
Bump version and add anaconda-cli-base as dependency
mattkram Jul 10, 2024
8bd7316
Remove entrypoint from recipe
mattkram Jul 10, 2024
0ae5b33
Use local path instead of git_url
mattkram Jul 10, 2024
fcc486b
test cli plugin
AlbertDeFusco Aug 20, 2024
7ee18c1
temporarily add anaconda-cloud/label/dev
AlbertDeFusco Aug 20, 2024
fcb990f
and in ci
AlbertDeFusco Aug 20, 2024
e9f2b06
pylint
AlbertDeFusco Aug 20, 2024
3035668
mypy
AlbertDeFusco Aug 20, 2024
cfbde6e
Remove RichHandler from root logger because of formatting conflict
mattkram Aug 27, 2024
149bb43
Satisfy the linter
mattkram Aug 27, 2024
dd38dd2
Ignore type hinting for optional import
mattkram Aug 27, 2024
3e38b0f
Revert change to version number
mattkram Aug 28, 2024
cf281dd
Use anaconda-cloud channel instead of the dev label
mattkram Aug 28, 2024
9480734
xfail tests related to anaconda-project
mattkram Aug 27, 2024
8d0f29d
Exclude anaconda-project from extras
mattkram Aug 28, 2024
660cd88
Bump dependency on anaconda-cli-base
mattkram Aug 28, 2024
b9d1a25
Make quotes consistent
mattkram Aug 28, 2024
9e336b4
Add anaconda-project back to extras just to be completely safe
mattkram Aug 28, 2024
11712a3
Fix line ending
mattkram Aug 28, 2024
3988982
Update setup.py
AlbertDeFusco Aug 28, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/check-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Install dependencies
run: |
conda install python=${{ matrix.python-version }} pip --file requirements.txt --file requirements-extra.txt
conda install -c defaults -c anaconda-cloud python=${{ matrix.python-version }} pip --file requirements.txt --file requirements-extra.txt
pip install -r requirements-dev.txt
python setup.py develop --no-deps

Expand Down Expand Up @@ -116,7 +116,7 @@ jobs:

- name: Install dependencies
run: |
conda install python=${{ matrix.python-version }} pip --file requirements.txt --file requirements-extra.txt
conda install -c defaults -c anaconda-cloud python=${{ matrix.python-version }} pip --file requirements.txt --file requirements-extra.txt
pip install -r requirements-dev.txt
python setup.py develop --no-deps

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ init:
@if [ -z "$${CONDA_SHLVL:+x}" ]; then echo "Conda is not installed." && exit 1; fi
@conda create \
--channel defaults \
--channel anaconda-cloud \
--yes \
--prefix $(conda_env_dir) \
python=3.11 \
Expand Down
202 changes: 202 additions & 0 deletions binstar_client/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""Defines the subcommand plugins for the new CLI defined in anaconda-cli-base.

We define a new subcommand called `anaconda org`, which nests all existing
anaconda-client subcommands beneath it. Additionally, we mount all of the
existing subcommands, with the exception of "login" and "logout" at the top
level of the CLI, although some of these are mounted silently. This is done to
maintain backwards compatibility while we work to deprecate some of them.

Rather than re-write all the CLI code in anaconda-client, we opt to dynamically
register each subcommand in the `load_legacy_subcommands` function.

Note: This module should not be imported, except as defined as a plugin
entrypoint in setup.py.

"""

import logging
import warnings
from argparse import ArgumentParser
from typing import Any
from typing import Callable

import typer
import typer.colors
from anaconda_cli_base.cli import app as main_app
from typer import Context, Typer

from binstar_client import commands as command_module
from binstar_client.scripts.cli import (
_add_subparser_modules as add_subparser_modules, main as binstar_main,
)

# All subcommands in anaconda-client
ALL_SUBCOMMANDS = {
"auth",
"channel",
"config",
"copy",
"download",
"groups",
"label",
"login",
"logout",
"move",
"notebook",
"package",
"remove",
"search",
"show",
"update",
"upload",
"whoami",
}
# These subcommands will be shown in the top-level help
NON_HIDDEN_SUBCOMMANDS = {
"upload",
}
# Any subcommands that should emit deprecation warnings, and show as deprecated in the help
DEPRECATED_SUBCOMMANDS = {
"notebook",
}

# The logger
log = logging.getLogger(__name__)
warnings.simplefilter("always")

app = Typer(
add_completion=False,
name="org",
help="Interact with anaconda.org",
no_args_is_help=True,
)


def _get_help_text(parser: ArgumentParser, name: str) -> str:
"""Extract the help text from the anaconda-client CLI Argument Parser."""
if parser._subparsers is None: # pylint: disable=protected-access
return ""
# MyPy says this was unreachable
# if parser._subparsers._actions is None: # pylint: disable=protected-access
# return ""
if parser._subparsers._actions[1].choices is None: # pylint: disable=protected-access
return ""
subcommand_parser = dict(parser._subparsers._actions[1].choices).get(name) # pylint: disable=protected-access
if subcommand_parser is None:
return ""
description = subcommand_parser.description
if description is None:
return ""
return description.strip()


def _deprecate(name: str, func: Callable) -> Callable:
"""Mark a named subcommand as deprecated.

Args:
name: The name of the subcommand.
f: The subcommand callable.

"""
def new_func(ctx: Context) -> Any:
msg = (
f"The existing anaconda-client commands will be deprecated. To maintain compatibility, "
f"please either pin `anaconda-client<2` or update your system call with the `org` prefix, "
f'e.g. "anaconda org {name} ..."'
)
log.warning(msg)
return func(ctx)

return new_func


def _subcommand(ctx: Context) -> None:
"""A common function to use for all subcommands.

In a proper typer/click app, this is the function that is decorated.

We use the typer.Context object to extract the args passed into the CLI, and then delegate
to the binstar_main function.

"""
args = []
# Ensure we capture the subcommand name if there is one
if ctx.info_name is not None:
args.append(ctx.info_name)
args.extend(ctx.args)
binstar_main(args, allow_plugin_main=False)


def _mount_subcommand(
*,
name: str,
help_text: str,
is_deprecated: bool,
mount_to_main: bool,
is_hidden_on_main: bool,
) -> None:
"""Mount an existing subcommand to the `anaconda org` typer application.

Args:
name: The name of the subcommand.
help_text: The help text for the subcommand
is_deprecated: If True, mark the subcommand as deprecated. This will cause a warning to be
emitted, and also add "(deprecated)" to the help text.
mount_to_main: If True, also mount the subcommand to the main typer app.
is_hidden_on_main: If True, the subcommand is registered as a hidden subcommand of the main CLI
for backwards-compatibility

"""
if is_deprecated:
deprecated_text = typer.style("(deprecated)", fg=typer.colors.RED, bold=True)
help_text = f"{deprecated_text} {help_text}"
func = _deprecate(name, _subcommand)
else:
func = _subcommand

# Mount the subcommand to the `anaconda org` application.
app.command(
name=name,
help=help_text,
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)(func)

# Exit early if we are not mounting to the main `anaconda` app
if not mount_to_main:
return

# Mount some CLI subcommands at the top-level, but optionally emit a deprecation warning
help_text = f"anaconda.org: {help_text + ' ' if help_text else ''}(alias for 'anaconda org {name}')"

main_app.command(
name=name,
help=help_text,
hidden=is_hidden_on_main,
context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True,
},
)(func)


def load_legacy_subcommands() -> None:
"""Load each of the legacy subcommands into its own typer subcommand.

This allows them to be called from the new CLI, without having to manually migrate.

"""
parser = ArgumentParser()
add_subparser_modules(parser, command_module)

for name in ALL_SUBCOMMANDS:
# TODO: Can we load the arguments, or at least the docstring to make the help nicer? # pylint: disable=fixme
_mount_subcommand(
name=name,
help_text=_get_help_text(parser, name),
is_deprecated=(name in DEPRECATED_SUBCOMMANDS),
mount_to_main=(name not in {"login", "logout", "whoami"}),
is_hidden_on_main=(name not in NON_HIDDEN_SUBCOMMANDS),
)


load_legacy_subcommands()
18 changes: 18 additions & 0 deletions binstar_client/utils/logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,31 @@ def format(self, record: logging.LogRecord) -> str:
return super().format(record)


try:
from rich.logging import RichHandler
except (ImportError, ModuleNotFoundError):
RichHandler = None # type: ignore


def _purge_rich_handler_from_logging_root() -> None:
# Remove all handlers associated with the root logger object.
# We do this since anaconda-cli-base defines the RichHandler, which conflicts with anaconda-client's logging
# We can remove this once we clean up logging.
for handler in logging.root.handlers[:]:
# Only remove the root RichHandler, and only if rich is installed
# This should always happen, but just being super careful here.
if RichHandler is not None and isinstance(handler, RichHandler):
logging.root.removeHandler(handler)


def setup_logging(
logger: logging.Logger,
log_level: int = logging.INFO,
show_traceback: bool = False,
disable_ssl_warnings: bool = False
) -> None:
"""Configure logging for the application."""
_purge_rich_handler_from_logging_root()
logger.setLevel(logging.DEBUG)

os.makedirs(config.USER_LOGDIR, exist_ok=True)
Expand Down
4 changes: 2 additions & 2 deletions conda.recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ package:
version: {{ data.get('version') }}

source:
git_url: ../
path: ..

build:
number: 0
script: {{ PYTHON }} -m pip install --no-build-isolation --no-deps .
entry_points:
- anaconda = binstar_client.scripts.cli:main
- binstar = binstar_client.scripts.cli:main
- conda-server = binstar_client.scripts.cli:main

Expand All @@ -35,6 +34,7 @@ requirements:
- setuptools >=58.0.4
- tqdm >=4.56.0
- urllib3 >=1.26.4
- anaconda-cli-base >=0.3.0

test:
requires:
Expand Down
8 changes: 6 additions & 2 deletions requirements-extra.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Additional requirements for complete experience

anaconda-project>=0.9.1
ruamel.yaml # Required by anaconda-project
# Disabling these extras since they break CI and the server doesn't support
# projects anyway. The problem is that anaconda-project has a circular
# dependency back onto anaconda-client.

# anaconda-project>=0.9.1
# ruamel.yaml # Required by anaconda-project
pillow>=8.2
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ requests-toolbelt>=0.9.1
setuptools>=58.0.4
tqdm>=4.56.0
urllib3>=1.26.4
anaconda-cli-base>=0.3.0
9 changes: 8 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
requirement.split('#', 1)[0].strip() for requirement in stream
)))

# This is temporarily here so we don't pull in the incompatible dependency in CI
# and during local development as we move to 1.13.0. But to not change the behavior
# around the "full" extra at all. We will soon explicitly drop this dependency.
extras_require.append("anaconda-project>=0.9.1")

__about__ = {}
with open(os.path.join(root, 'binstar_client', '__about__.py'), 'rt', encoding='utf-8') as stream:
exec(stream.read(), __about__)
Expand Down Expand Up @@ -46,9 +51,11 @@
packages=setuptools.find_packages(include=['binstar_client', 'binstar_client.*']),
entry_points={
'console_scripts': [
'anaconda = binstar_client.scripts.cli:main',
'binstar = binstar_client.scripts.cli:main',
'conda-server = binstar_client.scripts.cli:main',
],
'anaconda_cli.subcommand': [
'org = binstar_client.plugins:app',
]
},
)
Loading
Loading