• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 35612692096

21 Sep 2026 02:31PM UTC coverage: 74.245% (-0.001%) from 74.246%
35612692096

Pull #2523

github

web-flow
Merge 9e6f97f2d into a6809790c
Pull Request #2523: #2484 tab completion for ide install python triggers uv installation

5717 of 8554 branches covered (66.83%)

Branch coverage included in aggregate %.

14820 of 19107 relevant lines covered (77.56%)

3.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.64
cli/src/main/java/com/devonfw/tools/ide/tool/python/Python.java
1
package com.devonfw.tools.ide.tool.python;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.nio.file.StandardCopyOption;
6
import java.util.Set;
7

8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10

11
import com.devonfw.tools.ide.cli.CliException;
12
import com.devonfw.tools.ide.common.Tag;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.io.FileAccess;
15
import com.devonfw.tools.ide.log.IdeLogLevel;
16
import com.devonfw.tools.ide.process.EnvironmentContext;
17
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
18
import com.devonfw.tools.ide.tool.ToolCommandlet;
19
import com.devonfw.tools.ide.tool.ToolInstallRequest;
20
import com.devonfw.tools.ide.tool.ToolInstallation;
21
import com.devonfw.tools.ide.tool.repository.ToolRepository;
22
import com.devonfw.tools.ide.tool.uv.Uv;
23
import com.devonfw.tools.ide.version.VersionIdentifier;
24

25
/**
26
 * {@link ToolCommandlet} for <a href="https://www.python.org/">python</a>.
27
 */
28
public class Python extends LocalToolCommandlet {
29

30
  private static final Logger LOG = LoggerFactory.getLogger(Python.class);
3✔
31

32
  private static final VersionIdentifier PYTHON_MIN_VERSION = VersionIdentifier.of("3.8.2");
4✔
33

34
  /** The folder created by {@code uv venv} inside the software folder before it is renamed to the python installation. */
35
  static final String VENV_FOLDER = ".venv";
36

37
  private static final String FILE_PYVENV_CFG = "pyvenv.cfg";
38

39
  private static final String PYVENV_CFG_VERSION_INFO = "version_info";
40

41
  /**
42
   * The constructor.
43
   *
44
   * @param context the {@link IdeContext}.
45
   */
46
  public Python(IdeContext context) {
47

48
    super(context, "python", Set.of(Tag.PYTHON));
6✔
49
  }
1✔
50

51
  /**
52
   * /** Ensures {@code uv} is available before resolving Python versions.
53
   * <p>
54
   * The install path uses {@code uv python list} to resolve available versions, so {@code uv} is installed first if missing.
55
   *
56
   * @param request the {@link ToolInstallRequest} to complete.
57
   */
58
  @Override
59
  protected void completeRequest(ToolInstallRequest request) {
60

61
    Uv uv = this.context.getCommandletManager().getCommandlet(Uv.class);
7✔
62
    if (!uv.isInstalled()) {
3✔
63
      LOG.info("Installing uv to resolve the Python version.");
3✔
64
      uv.install();
3✔
65
    }
66
    super.completeRequest(request);
3✔
67
  }
1✔
68

69
  @Override
70
  protected void performToolInstallation(ToolInstallRequest request, Path installationPath) {
71

72
    VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion();
4✔
73
    if (resolvedVersion.compareVersion(PYTHON_MIN_VERSION).isLess()) {
5!
74
      throw new CliException("Python version must be at least " + this.PYTHON_MIN_VERSION);
×
75
    }
76

77
    FileAccess fileAccess = this.context.getFileAccess();
4✔
78
    if (Files.exists(installationPath)) {
5!
79
      fileAccess.backup(installationPath);
×
80
    }
81
    Path softwarePath = installationPath.getParent();
3✔
82
    Path venvPath = softwarePath.resolve(VENV_FOLDER);
4✔
83

84
    fileAccess.delete(venvPath);
3✔
85

86
    Uv uv = this.context.getCommandletManager().getCommandlet(Uv.class);
7✔
87

88
    uv.installPython(softwarePath, resolvedVersion, request.getProcessContext());
6✔
89
    renameVenvFolderToPython(fileAccess, softwarePath, installationPath);
5✔
90
    this.context.writeVersionFile(resolvedVersion, installationPath);
5✔
91
    createWindowsSymlinkBinFolder(fileAccess, installationPath);
4✔
92
    LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath);
18✔
93
  }
1✔
94

95
  @Override
96
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
97

98
    super.setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
99
    environmentContext.withEnvVar("VIRTUAL_ENV", toolInstallation.rootDir().toString());
7✔
100
    environmentContext.withEnvVar("UV_PROJECT_ENVIRONMENT", toolInstallation.rootDir().toString());
7✔
101
  }
1✔
102

103
  @Override
104
  protected boolean isIgnoreSoftwareRepo() {
105

106
    return true;
2✔
107
  }
108

109
  @Override
110
  protected boolean isIgnoreMissingSoftwareVersionFile() {
111

112
    // https://github.com/devonfw/IDEasy/issues/2190
113
    return true;
2✔
114
  }
115

116
  @Override
117
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
118

119
    VersionIdentifier version = super.getInstalledVersion(toolPath);
