Skip to content

Commit 0956830

Browse files
Strum355claude
andcommitted
refactor: move uv discovery logic from workspace.js to python_uv.js
Move discoverUvWorkspaceMembers and hasProjectMetadata to their provider file so workspace.js only retains generic scaffolding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f0718a2 commit 0956830

4 files changed

Lines changed: 99 additions & 72 deletions

File tree

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
1010
import { discoverMavenModules } from './providers/java_maven.js'
1111
import { discoverGradleSubprojects } from './providers/java_gradle.js'
1212
import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js'
13+
import { discoverUvWorkspaceMembers } from './providers/python_uv.js'
1314
import {
14-
discoverUvWorkspaceMembers,
1515
discoverWorkspaceCrates,
1616
discoverWorkspacePackages,
1717
filterManifestPathsByDiscoveryIgnore,

src/providers/python_uv.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33

4+
import fg from 'fast-glob'
45
import { parse as parseToml } from 'smol-toml'
56

67
import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js'
8+
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'
79

810
import Base_pyproject from './base_pyproject.js'
911
import { evaluateMarker } from './marker_evaluator.js'
@@ -177,3 +179,98 @@ export default class Python_uv extends Base_pyproject {
177179
return { directDeps, graph }
178180
}
179181
}
182+
183+
const DEFAULT_UV_DISCOVERY_IGNORE = [
184+
'**/__pycache__/**',
185+
'**/.venv/**',
186+
]
187+
188+
/**
189+
* Convert workspace glob patterns to manifest-file glob patterns,
190+
* correctly handling negation prefixes.
191+
*
192+
* @param {string[]} patterns - Workspace glob patterns (may include negations)
193+
* @param {string} manifestFileName - e.g. 'pyproject.toml'
194+
* @returns {string[]}
195+
*/
196+
function toManifestGlobPatterns(patterns, manifestFileName) {
197+
return patterns.map(p => {
198+
if (p.startsWith('!')) {
199+
return `!${p.slice(1)}/${manifestFileName}`
200+
}
201+
return `${p}/${manifestFileName}`
202+
})
203+
}
204+
205+
/**
206+
* Discover all pyproject.toml manifest paths in a uv workspace.
207+
* Parses `[tool.uv.workspace]` from root pyproject.toml and glob-expands member patterns.
208+
*
209+
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pyproject.toml and uv.lock)
210+
* @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}]
211+
* @returns {Promise<string[]>} Paths to pyproject.toml files (absolute)
212+
*/
213+
export async function discoverUvWorkspaceMembers(workspaceRoot, opts = {}) {
214+
const root = path.resolve(workspaceRoot)
215+
const rootPyproject = path.join(root, 'pyproject.toml')
216+
const uvLock = path.join(root, 'uv.lock')
217+
218+
if (!fs.existsSync(rootPyproject) || !fs.existsSync(uvLock)) {
219+
return []
220+
}
221+
222+
let parsed
223+
try {
224+
parsed = parseToml(fs.readFileSync(rootPyproject, 'utf-8'))
225+
} catch {
226+
return []
227+
}
228+
229+
const workspaceConfig = parsed?.tool?.uv?.workspace
230+
if (!workspaceConfig) {
231+
return []
232+
}
233+
234+
const memberPatterns = workspaceConfig.members
235+
if (!Array.isArray(memberPatterns) || memberPatterns.length === 0) {
236+
return []
237+
}
238+
239+
const excludePatterns = Array.isArray(workspaceConfig.exclude) ? workspaceConfig.exclude : []
240+
const excludeGlobs = excludePatterns
241+
.filter(p => typeof p === 'string' && p.trim())
242+
.map(p => `${p.trim()}/pyproject.toml`)
243+
244+
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_UV_DISCOVERY_IGNORE]
245+
const globOpts = {
246+
cwd: root,
247+
absolute: true,
248+
onlyFiles: true,
249+
ignore: [...ignorePatterns, ...excludeGlobs],
250+
followSymbolicLinks: false,
251+
}
252+
253+
const patterns = toManifestGlobPatterns(
254+
memberPatterns.filter(p => typeof p === 'string'),
255+
'pyproject.toml',
256+
)
257+
const manifestPaths = await fg(patterns, globOpts)
258+
259+
if (!manifestPaths.includes(rootPyproject) && hasProjectMetadata(parsed)) {
260+
manifestPaths.unshift(rootPyproject)
261+
}
262+
263+
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
264+
}
265+
266+
/**
267+
* @param {import('smol-toml').TomlTable} parsedPyProject
268+
* @returns {boolean}
269+
*/
270+
function hasProjectMetadata(parsedPyProject) {
271+
try {
272+
return typeof parsedPyProject?.project?.name === 'string' && parsedPyProject.project.name.trim() !== ''
273+
} catch {
274+
return false
275+
}
276+
}

