conda-workspaces 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.
@@ -0,0 +1,8 @@
1
+ """conda-workspaces: Project-scoped multi-environment workspace management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from ._version import __version__
7
+ except ImportError: # pragma: no cover
8
+ __version__ = "0.0.0.dev0"
@@ -0,0 +1,29 @@
1
+ """Standalone CLI entry point for ``cw`` (short for ``conda workspace``).
2
+
3
+ This module allows running conda-workspaces without going through the
4
+ conda plugin dispatch::
5
+
6
+ cw init
7
+ cw install
8
+ cw list
9
+ cw add -e dev pytest
10
+
11
+ It reuses the same parser and execute logic as ``conda workspace``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+
17
+ def main(args: list[str] | None = None) -> None:
18
+ """Entry point for the ``cw`` console script."""
19
+ from .cli.main import execute, generate_parser
20
+
21
+ parser = generate_parser()
22
+ parser.prog = "cw"
23
+
24
+ parsed = parser.parse_args(args)
25
+ raise SystemExit(execute(parsed))
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,7 @@
1
+ """CLI package for ``conda workspace``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .main import configure_parser, execute, generate_parser
6
+
7
+ __all__ = ["configure_parser", "execute", "generate_parser"]
@@ -0,0 +1,33 @@
1
+ """``conda workspace activate`` — print activation instructions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from conda.cli.common import print_activate
8
+
9
+ from ..context import WorkspaceContext
10
+ from ..exceptions import EnvironmentNotFoundError, EnvironmentNotInstalledError
11
+ from ..parsers import detect_and_parse
12
+
13
+ if TYPE_CHECKING:
14
+ import argparse
15
+
16
+
17
+ def execute_activate(args: argparse.Namespace) -> int:
18
+ """Print activation instructions for a workspace environment."""
19
+ manifest_path = getattr(args, "file", None)
20
+ _, config = detect_and_parse(manifest_path)
21
+ ctx = WorkspaceContext(config)
22
+
23
+ env_name = args.env_name
24
+
25
+ if env_name not in config.environments:
26
+ raise EnvironmentNotFoundError(env_name, list(config.environments.keys()))
27
+
28
+ if not ctx.env_exists(env_name):
29
+ raise EnvironmentNotInstalledError(env_name)
30
+
31
+ prefix = ctx.env_prefix(env_name)
32
+ print_activate(str(prefix))
33
+ return 0
@@ -0,0 +1,91 @@
1
+ """``conda workspace add`` — add dependencies to the workspace manifest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import tomlkit
8
+
9
+ from ..parsers import detect_workspace_file
10
+
11
+ if TYPE_CHECKING:
12
+ import argparse
13
+
14
+
15
+ def execute_add(args: argparse.Namespace) -> int:
16
+ """Add dependencies to the workspace manifest."""
17
+ manifest_path = getattr(args, "file", None) or detect_workspace_file()
18
+ specs = args.specs
19
+ is_pypi = getattr(args, "pypi", False)
20
+ feature = getattr(args, "feature", None)
21
+ environment = getattr(args, "environment", None)
22
+
23
+ # Determine which feature to target
24
+ target_feature = feature or environment # environment name == feature name
25
+
26
+ text = manifest_path.read_text(encoding="utf-8")
27
+ doc = tomlkit.loads(text)
28
+
29
+ dep_key = "pypi-dependencies" if is_pypi else "dependencies"
30
+
31
+ if manifest_path.name == "pyproject.toml":
32
+ _add_to_pyproject(doc, specs, dep_key, target_feature)
33
+ else:
34
+ _add_to_toml(doc, specs, dep_key, target_feature)
35
+
36
+ manifest_path.write_text(tomlkit.dumps(doc), encoding="utf-8")
37
+
38
+ label = "PyPI" if is_pypi else "conda"
39
+ location = f"feature '{target_feature}'" if target_feature else "default"
40
+
41
+ print(
42
+ f"Added {len(specs)} {label} dependency(ies)"
43
+ f" to {location} in {manifest_path.name}"
44
+ )
45
+ return 0
46
+
47
+
48
+ def _add_to_toml(
49
+ doc: tomlkit.TOMLDocument,
50
+ specs: list[str],
51
+ dep_key: str,
52
+ feature: str | None,
53
+ ) -> None:
54
+ """Add deps to a pixi.toml or conda.toml document."""
55
+ if feature:
56
+ feat_table = doc.setdefault("feature", tomlkit.table())
57
+ target = feat_table.setdefault(feature, tomlkit.table())
58
+ else:
59
+ target = doc
60
+
61
+ deps = target.setdefault(dep_key, tomlkit.table())
62
+ for spec in specs:
63
+ name, _, version = spec.partition(" ")
64
+ deps[name] = version.strip() if version.strip() else "*"
65
+
66
+
67
+ def _add_to_pyproject(
68
+ doc: tomlkit.TOMLDocument,
69
+ specs: list[str],
70
+ dep_key: str,
71
+ feature: str | None,
72
+ ) -> None:
73
+ """Add deps to a pyproject.toml with [tool.pixi.*] tables."""
74
+ tool = doc.setdefault("tool", tomlkit.table())
75
+
76
+ # Prefer conda-workspaces table if it exists, else pixi
77
+ if "conda-workspaces" in tool:
78
+ source = tool["conda-workspaces"]
79
+ else:
80
+ source = tool.setdefault("pixi", tomlkit.table())
81
+
82
+ if feature:
83
+ feat_table = source.setdefault("feature", tomlkit.table())
84
+ target = feat_table.setdefault(feature, tomlkit.table())
85
+ else:
86
+ target = source
87
+
88
+ deps = target.setdefault(dep_key, tomlkit.table())
89
+ for spec in specs:
90
+ name, _, version = spec.partition(" ")
91
+ deps[name] = version.strip() if version.strip() else "*"
@@ -0,0 +1,52 @@
1
+ """``conda workspace clean`` — remove installed workspace environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from conda.exceptions import CondaSystemExit, DryRunExit
8
+ from conda.reporters import confirm_yn
9
+
10
+ from ..context import WorkspaceContext
11
+ from ..envs import clean_all, list_installed_environments, remove_environment
12
+ from ..parsers import detect_and_parse
13
+
14
+ if TYPE_CHECKING:
15
+ import argparse
16
+
17
+
18
+ def execute_clean(args: argparse.Namespace) -> int:
19
+ """Remove installed workspace environments."""
20
+ manifest_path = getattr(args, "file", None)
21
+ _, config = detect_and_parse(manifest_path)
22
+ ctx = WorkspaceContext(config)
23
+
24
+ env_name = getattr(args, "environment", None)
25
+
26
+ try:
27
+ if env_name:
28
+ if not ctx.env_exists(env_name):
29
+ print(f"Environment '{env_name}' is not installed.")
30
+ return 0
31
+
32
+ confirm_yn(f"Remove environment '{env_name}'?")
33
+
34
+ remove_environment(ctx, env_name)
35
+ print(f"Removed environment '{env_name}'.")
36
+ else:
37
+ installed = list_installed_environments(ctx)
38
+ if not installed:
39
+ print("No environments installed.")
40
+ return 0
41
+
42
+ print(f"This will remove {len(installed)} environment(s):")
43
+ for name in installed:
44
+ print(f" - {name}")
45
+ confirm_yn("Continue?")
46
+
47
+ clean_all(ctx)
48
+ print(f"Removed {len(installed)} environment(s).")
49
+ except (CondaSystemExit, DryRunExit):
50
+ return 0
51
+
52
+ return 0
@@ -0,0 +1,71 @@
1
+ """``conda workspace info`` — show environment details."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ..context import WorkspaceContext
9
+ from ..envs import get_environment_info
10
+ from ..parsers import detect_and_parse
11
+ from ..resolver import resolve_environment
12
+
13
+ if TYPE_CHECKING:
14
+ import argparse
15
+
16
+
17
+ def execute_info(args: argparse.Namespace) -> int:
18
+ """Show details about an environment."""
19
+ manifest_path = getattr(args, "file", None)
20
+ _, config = detect_and_parse(manifest_path)
21
+ ctx = WorkspaceContext(config)
22
+
23
+ env_name = args.env_name
24
+ json_output = getattr(args, "json", False)
25
+
26
+ resolved = resolve_environment(config, env_name, ctx.platform)
27
+ install_info = get_environment_info(ctx, env_name)
28
+
29
+ info = {
30
+ "name": env_name,
31
+ "prefix": str(ctx.env_prefix(env_name)),
32
+ "installed": install_info["exists"],
33
+ "channels": [ch.canonical_name for ch in resolved.channels],
34
+ "platforms": resolved.platforms,
35
+ "solve_group": resolved.solve_group,
36
+ "conda_dependencies": {
37
+ name: dep.conda_build_form()
38
+ for name, dep in resolved.conda_dependencies.items()
39
+ },
40
+ "pypi_dependencies": {
41
+ name: str(dep) for name, dep in resolved.pypi_dependencies.items()
42
+ },
43
+ }
44
+
45
+ if install_info["exists"]:
46
+ info["packages_installed"] = install_info.get("packages", 0)
47
+
48
+ if json_output:
49
+ print(json.dumps(info, indent=2))
50
+ else:
51
+ print(f"Environment: {info['name']}")
52
+ print(f"Prefix: {info['prefix']}")
53
+ print(f"Installed: {'yes' if info['installed'] else 'no'}")
54
+ if info["installed"]:
55
+ print(f"Packages: {info.get('packages_installed', '?')}")
56
+ print(f"Channels: {', '.join(info['channels']) or '(none)'}")
57
+ print(f"Platforms: {', '.join(info['platforms']) or '(all)'}")
58
+ if info["solve_group"]:
59
+ print(f"Solve group: {info['solve_group']}")
60
+
61
+ if info["conda_dependencies"]:
62
+ print("\nConda dependencies:")
63
+ for name, spec in sorted(info["conda_dependencies"].items()):
64
+ print(f" - {spec}")
65
+
66
+ if info["pypi_dependencies"]:
67
+ print("\nPyPI dependencies:")
68
+ for name, spec in sorted(info["pypi_dependencies"].items()):
69
+ print(f" - {spec}")
70
+
71
+ return 0
@@ -0,0 +1,114 @@
1
+ """``conda workspace init`` — scaffold a new workspace manifest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ import tomlkit
9
+ from conda.base.context import context as conda_context
10
+
11
+ from ..exceptions import ManifestExistsError
12
+
13
+ if TYPE_CHECKING:
14
+ import argparse
15
+
16
+
17
+ def execute_init(args: argparse.Namespace) -> int:
18
+ """Create a new workspace manifest in the current directory."""
19
+ fmt = args.manifest_format
20
+ name = args.name or Path.cwd().name
21
+ channels = args.channels or ["conda-forge"]
22
+
23
+ if args.platforms:
24
+ platforms = args.platforms
25
+ else:
26
+ platforms = _detect_platforms()
27
+
28
+ if fmt == "pixi":
29
+ return _write_pixi_toml(name, channels, platforms)
30
+ elif fmt == "conda":
31
+ return _write_conda_toml(name, channels, platforms)
32
+ elif fmt == "pyproject":
33
+ return _write_pyproject_toml(name, channels, platforms)
34
+
35
+ return 1
36
+
37
+
38
+ def _detect_platforms() -> list[str]:
39
+ """Auto-detect a reasonable set of platforms."""
40
+ current = conda_context.subdir
41
+ # Include a sensible default set based on current platform
42
+ defaults = {"linux-64", "osx-64", "osx-arm64", "win-64"}
43
+ defaults.add(current)
44
+ return sorted(defaults)
45
+
46
+
47
+ def _write_pixi_toml(name: str, channels: list[str], platforms: list[str]) -> int:
48
+ path = Path("pixi.toml")
49
+ if path.exists():
50
+ raise ManifestExistsError(path)
51
+
52
+ doc = tomlkit.document()
53
+
54
+ ws = tomlkit.table()
55
+ ws.add("name", name)
56
+ ws.add("version", "0.1.0")
57
+ ws.add("channels", channels)
58
+ ws.add("platforms", platforms)
59
+ doc.add("workspace", ws)
60
+
61
+ deps = tomlkit.table()
62
+ doc.add("dependencies", deps)
63
+
64
+ path.write_text(tomlkit.dumps(doc), encoding="utf-8")
65
+ print(f"Created {path}")
66
+ return 0
67
+
68
+
69
+ def _write_conda_toml(name: str, channels: list[str], platforms: list[str]) -> int:
70
+ path = Path("conda.toml")
71
+ if path.exists():
72
+ raise ManifestExistsError(path)
73
+
74
+ doc = tomlkit.document()
75
+
76
+ ws = tomlkit.table()
77
+ ws.add("name", name)
78
+ ws.add("version", "0.1.0")
79
+ ws.add("channels", channels)
80
+ ws.add("platforms", platforms)
81
+ doc.add("workspace", ws)
82
+
83
+ deps = tomlkit.table()
84
+ doc.add("dependencies", deps)
85
+
86
+ path.write_text(tomlkit.dumps(doc), encoding="utf-8")
87
+ print(f"Created {path}")
88
+ return 0
89
+
90
+
91
+ def _write_pyproject_toml(name: str, channels: list[str], platforms: list[str]) -> int:
92
+ path = Path("pyproject.toml")
93
+
94
+ if path.exists():
95
+ text = path.read_text(encoding="utf-8")
96
+ doc = tomlkit.loads(text)
97
+ else:
98
+ doc = tomlkit.document()
99
+
100
+ tool = doc.setdefault("tool", tomlkit.table())
101
+ if "pixi" in tool:
102
+ raise ManifestExistsError("[tool.pixi] in pyproject.toml")
103
+
104
+ pixi = tomlkit.table()
105
+ ws = tomlkit.table()
106
+ ws.add("channels", channels)
107
+ ws.add("platforms", platforms)
108
+ pixi.add("workspace", ws)
109
+ pixi.add("dependencies", tomlkit.table())
110
+ tool.add("pixi", pixi)
111
+
112
+ path.write_text(tomlkit.dumps(doc), encoding="utf-8")
113
+ print(f"{'Updated' if path.exists() else 'Created'} {path}")
114
+ return 0
@@ -0,0 +1,67 @@
1
+ """``conda workspace install`` — create or update workspace environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..context import WorkspaceContext
8
+ from ..envs import install_environment
9
+ from ..lockfile import generate_lockfile, install_from_lockfile
10
+ from ..parsers import detect_and_parse
11
+ from ..resolver import resolve_all_environments, resolve_environment
12
+
13
+ if TYPE_CHECKING:
14
+ import argparse
15
+
16
+
17
+ def execute_install(args: argparse.Namespace) -> int:
18
+ """Install (create/update) workspace environments."""
19
+ manifest_path = getattr(args, "file", None)
20
+ _, config = detect_and_parse(manifest_path)
21
+ ctx = WorkspaceContext(config)
22
+
23
+ env_name = getattr(args, "environment", None)
24
+ force = getattr(args, "force_reinstall", False)
25
+ dry_run = getattr(args, "dry_run", False)
26
+ locked = getattr(args, "locked", False)
27
+
28
+ if locked:
29
+ return _install_locked(ctx, config, env_name)
30
+
31
+ if env_name:
32
+ resolved = resolve_environment(config, env_name, ctx.platform)
33
+ print(f"Installing environment '{env_name}'...")
34
+ install_environment(ctx, resolved, force_reinstall=force, dry_run=dry_run)
35
+ if not dry_run:
36
+ generate_lockfile(ctx, env_names=[env_name])
37
+ print(f"Environment '{env_name}' is ready at {ctx.env_prefix(env_name)}")
38
+ else:
39
+ resolved_all = resolve_all_environments(config, ctx.platform)
40
+ installed_names: list[str] = []
41
+ for name, resolved in resolved_all.items():
42
+ print(f"Installing environment '{name}'...")
43
+ install_environment(ctx, resolved, force_reinstall=force, dry_run=dry_run)
44
+ installed_names.append(name)
45
+ print(f" -> {ctx.env_prefix(name)}")
46
+ if not dry_run:
47
+ generate_lockfile(ctx, env_names=installed_names)
48
+ print(f"\n{len(resolved_all)} environment(s) installed.")
49
+
50
+ return 0
51
+
52
+
53
+ def _install_locked(ctx: WorkspaceContext, config: object, env_name: str | None) -> int:
54
+ """Install environments from existing lockfiles (no solving)."""
55
+ if env_name:
56
+ print(f"Installing environment '{env_name}' from lockfile...")
57
+ install_from_lockfile(ctx, env_name)
58
+ print(f"Environment '{env_name}' is ready at {ctx.env_prefix(env_name)}")
59
+ else:
60
+ resolved_all = resolve_all_environments(config, ctx.platform)
61
+ for name in resolved_all:
62
+ print(f"Installing environment '{name}' from lockfile...")
63
+ install_from_lockfile(ctx, name)
64
+ print(f" -> {ctx.env_prefix(name)}")
65
+ print(f"\n{len(resolved_all)} environment(s) installed from lockfiles.")
66
+
67
+ return 0
@@ -0,0 +1,55 @@
1
+ """``conda workspace list`` — list workspace environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ..context import WorkspaceContext
9
+ from ..envs import list_installed_environments
10
+ from ..parsers import detect_and_parse
11
+
12
+ if TYPE_CHECKING:
13
+ import argparse
14
+
15
+
16
+ def execute_list(args: argparse.Namespace) -> int:
17
+ """List environments defined in the workspace."""
18
+ manifest_path = getattr(args, "file", None)
19
+ _, config = detect_and_parse(manifest_path)
20
+ ctx = WorkspaceContext(config)
21
+
22
+ installed_only = getattr(args, "installed", False)
23
+ json_output = getattr(args, "json", False)
24
+
25
+ installed = set(list_installed_environments(ctx))
26
+
27
+ rows: list[dict[str, str | bool | list[str]]] = []
28
+ for name, env in sorted(config.environments.items()):
29
+ if installed_only and name not in installed:
30
+ continue
31
+ rows.append(
32
+ {
33
+ "name": name,
34
+ "features": env.features,
35
+ "solve_group": env.solve_group or "",
36
+ "installed": name in installed,
37
+ }
38
+ )
39
+
40
+ if json_output:
41
+ print(json.dumps(rows, indent=2))
42
+ else:
43
+ if not rows:
44
+ print("No environments found.")
45
+ return 0
46
+
47
+ # Header
48
+ print(f"{'Name':<20} {'Features':<30} {'Solve Group':<15} {'Installed'}")
49
+ print("-" * 75)
50
+ for row in rows:
51
+ feats = ", ".join(row["features"]) if row["features"] else "(default)" # type: ignore[arg-type]
52
+ status = "yes" if row["installed"] else "no"
53
+ print(f"{row['name']:<20} {feats:<30} {row['solve_group']:<15} {status}")
54
+
55
+ return 0
@@ -0,0 +1,43 @@
1
+ """``conda workspace lock`` — generate lockfiles for workspace environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..context import WorkspaceContext
8
+ from ..lockfile import generate_lockfile, lockfile_path
9
+ from ..parsers import detect_and_parse
10
+ from ..resolver import resolve_all_environments
11
+
12
+ if TYPE_CHECKING:
13
+ import argparse
14
+
15
+
16
+ def execute_lock(args: argparse.Namespace) -> int:
17
+ """Generate a ``conda.lock`` for installed workspace environments."""
18
+ manifest_path = getattr(args, "file", None)
19
+ _, config = detect_and_parse(manifest_path)
20
+ ctx = WorkspaceContext(config)
21
+
22
+ env_name = getattr(args, "environment", None)
23
+
24
+ if env_name:
25
+ if not ctx.env_exists(env_name):
26
+ from ..exceptions import EnvironmentNotInstalledError
27
+
28
+ raise EnvironmentNotInstalledError(env_name)
29
+ path = generate_lockfile(ctx, env_names=[env_name])
30
+ print(f"Lockfile written to {path}")
31
+ else:
32
+ resolved_all = resolve_all_environments(config, ctx.platform)
33
+ installed = [name for name in resolved_all if ctx.env_exists(name)]
34
+ skipped = [name for name in resolved_all if not ctx.env_exists(name)]
35
+ if installed:
36
+ path = generate_lockfile(ctx, env_names=installed)
37
+ for name in installed:
38
+ print(f" {name} -> {path}")
39
+ for name in skipped:
40
+ print(f" {name} — skipped (not installed)")
41
+ print(f"\n{len(installed)} environment(s) locked in {lockfile_path(ctx)}.")
42
+
43
+ return 0