forked from conda/conda
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add doctor subcommand plugin (conda#12124)
Introduce new `conda doctor` subcommand using the pluggy plugin framework. The `conda doctor` command checks for all expected files given the installed packages. --------- Co-authored-by: Travis Hathaway <[email protected]> Co-authored-by: Daniel Holth <[email protected]> Co-authored-by: Jannis Leidel <[email protected]> Co-authored-by: Bianca Henderson <[email protected]> Co-authored-by: Ken Odegard <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
- Loading branch information
1 parent
d371c98
commit 330fa91
Showing
8 changed files
with
310 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Copyright (C) 2012 Anaconda, Inc | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
from .doctor import cli as doctor | ||
|
||
plugins = [doctor] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Copyright (C) 2012 Anaconda, Inc | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
""" | ||
Provide 'conda doctor' environment check feature. | ||
""" | ||
# This file is here for packaging, not for importing submodules. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# Copyright (C) 2012 Anaconda, Inc | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
from __future__ import annotations | ||
|
||
import argparse | ||
|
||
from ....base.context import context, locate_prefix_by_name | ||
from ....cli.common import validate_prefix | ||
from ....cli.conda_argparse import add_parser_prefix | ||
from ....exceptions import CondaEnvException | ||
from ... import CondaSubcommand, hookimpl | ||
|
||
|
||
def get_parsed_args(argv: list[str]) -> argparse.Namespace: | ||
parser = argparse.ArgumentParser( | ||
"conda doctor", | ||
description="Display a health report for your environment.", | ||
) | ||
parser.add_argument( | ||
"-v", | ||
"--verbose", | ||
action="store_true", | ||
help="generate a detailed environment health report", | ||
) | ||
add_parser_prefix(parser) | ||
args = parser.parse_args(argv) | ||
|
||
return args | ||
|
||
|
||
def get_prefix(args: argparse.Namespace) -> str: | ||
""" | ||
Determine the correct prefix to use provided the CLI arguments and the context object. | ||
When not specified via CLI options, the default is the currently active prefix | ||
""" | ||
if args.name: | ||
return locate_prefix_by_name(args.name) | ||
|
||
if args.prefix: | ||
return validate_prefix(args.prefix) | ||
|
||
if context.active_prefix: | ||
return context.active_prefix | ||
|
||
raise CondaEnvException( | ||
"No environment specified. Activate an environment or specify the " | ||
"environment via `--name` or `--prefix`." | ||
) | ||
|
||
|
||
def execute(argv: list[str]) -> None: | ||
""" | ||
Run conda doctor subcommand. | ||
""" | ||
from .health_checks import display_health_checks | ||
|
||
args = get_parsed_args(argv) | ||
prefix = get_prefix(args) | ||
display_health_checks(prefix, verbose=args.verbose) | ||
|
||
|
||
@hookimpl | ||
def conda_subcommands(): | ||
yield CondaSubcommand( | ||
name="doctor", | ||
summary="A subcommand that displays environment health report", | ||
action=execute, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Copyright (C) 2012 Anaconda, Inc | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
from __future__ import annotations | ||
|
||
import json | ||
from pathlib import Path | ||
|
||
OK_MARK = "✅" | ||
REPORT_TITLE = "\nENVIRONMENT HEALTH REPORT\n" | ||
DETAILED_REPORT_TITLE = "\nDETAILED ENVIRONMENT HEALTH REPORT\n" | ||
MISSING_FILES_SUCCESS_MESSAGE = f"{OK_MARK} There are no packages with missing files.\n" | ||
|
||
|
||
def display_report_heading(prefix: str) -> None: | ||
""" | ||
Displays our report heading | ||
""" | ||
print("-" * 20) | ||
print(REPORT_TITLE) | ||
print(f"Environment Name: {Path(prefix).name}\n") | ||
|
||
|
||
def find_packages_with_missing_files(prefix: str | Path) -> dict[str, list[str]]: | ||
""" | ||
Finds packages listed in conda-meta which have missing files | ||
""" | ||
packages_with_missing_files = {} | ||
prefix = Path(prefix) | ||
for file in (prefix / "conda-meta").glob("*.json"): | ||
for file_name in json.loads(file.read_text()).get("files", []): | ||
# Add warnings if json file has missing "files" | ||
if not (prefix / file_name).exists(): | ||
packages_with_missing_files.setdefault(file.stem, []).append(file_name) | ||
return packages_with_missing_files | ||
|
||
|
||
def display_health_checks(prefix: str, verbose: bool) -> None: | ||
""" | ||
Prints health report | ||
""" | ||
display_report_heading(prefix) | ||
missing_files = find_packages_with_missing_files(prefix) | ||
if missing_files: | ||
print("Missing Files\n") | ||
for file, files in missing_files.items(): | ||
if verbose: | ||
delimiter = "\n " | ||
print(f"{file}:{delimiter}{delimiter.join(files)}\n") | ||
else: | ||
print(f"{file}: {len(files)}") | ||
|
||
print("\n") | ||
else: | ||
print(MISSING_FILES_SUCCESS_MESSAGE) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# Copyright (C) 2012 Anaconda, Inc | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
from argparse import Namespace | ||
from pathlib import Path | ||
from typing import Iterable | ||
|
||
import pytest | ||
from pytest import MonkeyPatch | ||
|
||
from conda.base.context import conda_tests_ctxt_mgmt_def_pol, context | ||
from conda.common.io import env_vars | ||
from conda.exceptions import ( | ||
CondaEnvException, | ||
DirectoryNotACondaEnvironmentError, | ||
EnvironmentNameNotFound, | ||
) | ||
from conda.plugins.subcommands.doctor.cli import get_prefix | ||
from conda.plugins.subcommands.doctor.health_checks import MISSING_FILES_SUCCESS_MESSAGE | ||
from conda.testing.helpers import run_inprocess_conda_command as run | ||
from conda.testing.integration import make_temp_env | ||
|
||
|
||
def test_conda_doctor_happy_path(): | ||
"""Make sure that we are able to call the ``conda doctor`` command""" | ||
|
||
out, err, code = run(f"conda doctor") | ||
|
||
assert not err # no error message | ||
assert not code # successful exit code | ||
|
||
|
||
def test_conda_doctor_happy_path_verbose(): | ||
"""Make sure that we are able to run ``conda doctor`` command with the --verbose flag""" | ||
|
||
out, err, code = run(f"conda doctor --verbose") | ||
|
||
assert not err # no error message | ||
assert not code # successful exit code | ||
|
||
|
||
def test_conda_doctor_happy_path_show_help(): | ||
"""Make sure that we are able to run ``conda doctor`` command with the --help flag""" | ||
|
||
out, err, code = run(f"conda doctor --help") | ||
|
||
assert "Display a health report for your environment." in out | ||
assert not err # no error message | ||
assert not code # successful exit code | ||
|
||
|
||
def test_conda_doctor_with_test_environment(): | ||
"""Make sure that we are able to call ``conda doctor`` command for a specific environment""" | ||
|
||
with make_temp_env() as prefix: | ||
out, err, code = run(f"conda doctor --prefix '{prefix}'") | ||
|
||
assert MISSING_FILES_SUCCESS_MESSAGE in out | ||
assert not err # no error message | ||
assert not code # successful exit code | ||
|
||
|
||
def test_get_prefix_name(): | ||
assert get_prefix(Namespace(name="base", prefix=None)) == context.root_prefix | ||
|
||
|
||
def test_get_prefix_bad_name(): | ||
with pytest.raises(EnvironmentNameNotFound): | ||
get_prefix(Namespace(name="invalid", prefix=None)) | ||
|
||
|
||
def test_get_prefix_prefix(): | ||
with make_temp_env() as prefix: | ||
assert get_prefix(Namespace(name=None, prefix=prefix)) == prefix | ||
|
||
|
||
def test_get_prefix_bad_prefix(tmp_path: Path): | ||
with pytest.raises(DirectoryNotACondaEnvironmentError): | ||
assert get_prefix(Namespace(name=None, prefix=tmp_path)) | ||
|
||
|
||
def test_get_prefix_active(): | ||
with make_temp_env() as prefix, env_vars( | ||
{"CONDA_PREFIX": prefix}, | ||
stack_callback=conda_tests_ctxt_mgmt_def_pol, | ||
): | ||
assert get_prefix(Namespace(name=None, prefix=None)) == prefix | ||
|
||
|
||
def test_get_prefix_not_active(monkeypatch: MonkeyPatch): | ||
monkeypatch.delenv("CONDA_PREFIX") | ||
with pytest.raises(CondaEnvException): | ||
get_prefix(Namespace(name=None, prefix=None)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Copyright (C) 2012 Anaconda, Inc | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
from __future__ import annotations | ||
|
||
import json | ||
import uuid | ||
from pathlib import Path | ||
from typing import Iterable | ||
|
||
import pytest | ||
|
||
from conda.base.context import conda_tests_ctxt_mgmt_def_pol | ||
from conda.common.io import env_vars | ||
from conda.plugins.subcommands.doctor.health_checks import ( | ||
display_health_checks, | ||
find_packages_with_missing_files, | ||
) | ||
from conda.testing.integration import make_temp_env | ||
|
||
|
||
@pytest.fixture | ||
def env_ok(tmp_path: Path) -> Iterable[tuple[Path, str, str, str]]: | ||
"""Fixture that returns a testing environment with no missing files""" | ||
package = uuid.uuid4().hex | ||
|
||
(tmp_path / "bin").mkdir(parents=True, exist_ok=True) | ||
(tmp_path / "lib").mkdir(parents=True, exist_ok=True) | ||
(tmp_path / "conda-meta").mkdir(parents=True, exist_ok=True) | ||
|
||
bin_doctor = f"bin/{package}" | ||
(tmp_path / bin_doctor).touch() | ||
|
||
lib_doctor = f"lib/{package}.py" | ||
(tmp_path / lib_doctor).touch() | ||
|
||
(tmp_path / "conda-meta" / f"{package}.json").write_text( | ||
json.dumps({"files": [bin_doctor, lib_doctor]}) | ||
) | ||
|
||
yield tmp_path, bin_doctor, lib_doctor, package | ||
|
||
|
||
@pytest.fixture | ||
def env_broken(env_ok: tuple[Path, str, str, str]) -> tuple[Path, str, str, str]: | ||
"""Fixture that returns a testing environment with missing files""" | ||
prefix, bin_doctor, _, _ = env_ok | ||
(prefix / bin_doctor).unlink() | ||
return env_ok | ||
|
||
|
||
def test_no_missing_files(env_ok: tuple[Path, str, str, str]): | ||
"""Test that runs for the case with no missing files""" | ||
prefix, _, _, _ = env_ok | ||
assert find_packages_with_missing_files(prefix) == {} | ||
|
||
|
||
def test_missing_files(env_broken: tuple[Path, str, str, str]): | ||
prefix, bin_doctor, _, package = env_broken | ||
assert find_packages_with_missing_files(prefix) == {package: [bin_doctor]} | ||
|
||
|
||
@pytest.mark.parametrize("verbose", [True, False]) | ||
def test_display_health_checks(env_ok: tuple[Path, str, str, str], verbose: bool): | ||
"""Run display_health_checks without and with missing files.""" | ||
prefix, bin_doctor, lib_doctor, package = env_ok | ||
with env_vars( | ||
{"CONDA_PREFIX": prefix}, | ||
stack_callback=conda_tests_ctxt_mgmt_def_pol, | ||
): | ||
display_health_checks(prefix, verbose=verbose) | ||
(prefix / bin_doctor).unlink() | ||
display_health_checks(prefix, verbose=verbose) |