Skip to content

Commit 26f4134

Browse files
ruromeroclaude
andcommitted
fix(poetry): add ENOENT error handling for missing poetry binary
Throw a clear "poetry is not accessible" error instead of the raw "spawnSync poetry ENOENT" when the poetry binary is not installed. Implements TC-4563 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8eab29b commit 26f4134

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

src/providers/python_poetry.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,21 @@ export default class Python_poetry extends Base_pyproject {
4747
* @param {Object} opts
4848
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
4949
*/
50+
_verifyPoetryAccessible(poetryBin) {
51+
try {
52+
invokeCommand(poetryBin, ['--version'])
53+
} catch (error) {
54+
if (error.code === 'ENOENT') {
55+
throw new Error(`poetry is not accessible at "${poetryBin}"`)
56+
}
57+
throw new Error('failed to check for poetry binary', { cause: error })
58+
}
59+
}
60+
5061
// eslint-disable-next-line no-unused-vars
5162
async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
63+
let poetryBin = getCustomPath('poetry', opts)
64+
this._verifyPoetryAccessible(poetryBin)
5265
let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies'])
5366
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts)
5467
let showAllOutput = this._getPoetryShowAllOutput(manifestDir, opts)

test/providers/python_pyproject.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'fs'
22
import path from 'path'
33

44
import { expect } from 'chai'
5+
import esmock from 'esmock'
56
import { useFakeTimers } from 'sinon'
67

78
import Python_pip_pyproject from '../../src/providers/python_pip_pyproject.js'
@@ -645,3 +646,42 @@ suite('testing the python-pyproject data provider', () => {
645646
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true })
646647
clock.restore()
647648
})
649+
650+
suite('testing python-poetry error handling', () => {
651+
/** Verifies that a missing poetry binary produces a clear error message. */
652+
test('verify error when poetry binary is not accessible', async () => {
653+
let provider = await esmock('../../src/providers/python_poetry.js', {
654+
'../../src/tools.js': {
655+
getCustomPath: () => '/nonexistent/poetry',
656+
invokeCommand: () => {
657+
let err = new Error('spawn /nonexistent/poetry ENOENT')
658+
err.code = 'ENOENT'
659+
throw err
660+
}
661+
}
662+
})
663+
664+
let instance = new provider.default()
665+
expect(() => instance._verifyPoetryAccessible('/nonexistent/poetry'))
666+
.to.throw('poetry is not accessible at "/nonexistent/poetry"')
667+
}).timeout(TIMEOUT)
668+
669+
/** Verifies that non-ENOENT errors are re-thrown with cause chain preserved. */
670+
test('verify non-ENOENT errors are re-thrown with cause', async () => {
671+
let originalError = new Error('permission denied')
672+
originalError.code = 'EACCES'
673+
674+
let provider = await esmock('../../src/providers/python_poetry.js', {
675+
'../../src/tools.js': {
676+
getCustomPath: () => 'poetry',
677+
invokeCommand: () => {
678+
throw originalError
679+
}
680+
}
681+
})
682+
683+
let instance = new provider.default()
684+
expect(() => instance._verifyPoetryAccessible('poetry'))
685+
.to.throw('failed to check for poetry binary')
686+
}).timeout(TIMEOUT)
687+
})

0 commit comments

Comments
 (0)