fix(notices): derive first-party Python projects

This commit is contained in:
Tianyi Cui
2026-07-31 17:16:37 +08:00
parent 76e9bcbba0
commit c7a04a10f9
2 changed files with 65 additions and 17 deletions
+13 -1
View File
@@ -1,7 +1,7 @@
import { readdirSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps } from './gen-third-party-notices.ts'
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps } from './gen-third-party-notices.ts'
const root = resolve(import.meta.dirname, '..')
@@ -157,6 +157,18 @@ describe('parsePyprojectRequirements', () => {
})
})
describe('collectPythonDependencies', () => {
it('excludes normalized local project names without exempting a third-party prefix', () => {
const pyprojects = [
'[project]\nname = "deepseek-harness-runtime-bin"\ndependencies = ["pydantic"]\n',
'[project]\nname = "deepseek-harness"\ndependencies = ["DeepSeek.Harness_Runtime-Bin", "deepseek-unrelated"]\n',
]
expect(() => collectPythonDependencies(pyprojects)).toThrow(
'python dependency deepseek-unrelated is missing from PYTHON_METADATA',
)
})
})
describe('isPermissive', () => {
it('accepts the licenses this project ships and rejects copyleft or unknown ones', () => {
expect(['MIT', 'ISC', 'BSD-3-Clause', 'Apache-2.0', 'MIT / Apache-2.0', '(MIT OR CC0-1.0)'].every(isPermissive)).toBe(true)
+52 -16
View File
@@ -75,6 +75,8 @@ const PYTHON_METADATA: Record<string, { license: string; repo: string; role: str
pytest: { license: 'MIT', repo: 'https://github.com/pytest-dev/pytest', role: 'test-only' },
}
type PythonMetadata = typeof PYTHON_METADATA
/** Tools fetched by scripts at build time, keyed by the pin the script owns. */
const BUILD_TIME_TOOLS = [
{
@@ -332,19 +334,24 @@ function optionalTomlTable(value: TomlValueWithoutBigInt | undefined, location:
}
/**
* Every requirement name a `pyproject.toml` declares: `requires` under
* Parse a `pyproject.toml` project identity and every requirement it declares:
* `requires` under
* `[build-system]`, `dependencies` under `[project]`, and every key under
* `[project.optional-dependencies]` and `[dependency-groups]`. A TOML parser
* owns comments, quoted keys, escapes, and array boundaries; unsupported
* requirement shapes fail instead of disappearing from the notices.
* @param text - the complete `pyproject.toml` contents.
* @returns each declared requirement's distribution name, in file order.
* @returns the local project name and declared requirement names.
*/
export function parsePyprojectRequirements(text: string): string[] {
function parsePyproject(text: string): { projectName?: string; requirements: string[] } {
const names: string[] = []
const document = parseToml(text, { integersAsBigInt: false })
const buildSystem = optionalTomlTable(document['build-system'], '[build-system]')
const project = optionalTomlTable(document.project, '[project]')
const projectName = project?.name
if (projectName !== undefined && typeof projectName !== 'string') {
throw new Error('gen-third-party-notices: [project].name must be a string.')
}
collectPythonRequirementArray(names, buildSystem?.requires, '[build-system].requires')
collectPythonRequirementArray(names, project?.dependencies, '[project].dependencies')
@@ -357,25 +364,54 @@ export function parsePyprojectRequirements(text: string): string[] {
for (const [group, requirements] of Object.entries(groups ?? {})) {
collectPythonRequirementArray(names, requirements, `[dependency-groups].${group}`, true)
}
return names
return projectName === undefined
? { requirements: names }
: { projectName, requirements: names }
}
/**
* Read every requirement name declared by one `pyproject.toml`.
* @param text - the complete `pyproject.toml` contents.
* @returns each declared requirement's distribution name, in file order.
*/
export function parsePyprojectRequirements(text: string): string[] {
return parsePyproject(text).requirements
}
/** Normalize a Python distribution name according to the packaging name rule. */
function normalizePythonDistributionName(name: string): string {
return name.toLowerCase().replace(/[-_.]+/g, '-')
}
/**
* Resolve external Python dependencies after excluding local project names.
* @param pyprojects - complete local `pyproject.toml` contents.
* @param metadata - disclosure metadata for every external dependency.
* @returns disclosed dependencies in normalized name order.
*/
export function collectPythonDependencies(
pyprojects: string[],
metadata: PythonMetadata = PYTHON_METADATA,
): { name: string; license: string; repo: string; role: string }[] {
const parsed = pyprojects.map(parsePyproject)
const firstParty = new Set(parsed.flatMap(({ projectName }) => (
projectName === undefined ? [] : [normalizePythonDistributionName(projectName)]
)))
const found = new Set(parsed
.flatMap(({ requirements }) => requirements.map(normalizePythonDistributionName))
.filter(name => !firstParty.has(name)))
return [...found].sort((a, b) => a.localeCompare(b)).map((name) => {
const entry = metadata[name]
if (entry === undefined) throw new Error(`gen-third-party-notices: python dependency ${name} is missing from PYTHON_METADATA.`)
return { name, ...entry }
})
}
/** Direct Python dependencies named by the `pyproject.toml` manifests under `python/`. */
function collectPython(): { name: string; license: string; repo: string; role: string }[] {
const found = new Set<string>()
const manifests = globSync('python/*/pyproject.toml', { cwd: root })
if (manifests.length === 0) throw new Error('gen-third-party-notices: no python/*/pyproject.toml found; the Python tree moved.')
for (const path of manifests) {
for (const name of parsePyprojectRequirements(readFileSync(resolve(root, path), 'utf8'))) {
if (name.startsWith('deepseek')) continue
found.add(name)
}
}
return [...found].sort((a, b) => a.localeCompare(b)).map((name) => {
const metadata = PYTHON_METADATA[name]
if (metadata === undefined) throw new Error(`gen-third-party-notices: python dependency ${name} is missing from PYTHON_METADATA.`)
return { name, ...metadata }
})
return collectPythonDependencies(manifests.map(path => readFileSync(resolve(root, path), 'utf8')))
}
/** pnpm-patched external packages, from `pnpm-workspace.yaml`. */