##// END OF EJS Templates
Add line magic for uv
akumor -
Show More
@@ -1,164 +1,181
1 """Implementation of packaging-related magic functions.
1 """Implementation of packaging-related magic functions.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (c) 2018 The IPython Development Team.
4 # Copyright (c) 2018 The IPython Development Team.
5 #
5 #
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7 #
7 #
8 # The full license is in the file COPYING.txt, distributed with this software.
8 # The full license is in the file COPYING.txt, distributed with this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 import functools
11 import functools
12 import os
12 import os
13 import re
13 import re
14 import shlex
14 import shlex
15 import sys
15 import sys
16 from pathlib import Path
16 from pathlib import Path
17
17
18 from IPython.core.magic import Magics, magics_class, line_magic
18 from IPython.core.magic import Magics, magics_class, line_magic
19
19
20
20
21 def is_conda_environment(func):
21 def is_conda_environment(func):
22 @functools.wraps(func)
22 @functools.wraps(func)
23 def wrapper(*args, **kwargs):
23 def wrapper(*args, **kwargs):
24 """Return True if the current Python executable is in a conda env"""
24 """Return True if the current Python executable is in a conda env"""
25 # TODO: does this need to change on windows?
25 # TODO: does this need to change on windows?
26 if not Path(sys.prefix, "conda-meta", "history").exists():
26 if not Path(sys.prefix, "conda-meta", "history").exists():
27 raise ValueError(
27 raise ValueError(
28 "The python kernel does not appear to be a conda environment. "
28 "The python kernel does not appear to be a conda environment. "
29 "Please use ``%pip install`` instead."
29 "Please use ``%pip install`` instead."
30 )
30 )
31 return func(*args, **kwargs)
31 return func(*args, **kwargs)
32
32
33 return wrapper
33 return wrapper
34
34
35
35
36 def _get_conda_like_executable(command):
36 def _get_conda_like_executable(command):
37 """Find the path to the given executable
37 """Find the path to the given executable
38
38
39 Parameters
39 Parameters
40 ----------
40 ----------
41
41
42 executable: string
42 executable: string
43 Value should be: conda, mamba or micromamba
43 Value should be: conda, mamba or micromamba
44 """
44 """
45 # Check for a environment variable bound to the base executable, both conda and mamba
45 # Check for a environment variable bound to the base executable, both conda and mamba
46 # set these when activating an environment.
46 # set these when activating an environment.
47 base_executable = "CONDA_EXE"
47 base_executable = "CONDA_EXE"
48 if "mamba" in command.lower():
48 if "mamba" in command.lower():
49 base_executable = "MAMBA_EXE"
49 base_executable = "MAMBA_EXE"
50 if base_executable in os.environ:
50 if base_executable in os.environ:
51 executable = Path(os.environ[base_executable])
51 executable = Path(os.environ[base_executable])
52 if executable.is_file():
52 if executable.is_file():
53 return str(executable.resolve())
53 return str(executable.resolve())
54
54
55 # Check if there is a conda executable in the same directory as the Python executable.
55 # Check if there is a conda executable in the same directory as the Python executable.
56 # This is the case within conda's root environment.
56 # This is the case within conda's root environment.
57 executable = Path(sys.executable).parent / command
57 executable = Path(sys.executable).parent / command
58 if executable.is_file():
58 if executable.is_file():
59 return str(executable)
59 return str(executable)
60
60
61 # Otherwise, attempt to extract the executable from conda history.
61 # Otherwise, attempt to extract the executable from conda history.
62 # This applies in any conda environment. Parsing this way is error prone because
62 # This applies in any conda environment. Parsing this way is error prone because
63 # different versions of conda and mamba include differing cmd values such as
63 # different versions of conda and mamba include differing cmd values such as
64 # `conda`, `conda-script.py`, or `path/to/conda`, here use the raw command provided.
64 # `conda`, `conda-script.py`, or `path/to/conda`, here use the raw command provided.
65 history = Path(sys.prefix, "conda-meta", "history").read_text(encoding="utf-8")
65 history = Path(sys.prefix, "conda-meta", "history").read_text(encoding="utf-8")
66 match = re.search(
66 match = re.search(
67 rf"^#\s*cmd:\s*(?P<command>.*{command})\s[create|install]",
67 rf"^#\s*cmd:\s*(?P<command>.*{command})\s[create|install]",
68 history,
68 history,
69 flags=re.MULTILINE,
69 flags=re.MULTILINE,
70 )
70 )
71 if match:
71 if match:
72 return match.groupdict()["command"]
72 return match.groupdict()["command"]
73
73
74 # Fallback: assume the executable is available on the system path.
74 # Fallback: assume the executable is available on the system path.
75 return command
75 return command
76
76
77
77
78 CONDA_COMMANDS_REQUIRING_PREFIX = {
78 CONDA_COMMANDS_REQUIRING_PREFIX = {
79 'install', 'list', 'remove', 'uninstall', 'update', 'upgrade',
79 'install', 'list', 'remove', 'uninstall', 'update', 'upgrade',
80 }
80 }
81 CONDA_COMMANDS_REQUIRING_YES = {
81 CONDA_COMMANDS_REQUIRING_YES = {
82 'install', 'remove', 'uninstall', 'update', 'upgrade',
82 'install', 'remove', 'uninstall', 'update', 'upgrade',
83 }
83 }
84 CONDA_ENV_FLAGS = {'-p', '--prefix', '-n', '--name'}
84 CONDA_ENV_FLAGS = {'-p', '--prefix', '-n', '--name'}
85 CONDA_YES_FLAGS = {'-y', '--y'}
85 CONDA_YES_FLAGS = {'-y', '--y'}
86
86
87
87
88 @magics_class
88 @magics_class
89 class PackagingMagics(Magics):
89 class PackagingMagics(Magics):
90 """Magics related to packaging & installation"""
90 """Magics related to packaging & installation"""
91
91
92 @line_magic
92 @line_magic
93 def pip(self, line):
93 def pip(self, line):
94 """Run the pip package manager within the current kernel.
94 """Run the pip package manager within the current kernel.
95
95
96 Usage:
96 Usage:
97 %pip install [pkgs]
97 %pip install [pkgs]
98 """
98 """
99 python = sys.executable
99 python = sys.executable
100 if sys.platform == "win32":
100 if sys.platform == "win32":
101 python = '"' + python + '"'
101 python = '"' + python + '"'
102 else:
102 else:
103 python = shlex.quote(python)
103 python = shlex.quote(python)
104
104
105 self.shell.system(" ".join([python, "-m", "pip", line]))
105 self.shell.system(" ".join([python, "-m", "pip", line]))
106
106
107 print("Note: you may need to restart the kernel to use updated packages.")
107 print("Note: you may need to restart the kernel to use updated packages.")
108
108
109 def _run_command(self, cmd, line):
109 def _run_command(self, cmd, line):
110 args = shlex.split(line)
110 args = shlex.split(line)
111 command = args[0] if len(args) > 0 else ""
111 command = args[0] if len(args) > 0 else ""
112 args = args[1:] if len(args) > 1 else [""]
112 args = args[1:] if len(args) > 1 else [""]
113
113
114 extra_args = []
114 extra_args = []
115
115
116 # When the subprocess does not allow us to respond "yes" during the installation,
116 # When the subprocess does not allow us to respond "yes" during the installation,
117 # we need to insert --yes in the argument list for some commands
117 # we need to insert --yes in the argument list for some commands
118 stdin_disabled = getattr(self.shell, 'kernel', None) is not None
118 stdin_disabled = getattr(self.shell, 'kernel', None) is not None
119 needs_yes = command in CONDA_COMMANDS_REQUIRING_YES
119 needs_yes = command in CONDA_COMMANDS_REQUIRING_YES
120 has_yes = set(args).intersection(CONDA_YES_FLAGS)
120 has_yes = set(args).intersection(CONDA_YES_FLAGS)
121 if stdin_disabled and needs_yes and not has_yes:
121 if stdin_disabled and needs_yes and not has_yes:
122 extra_args.append("--yes")
122 extra_args.append("--yes")
123
123
124 # Add --prefix to point conda installation to the current environment
124 # Add --prefix to point conda installation to the current environment
125 needs_prefix = command in CONDA_COMMANDS_REQUIRING_PREFIX
125 needs_prefix = command in CONDA_COMMANDS_REQUIRING_PREFIX
126 has_prefix = set(args).intersection(CONDA_ENV_FLAGS)
126 has_prefix = set(args).intersection(CONDA_ENV_FLAGS)
127 if needs_prefix and not has_prefix:
127 if needs_prefix and not has_prefix:
128 extra_args.extend(["--prefix", sys.prefix])
128 extra_args.extend(["--prefix", sys.prefix])
129
129
130 self.shell.system(" ".join([cmd, command] + extra_args + args))
130 self.shell.system(" ".join([cmd, command] + extra_args + args))
131 print("\nNote: you may need to restart the kernel to use updated packages.")
131 print("\nNote: you may need to restart the kernel to use updated packages.")
132
132
133 @line_magic
133 @line_magic
134 @is_conda_environment
134 @is_conda_environment
135 def conda(self, line):
135 def conda(self, line):
136 """Run the conda package manager within the current kernel.
136 """Run the conda package manager within the current kernel.
137
137
138 Usage:
138 Usage:
139 %conda install [pkgs]
139 %conda install [pkgs]
140 """
140 """
141 conda = _get_conda_like_executable("conda")
141 conda = _get_conda_like_executable("conda")
142 self._run_command(conda, line)
142 self._run_command(conda, line)
143
143
144 @line_magic
144 @line_magic
145 @is_conda_environment
145 @is_conda_environment
146 def mamba(self, line):
146 def mamba(self, line):
147 """Run the mamba package manager within the current kernel.
147 """Run the mamba package manager within the current kernel.
148
148
149 Usage:
149 Usage:
150 %mamba install [pkgs]
150 %mamba install [pkgs]
151 """
151 """
152 mamba = _get_conda_like_executable("mamba")
152 mamba = _get_conda_like_executable("mamba")
153 self._run_command(mamba, line)
153 self._run_command(mamba, line)
154
154
155 @line_magic
155 @line_magic
156 @is_conda_environment
156 @is_conda_environment
157 def micromamba(self, line):
157 def micromamba(self, line):
158 """Run the conda package manager within the current kernel.
158 """Run the conda package manager within the current kernel.
159
159
160 Usage:
160 Usage:
161 %micromamba install [pkgs]
161 %micromamba install [pkgs]
162 """
162 """
163 micromamba = _get_conda_like_executable("micromamba")
163 micromamba = _get_conda_like_executable("micromamba")
164 self._run_command(micromamba, line)
164 self._run_command(micromamba, line)
165
166 @line_magic
167 def uv(self, line):
168 """Run the uv package manager within the current kernel.
169
170 Usage:
171 %uv pip install [pkgs]
172 """
173 python = sys.executable
174 if sys.platform == "win32":
175 python = '"' + python + '"'
176 else:
177 python = shlex.quote(python)
178
179 self.shell.system(" ".join([python, "-m", "uv", line]))
180
181 print("Note: you may need to restart the kernel to use updated packages.")
General Comments 0
You need to be logged in to leave comments. Login now