Skip to content

Commit d4a23d1

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 c62aadf commit d4a23d1

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

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)