import { isNonEmptyString, isNullOrUndefined } from '@sindresorhus/is'; import { logger } from '../../../logger/index.ts'; import { regEx } from '../../../util/regex.ts'; import { normalizePythonDepName } from '../../datasource/pypi/common.ts'; import { PypiDatasource } from '../../datasource/pypi/index.ts'; import type { PackageDependency } from '../types.ts'; import type { Pep508ParseResult } from './types.ts'; const pep508Regex = regEx( /^(?[A-Z0-9._-]+)\s*(?:\[(?[A-Z0-9\s,._-]+)\])?\s*(?[^;]+)?(?:;\s*(?.*))?/i, ); export const depTypes = { dependencies: 'project.dependencies', optionalDependencies: 'project.optional-dependencies', dependencyGroups: 'dependency-groups', pdmDevDependencies: 'tool.pdm.dev-dependencies', uvDevDependencies: 'tool.uv.dev-dependencies', uvSources: 'tool.uv.sources', buildSystemRequires: 'build-system.requires', }; export function parsePEP508( value: string | null | undefined, ): Pep508ParseResult | null { if (isNullOrUndefined(value)) { return null; } const regExpExec = pep508Regex.exec(value); if (isNullOrUndefined(regExpExec) || isNullOrUndefined(regExpExec?.groups)) { logger.trace(`Pep508 could not be extracted`); return null; } const result: Pep508ParseResult = { packageName: regExpExec.groups.packageName, }; if (isNonEmptyString(regExpExec.groups.currentValue)) { if ( regExpExec.groups.currentValue.startsWith('(') && regExpExec.groups.currentValue.endsWith(')') ) { result.currentValue = regExpExec.groups.currentValue.slice(1, -1).trim(); } else { result.currentValue = regExpExec.groups.currentValue; } } if (isNonEmptyString(regExpExec.groups.marker)) { result.marker = regExpExec.groups.marker; } if (isNonEmptyString(regExpExec.groups.extras)) { // trim to remove allowed whitespace between brackets result.extras = regExpExec.groups.extras.split(',').map((e) => e.trim()); } return result; } export function pep508ToPackageDependency( depType: string, value: string, ): PackageDependency | null { const parsed = parsePEP508(value); if (isNullOrUndefined(parsed)) { return null; } const dep: PackageDependency = { packageName: normalizePythonDepName(parsed.packageName), depName: parsed.packageName, datasource: PypiDatasource.id, depType, }; if (isNullOrUndefined(parsed.currentValue)) { dep.skipReason = 'unspecified-version'; } else { dep.currentValue = parsed.currentValue; if (parsed.currentValue.startsWith('==')) { dep.currentVersion = parsed.currentValue.replace(regEx(/^==\s*/), ''); } } return dep; }