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

devonfw / IDEasy / 39463035262

20 Oct 2026 05:05PM UTC coverage: 74.834% (+0.03%) from 74.804%
39463035262

push

github

web-flow
#2701: stop exporting UV_PROJECT_ENVIRONMENT to unbreak uv projects (#2710)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

5887 of 8735 branches covered (67.4%)

Branch coverage included in aggregate %.

15360 of 19657 relevant lines covered (78.14%)

3.36 hits per line

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

63.46
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.List;
7
import java.util.Set;
8

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

12
import com.devonfw.tools.ide.cli.CliException;
13
import com.devonfw.tools.ide.common.Tag;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.io.FileAccess;
16
import com.devonfw.tools.ide.log.IdeLogLevel;
17
import com.devonfw.tools.ide.process.EnvironmentContext;
18
import com.devonfw.tools.ide.process.ProcessContext;
19
import com.devonfw.tools.ide.process.ProcessErrorHandling;
20
import com.devonfw.tools.ide.process.ProcessMode;
21
import com.devonfw.tools.ide.process.ProcessResult;
22
import com.devonfw.tools.ide.tool.AbstractLocalToolCommandlet;
23
import com.devonfw.tools.ide.tool.ToolInstallRequest;
24
import com.devonfw.tools.ide.tool.ToolInstallation;
25
import com.devonfw.tools.ide.tool.repository.ToolRepository;
26
import com.devonfw.tools.ide.tool.uv.Uv;
27
import com.devonfw.tools.ide.version.VersionIdentifier;
28

29
/**
30
 * {@link AbstractToolCommandlet} for <a href="https://www.python.org/">python</a>.
31
 */
32
public class Python extends AbstractLocalToolCommandlet {
33

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

36
  private static final VersionIdentifier PYTHON_MIN_VERSION = VersionIdentifier.of("3.8.2");
4 ✔
37

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

41
  private static final String FILE_PYVENV_CFG = "pyvenv.cfg";
42

43
  private static final String PYVENV_CFG_VERSION_INFO = "version_info";
44

45
  /**
46
   * The constructor.
47
   *
48
   * @param context the {@link IdeContext}.
49
   */
50
  public Python(IdeContext context) {
51

52
    super(context, "python", Set.of(Tag.PYTHON));
6 ✔
53
  }
1 ✔
54

55
  @Override
56
  protected void performToolInstallation(ToolInstallRequest request, Path installationPath) {
57

58
    VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion();
4 ✔
59
    if (resolvedVersion.compareVersion(PYTHON_MIN_VERSION).isLess()) {
5 !
60
      throw new CliException("Python version must be at least " + this.PYTHON_MIN_VERSION);
×
61
    }
62

63
    FileAccess fileAccess = this.context.getFileAccess();
4 ✔
64
    if (Files.exists(installationPath)) {
5 !
65
      fileAccess.backup(installationPath);
×
66
    }
67
    Path softwarePath = installationPath.getParent();
3 ✔
68
    Path venvPath = softwarePath.resolve(VENV_FOLDER);
4 ✔
69

70
    fileAccess.delete(venvPath);
3 ✔
71

72
    Uv uv = this.context.getCommandletManager().getCommandlet(Uv.class);
7 ✔
73

74
    uv.installPython(softwarePath, resolvedVersion, request.getProcessContext());
6 ✔
75
    renameVenvFolderToPython(fileAccess, softwarePath, installationPath);
5 ✔
76
    this.context.writeVersionFile(resolvedVersion, installationPath);
5 ✔
77
    createWindowsSymlinkBinFolder(fileAccess, installationPath);
4 ✔
78
    LOG.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath);
18 ✔
79
  }
1 ✔
80

81
  @Override
82
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
83

84
    super.setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5 ✔
85
    environmentContext.withEnvVar("VIRTUAL_ENV", toolInstallation.rootDir().toString());
7 ✔
86
  }
1 ✔
87

88
  @Override
89
  protected boolean isIgnoreSoftwareRepo() {
90

91
    return true;
2 ✔
92
  }
93

94
  @Override
95
  protected boolean isIgnoreMissingSoftwareVersionFile() {
96

97
    // https://github.com/devonfw/IDEasy/issues/2190
98
    return true;
×
99
  }
100

101
  @Override
102
  protected VersionIdentifier computeInstalledVersionFromLocalSoftwareFolder() {
103

104
    Path toolPath = getToolPath();
3 ✔
105
    VersionIdentifier version = readVersionFromPyvenvCfg(toolPath);
4 ✔
106
    if (version == null) {
2 !
107
      version = readVersionFromInterpreter(toolPath);
4 ✔
108
    }
109
    if (version != null) {
2 ✔
110
      LOG.debug("Determined version {} of python from the installation at {}.", version, toolPath);
5 ✔
111
    }
112
    return version;
2 ✔
113
  }