src/workspace.js

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ import path from 'node:path'
44
import fg from 'fast-glob'
55
import { load as yamlLoad } from 'js-yaml'
66
import micromatch from 'micromatch'
7-
import { parse as parseToml } from 'smol-toml'
87

98
import { getCustom, getCustomPath, invokeCommand } from './tools.js'
109

1110
/** Default paths skipped during JS workspace discovery (merged with user patterns). */
1211
const DEFAULT_WORKSPACE_DISCOVERY_IGNORE = [
1312
'**/node_modules/**',
1413
'**/.git/**',
15-
'**/__pycache__/**',
16-
'**/.venv/**',
1714
]
1815

1916
/**
@@ -273,70 +270,3 @@ export async function discoverWorkspaceCrates(workspaceRoot, opts = {}) {
273270
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
274271
}
275272

276-
/**
277-
* Discover all pyproject.toml manifest paths in a uv workspace.
278-
* Parses `[tool.uv.workspace]` from root pyproject.toml and glob-expands member patterns.
279-
*
280-
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pyproject.toml and uv.lock)
281-
* @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}]
282-
* @returns {Promise<string[]>} Paths to pyproject.toml files (absolute)
283-
*/
284-
export async function discoverUvWorkspaceMembers(workspaceRoot, opts = {}) {
285-
const root = path.resolve(workspaceRoot)
286-
const rootPyproject = path.join(root, 'pyproject.toml')
287-
const uvLock = path.join(root, 'uv.lock')
288-
289-
if (!fs.existsSync(rootPyproject) || !fs.existsSync(uvLock)) {
290-
return []
291-
}
292-
293-
let parsed
294-
try {
295-
parsed = parseToml(fs.readFileSync(rootPyproject, 'utf-8'))
296-
} catch {
297-
return []
298-
}
299-
300-
const workspaceConfig = parsed?.tool?.uv?.workspace
301-
if (!workspaceConfig) {
302-
return []
303-
}
304-
305-
const memberPatterns = workspaceConfig.members
306-
if (!Array.isArray(memberPatterns) || memberPatterns.length === 0) {
307-
return []
308-
}
309-
310-
const excludePatterns = Array.isArray(workspaceConfig.exclude) ? workspaceConfig.exclude : []
311-
const excludeGlobs = excludePatterns
312-
.filter(p => typeof p === 'string' && p.trim())
313-
.map(p => `${p.trim()}/pyproject.toml`)
314-
315-
const ignorePatterns = resolveWorkspaceDiscoveryIgnore(opts)
316-
const globOpts = buildWorkspaceDiscoveryGlobOptions(root, [...ignorePatterns, ...excludeGlobs])
317-
318-
const patterns = toManifestGlobPatterns(
319-
memberPatterns.filter(p => typeof p === 'string'),
320-
'pyproject.toml',
321-
)
322-
const manifestPaths = await fg(patterns, globOpts)
323-
324-
if (!manifestPaths.includes(rootPyproject) && hasProjectMetadata(rootPyproject)) {
325-
manifestPaths.unshift(rootPyproject)
326-
}
327-
328-
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
329-
}
330-
331-
/**
332-
* @param {string} pyprojectPath
333-
* @returns {boolean}
334-
*/
335-
function hasProjectMetadata(pyprojectPath) {
336-
try {
337-
const content = parseToml(fs.readFileSync(pyprojectPath, 'utf-8'))
338-
return typeof content?.project?.name === 'string' && content.project.name.trim() !== ''
339-
} catch {
340-
return false
341-
}
342-
}

test/providers/workspace.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import esmock from 'esmock'
77
import { discoverGoWorkspaceModules } from '../../src/providers/golang_gomodules.js'
88
import { discoverGradleSubprojects } from '../../src/providers/java_gradle.js'
99
import { discoverMavenModules } from '../../src/providers/java_maven.js'
10+
import { discoverUvWorkspaceMembers } from '../../src/providers/python_uv.js'
1011
import {
11-
discoverUvWorkspaceMembers,
1212
discoverWorkspaceCrates,
1313
discoverWorkspacePackages,
1414
filterManifestPathsByDiscoveryIgnore,

0 commit comments

Comments
 (0)