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

devonfw / IDEasy / 31463781565

11 Aug 2026 06:04AM UTC coverage: 72.772% (-0.1%) from 72.887%
31463781565

Pull #2287

github

web-flow
Merge f5142abf6 into 93b03e962
Pull Request #2287: #2284 Restore .ide.software.version from actual installed python version

5102 of 7767 branches covered (65.69%)

Branch coverage included in aggregate %.

13337 of 17571 relevant lines covered (75.9%)

3.23 hits per line

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

43.48
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.process.EnvironmentContext;
16
import com.devonfw.tools.ide.process.ProcessErrorHandling;
17
import com.devonfw.tools.ide.process.ProcessMode;
18
import com.devonfw.tools.ide.process.ProcessResult;
19
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
20
import com.devonfw.tools.ide.tool.ToolCommandlet;
21
import com.devonfw.tools.ide.tool.ToolInstallRequest;
22
import com.devonfw.tools.ide.tool.ToolInstallation;
23
import com.devonfw.tools.ide.tool.repository.ToolRepository;
24
import com.devonfw.tools.ide.tool.uv.Uv;
25
import com.devonfw.tools.ide.version.VersionIdentifier;
26

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

32
  private static final Logger LOG = LoggerFactory.getLogger(Python.class);
4✔
33

34
  private final VersionIdentifier PYTHON_MIN_VERSION = VersionIdentifier.of("3.8.2");
4✔
35

36
  /**
37
   * The constructor.
38
   *
39
   * @param context the {@link IdeContext}.
40
   */
41
  public Python(IdeContext context) {
42

43
    super(context, "python", Set.of(Tag.PYTHON));
6✔
44
  }
1✔
45

46
  @Override
47
  protected void performToolInstallation(ToolInstallRequest request, Path installationPath) {
48

49
    VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion();
4✔
50
    if (resolvedVersion.compareVersion(PYTHON_MIN_VERSION).isLess()) {
6!
51
      throw new CliException("Python version must be at least " + this.PYTHON_MIN_VERSION);
×
52
    }
53

54
    FileAccess fileAccess = this.context.getFileAccess();
4✔
55
    if (Files.exists(installationPath)) {
5!
56
      fileAccess.backup(installationPath);
×
57
    }
58
    Path softwarePath = installationPath.getParent();
3✔
59
    Uv uv = this.context.getCommandletManager().getCommandlet(Uv.class);
7✔
60

61
    uv.installPython(softwarePath, resolvedVersion, request.getProcessContext());
6✔
62
    renameVenvFolderToPython(fileAccess, softwarePath, installationPath);
5✔
63
    this.context.writeVersionFile(resolvedVersion, installationPath);
5✔
64
    createWindowsSymlinkBinFolder(fileAccess, installationPath);
4✔
65
    LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath);
18✔
66
  }
1✔
67

68
  @Override
69
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
70

71
    super.setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
72
    environmentContext.withEnvVar("VIRTUAL_ENV", toolInstallation.rootDir().toString());
7✔
73
    environmentContext.withEnvVar("UV_PROJECT_ENVIRONMENT", toolInstallation.rootDir().toString());
7✔
74
  }
1✔
75

76
  @Override
77
  protected boolean isIgnoreSoftwareRepo() {
78

79
    return true;
2✔
80
  }
81

82
  @Override
83
  protected boolean isIgnoreMissingSoftwareVersionFile() {
84

85
    // https://github.com/devonfw/IDEasy/issues/2190
86
    return true;
×
87
  }
88

89
  @Override
90
  protected VersionIdentifier getInstalledVersionWithMissingVersionFile(Path toolPath) {
91

92
    Path pythonExecutable = this.context.getSystemInfo().isWindows()
×
93
        ? toolPath.resolve("Scripts").resolve("python.exe")
×
94
        : toolPath.resolve("bin").resolve("python");
×
95

96
    if (!Files.exists(pythonExecutable)) {
×
97
      return null;
×
98
    }
99

100
    ProcessResult result = this.context.newProcess()
×
101
        .directory(toolPath)
×
102
        .errorHandling(ProcessErrorHandling.NONE)
×
103
        .executable(pythonExecutable)
×
104
        .addArgs("-c", "import sys; print(sys.version.split()[0])")
×
105
        .run(ProcessMode.DEFAULT_CAPTURE);
×
106

107
    if (!result.isSuccessful()) {
×
108
      return null;
×
109
    }
110

111
    String version = result.getOut().isEmpty() ? null : result.getOut().getFirst();
×
112
    if ((version == null) || version.isBlank()) {
×
113
      return null;
×
114
    }
115

116
    return VersionIdentifier.of(version.trim());
×
117
  }
118

119
  @Override
120
  public ToolRepository getToolRepository() {
121

122
    return this.context.getPythonRepository();
4✔
123
  }
124

125
  /**
126
   * 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"
127
   * directory.
128
   *
129
   * @param fileAccess the {@link FileAccess} utility for file operations.
130
   * @param installationPath the path where Python is installed.
131
   */
132
  private void createWindowsSymlinkBinFolder(FileAccess fileAccess, Path installationPath) {
133

134
    if (!this.context.getSystemInfo().isWindows()) {
5!
135
      return;
1✔
136
    }
137
    Path scriptsPath = installationPath.resolve("Scripts");
×
138
    Path binPath = installationPath.resolve("bin");
×
139
    fileAccess.symlink(scriptsPath, binPath);
×
140
  }
×
141

142
  /**
143
   * Renames the ".venv" folder into the installation path (Python).
144
   *
145
   * @param fileAccess the {@link FileAccess} utility for file operations.
146
   * @param softwarePath the path where the software is installed.
147
   * @param installationPath the target path where the ".venv" folder should be moved.
148
   */
149
  private void renameVenvFolderToPython(FileAccess fileAccess, Path softwarePath, Path installationPath) {
150

151
    Path venvPath = softwarePath.resolve(".venv");
4✔
152
    fileAccess.move(venvPath, installationPath, StandardCopyOption.REPLACE_EXISTING);
10✔
153
  }
1✔
154

155
}
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