114

115
  /**
116
   * @param installationPath the {@link Path} to the virtual environment.
117
   * @return the {@link VersionIdentifier} from the {@code version_info} entry of {@code pyvenv.cfg} or {@code null} if not available or not precise enough.
118
   */
119
  private VersionIdentifier readVersionFromPyvenvCfg(Path installationPath) {
120

121
    Path pyvenvCfg = installationPath.resolve(FILE_PYVENV_CFG);
4 ✔
122
    if (!Files.exists(pyvenvCfg)) {
5 !
123
      return null;
2 ✔
124
    }
125
    String content = this.context.getFileAccess().readFileContent(pyvenvCfg);
×
126
    for (String line : content.split("\\R")) {
×
127
      String[] keyAndValue = line.split("=", 2);
×
128
      if ((keyAndValue.length == 2) && keyAndValue[0].trim().equals(PYVENV_CFG_VERSION_INFO)) {
×
129
        String value = keyAndValue[1].trim();
×
130
        // uv only writes the minor version (e.g. "3.13") for its own interpreters what is too imprecise for us
131
        if (value.chars().filter(c -> c == '.').count() >= 2) {
×
132
          return VersionIdentifier.of(value);
×
133
        }
134
        LOG.debug("Ignoring imprecise version {} from {}.", value, pyvenvCfg);
×
135
      }
136
    }
137
    return null;
×
138
  }
139

140
  /**
141
   * @param installationPath the {@link Path} to the virtual environment.
142
   * @return the {@link VersionIdentifier} reported by the installed python interpreter or {@code null} if it could not be determined.
143
   */
144
  private VersionIdentifier readVersionFromInterpreter(Path installationPath) {
145

146
    Path binPath = this.context.getFileAccess().getBinPath(installationPath);
6 ✔
147
    Path binaryPath = this.context.getPath().findBinary(binPath.resolve(getBinaryName()));
9 ✔
148
    if (!Files.exists(binaryPath)) {
5 ✔
149
      LOG.debug("Python binary does not exist in {}.", binPath);
4 ✔
150
      return null;
2 ✔
151
    }
152
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.NONE).withPathEntry(binPath);
8 ✔
153
    ProcessResult result = runTool(pc, ProcessMode.DEFAULT_CAPTURE, List.of("--version"));
7 ✔
154
    String output = result.getSingleOutput(IdeLogLevel.DEBUG);
4 ✔
155
    if (output == null) {
2 !
156
      return null;
×
157
    }
158
    String version = output.trim();
3 ✔
159
    int lastSpace = version.lastIndexOf(' ');
4 ✔
160
    if (lastSpace >= 0) {
2 !
161
      version = version.substring(lastSpace + 1);
6 ✔
162
    }
163
    if (!version.isEmpty() && Character.isDigit(version.charAt(0))) {
8 !
164
      return VersionIdentifier.of(version);
3 ✔
165
    }
166
    LOG.debug("Could not parse version from output '{}' of {}.", output, binPath);
×
167
    return null;
×
168
  }
169

170
  @Override
171
  public ToolRepository getToolRepository() {
172

173
    return this.context.getPythonRepository();
4 ✔
174
  }
175

176
  /**
177
   * 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"
178
   * directory.
179
   *
180
   * @param fileAccess the {@link FileAccess} utility for file operations.
181
   * @param installationPath the path where Python is installed.
182
   */
183
  private void createWindowsSymlinkBinFolder(FileAccess fileAccess, Path installationPath) {
184

185
    if (!this.context.getSystemInfo().isWindows()) {
5 !
186
      return;
1 ✔
187
    }
188
    Path scriptsPath = installationPath.resolve("Scripts");
×
189
    Path binPath = installationPath.resolve("bin");
×
190
    fileAccess.symlink(scriptsPath, binPath);
×
191
  }
×
192

193
  /**
194
   * Renames the ".venv" folder into the installation path (Python).
195
   *
196
   * @param fileAccess the {@link FileAccess} utility for file operations.
197
   * @param softwarePath the path where the software is installed.
198
   * @param installationPath the target path where the ".venv" folder should be moved.
199
   */
200
  private void renameVenvFolderToPython(FileAccess fileAccess, Path softwarePath, Path installationPath) {
201

202
    Path venvPath = softwarePath.resolve(VENV_FOLDER);
4 ✔
203
    fileAccess.move(venvPath, installationPath, StandardCopyOption.REPLACE_EXISTING);
10 ✔
204
  }
1 ✔
205

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