Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 1x 1x 1x 7x 7x 7x 7x 7x 7x 2x 2x 7x 1x 1x 4x 4x 4x 7x 4x 7x 7x 7x 7x 1x 1x 1x | /**
* Python cacheKey implementation.
*
* Produces `py-${pythonVersion}-${pyprojectContentHash || 'no-config'}`.
*
* The "Python version" is best-effort: we look for a `requires-python`
* line in `pyproject.toml` (PEP 621) — this is a string like
* `>=3.10,<4.0` — and emit it verbatim. If we can't find one we fall
* back to the literal `unknown`. This is a CACHE INVALIDATION key, not
* a source-of-truth — its only job is to flip when the toolchain
* intent changes.
*
* Per contract invariant I-6 (cacheKey is stable for stable input AND
* changes when the language config changes): the function is purely a
* function of `(pyproject content)`. Two calls without any pyproject
* file produce the same `py-unknown-no-config` key.
*
* Per I-8 (different adapter prefixes): we emit `py-`, distinct from
* the TypeScript adapter's `ts-` and the Rust adapter's `rs-`.
*/
import { createHash } from 'node:crypto';
import { existsSync, readFileSync } from 'node:fs';
import type { CacheKeyInput } from '@opensip-tools/graph';
// Anchored to start-of-line; horizontal whitespace ([\t ]) and the
// inner `[^"'\n]` keep matching linear. Using `\s` would cross
// newlines and let pathological inputs explore O(n^2) prefixes.
const REQUIRES_PYTHON_RE = /^[\t ]*requires-python[\t ]*=[\t ]*["']([^"'\n]+)["']/m;
export function cacheKey(input: CacheKeyInput): string {
const { pythonVersion, configHash } = readConfig(input.configPathAbs);
return `py-${pythonVersion}-${configHash}`;
}
function readConfig(configPathAbs: string | undefined): {
readonly pythonVersion: string;
readonly configHash: string;
} {
if (configPathAbs === undefined || configPathAbs.length === 0) {
return { pythonVersion: 'unknown', configHash: 'no-config' };
}
if (!existsSync(configPathAbs)) {
return { pythonVersion: 'unknown', configHash: `missing:${configPathAbs}` };
}
let content: string;
try {
content = readFileSync(configPathAbs, 'utf8');
} catch {
/* v8 ignore next */
return { pythonVersion: 'unknown', configHash: `unreadable:${configPathAbs}` };
}
const match = REQUIRES_PYTHON_RE.exec(content);
const pythonVersion = match ? sanitize(match[1] ?? 'unknown') : 'unknown';
const configHash = createHash('sha256').update(content).digest('hex').slice(0, 16);
return { pythonVersion, configHash };
}
function sanitize(s: string): string {
// Keep cache-key strings shell- and filename-safe.
return s.replaceAll(/[^A-Za-z0-9._+-]/g, '_').slice(0, 32);
}
|