From 9902cf4485cc3af777cc299c3cccad767814fe20 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 19 Mar 2024 15:53:59 +0100
Subject: [PATCH] fix(manager/pipenv): better artifacts handling of deprecated
 python (#28023)

---
 lib/modules/manager/pipenv/artifacts.spec.ts | 49 +++++++++++++++++++-
 lib/modules/manager/pipenv/artifacts.ts      | 39 ++++++++++++++--
 2 files changed, 82 insertions(+), 6 deletions(-)

diff --git a/lib/modules/manager/pipenv/artifacts.spec.ts b/lib/modules/manager/pipenv/artifacts.spec.ts
index ca5692fcfd..ae859276eb 100644
--- a/lib/modules/manager/pipenv/artifacts.spec.ts
+++ b/lib/modules/manager/pipenv/artifacts.spec.ts
@@ -73,6 +73,7 @@ describe('modules/manager/pipenv/artifacts', () => {
     // python
     getPkgReleases.mockResolvedValueOnce({
       releases: [
+        { version: '3.6.5' },
         { version: '3.7.6' },
         { version: '3.8.5' },
         { version: '3.9.1' },
@@ -263,7 +264,51 @@ describe('modules/manager/pipenv/artifacts', () => {
 
   it('supports install mode', async () => {
     GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
-    pipFileLock._meta!.requires!.python_full_version = '3.7.6';
+    pipFileLock._meta!.requires!.python_version = '3.6';
+    fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
+    fs.ensureCacheDir.mockResolvedValueOnce(pipCacheDir);
+    fs.ensureCacheDir.mockResolvedValueOnce(virtualenvsCacheDir);
+    fs.readLocalFile.mockResolvedValueOnce(JSON.stringify(pipFileLock));
+    // pipenv
+    datasource.getPkgReleases.mockResolvedValueOnce({
+      releases: [{ version: '2023.1.2' }],
+    });
+    const execSnapshots = mockExecAll();
+    git.getRepoStatus.mockResolvedValue(
+      partial<StatusResult>({
+        modified: ['Pipfile.lock'],
+      }),
+    );
+    fs.readLocalFile.mockResolvedValueOnce('new lock');
+
+    expect(
+      await updateArtifacts({
+        packageFileName: 'Pipfile',
+        updatedDeps: [],
+        newPackageFileContent: 'some new content',
+        config,
+      }),
+    ).not.toBeNull();
+
+    expect(execSnapshots).toMatchObject([
+      { cmd: 'install-tool python 3.6.5' },
+      { cmd: 'install-tool pipenv 2013.6.12' },
+      {
+        cmd: 'pipenv lock',
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            PIPENV_CACHE_DIR: pipenvCacheDir,
+            PIP_CACHE_DIR: pipCacheDir,
+            WORKON_HOME: virtualenvsCacheDir,
+          },
+        },
+      },
+    ]);
+  });
+
+  it('defaults to latest if no lock constraints', async () => {
+    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
     fs.ensureCacheDir.mockResolvedValueOnce(pipenvCacheDir);
     fs.ensureCacheDir.mockResolvedValueOnce(pipCacheDir);
     fs.ensureCacheDir.mockResolvedValueOnce(virtualenvsCacheDir);
@@ -290,7 +335,7 @@ describe('modules/manager/pipenv/artifacts', () => {
     ).not.toBeNull();
 
     expect(execSnapshots).toMatchObject([
-      { cmd: 'install-tool python 3.7.6' },
+      { cmd: 'install-tool python 3.10.2' },
       { cmd: 'install-tool pipenv 2013.6.12' },
       {
         cmd: 'pipenv lock',
diff --git a/lib/modules/manager/pipenv/artifacts.ts b/lib/modules/manager/pipenv/artifacts.ts
index 3b33eb2987..86a517ce7d 100644
--- a/lib/modules/manager/pipenv/artifacts.ts
+++ b/lib/modules/manager/pipenv/artifacts.ts
@@ -1,3 +1,5 @@
+import is from '@sindresorhus/is';
+import semver from 'semver';
 import { TEMPORARY_ERROR } from '../../../constants/error-messages';
 import { logger } from '../../../logger';
 import type { HostRule } from '../../../types';
@@ -38,14 +40,17 @@ export function getPythonConstraint(
       logger.warn({ error: result.error }, 'Invalid Pipfile.lock');
       return undefined;
     }
-    if (result.data._meta?.requires?.python_version) {
-      const pythonVersion = result.data._meta.requires.python_version;
-      return `== ${pythonVersion}.*`;
-    }
+    // Exact python version has been included since 2022.10.9. It is more specific than the major.minor version
+    // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#2022109-2022-10-09
     if (result.data._meta?.requires?.python_full_version) {
       const pythonFullVersion = result.data._meta.requires.python_full_version;
       return `== ${pythonFullVersion}`;
     }
+    // Before 2022.10.9, only the major.minor version was included
+    if (result.data._meta?.requires?.python_version) {
+      const pythonVersion = result.data._meta.requires.python_version;
+      return `== ${pythonVersion}.*`;
+    }
   } catch (err) {
     // Do nothing
   }
@@ -76,6 +81,32 @@ export function getPipenvConstraint(
     if (result.data.develop?.pipenv?.version) {
       return result.data.develop.pipenv.version;
     }
+    // Exact python version has been included since 2022.10.9
+    const pythonFullVersion = result.data._meta?.requires?.python_full_version;
+    if (is.string(pythonFullVersion) && semver.valid(pythonFullVersion)) {
+      // python_full_version was added after 3.6 was already deprecated, so it should be impossible to have a 3.6 version
+      // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#2022109-2022-10-09
+      if (semver.satisfies(pythonFullVersion, '3.7.*')) {
+        // Python 3.7 support was dropped in pipenv 2023.10.20
+        // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#20231020-2023-10-20
+        return '< 2023.10.20';
+      }
+      // Future deprecations will go here
+    }
+    // Before 2022.10.9, only the major.minor version was included
+    const pythonVersion = result.data._meta?.requires?.python_version;
+    if (pythonVersion) {
+      if (pythonVersion === '3.6') {
+        // Python 3.6 was deprecated in 2022.4.20
+        // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#2022420-2022-04-20
+        return '< 2022.4.20';
+      }
+      if (pythonVersion === '3.7') {
+        // Python 3.7 was deprecated in 2023.10.20 but we shouldn't reach here unless we are < 2022.10.9
+        // https://github.com/pypa/pipenv/blob/main/CHANGELOG.md#20231020-2023-10-20
+        return '< 2022.10.9';
+      }
+    }
   } catch (err) {
     // Do nothing
   }
-- 
GitLab