conda-features 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conda_features/__init__.py +75 -0
- conda_features/cli/__init__.py +1 -0
- conda_features/cli/apply.py +26 -0
- conda_features/cli/cleanup.py +16 -0
- conda_features/cli/commands.py +35 -0
- conda_features/cli/common.py +90 -0
- conda_features/cli/configure.py +22 -0
- conda_features/cli/diagnose.py +13 -0
- conda_features/cli/disable.py +44 -0
- conda_features/cli/enable.py +22 -0
- conda_features/cli/explain.py +11 -0
- conda_features/cli/export.py +26 -0
- conda_features/cli/list.py +14 -0
- conda_features/cli/migrate.py +16 -0
- conda_features/cli/output.py +74 -0
- conda_features/cli/parser.py +50 -0
- conda_features/cli/repair.py +16 -0
- conda_features/cli/targets.py +28 -0
- conda_features/hooks.py +22 -0
- conda_features/models.py +291 -0
- conda_features/operations/__init__.py +1 -0
- conda_features/operations/configuration.py +193 -0
- conda_features/operations/dependencies.py +256 -0
- conda_features/operations/diagnostics.py +113 -0
- conda_features/operations/discovery.py +45 -0
- conda_features/operations/doctor.py +64 -0
- conda_features/operations/intent.py +250 -0
- conda_features/operations/lifecycle.py +146 -0
- conda_features/operations/settings.py +101 -0
- conda_features/operations/validation.py +120 -0
- conda_features/plugin.py +51 -0
- conda_features/steps/__init__.py +7 -0
- conda_features/steps/base.py +41 -0
- conda_features/steps/block.py +98 -0
- conda_features/steps/configuration.py +222 -0
- conda_features/steps/packages.py +107 -0
- conda_features/steps/runner.py +162 -0
- conda_features/steps/storage.py +85 -0
- conda_features-0.1.0.dist-info/METADATA +84 -0
- conda_features-0.1.0.dist-info/RECORD +43 -0
- conda_features-0.1.0.dist-info/WHEEL +4 -0
- conda_features-0.1.0.dist-info/entry_points.txt +2 -0
- conda_features-0.1.0.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Feature management for conda.
|
|
2
|
+
|
|
3
|
+
Install this distribution in the Python environment running conda. During
|
|
4
|
+
development, ``pixi run python -m conda feature --help`` uses that environment.
|
|
5
|
+
|
|
6
|
+
User commands::
|
|
7
|
+
|
|
8
|
+
conda feature list
|
|
9
|
+
conda feature explain sample
|
|
10
|
+
conda feature enable sample --dry-run
|
|
11
|
+
conda feature enable sample --yes
|
|
12
|
+
conda feature configure sample plugins.sample_limit=4 --yes
|
|
13
|
+
conda feature disable sample --yes
|
|
14
|
+
conda feature disable --all --yes
|
|
15
|
+
|
|
16
|
+
Use ``--prefix PATH`` for an existing environment, ``--system`` for the conda
|
|
17
|
+
installation, or ``--user`` (the default). Providers declare supported scopes.
|
|
18
|
+
``--file PATH`` selects a standalone user-scope file, with environment-variable
|
|
19
|
+
overrides still applied. Use a conda-recognized name (``.condarc``, ``condarc``,
|
|
20
|
+
or a YAML extension). Its parent directory must exist. Other scopes use
|
|
21
|
+
conda's normal configuration search path. ``--json`` produces one JSON result.
|
|
22
|
+
Exit codes are 0 for success or preview, 1 for an operation error, and 2 for
|
|
23
|
+
argument parsing errors. Noninteractive mutations require ``--yes``.
|
|
24
|
+
|
|
25
|
+
Providers register their settings with conda's ``conda_settings`` hook and
|
|
26
|
+
yield ``conda_features.models.Feature`` objects from a Pluggy hook. Register
|
|
27
|
+
the provider module in their pyproject.toml::
|
|
28
|
+
|
|
29
|
+
[project.entry-points.conda_features]
|
|
30
|
+
my-provider = "my_provider.features"
|
|
31
|
+
|
|
32
|
+
An example declaration::
|
|
33
|
+
|
|
34
|
+
from conda_features.models import Feature
|
|
35
|
+
from conda_features.hooks import hookimpl
|
|
36
|
+
|
|
37
|
+
@hookimpl
|
|
38
|
+
def conda_features():
|
|
39
|
+
yield Feature(
|
|
40
|
+
name="sample",
|
|
41
|
+
description="Enable the provider's optional behavior",
|
|
42
|
+
enabled_setting="plugins.sample_enabled",
|
|
43
|
+
settings=("plugins.sample_limit",),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
The provider reads its boolean setting to control behavior. This plugin does
|
|
47
|
+
not intercept provider execution. Declarations must be cheap to import and
|
|
48
|
+
must not access the network, credentials, or files. Installed providers are
|
|
49
|
+
trusted Python code, not sandboxed data. Only expose non-secret settings.
|
|
50
|
+
Required settings check for missing values, not login or service availability.
|
|
51
|
+
Status reports the selected file's saved choice separately from the effective
|
|
52
|
+
boolean setting and readiness. Runtime activity is unknown, even when a setting
|
|
53
|
+
is enabled and a prerequisite is blocked. Providers enforce their own checks.
|
|
54
|
+
|
|
55
|
+
Enable refuses unmet dependencies instead of implicitly opting into them.
|
|
56
|
+
Disable preserves other settings and installed packages. Changes take effect
|
|
57
|
+
on the next invocation. Repeating a persisted operation requires no write.
|
|
58
|
+
An explicit disable is saved even if a temporary override already disables it.
|
|
59
|
+
Writes preserve comments and unrelated settings, use atomic file replacement,
|
|
60
|
+
and reject previews whose destination changed. A sidecar lock coordinates this
|
|
61
|
+
plugin's writers, but external editors do not honor it.
|
|
62
|
+
|
|
63
|
+
Deployment providers can omit enabled_setting and declare steps such as
|
|
64
|
+
InstallPackages, SetSetting, and EnsureChannel. Feature coordinates their
|
|
65
|
+
planning, application, reverse-order disable, and recovery receipts.
|
|
66
|
+
Provider-specific steps implement Step or ManagedBlock. Plans carry public
|
|
67
|
+
changes and private apply data. The CLI confirms before applying. The separate
|
|
68
|
+
conda-features-examples package deploys real PyPI support and shell completion,
|
|
69
|
+
including installation of missing prerequisites. Bulk disable handles boolean
|
|
70
|
+
features and identifies deployments that need individual disable commands.
|
|
71
|
+
|
|
72
|
+
Without an installed provider, list reports an empty catalog. Authentication,
|
|
73
|
+
cross-environment dependency scanning, telemetry, migrations, export, and
|
|
74
|
+
doctor integration are not implemented.
|
|
75
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command-line parsing, dispatch, and rendering."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Apply a portable intent document to an explicitly selected destination."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure_parser(parser):
|
|
10
|
+
parser.add_argument("manifest", type=Path)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute(args, catalog, ctx, path, scope):
|
|
14
|
+
from ..operations.intent import IntentError, plan_intent
|
|
15
|
+
from .common import confirm_and_apply
|
|
16
|
+
|
|
17
|
+
if not (args.user or args.system or args.prefix or args.file):
|
|
18
|
+
raise IntentError(
|
|
19
|
+
"Applying intent requires an explicit --user, --system, --prefix, or --file destination."
|
|
20
|
+
)
|
|
21
|
+
try:
|
|
22
|
+
document = json.loads(args.manifest.read_text())
|
|
23
|
+
except (OSError, ValueError) as exc:
|
|
24
|
+
raise IntentError("Cannot read feature intent JSON.") from exc
|
|
25
|
+
plan = plan_intent(document, catalog, ctx, path, scope)
|
|
26
|
+
return confirm_and_apply(args, catalog, ctx, plan)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Remove obsolete opt-ins for unconditional graduated features."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_parser(parser):
|
|
7
|
+
parser.add_argument("feature")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args, catalog, ctx, path, scope):
|
|
11
|
+
from .common import confirm_and_apply, select_feature
|
|
12
|
+
|
|
13
|
+
feature = select_feature(catalog, args.feature)
|
|
14
|
+
return confirm_and_apply(
|
|
15
|
+
args, catalog, ctx, feature.cleanup(catalog, ctx, path, scope)
|
|
16
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Load the selected subcommand and render its result."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
|
|
8
|
+
from conda import CondaError
|
|
9
|
+
from conda.base.context import context
|
|
10
|
+
|
|
11
|
+
from ..operations.discovery import discover
|
|
12
|
+
from ..operations.intent import IntentError
|
|
13
|
+
from ..operations.validation import validate_catalog
|
|
14
|
+
from .output import render
|
|
15
|
+
from .targets import target
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute(args: Namespace) -> int:
|
|
19
|
+
try:
|
|
20
|
+
catalog = discover()
|
|
21
|
+
path, scope, ctx = target(args)
|
|
22
|
+
validate_catalog(catalog, ctx)
|
|
23
|
+
command = import_module(f".{args.operation}", package=__package__)
|
|
24
|
+
result = command.execute(args, catalog, ctx, path, scope)
|
|
25
|
+
except (CondaError, OSError, ValueError, TypeError, OverflowError) as exc:
|
|
26
|
+
message = str(exc)
|
|
27
|
+
if args.operation in ("export", "apply") and not isinstance(exc, IntentError):
|
|
28
|
+
message = "Intent operation failed. Inspect the local configuration and providers. Review with --dry-run and pass --yes only after approving the changes. Configuration values are omitted from this error."
|
|
29
|
+
if context.json:
|
|
30
|
+
print(json.dumps({"error": message}))
|
|
31
|
+
else:
|
|
32
|
+
print(message, file=sys.stderr)
|
|
33
|
+
return 1
|
|
34
|
+
render(result)
|
|
35
|
+
return 0
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shared feature selection and confirmed configuration writes."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from conda import CondaError
|
|
7
|
+
from conda.base.context import context
|
|
8
|
+
from conda.common.configuration import YamlRawParameter
|
|
9
|
+
|
|
10
|
+
from ..models import Catalog, Feature
|
|
11
|
+
from ..operations.configuration import apply_change
|
|
12
|
+
from ..operations.dependencies import guard_plan, projected_context, verify_guard
|
|
13
|
+
from ..operations.settings import fingerprint
|
|
14
|
+
from .targets import target
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def select_feature(catalog: Catalog, name: str) -> Feature:
|
|
18
|
+
if name not in catalog:
|
|
19
|
+
raise CondaError(f"Unknown feature: {name}")
|
|
20
|
+
return catalog[name][1]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def confirm_and_apply(args, catalog, ctx, plan):
|
|
24
|
+
guard_plan(args, catalog, plan)
|
|
25
|
+
result = {
|
|
26
|
+
key: plan[key]
|
|
27
|
+
for key in (
|
|
28
|
+
"operation",
|
|
29
|
+
"features",
|
|
30
|
+
"scope",
|
|
31
|
+
"path",
|
|
32
|
+
"changes",
|
|
33
|
+
"checked_environments",
|
|
34
|
+
"discovery",
|
|
35
|
+
"remove",
|
|
36
|
+
)
|
|
37
|
+
if key in plan
|
|
38
|
+
}
|
|
39
|
+
if args.dry_run:
|
|
40
|
+
result["message"] = "Preview only. No changes made."
|
|
41
|
+
elif not plan["changes"]:
|
|
42
|
+
result["message"] = "No changes needed."
|
|
43
|
+
else:
|
|
44
|
+
if not context.always_yes:
|
|
45
|
+
if context.json or not sys.stdin.isatty():
|
|
46
|
+
raise CondaError(
|
|
47
|
+
"Review with --dry-run, then pass --yes to apply noninteractively."
|
|
48
|
+
)
|
|
49
|
+
print(f"Configuration: {plan['path']} ({plan['scope']})", file=sys.stderr)
|
|
50
|
+
if plan["checked_environments"]:
|
|
51
|
+
print(
|
|
52
|
+
"Checked environments: " + ", ".join(plan["checked_environments"]),
|
|
53
|
+
file=sys.stderr,
|
|
54
|
+
)
|
|
55
|
+
print(plan["discovery"], file=sys.stderr)
|
|
56
|
+
for key, new_value in plan["changes"].items():
|
|
57
|
+
print(f" {key} = {new_value}", file=sys.stderr)
|
|
58
|
+
print("Proceed? [y/N] ", end="", file=sys.stderr, flush=True)
|
|
59
|
+
if sys.stdin.readline().strip().lower() not in ("y", "yes"):
|
|
60
|
+
raise CondaError("Cancelled. No changes made.")
|
|
61
|
+
verify_guard(plan)
|
|
62
|
+
if "plans" in plan:
|
|
63
|
+
current = ctx
|
|
64
|
+
expected = plan["fingerprint"]
|
|
65
|
+
for child in plan["plans"]:
|
|
66
|
+
if fingerprint(Path(child["path"])) != expected:
|
|
67
|
+
raise CondaError(
|
|
68
|
+
"Configuration changed after preview. Review the plan again."
|
|
69
|
+
)
|
|
70
|
+
child["fingerprint"] = expected
|
|
71
|
+
next_context = projected_context(child, current)
|
|
72
|
+
catalog[child["features"][-1]][1].apply(child, current)
|
|
73
|
+
expected = fingerprint(Path(child["path"]))
|
|
74
|
+
current = next_context
|
|
75
|
+
elif len(plan["features"]) == 1:
|
|
76
|
+
catalog[plan["features"][0]][1].apply(plan, ctx)
|
|
77
|
+
else:
|
|
78
|
+
apply_change(plan, ctx)
|
|
79
|
+
YamlRawParameter.cache_clear()
|
|
80
|
+
_, _, updated = target(args)
|
|
81
|
+
result["status"] = [
|
|
82
|
+
catalog[name][1].status(
|
|
83
|
+
catalog, updated, plan["scope"], path=Path(plan["path"])
|
|
84
|
+
)
|
|
85
|
+
for name in plan["features"]
|
|
86
|
+
]
|
|
87
|
+
result["message"] = (
|
|
88
|
+
"Configuration updated. Takes effect in the next conda invocation."
|
|
89
|
+
)
|
|
90
|
+
return result
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Validate and change a feature's declared settings."""
|
|
2
|
+
|
|
3
|
+
from conda import CondaError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_parser(parser):
|
|
7
|
+
parser.add_argument("feature")
|
|
8
|
+
parser.add_argument("settings", nargs="+", metavar="KEY=VALUE")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def execute(args, catalog, ctx, path, scope):
|
|
12
|
+
from .common import confirm_and_apply, select_feature
|
|
13
|
+
|
|
14
|
+
feature = select_feature(catalog, args.feature)
|
|
15
|
+
settings = {}
|
|
16
|
+
for item in args.settings:
|
|
17
|
+
key, separator, new_value = item.partition("=")
|
|
18
|
+
if not separator or not key or key in settings:
|
|
19
|
+
raise CondaError("Provide distinct declared settings as KEY=VALUE.")
|
|
20
|
+
settings[key] = new_value
|
|
21
|
+
plan = feature.configure(catalog, ctx, path, scope, settings)
|
|
22
|
+
return confirm_and_apply(args, catalog, ctx, plan)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Inspect local feature readiness and retained deployment receipts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_parser(parser):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args, catalog, ctx, path, scope):
|
|
11
|
+
from ..operations.diagnostics import diagnose
|
|
12
|
+
|
|
13
|
+
return diagnose(catalog, ctx, path, scope)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Preview and disable one feature or all features at a scope."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def configure_parser(parser):
|
|
5
|
+
parser.add_argument("feature", nargs="?")
|
|
6
|
+
parser.add_argument(
|
|
7
|
+
"--all",
|
|
8
|
+
action="store_true",
|
|
9
|
+
help="Disable all declared features at this scope.",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute(args, catalog, ctx, path, scope):
|
|
14
|
+
from ..operations.configuration import plan_change
|
|
15
|
+
from .common import confirm_and_apply, select_feature
|
|
16
|
+
|
|
17
|
+
if args.all:
|
|
18
|
+
deployments = [
|
|
19
|
+
name
|
|
20
|
+
for name, (_, feature) in catalog.items()
|
|
21
|
+
if feature.enabled_setting is None and scope in feature.scopes
|
|
22
|
+
]
|
|
23
|
+
boolean_catalog = {
|
|
24
|
+
name: item
|
|
25
|
+
for name, item in catalog.items()
|
|
26
|
+
if item[1].enabled_setting is not None
|
|
27
|
+
}
|
|
28
|
+
plan = plan_change(
|
|
29
|
+
boolean_catalog,
|
|
30
|
+
ctx,
|
|
31
|
+
path,
|
|
32
|
+
scope,
|
|
33
|
+
"disable",
|
|
34
|
+
args.feature,
|
|
35
|
+
all_features=True,
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
plan = select_feature(catalog, args.feature).disable(catalog, ctx, path, scope)
|
|
39
|
+
result = confirm_and_apply(args, catalog, ctx, plan)
|
|
40
|
+
if args.all and deployments:
|
|
41
|
+
result["message"] += (
|
|
42
|
+
" Disable deployment features individually: " + ", ".join(deployments) + "."
|
|
43
|
+
)
|
|
44
|
+
return result
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Preview and enable an installed feature."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def configure_parser(parser):
|
|
5
|
+
parser.add_argument("feature")
|
|
6
|
+
parser.add_argument(
|
|
7
|
+
"--with-dependencies",
|
|
8
|
+
action="store_true",
|
|
9
|
+
help="Preview prerequisite opt-ins at the selected scope.",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute(args, catalog, ctx, path, scope):
|
|
14
|
+
from .common import confirm_and_apply, select_feature
|
|
15
|
+
|
|
16
|
+
feature = select_feature(catalog, args.feature)
|
|
17
|
+
plan = (
|
|
18
|
+
feature.enable(catalog, ctx, path, scope, with_dependencies=True)
|
|
19
|
+
if args.with_dependencies
|
|
20
|
+
else feature.enable(catalog, ctx, path, scope)
|
|
21
|
+
)
|
|
22
|
+
return confirm_and_apply(args, catalog, ctx, plan)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Explain a feature's configured state and prerequisites."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def configure_parser(parser):
|
|
5
|
+
parser.add_argument("feature")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute(args, catalog, ctx, path, scope):
|
|
9
|
+
from .common import select_feature
|
|
10
|
+
|
|
11
|
+
return select_feature(catalog, args.feature).status(catalog, ctx, scope, path=path)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Export portable choices as one versioned JSON document."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_parser(parser):
|
|
7
|
+
parser.add_argument(
|
|
8
|
+
"features",
|
|
9
|
+
nargs="*",
|
|
10
|
+
help="Feature IDs to export. Defaults to all saved choices at this scope.",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(args, catalog, ctx, path, scope):
|
|
15
|
+
from .common import select_feature
|
|
16
|
+
|
|
17
|
+
names = args.features or list(catalog)
|
|
18
|
+
entries = [
|
|
19
|
+
select_feature(catalog, name).export_intent(catalog, ctx, path, scope)
|
|
20
|
+
for name in names
|
|
21
|
+
]
|
|
22
|
+
return {
|
|
23
|
+
"version": 1,
|
|
24
|
+
"scope": scope,
|
|
25
|
+
"features": [entry for entry in entries if entry is not None],
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""List installed feature declarations and their state."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def configure_parser(parser):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute(args, catalog, ctx, path, scope):
|
|
9
|
+
return {
|
|
10
|
+
"features": [
|
|
11
|
+
feature.status(catalog, ctx, scope, path=path)
|
|
12
|
+
for _, feature in catalog.values()
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Preview migration to a provider-declared replacement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_parser(parser):
|
|
7
|
+
parser.add_argument("feature")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args, catalog, ctx, path, scope):
|
|
11
|
+
from .common import confirm_and_apply, select_feature
|
|
12
|
+
|
|
13
|
+
feature = select_feature(catalog, args.feature)
|
|
14
|
+
return confirm_and_apply(
|
|
15
|
+
args, catalog, ctx, feature.migrate(catalog, ctx, path, scope)
|
|
16
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Human-readable and JSON command output."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from conda.base.context import context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render(result: dict) -> None:
|
|
9
|
+
if context.json or (
|
|
10
|
+
result.get("version") == 1 and "features" in result and "scope" in result
|
|
11
|
+
):
|
|
12
|
+
print(json.dumps(result, default=str))
|
|
13
|
+
elif "problems" in result:
|
|
14
|
+
for problem in result["problems"]:
|
|
15
|
+
print(f"{problem['feature']}: {problem['message']} {problem['action']}")
|
|
16
|
+
if not result["problems"]:
|
|
17
|
+
print("No local feature readiness problems found.")
|
|
18
|
+
print(result["message"])
|
|
19
|
+
elif "changes" in result:
|
|
20
|
+
print(f"Configuration: {result['path']} ({result['scope']})")
|
|
21
|
+
if "checked_environments" in result:
|
|
22
|
+
print(
|
|
23
|
+
"Checked environments: "
|
|
24
|
+
+ (", ".join(result["checked_environments"]) or "none")
|
|
25
|
+
)
|
|
26
|
+
print(result["discovery"])
|
|
27
|
+
for key, new_value in result["changes"].items():
|
|
28
|
+
print(f" {key} = {new_value}")
|
|
29
|
+
print(result["message"])
|
|
30
|
+
elif isinstance(result.get("features"), list):
|
|
31
|
+
if not result["features"]:
|
|
32
|
+
print("No feature providers installed.")
|
|
33
|
+
else:
|
|
34
|
+
print(
|
|
35
|
+
"Feature Maturity Requested Effective setting Readiness Description"
|
|
36
|
+
)
|
|
37
|
+
for feature in result["features"]:
|
|
38
|
+
print(
|
|
39
|
+
" ".join(
|
|
40
|
+
str(feature[key])
|
|
41
|
+
for key in (
|
|
42
|
+
"name",
|
|
43
|
+
"maturity",
|
|
44
|
+
"requested",
|
|
45
|
+
"effective",
|
|
46
|
+
"readiness",
|
|
47
|
+
"description",
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
print(f"{result['name']}: {result['description']}")
|
|
53
|
+
for key in ("provider", "maturity", "requested", "readiness"):
|
|
54
|
+
print(f"{key.capitalize()}: {result[key]}")
|
|
55
|
+
print(f"Effective setting: {result['effective']}")
|
|
56
|
+
print("Runtime: unknown (provider execution is not observed)")
|
|
57
|
+
if result.get("lifecycle") not in (None, "available"):
|
|
58
|
+
print(f"Lifecycle: {result['lifecycle']}")
|
|
59
|
+
if result.get("replacement"):
|
|
60
|
+
print(
|
|
61
|
+
f"Replacement: {result['replacement']}. Use feature migrate to preview the change."
|
|
62
|
+
)
|
|
63
|
+
if result.get("graduation") == "unconditional":
|
|
64
|
+
print(
|
|
65
|
+
"Functionality is now unconditional. Cleanup removes only the obsolete opt-in, not the functionality."
|
|
66
|
+
)
|
|
67
|
+
for key, sources in result["sources"].items():
|
|
68
|
+
print(f"{key}: sources {', '.join(sources) or 'default'}")
|
|
69
|
+
for reason in result["reasons"]:
|
|
70
|
+
print(reason)
|
|
71
|
+
if result["settings"]:
|
|
72
|
+
print("Configurable settings: " + ", ".join(result["settings"]))
|
|
73
|
+
if result["guidance"]:
|
|
74
|
+
print(result["guidance"])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Argument definitions for the feature subcommand."""
|
|
2
|
+
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
|
|
6
|
+
from conda.cli.helpers import add_output_and_prompt_options
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure_parser(parser: ArgumentParser) -> None:
|
|
10
|
+
parser.description = "Manage features declared by installed providers. Deployment providers may install prerequisites after confirmation."
|
|
11
|
+
parser.epilog = "Providers register a conda_features hook in the conda_features entry-point group. See help(conda_features) in Python for setup and usage."
|
|
12
|
+
commands = parser.add_subparsers(dest="operation", required=True)
|
|
13
|
+
for name in (
|
|
14
|
+
"list",
|
|
15
|
+
"explain",
|
|
16
|
+
"enable",
|
|
17
|
+
"disable",
|
|
18
|
+
"configure",
|
|
19
|
+
"diagnose",
|
|
20
|
+
"repair",
|
|
21
|
+
"migrate",
|
|
22
|
+
"cleanup",
|
|
23
|
+
"export",
|
|
24
|
+
"apply",
|
|
25
|
+
):
|
|
26
|
+
command = commands.add_parser(name)
|
|
27
|
+
add_output_and_prompt_options(command)
|
|
28
|
+
command.add_argument(
|
|
29
|
+
"--check-prefix",
|
|
30
|
+
action="append",
|
|
31
|
+
default=[],
|
|
32
|
+
metavar="PATH",
|
|
33
|
+
help="Also check this environment before mutation. Does not write to it.",
|
|
34
|
+
)
|
|
35
|
+
scope = command.add_mutually_exclusive_group()
|
|
36
|
+
scope.add_argument(
|
|
37
|
+
"--user", action="store_true", help="Use user configuration (default)."
|
|
38
|
+
)
|
|
39
|
+
scope.add_argument(
|
|
40
|
+
"--system",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Use this conda installation's configuration.",
|
|
43
|
+
)
|
|
44
|
+
scope.add_argument(
|
|
45
|
+
"-p", "--prefix", help="Target an existing conda environment."
|
|
46
|
+
)
|
|
47
|
+
scope.add_argument(
|
|
48
|
+
"--file", help="Use an explicit user-scope configuration file."
|
|
49
|
+
)
|
|
50
|
+
import_module(f".{name}", package=__package__).configure_parser(command)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Retry a saved feature choice using its normal deployment plan."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_parser(parser):
|
|
7
|
+
parser.add_argument("feature")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args, catalog, ctx, path, scope):
|
|
11
|
+
from .common import confirm_and_apply, select_feature
|
|
12
|
+
|
|
13
|
+
feature = select_feature(catalog, args.feature)
|
|
14
|
+
return confirm_and_apply(
|
|
15
|
+
args, catalog, ctx, feature.repair(catalog, ctx, path, scope)
|
|
16
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Resolve configuration destinations using conda contexts."""
|
|
2
|
+
|
|
3
|
+
from argparse import Namespace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from conda import CondaError
|
|
7
|
+
from conda.base.context import Context, context, user_rc_path
|
|
8
|
+
from conda.common.configuration import CONDARC_FILENAMES, YAML_EXTENSIONS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def target(args: Namespace) -> tuple[Path, str, Context]:
|
|
12
|
+
if args.file:
|
|
13
|
+
path = Path(args.file).expanduser().absolute()
|
|
14
|
+
if path.name not in CONDARC_FILENAMES and path.suffix not in YAML_EXTENSIONS:
|
|
15
|
+
raise CondaError(
|
|
16
|
+
"Configuration filename must be .condarc, condarc, or have a YAML extension."
|
|
17
|
+
)
|
|
18
|
+
return path, "user", Context(search_path=(path,))
|
|
19
|
+
if args.prefix:
|
|
20
|
+
prefix = Path(args.prefix).expanduser().absolute()
|
|
21
|
+
if not (prefix / "conda-meta").is_dir():
|
|
22
|
+
raise CondaError("The target prefix must be an existing conda environment.")
|
|
23
|
+
path, scope = prefix / ".condarc", "environment"
|
|
24
|
+
elif args.system:
|
|
25
|
+
path, scope = Path(context.root_prefix) / ".condarc", "system"
|
|
26
|
+
else:
|
|
27
|
+
path, scope = Path(user_rc_path).expanduser().absolute(), "user"
|
|
28
|
+
return path, scope, Context(argparse_args=args)
|
conda_features/hooks.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Pluggy interface for installed feature providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pluggy import HookimplMarker, HookspecMarker
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
from .models import Feature
|
|
13
|
+
|
|
14
|
+
hookimpl = HookimplMarker("conda_features")
|
|
15
|
+
hookspec = HookspecMarker("conda_features")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FeatureSpecs:
|
|
19
|
+
@hookspec
|
|
20
|
+
def conda_features(self) -> Iterable[Feature]:
|
|
21
|
+
"""Yield local feature declarations without performing setup or I/O."""
|
|
22
|
+
raise NotImplementedError
|