4✔
120
    if (version == null) {
2✔
121
      // the virtual environment can be recreated by uv or python and then the version file is lost, so we ask the
122
      // installation itself instead of reporting that python is not installed - see
123
      // https://github.com/devonfw/IDEasy/issues/2190
124
      version = readVersionFromPyvenvCfg(toolPath);
4✔
125
      if (version == null) {
2!
126
        version = readVersionFromInterpreter(toolPath);
4✔
127
      }
128
      if (version != null) {
2✔
129
        LOG.debug("Determined version {} of python from the installation at {}.", version, toolPath);
5✔
130
      }
131
    }
132
    return version;
2✔
133
  }
134

135
  /**
136
   * @param installationPath the {@link Path} to the virtual environment.
137
   * @return the {@link VersionIdentifier} from the {@code version_info} entry of {@code pyvenv.cfg} or {@code null} if not available or not precise enough.
138
   */
139
  private VersionIdentifier readVersionFromPyvenvCfg(Path installationPath) {
140

141
    Path pyvenvCfg = installationPath.resolve(FILE_PYVENV_CFG);
4✔
142
    if (!Files.exists(pyvenvCfg)) {
5!
143
      return null;
2✔
144
    }
145
    String content = this.context.getFileAccess().readFileContent(pyvenvCfg);
×
146
    for (String line : content.split("\\R")) {
×
147
      String[] keyAndValue = line.split("=", 2);
×
148
      if ((keyAndValue.length == 2) && keyAndValue[0].trim().equals(PYVENV_CFG_VERSION_INFO)) {
×
149
        String value = keyAndValue[1].trim();
×
150
        // uv only writes the minor version (e.g. "3.13") for its own interpreters what is too imprecise for us
151
        if (value.chars().filter(c -> c == '.').count() >= 2) {
×
152
          return VersionIdentifier.of(value);
×
153
        }
154
        LOG.debug("Ignoring imprecise version {} from {}.", value, pyvenvCfg);
×
155
      }
156
    }
157
    return null;
×
158
  }
159

160
  /**
161
   * @param installationPath the {@link Path} to the virtual environment.
162
   * @return the {@link VersionIdentifier} reported by the installed python interpreter or {@code null} if it could not be determined.
163
   */
164
  private VersionIdentifier readVersionFromInterpreter(Path installationPath) {
165

166
    Path binPath = this.context.getFileAccess().getBinPath(installationPath);
6✔
167
    Path binaryPath = binPath.resolve(getBinaryName());
5✔
168
    if (!Files.exists(binaryPath)) {
5✔
169
      binaryPath = binPath.resolve(getBinaryName() + ".exe");
6✔
170
    }
171
    if (!Files.exists(binaryPath)) {
5✔
172
      LOG.debug("Python binary does not exist in {}.", binPath);
4✔
173
      return null;
2✔
174
    }
175
    String output = this.context.newProcess().runAndGetSingleOutput(IdeLogLevel.DEBUG, binaryPath.toString(), "--version");
14✔
176
    if (output == null) {
2!
177
      return null;
×
178
    }
179
    String version = output.trim();
3✔
180
    int lastSpace = version.lastIndexOf(' ');
4✔
181
    if (lastSpace >= 0) {
2!
182
      version = version.substring(lastSpace + 1);
6✔
183
    }
184
    if (!version.isEmpty() && Character.isDigit(version.charAt(0))) {
8!
185
      return VersionIdentifier.of(version);
3✔
186
    }
187
    LOG.debug("Could not parse version from output '{}' of {}.", output, binaryPath);
×
188
    return null;
×
189
  }
190

191
  @Override
192
  public ToolRepository getToolRepository() {
193

194
    return this.context.getPythonRepository();
4✔
195
  }
196

197
  /**
198
   * Creates a symlink from the "Scripts" folder to the "bin" folder on Windows systems. This is necessary for compatibility with tools that expect a "bin"
199
   * directory.
200
   *
201
   * @param fileAccess the {@link FileAccess} utility for file operations.
202
   * @param installationPath the path where Python is installed.
203
   */
204
  private void createWindowsSymlinkBinFolder(FileAccess fileAccess, Path installationPath) {
205

206
    if (!this.context.getSystemInfo().isWindows()) {
5!
207
      return;
1✔
208
    }
209
    Path scriptsPath = installationPath.resolve("Scripts");
×
210
    Path binPath = installationPath.resolve("bin");
×
211
    fileAccess.symlink(scriptsPath, binPath);
×
212
  }
×
213

214
  /**
215
   * Renames the ".venv" folder into the installation path (Python).
216
   *
217
   * @param fileAccess the {@link FileAccess} utility for file operations.
218
   * @param softwarePath the path where the software is installed.
219
   * @param installationPath the target path where the ".venv" folder should be moved.
220
   */
221
  private void renameVenvFolderToPython(FileAccess fileAccess, Path softwarePath, Path installationPath) {
222

223
    Path venvPath = softwarePath.resolve(VENV_FOLDER);
4✔
224
    fileAccess.move(venvPath, installationPath, StandardCopyOption.REPLACE_EXISTING);
10✔
225
  }
1✔
226

227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc