From f2b6af88da4f1784f65a33febc7704250b64c72e Mon Sep 17 00:00:00 2001
From: Jamie Magee <jamie.magee@gmail.com>
Date: Tue, 3 Aug 2021 04:38:46 -0700
Subject: [PATCH] refactor(pypi): convert to class-based (#11064)

---
 lib/datasource/api.ts                         |   4 +-
 lib/datasource/metadata.spec.ts               |   8 +-
 lib/datasource/pypi/index.spec.ts             |   3 +-
 lib/datasource/pypi/index.ts                  | 434 ++++++++++--------
 lib/manager/pip_requirements/extract.ts       |   4 +-
 lib/manager/pip_setup/extract.ts              |   4 +-
 lib/manager/pipenv/extract.ts                 |   4 +-
 lib/manager/poetry/extract.ts                 |   4 +-
 lib/manager/setup-cfg/extract.ts              |   8 +-
 lib/workers/repository/init/vulnerability.ts  |   8 +-
 .../repository/process/lookup/index.spec.ts   |   4 +-
 11 files changed, 258 insertions(+), 227 deletions(-)

diff --git a/lib/datasource/api.ts b/lib/datasource/api.ts
index 9ad37f8fa7..b6c3d51137 100644
--- a/lib/datasource/api.ts
+++ b/lib/datasource/api.ts
@@ -23,7 +23,7 @@ import * as nuget from './nuget';
 import { OrbDatasource } from './orb';
 import * as packagist from './packagist';
 import * as pod from './pod';
-import * as pypi from './pypi';
+import { PypiDatasource } from './pypi';
 import * as repology from './repology';
 import { RubyVersionDatasource } from './ruby-version';
 import * as rubygems from './rubygems';
@@ -61,7 +61,7 @@ api.set('nuget', nuget);
 api.set('orb', new OrbDatasource());
 api.set('packagist', packagist);
 api.set('pod', pod);
-api.set('pypi', pypi);
+api.set('pypi', new PypiDatasource());
 api.set('repology', repology);
 api.set('ruby-version', new RubyVersionDatasource());
 api.set('rubygems', rubygems);
diff --git a/lib/datasource/metadata.spec.ts b/lib/datasource/metadata.spec.ts
index c630202485..76b1a1ee3b 100644
--- a/lib/datasource/metadata.spec.ts
+++ b/lib/datasource/metadata.spec.ts
@@ -2,7 +2,7 @@ import { getName } from '../../test/util';
 import * as datasourceMaven from './maven';
 import { addMetaData } from './metadata';
 import * as datasourceNpm from './npm';
-import * as datasourcePypi from './pypi';
+import { PypiDatasource } from './pypi';
 
 describe(getName(), () => {
   it('Should do nothing if dep is not specified', () => {
@@ -22,7 +22,7 @@ describe(getName(), () => {
       ],
     };
 
-    const datasource = datasourcePypi.id;
+    const datasource = PypiDatasource.id;
     const lookupName = 'django';
 
     addMetaData(dep, datasource, lookupName);
@@ -42,7 +42,7 @@ describe(getName(), () => {
       ],
     };
 
-    const datasource = datasourcePypi.id;
+    const datasource = PypiDatasource.id;
     const lookupName = 'mkdocs';
 
     addMetaData(dep, datasource, lookupName);
@@ -62,7 +62,7 @@ describe(getName(), () => {
         { version: '2.2.0', releaseTimestamp: '2019-07-16T18:29:00.000Z' },
       ],
     };
-    const datasource = datasourcePypi.id;
+    const datasource = PypiDatasource.id;
     const lookupName = 'django-filter';
 
     addMetaData(dep, datasource, lookupName);
diff --git a/lib/datasource/pypi/index.spec.ts b/lib/datasource/pypi/index.spec.ts
index c635b07517..4e06b1316e 100644
--- a/lib/datasource/pypi/index.spec.ts
+++ b/lib/datasource/pypi/index.spec.ts
@@ -2,7 +2,7 @@ import { getPkgReleases } from '..';
 import * as httpMock from '../../../test/http-mock';
 import { getName, loadFixture } from '../../../test/util';
 import * as hostRules from '../../util/host-rules';
-import { id as datasource } from '.';
+import { PypiDatasource } from '.';
 
 const res1: any = loadFixture('azure-cli-monitor.json');
 const res2: any = loadFixture('azure-cli-monitor-updated.json');
@@ -14,6 +14,7 @@ const dataRequiresPythonResponse = loadFixture(
 const mixedHyphensResponse = loadFixture('versions-html-mixed-hyphens.html');
 
 const baseUrl = 'https://pypi.org/pypi';
+const datasource = PypiDatasource.id;
 
 describe(getName(), () => {
   describe('getReleases', () => {
diff --git a/lib/datasource/pypi/index.ts b/lib/datasource/pypi/index.ts
index f68d0afe26..0ca3756a52 100644
--- a/lib/datasource/pypi/index.ts
+++ b/lib/datasource/pypi/index.ts
@@ -2,240 +2,266 @@ import url from 'url';
 import changelogFilenameRegex from 'changelog-filename-regex';
 import { logger } from '../../logger';
 import { parse } from '../../util/html';
-import { Http } from '../../util/http';
 import { ensureTrailingSlash } from '../../util/url';
 import * as pep440 from '../../versioning/pep440';
+import { Datasource } from '../datasource';
 import type { GetReleasesConfig, Release, ReleaseResult } from '../types';
 import type { PypiJSON, PypiJSONRelease, Releases } from './types';
 
-export const id = 'pypi';
-export const customRegistrySupport = true;
-export const defaultRegistryUrls = [
-  process.env.PIP_INDEX_URL || 'https://pypi.org/pypi/',
-];
-export const defaultVersioning = pep440.id;
-export const registryStrategy = 'merge';
-export const caching = true;
-
 const githubRepoPattern = /^https?:\/\/github\.com\/[^\\/]+\/[^\\/]+$/;
-const http = new Http(id);
 
-function normalizeName(input: string): string {
-  return input.toLowerCase().replace(/(-|\.)/g, '_');
-}
+export class PypiDatasource extends Datasource {
+  static readonly id = 'pypi';
 
-async function getDependency(
-  packageName: string,
-  hostUrl: string
-): Promise<ReleaseResult | null> {
-  const lookupUrl = url.resolve(hostUrl, `${packageName}/json`);
-  const dependency: ReleaseResult = { releases: null };
-  logger.trace({ lookupUrl }, 'Pypi api got lookup');
-  const rep = await http.getJson<PypiJSON>(lookupUrl);
-  const dep = rep?.body;
-  if (!dep) {
-    logger.trace({ dependency: packageName }, 'pip package not found');
-    return null;
-  }
-  if (rep.authorization) {
-    dependency.isPrivate = true;
-  }
-  logger.trace({ lookupUrl }, 'Got pypi api result');
-  if (
-    !(dep.info && normalizeName(dep.info.name) === normalizeName(packageName))
-  ) {
-    logger.warn(
-      { lookupUrl, lookupName: packageName, returnedName: dep.info.name },
-      'Returned name does not match with requested name'
-    );
-    return null;
+  constructor() {
+    super(PypiDatasource.id);
   }
 
-  if (dep.info?.home_page) {
-    dependency.homepage = dep.info.home_page;
-    if (githubRepoPattern.exec(dep.info.home_page)) {
-      dependency.sourceUrl = dep.info.home_page.replace('http://', 'https://');
-    }
-  }
+  override readonly caching = true;
 
-  if (dep.info?.project_urls) {
-    for (const [name, projectUrl] of Object.entries(dep.info.project_urls)) {
-      const lower = name.toLowerCase();
-
-      if (
-        !dependency.sourceUrl &&
-        (lower.startsWith('repo') ||
-          lower === 'code' ||
-          lower === 'source' ||
-          githubRepoPattern.exec(projectUrl))
-      ) {
-        dependency.sourceUrl = projectUrl;
-      }
+  override readonly customRegistrySupport = true;
 
-      if (
-        !dependency.changelogUrl &&
-        ([
-          'changelog',
-          'change log',
-          'changes',
-          'release notes',
-          'news',
-          "what's new",
-        ].includes(lower) ||
-          changelogFilenameRegex.exec(lower))
-      ) {
-        // from https://github.com/pypa/warehouse/blob/418c7511dc367fb410c71be139545d0134ccb0df/warehouse/templates/packaging/detail.html#L24
-        dependency.changelogUrl = projectUrl;
+  override readonly defaultRegistryUrls = [
+    process.env.PIP_INDEX_URL || 'https://pypi.org/pypi/',
+  ];
+
+  override readonly defaultVersioning = pep440.id;
+
+  override readonly registryStrategy = 'merge';
+
+  async getReleases({
+    lookupName,
+    registryUrl,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    let dependency: ReleaseResult = null;
+    const hostUrl = ensureTrailingSlash(registryUrl);
+
+    // not all simple indexes use this identifier, but most do
+    if (hostUrl.endsWith('/simple/') || hostUrl.endsWith('/+simple/')) {
+      logger.trace(
+        { lookupName, hostUrl },
+        'Looking up pypi simple dependency'
+      );
+      dependency = await this.getSimpleDependency(lookupName, hostUrl);
+    } else {
+      logger.trace({ lookupName, hostUrl }, 'Looking up pypi api dependency');
+      try {
+        // we need to resolve early here so we can catch any 404s and fallback to a simple lookup
+        dependency = await this.getDependency(lookupName, hostUrl);
+      } catch (err) {
+        if (err.statusCode !== 404) {
+          throw err;
+        }
+
+        // error contacting json-style api -- attempt to fallback to a simple-style api
+        logger.trace(
+          { lookupName, hostUrl },
+          'Looking up pypi simple dependency via fallback'
+        );
+        dependency = await this.getSimpleDependency(lookupName, hostUrl);
       }
     }
+    return dependency;
   }
 
-  dependency.releases = [];
-  if (dep.releases) {
-    const versions = Object.keys(dep.releases);
-    dependency.releases = versions.map((version) => {
-      const releases = dep.releases[version] || [];
-      const { upload_time: releaseTimestamp } = releases[0] || {};
-      const isDeprecated = releases.some(({ yanked }) => yanked);
-      const result: Release = {
-        version,
-        releaseTimestamp,
-      };
-      if (isDeprecated) {
-        result.isDeprecated = isDeprecated;
-      }
-      // There may be multiple releases with different requires_python, so we return all in an array
-      result.constraints = {
-        python: releases.map(({ requires_python }) => requires_python),
-      };
-      return result;
-    });
+  private static normalizeName(input: string): string {
+    return input.toLowerCase().replace(/(-|\.)/g, '_');
   }
-  return dependency;
-}
 
-function extractVersionFromLinkText(
-  text: string,
-  packageName: string
-): string | null {
-  const srcPrefixes = [`${packageName}-`, `${packageName.replace(/-/g, '_')}-`];
-  for (const prefix of srcPrefixes) {
-    const suffix = '.tar.gz';
-    if (text.startsWith(prefix) && text.endsWith(suffix)) {
-      return text.replace(prefix, '').replace(/\.tar\.gz$/, '');
+  private async getDependency(
+    packageName: string,
+    hostUrl: string
+  ): Promise<ReleaseResult | null> {
+    const lookupUrl = url.resolve(hostUrl, `${packageName}/json`);
+    const dependency: ReleaseResult = { releases: null };
+    logger.trace({ lookupUrl }, 'Pypi api got lookup');
+    const rep = await this.http.getJson<PypiJSON>(lookupUrl);
+    const dep = rep?.body;
+    if (!dep) {
+      logger.trace({ dependency: packageName }, 'pip package not found');
+      return null;
+    }
+    if (rep.authorization) {
+      dependency.isPrivate = true;
+    }
+    logger.trace({ lookupUrl }, 'Got pypi api result');
+    if (
+      !(
+        dep.info &&
+        PypiDatasource.normalizeName(dep.info.name) ===
+          PypiDatasource.normalizeName(packageName)
+      )
+    ) {
+      logger.warn(
+        { lookupUrl, lookupName: packageName, returnedName: dep.info.name },
+        'Returned name does not match with requested name'
+      );
+      return null;
     }
-  }
 
-  // pep-0427 wheel packages
-  //  {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl.
-  const wheelPrefix = packageName.replace(/[^\w\d.]+/g, '_') + '-';
-  const wheelSuffix = '.whl';
-  if (
-    text.startsWith(wheelPrefix) &&
-    text.endsWith(wheelSuffix) &&
-    text.split('-').length > 2
-  ) {
-    return text.split('-')[1];
-  }
+    if (dep.info?.home_page) {
+      dependency.homepage = dep.info.home_page;
+      if (githubRepoPattern.exec(dep.info.home_page)) {
+        dependency.sourceUrl = dep.info.home_page.replace(
+          'http://',
+          'https://'
+        );
+      }
+    }
 
-  return null;
-}
+    if (dep.info?.project_urls) {
+      for (const [name, projectUrl] of Object.entries(dep.info.project_urls)) {
+        const lower = name.toLowerCase();
 
-function cleanSimpleHtml(html: string): string {
-  return (
-    html
-      .replace(/<\/?pre>/, '')
-      // Certain simple repositories like artifactory don't escape > and <
-      .replace(
-        /data-requires-python="([^"]*?)>([^"]*?)"/g,
-        'data-requires-python="$1&gt;$2"'
-      )
-      .replace(
-        /data-requires-python="([^"]*?)<([^"]*?)"/g,
-        'data-requires-python="$1&lt;$2"'
-      )
-  );
-}
+        if (
+          !dependency.sourceUrl &&
+          (lower.startsWith('repo') ||
+            lower === 'code' ||
+            lower === 'source' ||
+            githubRepoPattern.exec(projectUrl))
+        ) {
+          dependency.sourceUrl = projectUrl;
+        }
 
-async function getSimpleDependency(
-  packageName: string,
-  hostUrl: string
-): Promise<ReleaseResult | null> {
-  const lookupUrl = url.resolve(hostUrl, ensureTrailingSlash(packageName));
-  const dependency: ReleaseResult = { releases: null };
-  const response = await http.get(lookupUrl);
-  const dep = response?.body;
-  if (!dep) {
-    logger.trace({ dependency: packageName }, 'pip package not found');
-    return null;
-  }
-  if (response.authorization) {
-    dependency.isPrivate = true;
-  }
-  const root = parse(cleanSimpleHtml(dep));
-  const links = root.querySelectorAll('a');
-  const releases: Releases = {};
-  for (const link of Array.from(links)) {
-    const version = extractVersionFromLinkText(link.text, packageName);
-    if (version) {
-      const release: PypiJSONRelease = {
-        yanked: link.hasAttribute('data-yanked'),
-      };
-      const requiresPython = link.getAttribute('data-requires-python');
-      if (requiresPython) {
-        release.requires_python = requiresPython;
-      }
-      if (!releases[version]) {
-        releases[version] = [];
+        if (
+          !dependency.changelogUrl &&
+          ([
+            'changelog',
+            'change log',
+            'changes',
+            'release notes',
+            'news',
+            "what's new",
+          ].includes(lower) ||
+            changelogFilenameRegex.exec(lower))
+        ) {
+          // from https://github.com/pypa/warehouse/blob/418c7511dc367fb410c71be139545d0134ccb0df/warehouse/templates/packaging/detail.html#L24
+          dependency.changelogUrl = projectUrl;
+        }
       }
-      releases[version].push(release);
     }
-  }
-  const versions = Object.keys(releases);
-  dependency.releases = versions.map((version) => {
-    const versionReleases = releases[version] || [];
-    const isDeprecated = versionReleases.some(({ yanked }) => yanked);
-    const result: Release = { version };
-    if (isDeprecated) {
-      result.isDeprecated = isDeprecated;
+
+    dependency.releases = [];
+    if (dep.releases) {
+      const versions = Object.keys(dep.releases);
+      dependency.releases = versions.map((version) => {
+        const releases = dep.releases[version] || [];
+        const { upload_time: releaseTimestamp } = releases[0] || {};
+        const isDeprecated = releases.some(({ yanked }) => yanked);
+        const result: Release = {
+          version,
+          releaseTimestamp,
+        };
+        if (isDeprecated) {
+          result.isDeprecated = isDeprecated;
+        }
+        // There may be multiple releases with different requires_python, so we return all in an array
+        result.constraints = {
+          python: releases.map(({ requires_python }) => requires_python),
+        };
+        return result;
+      });
     }
-    // There may be multiple releases with different requires_python, so we return all in an array
-    result.constraints = {
-      python: versionReleases.map(({ requires_python }) => requires_python),
-    };
-    return result;
-  });
-  return dependency;
-}
+    return dependency;
+  }
 
-export async function getReleases({
-  lookupName,
-  registryUrl,
-}: GetReleasesConfig): Promise<ReleaseResult | null> {
-  let dependency: ReleaseResult = null;
-  const hostUrl = ensureTrailingSlash(registryUrl);
-
-  // not all simple indexes use this identifier, but most do
-  if (hostUrl.endsWith('/simple/') || hostUrl.endsWith('/+simple/')) {
-    logger.trace({ lookupName, hostUrl }, 'Looking up pypi simple dependency');
-    dependency = await getSimpleDependency(lookupName, hostUrl);
-  } else {
-    logger.trace({ lookupName, hostUrl }, 'Looking up pypi api dependency');
-    try {
-      // we need to resolve early here so we can catch any 404s and fallback to a simple lookup
-      dependency = await getDependency(lookupName, hostUrl);
-    } catch (err) {
-      if (err.statusCode !== 404) {
-        throw err;
+  private static extractVersionFromLinkText(
+    text: string,
+    packageName: string
+  ): string | null {
+    const srcPrefixes = [
+      `${packageName}-`,
+      `${packageName.replace(/-/g, '_')}-`,
+    ];
+    for (const prefix of srcPrefixes) {
+      const suffix = '.tar.gz';
+      if (text.startsWith(prefix) && text.endsWith(suffix)) {
+        return text.replace(prefix, '').replace(/\.tar\.gz$/, '');
       }
+    }
 
-      // error contacting json-style api -- attempt to fallback to a simple-style api
-      logger.trace(
-        { lookupName, hostUrl },
-        'Looking up pypi simple dependency via fallback'
+    // pep-0427 wheel packages
+    //  {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl.
+    const wheelPrefix = packageName.replace(/[^\w\d.]+/g, '_') + '-';
+    const wheelSuffix = '.whl';
+    if (
+      text.startsWith(wheelPrefix) &&
+      text.endsWith(wheelSuffix) &&
+      text.split('-').length > 2
+    ) {
+      return text.split('-')[1];
+    }
+
+    return null;
+  }
+
+  private static cleanSimpleHtml(html: string): string {
+    return (
+      html
+        .replace(/<\/?pre>/, '')
+        // Certain simple repositories like artifactory don't escape > and <
+        .replace(
+          /data-requires-python="([^"]*?)>([^"]*?)"/g,
+          'data-requires-python="$1&gt;$2"'
+        )
+        .replace(
+          /data-requires-python="([^"]*?)<([^"]*?)"/g,
+          'data-requires-python="$1&lt;$2"'
+        )
+    );
+  }
+
+  private async getSimpleDependency(
+    packageName: string,
+    hostUrl: string
+  ): Promise<ReleaseResult | null> {
+    const lookupUrl = url.resolve(hostUrl, ensureTrailingSlash(packageName));
+    const dependency: ReleaseResult = { releases: null };
+    const response = await this.http.get(lookupUrl);
+    const dep = response?.body;
+    if (!dep) {
+      logger.trace({ dependency: packageName }, 'pip package not found');
+      return null;
+    }
+    if (response.authorization) {
+      dependency.isPrivate = true;
+    }
+    const root = parse(PypiDatasource.cleanSimpleHtml(dep));
+    const links = root.querySelectorAll('a');
+    const releases: Releases = {};
+    for (const link of Array.from(links)) {
+      const version = PypiDatasource.extractVersionFromLinkText(
+        link.text,
+        packageName
       );
-      dependency = await getSimpleDependency(lookupName, hostUrl);
+      if (version) {
+        const release: PypiJSONRelease = {
+          yanked: link.hasAttribute('data-yanked'),
+        };
+        const requiresPython = link.getAttribute('data-requires-python');
+        if (requiresPython) {
+          release.requires_python = requiresPython;
+        }
+        if (!releases[version]) {
+          releases[version] = [];
+        }
+        releases[version].push(release);
+      }
     }
+    const versions = Object.keys(releases);
+    dependency.releases = versions.map((version) => {
+      const versionReleases = releases[version] || [];
+      const isDeprecated = versionReleases.some(({ yanked }) => yanked);
+      const result: Release = { version };
+      if (isDeprecated) {
+        result.isDeprecated = isDeprecated;
+      }
+      // There may be multiple releases with different requires_python, so we return all in an array
+      result.constraints = {
+        python: versionReleases.map(({ requires_python }) => requires_python),
+      };
+      return result;
+    });
+    return dependency;
   }
-  return dependency;
 }
diff --git a/lib/manager/pip_requirements/extract.ts b/lib/manager/pip_requirements/extract.ts
index f5e357bbea..d225a7790a 100644
--- a/lib/manager/pip_requirements/extract.ts
+++ b/lib/manager/pip_requirements/extract.ts
@@ -1,7 +1,7 @@
 // based on https://www.python.org/dev/peps/pep-0508/#names
 import { RANGE_PATTERN } from '@renovate/pep440/lib/specifier';
 import { getAdminConfig } from '../../config/admin';
-import * as datasourcePypi from '../../datasource/pypi';
+import { PypiDatasource } from '../../datasource/pypi';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
 import { isSkipComment } from '../../util/ignore';
@@ -69,7 +69,7 @@ export function extractPackageFile(
         ...dep,
         depName,
         currentValue,
-        datasource: datasourcePypi.id,
+        datasource: PypiDatasource.id,
       };
       if (currentValue?.startsWith('==')) {
         dep.currentVersion = currentValue.replace(/^==\s*/, '');
diff --git a/lib/manager/pip_setup/extract.ts b/lib/manager/pip_setup/extract.ts
index 762ff892c0..ddcfb0c111 100644
--- a/lib/manager/pip_setup/extract.ts
+++ b/lib/manager/pip_setup/extract.ts
@@ -1,5 +1,5 @@
 import { getAdminConfig } from '../../config/admin';
-import * as datasourcePypi from '../../datasource/pypi';
+import { PypiDatasource } from '../../datasource/pypi';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
 import { exec } from '../../util/exec';
@@ -122,7 +122,7 @@ export async function extractPackageFile(
         depName,
         currentValue,
         managerData: { lineNumber },
-        datasource: datasourcePypi.id,
+        datasource: PypiDatasource.id,
       };
       return dep;
     })
diff --git a/lib/manager/pipenv/extract.ts b/lib/manager/pipenv/extract.ts
index ddd9107ded..9c6cbe5361 100644
--- a/lib/manager/pipenv/extract.ts
+++ b/lib/manager/pipenv/extract.ts
@@ -1,7 +1,7 @@
 import toml from '@iarna/toml';
 import { RANGE_PATTERN } from '@renovate/pep440/lib/specifier';
 import is from '@sindresorhus/is';
-import * as datasourcePypi from '../../datasource/pypi';
+import { PypiDatasource } from '../../datasource/pypi';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
 import { localPathExists } from '../../util/fs';
@@ -77,7 +77,7 @@ function extractFromSection(
       if (skipReason) {
         dep.skipReason = skipReason;
       } else {
-        dep.datasource = datasourcePypi.id;
+        dep.datasource = PypiDatasource.id;
       }
       if (nestedVersion) {
         dep.managerData.nestedVersion = nestedVersion;
diff --git a/lib/manager/poetry/extract.ts b/lib/manager/poetry/extract.ts
index d57ad2c9c5..b957234a62 100644
--- a/lib/manager/poetry/extract.ts
+++ b/lib/manager/poetry/extract.ts
@@ -1,6 +1,6 @@
 import { parse } from '@iarna/toml';
 import is from '@sindresorhus/is';
-import * as datasourcePypi from '../../datasource/pypi';
+import { PypiDatasource } from '../../datasource/pypi';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
 import {
@@ -65,7 +65,7 @@ function extractFromSection(
       depType: section,
       currentValue: currentValue as string,
       managerData: { nestedVersion },
-      datasource: datasourcePypi.id,
+      datasource: PypiDatasource.id,
     };
     if (dep.depName in poetryLockfile) {
       dep.lockedVersion = poetryLockfile[dep.depName].version;
diff --git a/lib/manager/setup-cfg/extract.ts b/lib/manager/setup-cfg/extract.ts
index 6db1579443..3437f0efbe 100644
--- a/lib/manager/setup-cfg/extract.ts
+++ b/lib/manager/setup-cfg/extract.ts
@@ -1,4 +1,4 @@
-import { id as datasource } from '../../datasource/pypi';
+import { PypiDatasource } from '../../datasource/pypi';
 import pep440 from '../../versioning/pep440';
 import type { PackageDependency, PackageFile, Result } from '../types';
 
@@ -41,7 +41,11 @@ function parseDep(
     currentValue &&
     pep440.isValid(currentValue)
   ) {
-    const dep: PackageDependency = { datasource, depName, currentValue };
+    const dep: PackageDependency = {
+      datasource: PypiDatasource.id,
+      depName,
+      currentValue,
+    };
     const depType = getDepType(section, record);
     if (depType) {
       dep.depType = depType;
diff --git a/lib/workers/repository/init/vulnerability.ts b/lib/workers/repository/init/vulnerability.ts
index ba0bdab0c6..4bd4a5e871 100644
--- a/lib/workers/repository/init/vulnerability.ts
+++ b/lib/workers/repository/init/vulnerability.ts
@@ -3,7 +3,7 @@ import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages';
 import * as datasourceMaven from '../../../datasource/maven';
 import { id as npmId } from '../../../datasource/npm';
 import * as datasourceNuget from '../../../datasource/nuget';
-import * as datasourcePypi from '../../../datasource/pypi';
+import { PypiDatasource } from '../../../datasource/pypi';
 import * as datasourceRubygems from '../../../datasource/rubygems';
 import { logger } from '../../../logger';
 import { platform } from '../../../platform';
@@ -89,7 +89,7 @@ export async function detectVulnerabilityAlerts(
         MAVEN: datasourceMaven.id,
         NPM: npmId,
         NUGET: datasourceNuget.id,
-        PIP: datasourcePypi.id,
+        PIP: PypiDatasource.id,
         RUBYGEMS: datasourceRubygems.id,
       };
       const datasource =
@@ -109,7 +109,7 @@ export async function detectVulnerabilityAlerts(
           vulnerableRequirements = `< ${firstPatchedVersion}`;
         }
       }
-      if (datasource === datasourcePypi.id) {
+      if (datasource === PypiDatasource.id) {
         vulnerableRequirements = vulnerableRequirements.replace(/^= /, '== ');
       }
       combinedAlerts[fileName] ||= {};
@@ -179,7 +179,7 @@ export async function detectVulnerabilityAlerts(
             logger.warn({ err }, 'Error generating vulnerability PR notes');
           }
           const allowedVersions =
-            datasource === datasourcePypi.id
+            datasource === PypiDatasource.id
               ? `==${val.firstPatchedVersion}`
               : val.firstPatchedVersion;
           let matchRule: PackageRule = {
diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts
index d1839fba20..23a0b92142 100644
--- a/lib/workers/repository/process/lookup/index.spec.ts
+++ b/lib/workers/repository/process/lookup/index.spec.ts
@@ -15,7 +15,7 @@ import * as datasourceGithubReleases from '../../../../datasource/github-release
 import { id as datasourceGithubTagsId } from '../../../../datasource/github-tags';
 import { id as datasourceNpmId } from '../../../../datasource/npm';
 import { id as datasourcePackagistId } from '../../../../datasource/packagist';
-import { id as datasourcePypiId } from '../../../../datasource/pypi';
+import { PypiDatasource } from '../../../../datasource/pypi';
 import { id as dockerVersioningId } from '../../../../versioning/docker';
 import { id as gitVersioningId } from '../../../../versioning/git';
 import { id as npmVersioningId } from '../../../../versioning/npm';
@@ -1011,7 +1011,7 @@ describe(getName(), () => {
     });
     it('handles pypi 404', async () => {
       config.depName = 'foo';
-      config.datasource = datasourcePypiId;
+      config.datasource = PypiDatasource.id;
       config.packageFile = 'requirements.txt';
       config.currentValue = '1.0.0';
       httpMock
-- 
GitLab