import toml from '@iarna/toml' ; import { RANGE_PATTERN } from '@renovatebot/pep440' ; import is from '@sindresorhus/is' ; import { PypiDatasource } from '../../datasource/pypi' ; import { logger } from '../../logger' ; import { SkipReason } from '../../types' ; import { localPathExists } from '../../util/fs' ; import { regEx } from '../../util/regex' ; import type { PackageDependency , PackageFile } from '../types' ; import type { PipFile } from './types' ; const packageRegex = regEx ( /^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$/i ) ; const rangePattern = RANGE_PATTERN ; const specifierPartPattern = `\\s*${ rangePattern . replace ( regEx ( /\?<\w+>/g ) , '?:' ) }\\s*` ; const specifierPattern = `${ specifierPartPattern }(?:,${ specifierPartPattern })*` ; const specifierRegex = regEx ( `^${ specifierPattern }$` ) ; function extractFromSection ( pipfile , section ) { if ( ! ( section in pipfile ) ) { return [ ] ; } const pipfileSection = pipfile [ section ] ; const deps = Object . entries ( pipfileSection ) . map ( ( x ) => { const [ depName , requirements ] = x ; let currentValue ; let nestedVersion ; let skipReason ; if ( requirements . git ) { skipReason = SkipReason . GitDependency ; } else if ( requirements . file ) { skipReason = SkipReason . FileDependency ; } else if ( requirements . path ) { skipReason = SkipReason . LocalDependency ; } else if ( requirements . version ) { currentValue = requirements . version ; nestedVersion = true ; } else if ( is . object ( requirements ) ) { skipReason = SkipReason . AnyVersion ; } else { currentValue = requirements ; } if ( currentValue === '*' ) { skipReason = SkipReason . AnyVersion ; } if ( ! skipReason ) { const packageMatches = packageRegex . exec ( depName ) ; if ( ! packageMatches ) { logger . debug ( `Skipping dependency with malformed package name "${ depName }".` ) ; skipReason = SkipReason . InvalidName ; } const specifierMatches = specifierRegex . exec ( currentValue ) ; if ( ! specifierMatches ) { logger . debug ( `Skipping dependency with malformed version specifier "${ currentValue }".` ) ; skipReason = SkipReason . InvalidVersion ; } } const dep = { depType : section , depName , managerData : { } , } ; if ( currentValue ) { dep . currentValue = currentValue ; } if ( skipReason ) { dep . skipReason = skipReason ; } else { dep . datasource = PypiDatasource . id ; } if ( nestedVersion ) { dep . managerData . nestedVersion = nestedVersion ; } if ( requirements . index ) { if ( is . array ( pipfile . source ) ) { const source = pipfile . source . find ( ( item ) => item . name === requirements . index ) ; if ( source ) { dep . registryUrls = [ source . url ] ; } } } return dep ; } ) . filter ( Boolean ) ; return deps ; } export async function extractPackageFile ( content , fileName ) { logger . debug ( 'pipenv.extractPackageFile()' ) ; let pipfile ; try { pipfile = toml . parse ( content ) as any ; } catch ( err ) { logger . debug ( { err } , 'Error parsing Pipfile' ) ; return null ; } const res = { deps : [ ] } ; if ( pipfile . source ) { res . registryUrls = pipfile . source . map ( ( source ) => source . url ) ; } res . deps = [ ... extractFromSection ( pipfile , 'packages' ) , ... extractFromSection ( pipfile , 'dev-packages' ) , ] ; if ( ! res . deps . length ) { return null ; } const constraints = { } ; if ( is . nonEmptyString ( pipfile . requires ?. python_version ) ) { constraints . python = `== ${ pipfile . requires . python_version }.*` ; } else if ( is . nonEmptyString ( pipfile . requires ?. python_full_version ) ) { constraints . python = `== ${ pipfile . requires . python_full_version }` ; } if ( is . nonEmptyString ( pipfile . packages ?. pipenv ) ) { constraints . pipenv = pipfile . packages . pipenv ; } else if ( is . nonEmptyString ( pipfile [ 'dev-packages' ] ?. pipenv ) ) { constraints . pipenv = pipfile [ 'dev-packages' ] . pipenv ; } const lockFileName = fileName + '.lock' ; if ( await localPathExists ( lockFileName ) ) { res . lockFiles = [ lockFileName ] ; } res . constraints = constraints ; return res ; }