Compare commits

..
1 Commits
Author SHA1 Message Date
Christoph 5a61161992 try rust parser 2025-08-25 22:03:21 +02:00
194 changed files with 3572 additions and 12708 deletions
-1
View File
@@ -9,4 +9,3 @@ __pycache__
.nox
*.g4
.antlr
.claude
+12 -35
View File
@@ -10,21 +10,13 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
"!**/node_modules/**"
],
"skipFiles": ["<node_internals>/**"],
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/client/**/*.js"],
"autoAttachChildProcesses": true,
"preLaunchTask": "NX Post Support: Compile Debug"
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
},
{
"name": "Python Attach",
@@ -42,24 +34,10 @@
"name": "Debug Extension (hidden)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
"!**/node_modules/**"
],
"skipFiles": ["<node_internals>/**"],
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/client/**/*.js"],
"env": {
"USE_DEBUGPY": "True",
"NXPS_DEBUG_HOST": "127.0.0.1",
"NXPS_DEBUG_PORT": "5678"
"USE_DEBUGPY": "True"
},
"presentation": {
"hidden": true,
@@ -71,9 +49,8 @@
"name": "Python debug server (hidden)",
"type": "debugpy",
"request": "attach",
"listen": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": false,
"logToFile": true,
"listen": { "host": "localhost", "port": 5678 },
"justMyCode": true,
"presentation": {
"hidden": true,
"group": "",
@@ -86,7 +63,7 @@
"name": "Debug Extension and Python",
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
"stopAll": true,
"preLaunchTask": "NX Post Support: Compile Debug",
"preLaunchTask": "npm: watch",
"presentation": {
"hidden": false,
"group": "",
-57
View File
@@ -1,57 +0,0 @@
param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceRoot,
[Parameter(Mandatory = $true)]
[string]$DebugRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function ConvertFrom-ExtendedWindowsPath {
param([string]$Path)
if ($Path.StartsWith("\\?\UNC\", [System.StringComparison]::OrdinalIgnoreCase)) {
return "\\" + $Path.Substring(8)
}
if ($Path.StartsWith("\\?\", [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring(4)
}
return $Path
}
$workspacePath = ConvertFrom-ExtendedWindowsPath $WorkspaceRoot
$workspaceItem = Get-Item -LiteralPath $workspacePath
if (-not $workspaceItem.PSIsContainer) {
throw "Workspace root is not a directory: $workspacePath"
}
$workspacePath = $workspaceItem.FullName
$debugPath = ConvertFrom-ExtendedWindowsPath $DebugRoot
$tempPath = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd("\")
$debugParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $debugPath)).TrimEnd("\")
if (-not $debugParent.Equals($tempPath, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Debug alias must be located directly below the user temp directory: $debugPath"
}
if (Test-Path -LiteralPath $debugPath) {
$debugItem = Get-Item -LiteralPath $debugPath -Force
if ($debugItem.LinkType -ne "Junction") {
throw "Debug alias exists but is not a junction: $debugPath"
}
$currentTarget = (Get-Item -LiteralPath $debugItem.Target).FullName
if (-not $currentTarget.Equals($workspacePath, [System.StringComparison]::OrdinalIgnoreCase)) {
# Removing a junction removes only the link, never the target directory.
Remove-Item -LiteralPath $debugPath -Force
}
}
if (-not (Test-Path -LiteralPath $debugPath)) {
New-Item -ItemType Junction -Path $debugPath -Target $workspacePath | Out-Null
}
Write-Output "Debug extension path: $debugPath -> $workspacePath"
& npm.cmd --prefix $workspacePath run compile:debug
exit $LASTEXITCODE
-26
View File
@@ -1,26 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "NX Post Support: Compile Debug",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoLogo",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"& { param([string]$WorkspaceRoot, [string]$DebugRoot); $scriptRoot = $WorkspaceRoot; if ($scriptRoot.StartsWith('\\\\?\\')) { $scriptRoot = $scriptRoot.Substring(4) }; & (Join-Path $scriptRoot '.vscode\\prepare-debug.ps1') -WorkspaceRoot $WorkspaceRoot -DebugRoot $DebugRoot; exit $LASTEXITCODE }",
"${workspaceFolder}",
"${env:TEMP}\\nx-post-support-vscode-debug"
],
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated",
"clear": true
}
}
]
}
-19
View File
@@ -1,14 +1,3 @@
## Unreleased
- Prevent truncated TCL inlay hints and add configurable parameter hint modes
- Add inlay hints for built-in NX procedures, variadic arguments, and visible ranges
- Add inlay hint documentation and navigation to custom procedure definitions
- Add signature help for custom TCL procedures and built-in NX/MOM procedures
- Clean stale TCL indexes on close, delete, and rename operations
- Make background parsing and index updates thread-safe
- Improve TCL response times with debounced edits and cached semantic, inlay, hover, completion, and variable indexes
- Debounce CDL/DEF diagnostics and remove per-line diagnostic logging
## [0.0.1]
- Initial release
@@ -24,11 +13,3 @@
## [0.2.0]
- Add DEF File Support
## [2026.6.100]
- Fix several bugs
## [2026.6.200]
- Fix foramtting bug
+2 -34
View File
@@ -9,7 +9,6 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
- **Auto-completion** - Context-aware code completion for faster development
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
## Supported File Types
@@ -20,9 +19,8 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
## Installation
1. Install from the VS Code Marketplace
2. Install Python 3.11 or higher
3. Open any `.cdl`, `.tcl`, or `.def` file
4. The extension will automatically activate and provide language support
2. Open any `.cdl`, `.tcl`, or `.def` file
3. The extension will automatically activate and provide language support
## Configuration
@@ -31,12 +29,6 @@ The extension can be configured through VS Code settings:
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
- `nx-post-support.inlayHints.parameterNames` - Show parameter names for `all`, only `literals`, or `none` (default: `all`)
- `nx-post-support.inlayHints.suppressWhenArgumentMatchesName` - Hide redundant hints such as `value:` before `$value` (default: true)
TCL files default to unlimited inlay hint length so that VS Code does not
truncate later parameter names on a line. An explicit user setting for
`editor.inlayHints.maximumLength` still takes precedence.
## Usage
@@ -47,30 +39,6 @@ Simply open any supported file type and enjoy:
- Code completion
- Code formatting (Format Document command)
- Hover information
- Signature help while entering procedure arguments
## Development and debugging
Install the root and client dependencies before the first debug session:
```powershell
npm install
npm install --prefix client
```
Use one of the checked-in VS Code launch configurations:
- **Run Extension** debugs the TypeScript extension host.
- **Debug Extension and Python** debugs both the TypeScript extension and the
Python language server. This is the recommended configuration for LSP work.
- **Python Attach** attaches manually to an already running Python process.
The launch configuration creates a fresh non-minified bundle with embedded
source maps and opens `test/test.tcl` so the extension activates immediately.
For combined debugging, the Python adapter listens on `127.0.0.1:5678`; the
language server waits for that adapter before initialization. The NX
Postprocessor Support output channel reports `Python debug mode: enabled` and
shows `_debug_server.py` in the server command when the debug path is active.
## Contributing
-22
View File
@@ -14,7 +14,6 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@types/vscode": "^1.96.0"
},
@@ -22,27 +21,6 @@
"vscode": "^1.96.0"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
-1
View File
@@ -12,7 +12,6 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@types/vscode": "^1.96.0"
}
-116
View File
@@ -1,116 +0,0 @@
export interface CdlEventHandler {
eventName: string
parameterNames: string[]
}
function structuralCode(line: string): string {
let result = ""
let inString = false
let escaped = false
for (const character of line) {
if (escaped) {
escaped = false
result += inString ? " " : character
continue
}
if (character === "\\") {
escaped = true
result += inString ? " " : character
continue
}
if (character === '"') {
inString = !inString
result += " "
continue
}
if (character === "#" && !inString) {
break
}
result += inString ? " " : character
}
return result
}
function braceDelta(line: string): number {
let delta = 0
for (const character of structuralCode(line)) {
if (character === "{") {
delta += 1
} else if (character === "}") {
delta -= 1
}
}
return delta
}
export function cdlEventHandlerAtLine(
source: string,
declarationLine: number
): CdlEventHandler | undefined {
const lines = source.split(/\r?\n/)
const declaration = lines[declarationLine]
if (declaration === undefined) {
return undefined
}
const eventMatch = /^\s*EVENT\s+([^\s{]+)/.exec(structuralCode(declaration))
if (!eventMatch) {
return undefined
}
const parameterNames: string[] = []
let eventOpened = false
let depth = 0
for (let lineNumber = declarationLine; lineNumber < lines.length; lineNumber++) {
const line = lines[lineNumber]
const code = structuralCode(line)
if (eventOpened && depth === 1) {
const parameterMatch = /^\s*PARAM\s+([^\s{]+)/.exec(code)
if (parameterMatch) {
parameterNames.push(parameterMatch[1])
}
}
const delta = braceDelta(line)
if (!eventOpened && delta > 0) {
eventOpened = true
}
if (eventOpened) {
depth += delta
if (depth <= 0) {
break
}
}
}
return {
eventName: eventMatch[1],
parameterNames
}
}
function momEventName(eventName: string): string {
return `MOM_${eventName.replace(/^MOM_/i, "")}`
}
function momVariableName(parameterName: string): string {
return `mom_${parameterName.replace(/^mom_/i, "")}`
}
export function createCdlEventHandlerSnippet(handler: CdlEventHandler): string {
const globals = [
...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter)))
]
const lines = [`proc ${momEventName(handler.eventName)} { } {`]
if (globals.length > 0) {
lines.push(...globals.map((variable) => ` global ${variable}`), "")
}
lines.push(" #Put your UDE Handler Tcl here", "", "}")
return lines.join("\n")
}
+27 -137
View File
@@ -1,10 +1,4 @@
import * as vscode from "vscode"
import {
cdlEventHandlerAtLine,
createCdlEventHandlerSnippet
} from "./cdlEventHandler"
const MACHINE_HEADER_REGEX = /^MACHINE\s+\S+/
export function formatCdlFile(content: string): string {
let indentLevel = 0
@@ -43,19 +37,15 @@ export function formatDefFile(content: string): string {
}
export function isFirstLineMachine(content: string): boolean {
let lineStart = 0
while (lineStart <= content.length) {
const newline = content.indexOf("\n", lineStart)
const lineEnd = newline === -1 ? content.length : newline
const line = content.slice(lineStart, lineEnd).trim()
const lines = content.split("\n").map((line) => line.trim())
for (const line of lines) {
console.log(line)
if (line === "" || line.startsWith("#")) {
if (newline === -1) {
return false
}
lineStart = newline + 1
console.log("skipping line")
continue
}
return MACHINE_HEADER_REGEX.test(line)
const machineRegex = /^MACHINE\s+\S+/
return machineRegex.test(line)
}
return false
}
@@ -63,11 +53,10 @@ export function isFirstLineMachine(content: string): boolean {
export function diagnosticHandler(document: vscode.TextDocument) {
const diagnostics: vscode.Diagnostic[] = []
if (document.languageId === "cdl" || document.languageId === "def") {
const text = document.getText()
if (!isFirstLineMachine(text)) {
if (!isFirstLineMachine(document.getText())) {
const range = new vscode.Range(
document.positionAt(0),
document.positionAt(text.length)
document.positionAt(document.getText().length)
)
const diagnostic = new vscode.Diagnostic(
range,
@@ -121,132 +110,33 @@ export function completionHandlerCdl(document: vscode.TextDocument, position: vs
}
export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.Position) {
const line = document.lineAt(position.line).text
const eventMatch = /^\s*EVENT\s+([^\s{]+)/.exec(line)
if (eventMatch) {
const eventStart = line.indexOf(eventMatch[1], eventMatch.index)
const declarationEnd = eventStart + eventMatch[1].length
if (position.character <= declarationEnd) {
const handler = cdlEventHandlerAtLine(document.getText(), position.line)
if (handler) {
const markdown = new vscode.MarkdownString()
markdown.appendCodeblock(createCdlEventHandlerSnippet(handler), "tcl")
return new vscode.Hover(
markdown,
new vscode.Range(position.line, eventMatch.index, position.line, declarationEnd)
)
const wordRange = document.getWordRangeAtPosition(position)
const word = document.getText(wordRange)
const text = document.getText()
const lines = text.split("\n")
let hoverText: string | undefined
for (const line of lines) {
const words = line.split(/\s+/)
const wordIndex = words.indexOf(word)
if (wordIndex > 0) {
if (words[wordIndex - 1] === "EVENT") {
hoverText = `MOM_${word}`
} else if (words[wordIndex - 1] === "PARAM") {
hoverText = `mom_${word}`
}
break
}
}
const parameterMatch = /^\s*PARAM\s+([^\s{]+)/.exec(line)
if (parameterMatch) {
const parameterStart = line.indexOf(parameterMatch[1], parameterMatch.index)
const parameterEnd = parameterStart + parameterMatch[1].length
if (position.character >= parameterStart && position.character <= parameterEnd) {
return new vscode.Hover(`mom_${parameterMatch[1].replace(/^mom_/i, "")}`)
if (hoverText) {
return new vscode.Hover(hoverText)
}
}
return undefined
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export function cdlEventAtPosition(
document: vscode.TextDocument,
position: vscode.Position
): string | undefined {
const line = document.lineAt(position.line).text
const match = /^\s*EVENT\s+([^\s{]+)/.exec(line)
if (!match) {
return undefined
}
const declarationStart = match.index
const eventEnd = line.indexOf(match[1], match.index) + match[1].length
if (position.character < declarationStart || position.character > eventEnd) {
return undefined
}
return match[1]
}
export async function definitionCdlEventHandler(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Location[] | undefined> {
const eventName = cdlEventAtPosition(document, position)
if (!eventName) {
return undefined
}
const handlerName = `MOM_${eventName}`
try {
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
"vscode.executeWorkspaceSymbolProvider",
handlerName
)
const indexedLocations = (symbols || [])
.filter(
(symbol) =>
symbol.kind === vscode.SymbolKind.Function &&
(symbol.name === handlerName ||
symbol.name.endsWith(`::${handlerName}`))
)
.map((symbol) => symbol.location)
if (indexedLocations.length > 0) {
return indexedLocations
}
} catch {
// The Tcl language server may still be starting; use the file fallback below.
}
const declaration = new RegExp(
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
)
const tclFiles = await vscode.workspace.findFiles(
"**/*.tcl",
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
)
const locations: vscode.Location[] = []
for (const uri of tclFiles) {
if (token.isCancellationRequested) {
return undefined
}
let tclDocument: vscode.TextDocument
try {
tclDocument = await vscode.workspace.openTextDocument(uri)
} catch {
continue
}
for (let lineNumber = 0; lineNumber < tclDocument.lineCount; lineNumber++) {
const line = tclDocument.lineAt(lineNumber).text
const match = declaration.exec(line)
if (!match) {
continue
}
const start = line.indexOf(handlerName, match.index)
locations.push(
new vscode.Location(
uri,
new vscode.Range(
lineNumber,
start,
lineNumber,
start + handlerName.length
)
)
)
}
}
return locations.length > 0 ? locations : undefined
}
export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const symbols: vscode.DocumentSymbol[] = []
const lines = document.getText().split("\n")
+18 -50
View File
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
import * as fsapi from "fs-extra"
import { Disposable, env, LogOutputChannel, workspace } from "vscode"
import { Disposable, env, LogOutputChannel } from "vscode"
import { State } from "vscode-languageclient"
import {
LanguageClient,
@@ -24,13 +24,6 @@ import { isVirtualWorkspace } from "./vscodeapi"
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
let _disposables: Disposable[] = []
export function disposeServerResources(): void {
_disposables.forEach((disposable) => disposable.dispose())
_disposables = []
}
async function createServer(
settings: ISettings,
serverId: string,
@@ -39,26 +32,13 @@ async function createServer(
initializationOptions: IInitOptions
): Promise<LanguageClient> {
const command = settings.interpreter[0]
if (!command) {
throw new Error("No Python interpreter is configured for the language server.")
}
const cwd = settings.cwd
// Set debugger path needed for debugging python code.
const newEnv = { ...process.env }
const debuggerPath = await getDebuggerPath()
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
const debugRequested = newEnv.USE_DEBUGPY?.toLowerCase() === "true"
if (debugRequested && !isDebugScript) {
throw new Error(`Python debug bootstrap not found: ${DEBUG_SERVER_SCRIPT_PATH}`)
}
const debuggerPath = debugRequested ? await getDebuggerPath() : undefined
if (debugRequested && !debuggerPath) {
throw new Error(
"Python debugging was requested, but the Python Debugger extension did not provide debugpy."
)
}
if (debugRequested && debuggerPath) {
if (newEnv.USE_DEBUGPY && debuggerPath) {
newEnv.DEBUGPY_PATH = debuggerPath
} else {
newEnv.USE_DEBUGPY = "False"
@@ -70,13 +50,10 @@ async function createServer(
// Set notification type
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH
const interpreterArgs = settings.interpreter.slice(1)
if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) {
interpreterArgs.push("-Xfrozen_modules=off")
}
const args = interpreterArgs.concat([serverScript])
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
const args =
newEnv.USE_DEBUGPY === "False" || !isDebugScript
? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH])
: settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH])
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
const serverOptions: ServerOptions = {
@@ -86,7 +63,6 @@ async function createServer(
}
// Options to control the language client
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
const clientOptions: LanguageClientOptions = {
// Register the server for python documents
documentSelector: isVirtualWorkspace()
@@ -100,16 +76,13 @@ async function createServer(
outputChannel: outputChannel,
traceOutputChannel: outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never,
synchronize: {
fileEvents: tclFileWatcher
},
initializationOptions
}
_disposables.push(tclFileWatcher)
return new LanguageClient(serverId, serverName, serverOptions, clientOptions)
}
let _disposables: Disposable[] = []
export async function restartServer(
serverId: string,
serverName: string,
@@ -119,22 +92,16 @@ export async function restartServer(
if (lsClient) {
traceInfo(`Server: Stop requested`)
await lsClient.stop()
disposeServerResources()
_disposables.forEach((d) => d.dispose())
_disposables = []
}
const projectRoot = await getProjectRoot()
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
try {
const newLSClient = await createServer(
workspaceSetting,
serverId,
serverName,
outputChannel,
{
const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, {
settings: await getExtensionSettings(serverId, true),
globalSettings: await getGlobalSettings(serverId, false)
}
)
})
traceInfo(`Server: Start requested.`)
_disposables.push(
newLSClient.onDidChangeState((e) => {
@@ -151,13 +118,14 @@ export async function restartServer(
}
})
)
try {
await newLSClient.start()
} catch (ex) {
traceError(`Server: Start failed: ${ex}`)
return undefined
}
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
} catch (ex) {
traceError(`Server: Start failed: ${ex}`)
disposeServerResources()
return undefined
}
}
+3 -23
View File
@@ -18,12 +18,6 @@ export interface ISettings {
interpreter: string[]
importStrategy: string
showNotifications: string
formatter: boolean
inlayHint: boolean
inlayHints: {
parameterNames: "all" | "literals" | "none"
suppressWhenArgumentMatchesName: boolean
}
}
export function getExtensionSettings(
@@ -86,13 +80,7 @@ export async function getWorkspaceSettings(
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
showNotifications: config.get<string>(`showNotifications`) ?? "off",
formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true,
inlayHints: {
parameterNames:
config.get<"all" | "literals" | "none">(`inlayHints.parameterNames`) ?? "all",
suppressWhenArgumentMatchesName:
config.get<boolean>(`inlayHints.suppressWhenArgumentMatchesName`) ?? true
}
inlayHint: config.get<boolean>(`inlayHint`) ?? true
}
return workspaceSetting
}
@@ -125,13 +113,7 @@ export async function getGlobalSettings(
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true,
inlayHints: {
parameterNames:
config.get<"all" | "literals" | "none">(`inlayHints.parameterNames`) ?? "all",
suppressWhenArgumentMatchesName:
config.get<boolean>(`inlayHints.suppressWhenArgumentMatchesName`) ?? true
}
inlayHint: config.get<boolean>(`inlayHint`) ?? true
}
return setting
}
@@ -147,9 +129,7 @@ export function checkIfConfigurationChanged(
`${namespace}.importStrategy`,
`${namespace}.showNotifications`,
`${namespace}.formatter`,
`${namespace}.inlayHint`,
`${namespace}.inlayHints.parameterNames`,
`${namespace}.inlayHints.suppressWhenArgumentMatchesName`
`${namespace}.inlayHint`
]
const changed = settings.map((s) => e.affectsConfiguration(s))
return changed.includes(true)
+15 -96
View File
@@ -13,8 +13,7 @@ import {
isFirstLineMachine,
diagnosticHandler,
cdlDocumentSymbolProvider,
defDocumentSymbolProvider,
definitionCdlEventHandler
defDocumentSymbolProvider
} from "./common/handlers"
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
import {
@@ -24,13 +23,13 @@ import {
onDidChangePythonInterpreter,
resolveInterpreter
} from "./common/python"
import { disposeServerResources, restartServer } from "./common/server"
import { restartServer } from "./common/server"
import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings"
import { loadServerDefaults } from "./common/setup"
import { getLSClientTraceLevel } from "./common/utilities"
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
let client: LanguageClient | undefined
let client: LanguageClient
export async function activate(context: vscode.ExtensionContext) {
// This is required to get server name and module. This should be
@@ -38,7 +37,6 @@ export async function activate(context: vscode.ExtensionContext) {
const serverInfo = loadServerDefaults()
const serverName = serverInfo.name
const serverId = serverInfo.module
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
// Setup logging
const outputChannel = createOutputChannel(serverName)
@@ -63,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext) {
traceLog(`Module: ${serverInfo.module}`)
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
const runServerImpl = async () => {
const runServer = async () => {
const interpreter = getInterpreterFromSetting(serverId)
if (interpreter && interpreter.length > 0) {
if (checkVersion(await resolveInterpreter(interpreter))) {
@@ -92,25 +90,10 @@ export async function activate(context: vscode.ExtensionContext) {
)
}
// Serialize server (re)starts. Overlapping triggers (interpreter change,
// config change, restart command, initial activation) would otherwise each
// read the stale module-level `client`, start a new server and leave the
// previous one running orphaned -> hints/hover shown multiple times.
let runServerQueue: Promise<void> = Promise.resolve()
const runServer = () => {
runServerQueue = runServerQueue.catch(() => undefined).then(() => runServerImpl())
return runServerQueue
}
if (!pythonDebugMode) {
context.subscriptions.push(
onDidChangePythonInterpreter(async () => {
await runServer()
})
)
}
context.subscriptions.push(
}),
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
if (checkIfConfigurationChanged(e, serverId)) {
await runServer()
@@ -122,15 +105,6 @@ export async function activate(context: vscode.ExtensionContext) {
)
setImmediate(async () => {
if (pythonDebugMode) {
// A debugpy listen session is attached to exactly one process. Do not
// subscribe to interpreter changes during startup, as the Python
// extension can emit a duplicate event and restart that process.
traceLog("Python debug mode: starting one stable server session")
await runServer()
return
}
const interpreter = getInterpreterFromSetting(serverId)
if (interpreter === undefined || interpreter.length === 0) {
traceLog(`Python extension loading`)
@@ -141,10 +115,6 @@ export async function activate(context: vscode.ExtensionContext) {
}
})
// Folding ranges for TCL are provided by the language server
// (folding_range_provider in lsp_server.py). No client-side provider here to
// avoid duplicate folding regions.
//
const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "cdl" },
@@ -193,16 +163,6 @@ export async function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(hoverCdlProvider)
const definitionCdlEventProvider = vscode.languages.registerDefinitionProvider(
{ scheme: "file", language: "cdl" },
{
provideDefinition(document, position, token) {
return definitionCdlEventHandler(document, position, token)
}
}
)
context.subscriptions.push(definitionCdlEventProvider)
const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "def" },
{
@@ -246,73 +206,32 @@ export async function activate(context: vscode.ExtensionContext) {
const diagnosticCollectionDef = vscode.languages.createDiagnosticCollection("def")
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
const diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>()
const updateDiagnostics = (document: vscode.TextDocument) => {
// Check if the first line of the CDL file contains "MACHINE"
vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl" || document.languageId === "def") {
if (document.languageId === "cdl") {
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
} else if (document.languageId === "def") {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
}
}
const scheduleDiagnostics = (document: vscode.TextDocument) => {
const key = document.uri.toString()
const previous = diagnosticTimers.get(key)
if (previous !== undefined) {
clearTimeout(previous)
}
diagnosticTimers.set(
key,
setTimeout(() => {
diagnosticTimers.delete(key)
updateDiagnostics(document)
}, 120)
)
}
})
// Check if the first line of the CDL file contains "MACHINE"
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl" || document.languageId === "def") {
updateDiagnostics(document)
}
}),
vscode.workspace.onDidChangeTextDocument((event) => {
const document = event.document
if (document.languageId === "cdl" || document.languageId === "def") {
scheduleDiagnostics(document)
}
}),
vscode.workspace.onDidCloseTextDocument((document) => {
const key = document.uri.toString()
const timer = diagnosticTimers.get(key)
if (timer !== undefined) {
clearTimeout(timer)
diagnosticTimers.delete(key)
}
diagnosticCollectionCdl.delete(document.uri)
diagnosticCollectionDef.delete(document.uri)
}),
{
dispose() {
for (const timer of diagnosticTimers.values()) {
clearTimeout(timer)
}
diagnosticTimers.clear()
}
}
)
for (const document of vscode.workspace.textDocuments) {
if (document.languageId === "cdl" || document.languageId === "def") {
updateDiagnostics(document)
if (document.languageId === "cdl") {
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
} else if (document.languageId === "def") {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
}
}
})
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
disposeServerResources()
return undefined
}
return client.stop().finally(disposeServerResources)
return client.stop()
}
+2 -4
View File
@@ -1,21 +1,19 @@
const esbuild = require("esbuild")
const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
async function main() {
const ctx = await esbuild.context({
absWorkingDir: __dirname,
entryPoints: ["client/src/extension.ts"],
bundle: true,
format: "cjs",
minify: production,
sourcemap: !production,
sourcesContent: !production,
sourcesContent: false,
platform: "node",
// outdir: "out",
outfile: path.join(__dirname, "dist", "extension.js"),
outfile: "./dist/extension.js",
external: ["vscode"],
logLevel: "silent",
plugins: [
-7
View File
@@ -3,13 +3,6 @@
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "#"
},
"indentationRules": {
// Ignore pure comment lines so they don't terminate folds inside Tcl brace blocks.
"unIndentedLinePattern": "^\\s*#.*$",
// Opening braces on their own line are common in NX Tcl.
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*)$",
"decreaseIndentPattern": "^\\s*[\\}\\]\\)].*$"
},
// symbols used as brackets
"brackets": [
["{", "}"],
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "nx-post-support",
"version": "2025.9.200",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nx-post-support",
"version": "2025.9.200",
"version": "0.3.0",
"devDependencies": {
"@types/vscode": "^1.96.0",
"@vscode/vsce": "^3.2.1",
+1 -30
View File
@@ -2,12 +2,9 @@
"name": "nx-post-support",
"displayName": "NX Postprocessor Support",
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
"version": "2026.8.200",
"version": "2025.9.100",
"publisher": "Christoph",
"icon": "images/nx-1.png",
"extensionDependencies": [
"ms-python.python"
],
"serverInfo": {
"name": "NX Postprocessor Support",
"module": "nx-post-support"
@@ -96,26 +93,6 @@
"default": true,
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
},
"nx-post-support.inlayHints.parameterNames": {
"type": "string",
"default": "all",
"enum": [
"all",
"literals",
"none"
],
"enumDescriptions": [
"Show parameter name hints for all arguments.",
"Show parameter name hints only for literal arguments.",
"Do not show parameter name hints."
],
"description": "Controls which TCL procedure arguments receive parameter name hints."
},
"nx-post-support.inlayHints.suppressWhenArgumentMatchesName": {
"type": "boolean",
"default": true,
"description": "Hide a parameter hint when a variable argument already has the same name, for example `output` in `my_proc $output`."
},
"nx-post-support.importStrategy": {
"default": "useBundled",
"description": "Defines where `NX Postprocessor Support` is imported from.",
@@ -140,16 +117,10 @@
"type": "array"
}
}
},
"configurationDefaults": {
"[tcl]": {
"editor.inlayHints.maximumLength": 0
}
}
},
"scripts": {
"compile": "node esbuild.js --production",
"compile:debug": "node esbuild.js",
"watch": "node esbuild.js --watch",
"package": "node esbuild.js --production"
},
View File
+5 -5
View File
@@ -176,7 +176,7 @@ def attrib(
type: None = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -200,7 +200,7 @@ def attrib(
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -223,7 +223,7 @@ def attrib(
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -246,7 +246,7 @@ def attrib(
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -308,7 +308,7 @@ def attrs(
match_args: bool = ...,
unsafe_hash: bool | None = ...,
) -> Callable[[_C], _C]: ...
def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ...
def fields(cls: type[AttrsInstance]) -> Any: ...
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
def validate(inst: AttrsInstance) -> None: ...
def resolve_types(
+3 -8
View File
@@ -10,6 +10,7 @@ from typing import _GenericAlias
PYPY = platform.python_implementation() == "PyPy"
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
@@ -17,16 +18,10 @@ PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
if PY_3_14_PLUS:
if PY_3_14_PLUS: # pragma: no cover
import annotationlib
# We request forward-ref annotations to not break in the presence of
# forward references.
def _get_annotations(cls):
return annotationlib.get_annotations(
cls, format=annotationlib.Format.FORWARDREF
)
_get_annotations = annotationlib.get_annotations
else:
+16 -45
View File
@@ -3,28 +3,11 @@
import copy
from ._compat import get_generic_base
from ._compat import PY_3_9_PLUS, get_generic_base
from ._make import _OBJ_SETATTR, NOTHING, fields
from .exceptions import AttrsAttributeNotFoundError
_ATOMIC_TYPES = frozenset(
{
type(None),
bool,
int,
float,
str,
complex,
bytes,
type(...),
type,
range,
property,
}
)
def asdict(
inst,
recurse=True,
@@ -88,10 +71,7 @@ def asdict(
v = value_serializer(inst, a, v)
if recurse is True:
value_type = type(v)
if value_type in _ATOMIC_TYPES:
rv[a.name] = v
elif has(value_type):
if has(v.__class__):
rv[a.name] = asdict(
v,
recurse=True,
@@ -100,8 +80,8 @@ def asdict(
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif issubclass(value_type, (tuple, list, set, frozenset)):
cf = value_type if retain_collection_types is True else list
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain_collection_types is True else list
items = [
_asdict_anything(
i,
@@ -121,7 +101,7 @@ def asdict(
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv[a.name] = cf(*items)
elif issubclass(value_type, dict):
elif isinstance(v, dict):
df = dict_factory
rv[a.name] = df(
(
@@ -162,12 +142,7 @@ def _asdict_anything(
"""
``asdict`` only works on attrs instances, this works on anything.
"""
val_type = type(val)
if val_type in _ATOMIC_TYPES:
rv = val
if value_serializer is not None:
rv = value_serializer(None, None, rv)
elif getattr(val_type, "__attrs_attrs__", None) is not None:
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
# Attrs class.
rv = asdict(
val,
@@ -177,7 +152,7 @@ def _asdict_anything(
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif issubclass(val_type, (tuple, list, set, frozenset)):
elif isinstance(val, (tuple, list, set, frozenset)):
if retain_collection_types is True:
cf = val.__class__
elif is_key:
@@ -198,7 +173,7 @@ def _asdict_anything(
for i in val
]
)
elif issubclass(val_type, dict):
elif isinstance(val, dict):
df = dict_factory
rv = df(
(
@@ -278,11 +253,8 @@ def astuple(
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
value_type = type(v)
if recurse is True:
if value_type in _ATOMIC_TYPES:
rv.append(v)
elif has(value_type):
if has(v.__class__):
rv.append(
astuple(
v,
@@ -292,7 +264,7 @@ def astuple(
retain_collection_types=retain,
)
)
elif issubclass(value_type, (tuple, list, set, frozenset)):
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain is True else list
items = [
(
@@ -316,8 +288,8 @@ def astuple(
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv.append(cf(*items))
elif issubclass(value_type, dict):
df = value_type if retain is True else dict
elif isinstance(v, dict):
df = v.__class__ if retain is True else dict
rv.append(
df(
(
@@ -478,11 +450,10 @@ def resolve_types(
if getattr(cls, "__attrs_types_resolved__", None) != cls:
import typing
kwargs = {
"globalns": globalns,
"localns": localns,
"include_extras": include_extras,
}
kwargs = {"globalns": globalns, "localns": localns}
if PY_3_9_PLUS:
kwargs["include_extras"] = include_extras
hints = typing.get_type_hints(cls, **kwargs)
for field in fields(cls) if attribs is None else attribs:
+111 -394
View File
@@ -12,7 +12,6 @@ import linecache
import sys
import types
import unicodedata
import weakref
from collections.abc import Callable, Mapping
from functools import cached_property
@@ -114,7 +113,7 @@ def attrib(
type=None,
converter=None,
factory=None,
kw_only=None,
kw_only=False,
eq=None,
order=None,
on_setattr=None,
@@ -157,9 +156,6 @@ def attrib(
*eq*, *order*, and *cmp* also accept a custom callable
.. versionchanged:: 21.1.0 *cmp* undeprecated
.. versionadded:: 22.2.0 *alias*
.. versionchanged:: 25.4.0
*kw_only* can now be None, and its default is also changed from False to
None.
"""
eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq, order, True
@@ -377,12 +373,7 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
def _transform_attrs(
cls,
these,
auto_attribs,
kw_only,
collect_by_mro,
field_transformer,
cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
) -> _Attributes:
"""
Transform all `_CountingAttr`s on a class into `Attribute`s.
@@ -437,15 +428,8 @@ def _transform_attrs(
)
fca = Attribute.from_counting_attr
no = ClassProps.KeywordOnly.NO
own_attrs = [
fca(
attr_name,
ca,
kw_only is not no,
anns.get(attr_name),
)
for attr_name, ca in ca_list
fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list
]
if collect_by_mro:
@@ -457,21 +441,12 @@ def _transform_attrs(
cls, {a.name for a in own_attrs}
)
if kw_only is ClassProps.KeywordOnly.FORCE:
if kw_only:
own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
attrs = base_attrs + own_attrs
# Resolve default field alias before executing field_transformer, so that
# the transformer receives fully populated Attribute objects with usable
# alias values.
for a in attrs:
if not a.alias:
# Evolve is very slow, so we hold our nose and do it dirty.
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
if field_transformer is not None:
attrs = tuple(field_transformer(cls, attrs))
@@ -489,12 +464,13 @@ def _transform_attrs(
if had_default is False and a.default is not NOTHING:
had_default = True
# Resolve default field alias for any new attributes that the
# field_transformer may have added without setting an alias.
# Resolve default field alias after executing field_transformer.
# This allows field_transformer to differentiate between explicit vs
# default aliases and supply their own defaults.
for a in attrs:
if not a.alias:
# Evolve is very slow, so we hold our nose and do it dirty.
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
# Create AttrsClass *after* applying the field_transformer since it may
# add or remove attributes!
@@ -577,7 +553,7 @@ def _frozen_delattrs(self, name):
"""
Attached to frozen classes as __delattr__.
"""
if isinstance(self, BaseException) and name == "__notes__":
if isinstance(self, BaseException) and name in ("__notes__",):
BaseException.__delattr__(self, name)
return
@@ -675,31 +651,38 @@ class _ClassBuilder:
self,
cls: type,
these,
auto_attribs: bool,
props: ClassProps,
has_custom_setattr: bool,
slots,
frozen,
weakref_slot,
getstate_setstate,
auto_attribs,
kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_custom_setattr,
field_transformer,
):
attrs, base_attrs, base_map = _transform_attrs(
cls,
these,
auto_attribs,
props.kw_only,
props.collected_fields_by_mro,
props.field_transformer,
kw_only,
collect_by_mro,
field_transformer,
)
self._cls = cls
self._cls_dict = dict(cls.__dict__) if props.is_slotted else {}
self._cls_dict = dict(cls.__dict__) if slots else {}
self._attrs = attrs
self._base_names = {a.name for a in base_attrs}
self._base_attr_map = base_map
self._attr_names = tuple(a.name for a in attrs)
self._slots = props.is_slotted
self._frozen = props.is_frozen
self._weakref_slot = props.has_weakref_slot
self._cache_hash = (
props.hashability is ClassProps.Hashability.HASHABLE_CACHED
)
self._slots = slots
self._frozen = frozen
self._weakref_slot = weakref_slot
self._cache_hash = cache_hash
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
self._pre_init_has_args = False
if self._has_pre_init:
@@ -710,21 +693,20 @@ class _ClassBuilder:
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)
self._is_exc = props.is_exception
self._on_setattr = props.on_setattr_hook
self._is_exc = is_exc
self._on_setattr = on_setattr
self._has_custom_setattr = has_custom_setattr
self._wrote_own_setattr = False
self._cls_dict["__attrs_attrs__"] = self._attrs
self._cls_dict["__attrs_props__"] = props
if props.is_frozen:
if frozen:
self._cls_dict["__setattr__"] = _frozen_setattrs
self._cls_dict["__delattr__"] = _frozen_delattrs
self._wrote_own_setattr = True
elif self._on_setattr in (
elif on_setattr in (
_DEFAULT_ON_SETATTR,
setters.validate,
setters.convert,
@@ -740,18 +722,18 @@ class _ClassBuilder:
break
if (
(
self._on_setattr == _DEFAULT_ON_SETATTR
on_setattr == _DEFAULT_ON_SETATTR
and not (has_validator or has_converter)
)
or (self._on_setattr == setters.validate and not has_validator)
or (self._on_setattr == setters.convert and not has_converter)
or (on_setattr == setters.validate and not has_validator)
or (on_setattr == setters.convert and not has_converter)
):
# If class-level on_setattr is set to convert + validate, but
# there's no field to convert or validate, pretend like there's
# no on_setattr.
self._on_setattr = None
if props.added_pickling:
if getstate_setstate:
(
self._cls_dict["__getstate__"],
self._cls_dict["__setstate__"],
@@ -802,7 +784,6 @@ class _ClassBuilder:
self._eval_snippets()
if self._slots is True:
cls = self._create_slots_class()
self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)
else:
cls = self._patch_original_class()
if PY_3_10_PLUS:
@@ -864,10 +845,6 @@ class _ClassBuilder:
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")
}
# 3.14.0rc2+
if hasattr(sys, "_clear_type_descriptors"):
sys._clear_type_descriptors(self._cls)
# If our class doesn't have its own implementation of __setattr__
# (either from the user or by us), check the bases, if one of them has
# an attrs-made __setattr__, that needs to be reset. We don't walk the
@@ -1104,7 +1081,9 @@ class _ClassBuilder:
return self
def add_replace(self):
self._cls_dict["__replace__"] = self._add_method_dunders(evolve)
self._cls_dict["__replace__"] = self._add_method_dunders(
lambda self, **changes: evolve(self, **changes)
)
return self
def add_match_args(self):
@@ -1347,7 +1326,6 @@ def attrs(
field_transformer=None,
match_args=True,
unsafe_hash=None,
force_kw_only=True,
):
r"""
A class decorator that adds :term:`dunder methods` according to the
@@ -1414,10 +1392,6 @@ def attrs(
If a class has an *inherited* classmethod called
``__attrs_init_subclass__``, it is executed after the class is created.
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
.. versionchanged:: 25.4.0
*kw_only* now only applies to attributes defined in the current class,
and respects attribute-level ``kw_only=False`` settings.
.. versionadded:: 25.4.0 *force_kw_only*
"""
if repr_ns is not None:
import warnings
@@ -1439,7 +1413,6 @@ def attrs(
on_setattr = setters.pipe(*on_setattr)
def wrap(cls):
nonlocal hash
is_frozen = frozen or _has_frozen_base_class(cls)
is_exc = auto_exc is True and issubclass(cls, BaseException)
has_own_setattr = auto_detect and _has_own_attribute(
@@ -1450,112 +1423,84 @@ def attrs(
msg = "Can't freeze a class with a custom __setattr__."
raise ValueError(msg)
eq = not is_exc and _determine_whether_to_implement(
cls, eq_, auto_detect, ("__eq__", "__ne__")
)
Hashability = ClassProps.Hashability
if is_exc:
hashability = Hashability.LEAVE_ALONE
elif hash is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif hash is False:
hashability = Hashability.LEAVE_ALONE
elif hash is None:
if auto_detect is True and _has_own_attribute(cls, "__hash__"):
hashability = Hashability.LEAVE_ALONE
elif eq is True and is_frozen is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif eq is False:
hashability = Hashability.LEAVE_ALONE
else:
hashability = Hashability.UNHASHABLE
else:
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)
KeywordOnly = ClassProps.KeywordOnly
if kw_only:
kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES
else:
kwo = KeywordOnly.NO
props = ClassProps(
is_exception=is_exc,
is_frozen=is_frozen,
is_slotted=slots,
collected_fields_by_mro=collect_by_mro,
added_init=_determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
),
added_repr=_determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
),
added_eq=eq,
added_ordering=not is_exc
and _determine_whether_to_implement(
builder = _ClassBuilder(
cls,
order_,
auto_detect,
("__lt__", "__le__", "__gt__", "__ge__"),
),
hashability=hashability,
added_match_args=match_args,
kw_only=kwo,
has_weakref_slot=weakref_slot,
added_str=str,
added_pickling=_determine_whether_to_implement(
these,
slots,
is_frozen,
weakref_slot,
_determine_whether_to_implement(
cls,
getstate_setstate,
auto_detect,
("__getstate__", "__setstate__"),
default=slots,
),
on_setattr_hook=on_setattr,
field_transformer=field_transformer,
auto_attribs,
kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_own_setattr,
field_transformer,
)
if not props.is_hashable and cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
builder = _ClassBuilder(
cls,
these,
auto_attribs=auto_attribs,
props=props,
has_custom_setattr=has_own_setattr,
)
if props.added_repr:
if _determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
):
builder.add_repr(repr_ns)
if props.added_str:
if str is True:
builder.add_str()
if props.added_eq:
eq = _determine_whether_to_implement(
cls, eq_, auto_detect, ("__eq__", "__ne__")
)
if not is_exc and eq is True:
builder.add_eq()
if props.added_ordering:
if not is_exc and _determine_whether_to_implement(
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
):
builder.add_order()
if not frozen:
builder.add_setattr()
if props.is_hashable:
nonlocal hash
if (
hash is None
and auto_detect is True
and _has_own_attribute(cls, "__hash__")
):
hash = False
if hash is not True and hash is not False and hash is not None:
# Can't use `hash in` because 1 == True for example.
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)
if hash is False or (hash is None and eq is False) or is_exc:
# Don't do anything. Should fall back to __object__'s __hash__
# which is by id.
if cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
elif hash is True or (
hash is None and eq is True and is_frozen is True
):
# Build a __hash__ if told so, or if it's safe.
builder.add_hash()
elif props.hashability is Hashability.UNHASHABLE:
else:
# Raise TypeError on attempts to hash.
if cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
builder.make_unhashable()
if props.added_init:
if _determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
):
builder.add_init()
else:
builder.add_attrs_init()
@@ -1890,16 +1835,16 @@ def _add_repr(cls, ns=None, attrs=None):
def fields(cls):
"""
Return the tuple of *attrs* attributes for a class or instance.
Return the tuple of *attrs* attributes for a class.
The tuple also allows accessing the fields by their names (see below for
examples).
Args:
cls (type): Class or instance to introspect.
cls (type): Class to introspect.
Raises:
TypeError: If *cls* is neither a class nor an *attrs* instance.
TypeError: If *cls* is not a class.
attrs.exceptions.NotAnAttrsClassError:
If *cls* is not an *attrs* class.
@@ -1910,18 +1855,13 @@ def fields(cls):
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
by name.
.. versionchanged:: 23.1.0 Add support for generic classes.
.. versionchanged:: 26.1.0 Add support for instances.
"""
generic_base = get_generic_base(cls)
if generic_base is None and not isinstance(cls, type):
type_ = type(cls)
if getattr(type_, "__attrs_attrs__", None) is None:
msg = "Passed object must be a class or attrs instance."
msg = "Passed object must be a class."
raise TypeError(msg)
return fields(type_)
attrs = getattr(cls, "__attrs_attrs__", None)
if attrs is None:
@@ -2027,7 +1967,7 @@ def _make_init_script(
attr_dict[a.name] = a
if a.on_setattr is not None:
if frozen is True and a.on_setattr is not setters.NO_OP:
if frozen is True:
msg = "Frozen classes can't use on_setattr."
raise ValueError(msg)
@@ -2186,9 +2126,8 @@ def _attrs_to_init_script(
)
lines.extend(extra_lines)
args = [] # Parameters in the definition of __init__
pre_init_args = [] # Parameters in the call to __attrs_pre_init__
kw_only_args = [] # Used for both 'args' and 'pre_init_args' above
args = []
kw_only_args = []
attrs_to_validate = []
# This is a dictionary of names to validator and converter callables.
@@ -2266,7 +2205,6 @@ def _attrs_to_init_script(
kw_only_args.append(arg)
else:
args.append(arg)
pre_init_args.append(arg_name)
if converter is not None:
lines.append(
@@ -2286,7 +2224,6 @@ def _attrs_to_init_script(
kw_only_args.append(arg)
else:
args.append(arg)
pre_init_args.append(arg_name)
lines.append(f"if {arg_name} is not NOTHING:")
init_factory_name = _INIT_FACTORY_PAT % (a.name,)
@@ -2329,7 +2266,6 @@ def _attrs_to_init_script(
kw_only_args.append(arg_name)
else:
args.append(arg_name)
pre_init_args.append(arg_name)
if converter is not None:
lines.append(
@@ -2386,7 +2322,7 @@ def _attrs_to_init_script(
lines.append(f"BaseException.__init__(self, {vals})")
args = ", ".join(args)
pre_init_args = ", ".join(pre_init_args)
pre_init_args = args
if kw_only_args:
# leading comma & kw_only args
args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}"
@@ -2401,7 +2337,7 @@ def _attrs_to_init_script(
pre_init_args += pre_init_kw_only_args
if call_pre_init and pre_init_has_args:
# If pre init method has arguments, pass the values given to __init__.
# If pre init method has arguments, pass same arguments as `__init__`.
lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
# Python <3.12 doesn't allow backslashes in f-strings.
@@ -2440,8 +2376,6 @@ class Attribute:
- ``name`` (`str`): The name of the attribute.
- ``alias`` (`str`): The __init__ parameter name of the attribute, after
any explicit overrides and default private-attribute-name handling.
- ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically
generated (``True``) or explicitly provided by the user (``False``).
- ``inherited`` (`bool`): Whether or not that attribute has been inherited
from a base class.
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
@@ -2467,7 +2401,6 @@ class Attribute:
equality checks and hashing anymore.
.. versionadded:: 21.1.0 *eq_key* and *order_key*
.. versionadded:: 22.2.0 *alias*
.. versionadded:: 26.1.0 *alias_is_default*
For the full version history of the fields, see `attr.ib`.
"""
@@ -2492,7 +2425,6 @@ class Attribute:
"inherited",
"on_setattr",
"alias",
"alias_is_default",
)
def __init__(
@@ -2515,7 +2447,6 @@ class Attribute:
order_key=None,
on_setattr=None,
alias=None,
alias_is_default=None,
):
eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq_key or eq, order_key or order, True
@@ -2550,20 +2481,12 @@ class Attribute:
bound_setattr("inherited", inherited)
bound_setattr("on_setattr", on_setattr)
bound_setattr("alias", alias)
bound_setattr(
"alias_is_default",
alias is None if alias_is_default is None else alias_is_default,
)
def __setattr__(self, name, value):
raise FrozenInstanceError
@classmethod
def from_counting_attr(
cls, name: str, ca: _CountingAttr, kw_only: bool, type=None
):
# The 'kw_only' argument is the class-level setting, and is used if the
# attribute itself does not explicitly set 'kw_only'.
def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
# type holds the annotated value. deal with conflicts:
if type is None:
type = ca.type
@@ -2582,14 +2505,13 @@ class Attribute:
ca.metadata,
type,
ca.converter,
kw_only if ca.kw_only is None else ca.kw_only,
ca.kw_only,
ca.eq,
ca.eq_key,
ca.order,
ca.order_key,
ca.on_setattr,
ca.alias,
ca.alias is None,
)
# Don't use attrs.evolve since fields(Attribute) doesn't work
@@ -2608,20 +2530,6 @@ class Attribute:
new._setattrs(changes.items())
if "alias" in changes and "alias_is_default" not in changes:
# Explicit alias provided -- no longer the default.
_OBJ_SETATTR.__get__(new)("alias_is_default", False)
elif (
"name" in changes
and "alias" not in changes
# Don't auto-generate alias if the user picked picked the old one.
and self.alias_is_default
):
# Name changed, alias was auto-generated -- update it.
_OBJ_SETATTR.__get__(new)(
"alias", _default_init_alias_for(new.name)
)
return new
# Don't use _add_pickle since fields(Attribute) doesn't work
@@ -2638,17 +2546,6 @@ class Attribute:
"""
Play nice with pickle.
"""
if len(state) < len(self.__slots__):
# Pre-26.1.0 pickle without alias_is_default -- infer it
# heuristically.
state_dict = dict(zip(self.__slots__, state))
alias_is_default = state_dict.get(
"alias"
) is None or state_dict.get("alias") == _default_init_alias_for(
state_dict["name"]
)
state = (*state, alias_is_default)
self._setattrs(zip(self.__slots__, state))
def _setattrs(self, name_values_pairs):
@@ -2672,7 +2569,7 @@ _a = [
name=name,
default=NOTHING,
validator=None,
repr=(name != "alias_is_default"),
repr=True,
cmp=None,
eq=True,
order=False,
@@ -2844,188 +2741,6 @@ class _CountingAttr:
_CountingAttr = _add_eq(_add_repr(_CountingAttr))
class ClassProps:
"""
Effective class properties as derived from parameters to `attr.s()` or
`define()` decorators.
This is the same data structure that *attrs* uses internally to decide how
to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
is_exception (bool):
Whether the class is treated as an exception class.
is_slotted (bool):
Whether the class is `slotted <slotted classes>`.
has_weakref_slot (bool):
Whether the class has a slot for weak references.
is_frozen (bool):
Whether the class is frozen.
kw_only (KeywordOnly):
Whether / how the class enforces keyword-only arguments on the
``__init__`` method.
collected_fields_by_mro (bool):
Whether the class fields were collected by method resolution order.
That is, correctly but unlike `dataclasses`.
added_init (bool):
Whether the class has an *attrs*-generated ``__init__`` method.
added_repr (bool):
Whether the class has an *attrs*-generated ``__repr__`` method.
added_eq (bool):
Whether the class has *attrs*-generated equality methods.
added_ordering (bool):
Whether the class has *attrs*-generated ordering methods.
hashability (Hashability): How `hashable <hashing>` the class is.
added_match_args (bool):
Whether the class supports positional `match <match>` over its
fields.
added_str (bool):
Whether the class has an *attrs*-generated ``__str__`` method.
added_pickling (bool):
Whether the class has *attrs*-generated ``__getstate__`` and
``__setstate__`` methods for `pickle`.
on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None):
The class's ``__setattr__`` hook.
field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None):
The class's `field transformers <transform-fields>`.
.. versionadded:: 25.4.0
"""
class Hashability(enum.Enum):
"""
The hashability of a class.
.. versionadded:: 25.4.0
"""
HASHABLE = "hashable"
"""Write a ``__hash__``."""
HASHABLE_CACHED = "hashable_cache"
"""Write a ``__hash__`` and cache the hash."""
UNHASHABLE = "unhashable"
"""Set ``__hash__`` to ``None``."""
LEAVE_ALONE = "leave_alone"
"""Don't touch ``__hash__``."""
class KeywordOnly(enum.Enum):
"""
How attributes should be treated regarding keyword-only parameters.
.. versionadded:: 25.4.0
"""
NO = "no"
"""Attributes are not keyword-only."""
YES = "yes"
"""Attributes in current class without kw_only=False are keyword-only."""
FORCE = "force"
"""All attributes are keyword-only."""
__slots__ = ( # noqa: RUF023 -- order matters for __init__
"is_exception",
"is_slotted",
"has_weakref_slot",
"is_frozen",
"kw_only",
"collected_fields_by_mro",
"added_init",
"added_repr",
"added_eq",
"added_ordering",
"hashability",
"added_match_args",
"added_str",
"added_pickling",
"on_setattr_hook",
"field_transformer",
)
def __init__(
self,
is_exception,
is_slotted,
has_weakref_slot,
is_frozen,
kw_only,
collected_fields_by_mro,
added_init,
added_repr,
added_eq,
added_ordering,
hashability,
added_match_args,
added_str,
added_pickling,
on_setattr_hook,
field_transformer,
):
self.is_exception = is_exception
self.is_slotted = is_slotted
self.has_weakref_slot = has_weakref_slot
self.is_frozen = is_frozen
self.kw_only = kw_only
self.collected_fields_by_mro = collected_fields_by_mro
self.added_init = added_init
self.added_repr = added_repr
self.added_eq = added_eq
self.added_ordering = added_ordering
self.hashability = hashability
self.added_match_args = added_match_args
self.added_str = added_str
self.added_pickling = added_pickling
self.on_setattr_hook = on_setattr_hook
self.field_transformer = field_transformer
@property
def is_hashable(self):
return (
self.hashability is ClassProps.Hashability.HASHABLE
or self.hashability is ClassProps.Hashability.HASHABLE_CACHED
)
_cas = [
Attribute(
name=name,
default=NOTHING,
validator=None,
repr=True,
cmp=None,
eq=True,
order=False,
hash=True,
init=True,
inherited=False,
alias=_default_init_alias_for(name),
)
for name in ClassProps.__slots__
]
ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas)
class Factory:
"""
Stores a factory callable.
@@ -3133,7 +2848,9 @@ class Converter:
value, field
)
else:
self.__call__ = self.converter
self.__call__ = lambda value, instance, field: self.converter(
value, instance, field
)
rt = ex.get_return_type()
if rt is not None:
+7 -58
View File
@@ -17,7 +17,7 @@ from ._make import (
attrib,
attrs,
)
from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError
from .exceptions import UnannotatedAttributeError
def define(
@@ -43,7 +43,6 @@ def define(
on_setattr=None,
field_transformer=None,
match_args=True,
force_kw_only=False,
):
r"""
A class decorator that adds :term:`dunder methods` according to
@@ -77,7 +76,7 @@ def define(
sensible ``__ne__`` by default, so it *should* be enough to only
implement ``__eq__`` in most cases).
Passing :data:`True` or :data:`False` to *init*, *repr*, *eq*, or *hash*
Passing True or False` to *init*, *repr*, *eq*, or *hash*
overrides whatever *auto_detect* would determine.
auto_exc (bool):
@@ -215,12 +214,8 @@ def define(
5. Subclasses of a frozen class are frozen too.
kw_only (bool):
Make attributes keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored). Attributes that
explicitly set ``kw_only=False`` are not affected; base class
attributes are also not affected.
Also see *force_kw_only*.
Make all attributes keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored).
weakref_slot (bool):
Make instances weak-referenceable. This has no effect unless
@@ -249,15 +244,6 @@ def define(
See also `issue #428
<https://github.com/python-attrs/attrs/issues/428>`_.
force_kw_only (bool):
A back-compat flag for restoring pre-25.4.0 behavior. If True and
``kw_only=True``, all attributes are made keyword-only, including
base class attributes, and those set to ``kw_only=False`` at the
attribute level. Defaults to False.
See also `issue #980
<https://github.com/python-attrs/attrs/issues/980>`_.
getstate_setstate (bool | None):
.. note::
@@ -333,11 +319,6 @@ def define(
.. versionadded:: 24.3.0
Unless already present, a ``__replace__`` method is automatically
created for `copy.replace` (Python 3.13+ only).
.. versionchanged:: 25.4.0
*kw_only* now only applies to attributes defined in the current class,
and respects attribute-level ``kw_only=False`` settings.
.. versionadded:: 25.4.0
Added *force_kw_only* to go back to the previous *kw_only* behavior.
.. note::
@@ -356,7 +337,6 @@ def define(
- *auto_exc=True*
- *auto_detect=True*
- *order=False*
- *force_kw_only=False*
- Some options that were only relevant on Python 2 or were kept around
for backwards-compatibility have been removed.
@@ -386,7 +366,6 @@ def define(
on_setattr=on_setattr,
field_transformer=field_transformer,
match_args=match_args,
force_kw_only=force_kw_only,
)
def wrap(cls):
@@ -445,7 +424,7 @@ def field(
type=None,
converter=None,
factory=None,
kw_only=None,
kw_only=False,
eq=None,
order=None,
on_setattr=None,
@@ -571,10 +550,9 @@ def field(
itself. You can use it as part of your own code or for `static type
checking <types>`.
kw_only (bool | None):
kw_only (bool):
Make this attribute keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored). If None (default),
mirror the setting from `attrs.define`.
``init`` is False, this parameter is ignored).
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]):
Allows to overwrite the *on_setattr* setting from `attr.s`. If left
@@ -594,9 +572,6 @@ def field(
.. versionadded:: 23.1.0
The *type* parameter has been re-added; mostly for `attrs.make_class`.
Please note that type checkers ignore this metadata.
.. versionchanged:: 25.4.0
*kw_only* can now be None, and its default is also changed from False to
None.
.. seealso::
@@ -646,29 +621,3 @@ def astuple(inst, *, recurse=True, filter=None):
return _astuple(
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
)
def inspect(cls):
"""
Inspect the class and return its effective build parameters.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Args:
cls: The *attrs*-decorated class to inspect.
Returns:
The effective build parameters of the class.
Raises:
NotAnAttrsClassError: If the class is not an *attrs*-decorated class.
.. versionadded:: 25.4.0
"""
try:
return cls.__dict__["__attrs_props__"]
except KeyError:
msg = f"{cls!r} is not an attrs-decorated class."
raise NotAnAttrsClassError(msg) from None
-3
View File
@@ -84,6 +84,3 @@ class VersionInfo:
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
# have to do anything special with releaselevel for now.
return us < them
def __hash__(self):
return hash((self.year, self.minor, self.micro, self.releaselevel))
+3 -3
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import ClassVar
class FrozenError(AttributeError):
"""
@@ -14,10 +16,8 @@ class FrozenError(AttributeError):
.. versionadded:: 20.1.0
"""
def __init__(self):
msg = "can't set attribute"
super().__init__(msg)
self.msg = msg
args: ClassVar[tuple[str]] = [msg]
class FrozenInstanceError(FrozenError):
+12 -52
View File
@@ -79,14 +79,12 @@ def disabled():
This context manager is not thread-safe!
.. versionadded:: 21.3.0
.. versionchanged:: 26.1.0 The contextmanager is nestable.
"""
prev = get_run_validators()
set_run_validators(False)
try:
yield
finally:
set_run_validators(prev)
set_run_validators(True)
@attrs(repr=False, slots=True, unsafe_hash=True)
@@ -363,32 +361,26 @@ def deep_iterable(member_validator, iterable_validator=None):
A validator that performs deep validation of an iterable.
Args:
member_validator: Validator(s) to apply to iterable members.
member_validator: Validator to apply to iterable members.
iterable_validator:
Validator(s) to apply to iterable itself (optional).
Validator to apply to iterable itself (optional).
Raises
TypeError: if any sub-validators fail
.. versionadded:: 19.1.0
.. versionchanged:: 25.4.0
*member_validator* and *iterable_validator* can now be a list or tuple
of validators.
"""
if isinstance(member_validator, (list, tuple)):
member_validator = and_(*member_validator)
if isinstance(iterable_validator, (list, tuple)):
iterable_validator = and_(*iterable_validator)
return _DeepIterable(member_validator, iterable_validator)
@attrs(repr=False, slots=True, unsafe_hash=True)
class _DeepMapping:
key_validator = attrib(validator=optional(is_callable()))
value_validator = attrib(validator=optional(is_callable()))
mapping_validator = attrib(validator=optional(is_callable()))
key_validator = attrib(validator=is_callable())
value_validator = attrib(validator=is_callable())
mapping_validator = attrib(default=None, validator=optional(is_callable()))
def __call__(self, inst, attr, value):
"""
@@ -398,62 +390,30 @@ class _DeepMapping:
self.mapping_validator(inst, attr, value)
for key in value:
if self.key_validator is not None:
self.key_validator(inst, attr, key)
if self.value_validator is not None:
self.value_validator(inst, attr, value[key])
def __repr__(self):
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
def deep_mapping(
key_validator=None, value_validator=None, mapping_validator=None
):
def deep_mapping(key_validator, value_validator, mapping_validator=None):
"""
A validator that performs deep validation of a dictionary.
All validators are optional, but at least one of *key_validator* or
*value_validator* must be provided.
Args:
key_validator: Validator(s) to apply to dictionary keys.
key_validator: Validator to apply to dictionary keys.
value_validator: Validator(s) to apply to dictionary values.
value_validator: Validator to apply to dictionary values.
mapping_validator:
Validator(s) to apply to top-level mapping attribute.
Validator to apply to top-level mapping attribute (optional).
.. versionadded:: 19.1.0
.. versionchanged:: 25.4.0
*key_validator* and *value_validator* are now optional, but at least one
of them must be provided.
.. versionchanged:: 25.4.0
*key_validator*, *value_validator*, and *mapping_validator* can now be a
list or tuple of validators.
Raises:
TypeError: If any sub-validator fails on validation.
ValueError:
If neither *key_validator* nor *value_validator* is provided on
instantiation.
TypeError: if any sub-validators fail
"""
if key_validator is None and value_validator is None:
msg = (
"At least one of key_validator or value_validator must be provided"
)
raise ValueError(msg)
if isinstance(key_validator, (list, tuple)):
key_validator = and_(*key_validator)
if isinstance(value_validator, (list, tuple)):
value_validator = and_(*value_validator)
if isinstance(mapping_validator, (list, tuple)):
mapping_validator = and_(*mapping_validator)
return _DeepMapping(key_validator, value_validator, mapping_validator)
@@ -525,7 +485,7 @@ def gt(val):
A validator that raises `ValueError` if the initializer is called with a
number smaller or equal to *val*.
The validator uses `operator.gt` to compare the values.
The validator uses `operator.ge` to compare the values.
Args:
val: Exclusive lower bound for values
+6 -60
View File
@@ -20,9 +20,6 @@ _T = TypeVar("_T")
_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_T3 = TypeVar("_T3")
_T4 = TypeVar("_T4")
_T5 = TypeVar("_T5")
_T6 = TypeVar("_T6")
_I = TypeVar("_I", bound=Iterable)
_K = TypeVar("_K")
_V = TypeVar("_V")
@@ -54,7 +51,7 @@ def optional(
validator: (
_ValidatorType[_T]
| list[_ValidatorType[_T]]
| tuple[_ValidatorType[_T], ...]
| tuple[_ValidatorType[_T]]
),
) -> _ValidatorType[_T | None]: ...
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
@@ -66,19 +63,12 @@ def matches_re(
) -> _ValidatorType[AnyStr]: ...
def deep_iterable(
member_validator: _ValidatorArgType[_T],
iterable_validator: _ValidatorArgType[_I] | None = ...,
iterable_validator: _ValidatorType[_I] | None = ...,
) -> _ValidatorType[_I]: ...
@overload
def deep_mapping(
key_validator: _ValidatorArgType[_K],
value_validator: _ValidatorArgType[_V] | None = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
@overload
def deep_mapping(
key_validator: _ValidatorArgType[_K] | None = ...,
value_validator: _ValidatorArgType[_V] = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
key_validator: _ValidatorType[_K],
value_validator: _ValidatorType[_V],
mapping_validator: _ValidatorType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
def is_callable() -> _ValidatorType[_T]: ...
def lt(val: _T) -> _ValidatorType[_T]: ...
@@ -93,48 +83,4 @@ def not_(
msg: str | None = None,
exc_types: type[Exception] | Iterable[type[Exception]] = ...,
) -> _ValidatorType[_T]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
) -> _ValidatorType[_T1 | _T2]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
) -> _ValidatorType[_T1 | _T2 | _T3]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
__v4: _ValidatorType[_T4],
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
__v4: _ValidatorType[_T4],
__v5: _ValidatorType[_T5],
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
__v4: _ValidatorType[_T4],
__v5: _ValidatorType[_T5],
__v6: _ValidatorType[_T6],
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ...
@overload
def or_(
__v1: _ValidatorType[Any],
__v2: _ValidatorType[Any],
__v3: _ValidatorType[Any],
__v4: _ValidatorType[Any],
__v5: _ValidatorType[Any],
__v6: _ValidatorType[Any],
*validators: _ValidatorType[Any],
) -> _ValidatorType[Any]: ...
def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
@@ -0,0 +1 @@
pip
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: attrs
Version: 26.1.0
Version: 25.3.0
Summary: Classes Without Boilerplate
Project-URL: Documentation, https://www.attrs.org/
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
@@ -12,16 +12,62 @@ License-Expression: MIT
License-File: LICENSE
Keywords: attribute,boilerplate,class
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Python: >=3.8
Provides-Extra: benchmark
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'benchmark'
Requires-Dist: hypothesis; extra == 'benchmark'
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
Requires-Dist: pympler; extra == 'benchmark'
Requires-Dist: pytest-codspeed; extra == 'benchmark'
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
Requires-Dist: pytest-xdist[psutil]; extra == 'benchmark'
Requires-Dist: pytest>=4.3.0; extra == 'benchmark'
Provides-Extra: cov
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'cov'
Requires-Dist: coverage[toml]>=5.3; extra == 'cov'
Requires-Dist: hypothesis; extra == 'cov'
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
Requires-Dist: pympler; extra == 'cov'
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
Requires-Dist: pytest-xdist[psutil]; extra == 'cov'
Requires-Dist: pytest>=4.3.0; extra == 'cov'
Provides-Extra: dev
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'dev'
Requires-Dist: hypothesis; extra == 'dev'
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
Requires-Dist: pre-commit-uv; extra == 'dev'
Requires-Dist: pympler; extra == 'dev'
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
Requires-Dist: pytest-xdist[psutil]; extra == 'dev'
Requires-Dist: pytest>=4.3.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: cogapp; extra == 'docs'
Requires-Dist: furo; extra == 'docs'
Requires-Dist: myst-parser; extra == 'docs'
Requires-Dist: sphinx; extra == 'docs'
Requires-Dist: sphinx-notfound-page; extra == 'docs'
Requires-Dist: sphinxcontrib-towncrier; extra == 'docs'
Requires-Dist: towncrier; extra == 'docs'
Provides-Extra: tests
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'tests'
Requires-Dist: hypothesis; extra == 'tests'
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
Requires-Dist: pympler; extra == 'tests'
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
Requires-Dist: pytest-xdist[psutil]; extra == 'tests'
Requires-Dist: pytest>=4.3.0; extra == 'tests'
Provides-Extra: tests-mypy
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
Description-Content-Type: text/markdown
<p align="center">
@@ -32,7 +78,7 @@ Description-Content-Type: text/markdown
*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)).
Trusted by NASA for [Mars missions since 2020](https://github.com/readme/featured/nasa-ingenuity-helicopter)!
[Trusted by NASA](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-achievement) for Mars missions since 2020!
Its main goal is to help you to write **concise** and **correct** software without slowing down your code.
@@ -50,15 +96,15 @@ Especially those generously supporting us at the *The Organization* tier and hig
import pathlib, tomllib
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]:
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/26.1.0/_static/sponsors/{sponsor["img"]}" width="190" /></a>')
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/25.3.0/_static/sponsors/{sponsor["img"]}" width="190" /></a>')
]]] -->
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Variomedia.svg" width="190" /></a>
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Tidelift.svg" width="190" /></a>
<a href="https://kraken.tech/"><img title="Kraken Tech" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Kraken.svg" width="190" /></a>
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/26.1.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
<a href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=structlog"><img title="TestMu AI" src="https://www.attrs.org/en/26.1.0/_static/sponsors/TestMu-AI.svg" width="190" /></a>
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Polar.svg" width="190" /></a>
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Variomedia.svg" width="190" /></a>
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Tidelift.svg" width="190" /></a>
<a href="https://klaviyo.com/"><img title="Klaviyo" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Klaviyo.svg" width="190" /></a>
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
<a href="https://www.emsys-renewables.com/"><img title="emsys renewables" src="https://www.attrs.org/en/25.3.0/_static/sponsors/emsys-renewables.svg" width="190" /></a>
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/25.3.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Polar.svg" width="190" /></a>
<!-- [[[end]]] -->
</p>
@@ -174,23 +220,10 @@ Save time, reduce risk, and improve code health, while paying the maintainers of
## Release Information
### Backwards-incompatible Changes
- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`.
The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`).
[#1509](https://github.com/python-attrs/attrs/issues/1509)
### Changes
- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator.
[#1496](https://github.com/python-attrs/attrs/issues/1496)
- The `attrs.validators.disabled()` contextmanager can now be nested.
[#1513](https://github.com/python-attrs/attrs/issues/1513)
- Frozen classes can set `on_setattr=attrs.setters.NO_OP` in addition to `None`.
[#1515](https://github.com/python-attrs/attrs/issues/1515)
- It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`.
[#1529](https://github.com/python-attrs/attrs/issues/1529)
- Restore support for generator-based `field_transformer`s.
[#1417](https://github.com/python-attrs/attrs/issues/1417)
+56
View File
@@ -0,0 +1,56 @@
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
attr/__init__.pyi,sha256=QIXnnHPoucmDWkbpNsWTP-cgJ1bn8le7DjyRa_wYdew,11281
attr/__pycache__/__init__.cpython-311.pyc,,
attr/__pycache__/_cmp.cpython-311.pyc,,
attr/__pycache__/_compat.cpython-311.pyc,,
attr/__pycache__/_config.cpython-311.pyc,,
attr/__pycache__/_funcs.cpython-311.pyc,,
attr/__pycache__/_make.cpython-311.pyc,,
attr/__pycache__/_next_gen.cpython-311.pyc,,
attr/__pycache__/_version_info.cpython-311.pyc,,
attr/__pycache__/converters.cpython-311.pyc,,
attr/__pycache__/exceptions.cpython-311.pyc,,
attr/__pycache__/filters.cpython-311.pyc,,
attr/__pycache__/setters.cpython-311.pyc,,
attr/__pycache__/validators.cpython-311.pyc,,
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
attr/_compat.py,sha256=4hlXbWhdDjQCDK6FKF1EgnZ3POiHgtpp54qE0nxaGHg,2704
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
attr/_funcs.py,sha256=5-tUKJtp3h5El55EcDl6GWXFp68fT8D8U7uCRN6497I,15854
attr/_make.py,sha256=lBUPPmxiA1BeHzB6OlHoCEh--tVvM1ozXO8eXOa6g4c,96664
attr/_next_gen.py,sha256=7FRkbtl_N017SuBhf_Vw3mw2c2pGZhtCGOzadgz7tp4,24395
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
attr/_version_info.py,sha256=exSqb3b5E-fMSsgZAlEw9XcLpEgobPORCZpcaEglAM4,2121
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
attr/exceptions.py,sha256=HRFq4iybmv7-DcZwyjl6M1euM2YeJVK_hFxuaBGAngI,1977
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
attr/validators.py,sha256=WaB1HLAHHqRHWsrv_K9H-sJ7ESil3H3Cmv2d8TtVZx4,20046
attr/validators.pyi,sha256=s2WhKPqskxbsckJfKk8zOuuB088GfgpyxcCYSNFLqNU,2603
attrs-25.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
attrs-25.3.0.dist-info/METADATA,sha256=W38cREj7s1wqNf1fg4hVwZmL1xh0AdSp4IhtTMROinw,10993
attrs-25.3.0.dist-info/RECORD,,
attrs-25.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs-25.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
attrs-25.3.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
attrs/__init__.py,sha256=qeQJZ4O08yczSn840v9bYOaZyRE81WsVi-QCrY3krCU,1107
attrs/__init__.pyi,sha256=nZmInocjM7tHV4AQw0vxO_fo6oJjL_PonlV9zKKW8DY,7931
attrs/__pycache__/__init__.cpython-311.pyc,,
attrs/__pycache__/converters.cpython-311.pyc,,
attrs/__pycache__/exceptions.cpython-311.pyc,,
attrs/__pycache__/filters.cpython-311.pyc,,
attrs/__pycache__/setters.cpython-311.pyc,,
attrs/__pycache__/validators.cpython-311.pyc,,
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
@@ -1,4 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.29.0
Generator: hatchling 1.27.0
Root-Is-Purelib: true
Tag: py3-none-any
@@ -1 +0,0 @@
uv
-37
View File
@@ -1,37 +0,0 @@
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
attr/__init__.pyi,sha256=pVGImAUVovq2_TYl_r_HIYnGlyOaoCuEhxo-EvsnnSc,11325
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
attr/_compat.py,sha256=x0g7iEUOnBVJC72zyFCgb1eKqyxS-7f2LGnNyZ_r95s,2829
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
attr/_funcs.py,sha256=Ix5IETTfz5F01F-12MF_CSFomIn2h8b67EVVz2gCtBE,16479
attr/_make.py,sha256=H7OH2eWS5CnBzLUjNFE1WymfPrmF1r8fv2RPdt9MuYA,106129
attr/_next_gen.py,sha256=BQtCUlzwg2gWHTYXBQvrEYBnzBUrDvO57u0Py6UCPhc,26274
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
attr/_version_info.py,sha256=w4R-FYC3NK_kMkGUWJlYP4cVAlH9HRaC-um3fcjYkHM,2222
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
attr/exceptions.py,sha256=b4vMbnoQ3VpwWZhqrYi_ssXVCK8o2c4HQSS09cSUM9o,1990
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
attr/validators.py,sha256=m3QRzZTANr4f2C4eVdUoFg11NgXWak8Wat4qQTGhvcs,21553
attr/validators.pyi,sha256=gM1ZmHaBckyYWI2EirpRNzqm3B19cw5Iq6B4Kno9YCM,4087
attrs-26.1.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
attrs-26.1.0.dist-info/METADATA,sha256=TNQOaQ8jvzfLytNO_WdY4GLfHfB8hoM_fjzpW_H6OMw,8754
attrs-26.1.0.dist-info/RECORD,,
attrs-26.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs-26.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
attrs-26.1.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
attrs/__init__.py,sha256=RxaAZNwYiEh-fcvHLZNpQ_DWKni73M_jxEPEftiq1Zc,1183
attrs/__init__.pyi,sha256=2gV79g9UxJppGSM48hAZJ6h_MHb70dZoJL31ZNJeZYI,9416
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
+1 -4
View File
@@ -22,8 +22,7 @@ from attr import (
resolve_types,
validate,
)
from attr._make import ClassProps
from attr._next_gen import asdict, astuple, inspect
from attr._next_gen import asdict, astuple
from . import converters, exceptions, filters, setters, validators
@@ -32,7 +31,6 @@ __all__ = [
"NOTHING",
"Attribute",
"AttrsInstance",
"ClassProps",
"Converter",
"Factory",
"NothingType",
@@ -60,7 +58,6 @@ __all__ = [
"filters",
"frozen",
"has",
"inspect",
"make_class",
"mutable",
"resolve_types",
+7 -58
View File
@@ -77,7 +77,7 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: bool | None = ...,
order: bool | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -98,10 +98,10 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType
| list[_ConverterType]
| tuple[_ConverterType, ...]
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -121,10 +121,10 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType
| list[_ConverterType]
| tuple[_ConverterType, ...]
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -144,10 +144,10 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType
| list[_ConverterType]
| tuple[_ConverterType, ...]
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ...,
kw_only: bool = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -261,54 +261,3 @@ def frozen(
field_transformer: _FieldTransformer | None = ...,
match_args: bool = ...,
) -> Callable[[_C], _C]: ...
class ClassProps:
# XXX: somehow when defining/using enums Mypy starts looking at our own
# (untyped) code and causes tons of errors.
Hashability: Any
KeywordOnly: Any
is_exception: bool
is_slotted: bool
has_weakref_slot: bool
is_frozen: bool
# kw_only: ClassProps.KeywordOnly
kw_only: Any
collected_fields_by_mro: bool
added_init: bool
added_repr: bool
added_eq: bool
added_ordering: bool
# hashability: ClassProps.Hashability
hashability: Any
added_match_args: bool
added_str: bool
added_pickling: bool
on_setattr_hook: _OnSetAttrType | None
field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None
def __init__(
self,
is_exception: bool,
is_slotted: bool,
has_weakref_slot: bool,
is_frozen: bool,
# kw_only: ClassProps.KeywordOnly
kw_only: Any,
collected_fields_by_mro: bool,
added_init: bool,
added_repr: bool,
added_eq: bool,
added_ordering: bool,
# hashability: ClassProps.Hashability
hashability: Any,
added_match_args: bool,
added_str: bool,
added_pickling: bool,
on_setattr_hook: _OnSetAttrType,
field_transformer: Callable[[Attribute[Any]], Attribute[Any]],
) -> None: ...
@property
def is_hashable(self) -> bool: ...
def inspect(cls: type) -> ClassProps: ...
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
pip
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: cattrs
Version: 26.1.0
Version: 25.1.1
Summary: Composable complex class support for attrs and dataclasses.
Project-URL: Homepage, https://catt.rs
Project-URL: Changelog, https://catt.rs/en/latest/history.html
@@ -14,18 +14,18 @@ Keywords: attrs,dataclasses,serialization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: attrs>=25.4.0
Requires-Python: >=3.9
Requires-Dist: attrs>=24.3.0
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11'
Requires-Dist: typing-extensions>=4.14.0
Requires-Dist: typing-extensions>=4.12.2
Provides-Extra: bson
Requires-Dist: pymongo>=4.4.0; extra == 'bson'
Provides-Extra: cbor2
@@ -35,14 +35,11 @@ Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
Provides-Extra: msgspec
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
Provides-Extra: orjson
Requires-Dist: orjson>=3.11.3; (implementation_name == 'cpython') and extra == 'orjson'
Requires-Dist: orjson>=3.10.7; (implementation_name == 'cpython') and extra == 'orjson'
Provides-Extra: pyyaml
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
Provides-Extra: tomlkit
Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
Provides-Extra: tomllib
Requires-Dist: tomli-w>=1.1.0; extra == 'tomllib'
Requires-Dist: tomli>=1.1.0; (python_version < '3.11') and extra == 'tomllib'
Provides-Extra: ujson
Requires-Dist: ujson>=5.10.0; extra == 'ujson'
Description-Content-Type: text/markdown
+102
View File
@@ -0,0 +1,102 @@
cattr/__init__.py,sha256=bYrmwTYSdYC_ut1xW31V7mxhXBlJQKs8EECgtUBgAuc,906
cattr/__pycache__/__init__.cpython-311.pyc,,
cattr/__pycache__/converters.cpython-311.pyc,,
cattr/__pycache__/disambiguators.cpython-311.pyc,,
cattr/__pycache__/dispatch.cpython-311.pyc,,
cattr/__pycache__/errors.cpython-311.pyc,,
cattr/__pycache__/gen.cpython-311.pyc,,
cattr/converters.py,sha256=rQhY4J8r7QTZh5WICuFe4GWO1v0DS3DgQ9r569zd6jg,192
cattr/disambiguators.py,sha256=ugD1fq1Z5x1pGu5P1lMzcT-IEi1q7IfQJIHEdmg62vM,103
cattr/dispatch.py,sha256=uVEOgHWR9Hn5tm-wIw-bDccqrxJByVi8yRKaYyvL67k,125
cattr/errors.py,sha256=V4RhoCObwGrlaM3oyn1H_FYxGR8iAB9dG5NxFDYM548,343
cattr/gen.py,sha256=hWyKoZ_d2D36Jz_npspyGw8s9pWtUA69sXf0R3uOvgM,597
cattr/preconf/__init__.py,sha256=NqPE7uhVfcP-PggkUpsbfAutMo8oHjcoB1cvjgLft-s,78
cattr/preconf/__pycache__/__init__.cpython-311.pyc,,
cattr/preconf/__pycache__/bson.cpython-311.pyc,,
cattr/preconf/__pycache__/json.cpython-311.pyc,,
cattr/preconf/__pycache__/msgpack.cpython-311.pyc,,
cattr/preconf/__pycache__/orjson.cpython-311.pyc,,
cattr/preconf/__pycache__/pyyaml.cpython-311.pyc,,
cattr/preconf/__pycache__/tomlkit.cpython-311.pyc,,
cattr/preconf/__pycache__/ujson.cpython-311.pyc,,
cattr/preconf/bson.py,sha256=Bn4hJxac7OthGg_CR4LCPeBp_fz4kx3QniBVOZhguGs,195
cattr/preconf/json.py,sha256=LpqYuO3oePDxbQtKFKB0SaoeAi3Z_agIgyNn1VQSIVo,206
cattr/preconf/msgpack.py,sha256=pyJ9L9ekNlZ0IQHbJ9Ay_fi_NOqY5_rE_q-UnD94-RM,207
cattr/preconf/orjson.py,sha256=Adh-7csx4eqCjx22zipMFgSlDXbR554wvgNHEb8Q5JM,203
cattr/preconf/pyyaml.py,sha256=Fy40bejjp7uqgoLhTA_p4wZYF0uFaguHbUK9zs9LoC0,203
cattr/preconf/tomlkit.py,sha256=_gADJ_UYpj3EiNXGYjAfSOkcoFIkLpYVOFfLEqBfIJQ,207
cattr/preconf/ujson.py,sha256=IzEa7QUcYOaSUMiLQsFEWJnBihmmOLhehsM-5cPY9NI,199
cattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs-25.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
cattrs-25.1.1.dist-info/METADATA,sha256=ODqSak3dhIZZjmFa-SZT8Si32_3ey_oo2tUefYx0QtU,8388
cattrs-25.1.1.dist-info/RECORD,,
cattrs-25.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs-25.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
cattrs-25.1.1.dist-info/licenses/LICENSE,sha256=9fudHt43qIykf0IMSZ3KD0oFvJk-Esd9I1IKrSkcAb8,1074
cattrs/__init__.py,sha256=UhiFdxf81gCuBBA6FutoE1oOzthzF_PkAdoE2AVslIo,1901
cattrs/__pycache__/__init__.cpython-311.pyc,,
cattrs/__pycache__/_compat.cpython-311.pyc,,
cattrs/__pycache__/_generics.cpython-311.pyc,,
cattrs/__pycache__/cols.cpython-311.pyc,,
cattrs/__pycache__/converters.cpython-311.pyc,,
cattrs/__pycache__/disambiguators.cpython-311.pyc,,
cattrs/__pycache__/dispatch.cpython-311.pyc,,
cattrs/__pycache__/errors.cpython-311.pyc,,
cattrs/__pycache__/fns.cpython-311.pyc,,
cattrs/__pycache__/literals.cpython-311.pyc,,
cattrs/__pycache__/typealiases.cpython-311.pyc,,
cattrs/__pycache__/types.cpython-311.pyc,,
cattrs/__pycache__/v.cpython-311.pyc,,
cattrs/_compat.py,sha256=dMRB8a8RkxFdnQDKRpamresVF_SBkksM2_ZMAuL0s2w,11987
cattrs/_generics.py,sha256=keExDE2CGIer8ci12SoJ_rXYTLva9P29uLlvyb_fxtM,966
cattrs/cols.py,sha256=mWDchfvjMQ6uACKSfdZs05YiXrWdph2HOJfaqY3D_EI,8848
cattrs/converters.py,sha256=ui4BSAxnV1J6Oh0pYN2oOHgp-mIybsXW-M8SXPDNzlo,54262
cattrs/disambiguators.py,sha256=eUyWMtW6bQJcXGOWiraq1pFcMlUbo9BJHxFY--KF6Lk,6867
cattrs/dispatch.py,sha256=9qA-pmsPvgrM6MGP8Ev2gVP6YL2rXvoBla_C0VgHxQ0,6780
cattrs/errors.py,sha256=6IGfE-wVQbOaDNN4xAQJf7Hk_t2QNV0Zem64D6yMZrU,4168
cattrs/fns.py,sha256=z5z1VZOZv8t5LwG8cBM_tIXg-_PlQUOyZb9wIrXNqlw,626
cattrs/gen/__init__.py,sha256=bpRGHd3G0UTpWcHRcJTNAUcWmX-evoPHGvpH6u29FDU,38842
cattrs/gen/__pycache__/__init__.cpython-311.pyc,,
cattrs/gen/__pycache__/_consts.cpython-311.pyc,,
cattrs/gen/__pycache__/_generics.cpython-311.pyc,,
cattrs/gen/__pycache__/_lc.cpython-311.pyc,,
cattrs/gen/__pycache__/_shared.cpython-311.pyc,,
cattrs/gen/__pycache__/typeddicts.cpython-311.pyc,,
cattrs/gen/_consts.py,sha256=ZwT_m2J3S7p-UjltpbA1WtfQZLNj9KhmFYCAv6Zl-g0,511
cattrs/gen/_generics.py,sha256=_DyXCGql2QIxGhAv3_B1hsi80uPK8PhK2hhZa95YOlo,3011
cattrs/gen/_lc.py,sha256=4fjeUsmgQcCAIjnNndBic0gf5qKmxVS3CZHqUQ9Rw5g,882
cattrs/gen/_shared.py,sha256=xKsfcVtpyYIir9AW8VuOVoiSbaEI7tsSL0JpUCIUX-g,2296
cattrs/gen/typeddicts.py,sha256=Ck3QMr_B1T7vwxyRjfZPHafKphN2hndL181dpQNxzPs,21254
cattrs/literals.py,sha256=0kzAewmWk9ikJGoKq4ysnAR22DMawG3iNqLl8NLgpk0,331
cattrs/preconf/__init__.py,sha256=P7czFRcjeN6zBcdwUyeBloniltlJptCa8Yd2uFGlz9w,1527
cattrs/preconf/__pycache__/__init__.cpython-311.pyc,,
cattrs/preconf/__pycache__/bson.cpython-311.pyc,,
cattrs/preconf/__pycache__/cbor2.cpython-311.pyc,,
cattrs/preconf/__pycache__/json.cpython-311.pyc,,
cattrs/preconf/__pycache__/msgpack.cpython-311.pyc,,
cattrs/preconf/__pycache__/msgspec.cpython-311.pyc,,
cattrs/preconf/__pycache__/orjson.cpython-311.pyc,,
cattrs/preconf/__pycache__/pyyaml.cpython-311.pyc,,
cattrs/preconf/__pycache__/tomlkit.cpython-311.pyc,,
cattrs/preconf/__pycache__/ujson.cpython-311.pyc,,
cattrs/preconf/bson.py,sha256=6p1kmOFMjswSXFCb1hKJeNvr3kNsAm1gfX_DA6igq8E,4201
cattrs/preconf/cbor2.py,sha256=LnREcjpOp_402poUGRVIhDWI4f_R1wvJkdKvs5MrTGU,2022
cattrs/preconf/json.py,sha256=zTrkfjOxXZFwwabNESeafy-C7MEpn7Cw5AdhkkeOjU4,2631
cattrs/preconf/msgpack.py,sha256=dZE9tsAA5qX3pSc3MZmlGuvJ5q_wI6mANDyugKXKj-E,2325
cattrs/preconf/msgspec.py,sha256=Ds0rPW4900zsBLqfupF-smkg9_Kwyx7D_Vh9a0yJB8M,7250
cattrs/preconf/orjson.py,sha256=5MBcUsyp3eGsHgLfLtt8-q90L2mxjD0ttnrWBUIwouo,3870
cattrs/preconf/pyyaml.py,sha256=w0aM_gJ6VhZf-Zpu_UlJki7rdgv4mfaSXElPofB3nlE,2378
cattrs/preconf/tomlkit.py,sha256=gJWGJjMONCViTMZuphOg2xXzjQt3SCEVFVdoKgDjqc8,3148
cattrs/preconf/ujson.py,sha256=wRLidBM8aWucFkCQ9haiktY8xYoCdanDhQuKJLQJgGM,2425
cattrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs/strategies/__init__.py,sha256=nkZWCzSRYcS-75FMfk52mioZSuWykaN8hB39Vig5Xkg,339
cattrs/strategies/__pycache__/__init__.cpython-311.pyc,,
cattrs/strategies/__pycache__/_class_methods.cpython-311.pyc,,
cattrs/strategies/__pycache__/_subclasses.cpython-311.pyc,,
cattrs/strategies/__pycache__/_unions.cpython-311.pyc,,
cattrs/strategies/_class_methods.py,sha256=O5xhQCzNpuFiDNDMlbcyeOVqyrV65NhMZNRsG3jnoBU,2591
cattrs/strategies/_subclasses.py,sha256=aCE2UQjevZQHMnPOPyl2qR_hgRpgRUt1j9lE4qZ3hNc,9365
cattrs/strategies/_unions.py,sha256=YBBklVSWJ-7DSkLDLpumwAJJ39ALuSGyB6W0Ptz5Rz4,9355
cattrs/typealiases.py,sha256=toHavC2kJsIcxThwvATPO5JShzKeC8kIl9KqteFohbw,1619
cattrs/types.py,sha256=cqvfmzliYfrvPswxlW_tN4DmhQ2xpAKQvVbNBJaxiWs,278
cattrs/v.py,sha256=IqUajgJFCKJYf-4S9TCKRtJcmmK4c3En69TGuf2FKOs,4126
@@ -1,4 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.28.0
Generator: hatchling 1.27.0
Root-Is-Purelib: true
Tag: py3-none-any
@@ -1 +0,0 @@
uv
@@ -1,58 +0,0 @@
cattr/__init__.py,sha256=bYrmwTYSdYC_ut1xW31V7mxhXBlJQKs8EECgtUBgAuc,906
cattr/converters.py,sha256=rQhY4J8r7QTZh5WICuFe4GWO1v0DS3DgQ9r569zd6jg,192
cattr/disambiguators.py,sha256=ugD1fq1Z5x1pGu5P1lMzcT-IEi1q7IfQJIHEdmg62vM,103
cattr/dispatch.py,sha256=uVEOgHWR9Hn5tm-wIw-bDccqrxJByVi8yRKaYyvL67k,125
cattr/errors.py,sha256=V4RhoCObwGrlaM3oyn1H_FYxGR8iAB9dG5NxFDYM548,343
cattr/gen.py,sha256=hWyKoZ_d2D36Jz_npspyGw8s9pWtUA69sXf0R3uOvgM,597
cattr/preconf/__init__.py,sha256=NqPE7uhVfcP-PggkUpsbfAutMo8oHjcoB1cvjgLft-s,78
cattr/preconf/bson.py,sha256=Bn4hJxac7OthGg_CR4LCPeBp_fz4kx3QniBVOZhguGs,195
cattr/preconf/json.py,sha256=LpqYuO3oePDxbQtKFKB0SaoeAi3Z_agIgyNn1VQSIVo,206
cattr/preconf/msgpack.py,sha256=pyJ9L9ekNlZ0IQHbJ9Ay_fi_NOqY5_rE_q-UnD94-RM,207
cattr/preconf/orjson.py,sha256=Adh-7csx4eqCjx22zipMFgSlDXbR554wvgNHEb8Q5JM,203
cattr/preconf/pyyaml.py,sha256=Fy40bejjp7uqgoLhTA_p4wZYF0uFaguHbUK9zs9LoC0,203
cattr/preconf/tomlkit.py,sha256=_gADJ_UYpj3EiNXGYjAfSOkcoFIkLpYVOFfLEqBfIJQ,207
cattr/preconf/ujson.py,sha256=IzEa7QUcYOaSUMiLQsFEWJnBihmmOLhehsM-5cPY9NI,199
cattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs-26.1.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
cattrs-26.1.0.dist-info/METADATA,sha256=d94QudQ0gM0Zy_KNPtL7n2gEea7s-Vir2t4_rLD4zLA,8542
cattrs-26.1.0.dist-info/RECORD,,
cattrs-26.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs-26.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
cattrs-26.1.0.dist-info/licenses/LICENSE,sha256=9fudHt43qIykf0IMSZ3KD0oFvJk-Esd9I1IKrSkcAb8,1074
cattrs/__init__.py,sha256=UhiFdxf81gCuBBA6FutoE1oOzthzF_PkAdoE2AVslIo,1901
cattrs/_compat.py,sha256=5dOpD6O8zUVZcreaJ5wSt9n-Nut4n79ibrka5Td6l84,12251
cattrs/_generics.py,sha256=ERYo_kX-Z6UiwOcYI3WLrlHO6Ya66c_whpko6es5UeI,966
cattrs/cols.py,sha256=-KmFikvZu7cL_g68ytx0Ad6danfjVY5Ry3w1XRtTaWk,10336
cattrs/converters.py,sha256=BPpSPuhMuhQz2qFl_OAeIZ8CrI95A8gy6khuvU35bN8,55322
cattrs/disambiguators.py,sha256=uydD6QXPve1-0dY_3m_FoyOqowvTvEKXzrdb-5xkRPE,6863
cattrs/dispatch.py,sha256=9qA-pmsPvgrM6MGP8Ev2gVP6YL2rXvoBla_C0VgHxQ0,6780
cattrs/enums.py,sha256=noADdwXP9TSpyIyjE6WR2iIbGPmigirvArKAoHWJmDI,1130
cattrs/errors.py,sha256=CLW6Uev31cOxo6cWFb0-g-FAbYJD2mYmR5xTxDIVjDw,4344
cattrs/fns.py,sha256=z5z1VZOZv8t5LwG8cBM_tIXg-_PlQUOyZb9wIrXNqlw,626
cattrs/gen/__init__.py,sha256=wrmi87jGEfmWcbbNkZXk1Gqn9PLwXerKUDJoCQBCRek,42553
cattrs/gen/_consts.py,sha256=ZwT_m2J3S7p-UjltpbA1WtfQZLNj9KhmFYCAv6Zl-g0,511
cattrs/gen/_generics.py,sha256=_DyXCGql2QIxGhAv3_B1hsi80uPK8PhK2hhZa95YOlo,3011
cattrs/gen/_lc.py,sha256=4fjeUsmgQcCAIjnNndBic0gf5qKmxVS3CZHqUQ9Rw5g,882
cattrs/gen/_shared.py,sha256=QctJIsgfhZqgdALdD-L9FUEX1k2uNUpJQW4NbcGLVWQ,2747
cattrs/gen/typeddicts.py,sha256=svG8RjqNt411KzkRFEKp6zsJHJvrwZNe1UQIhQewv7w,22085
cattrs/literals.py,sha256=0kzAewmWk9ikJGoKq4ysnAR22DMawG3iNqLl8NLgpk0,331
cattrs/preconf/__init__.py,sha256=YMY1ADc7OzpuZ2cdQZbH5kUKZF30UYd6owf3BOT4TO0,1435
cattrs/preconf/bson.py,sha256=I0632WE3L7VN5aA9T3PJ1Jq_iy5XOfTsjJcKjQSEMAM,4211
cattrs/preconf/cbor2.py,sha256=o13uyALyX2dvxp6HRYq_xHeB84nEGyBOB0V8OcohY30,2035
cattrs/preconf/json.py,sha256=JiAetUezqQ_ixjRydLCavkvnIQxQCTyQXKzViC-vO6A,2644
cattrs/preconf/msgpack.py,sha256=12c3sE5tfIVVhxHD4QmZaSGmxTvwHfrtTdk0ohOCcLI,2338
cattrs/preconf/msgspec.py,sha256=Hea-jCkcqH7wtONAHjpe9EaaqhGodQFfkLtKuWdV09k,7384
cattrs/preconf/orjson.py,sha256=o1GZwvH2hlLbovmsB7OH3fDkcROhHOAQKMsrDW3VTsY,3883
cattrs/preconf/pyyaml.py,sha256=w0aM_gJ6VhZf-Zpu_UlJki7rdgv4mfaSXElPofB3nlE,2378
cattrs/preconf/tomlkit.py,sha256=sPrBZgq7Zg78y31SAPhBuSWQllGxXXysyI-2-T6MY6U,3311
cattrs/preconf/tomllib.py,sha256=wDOhVcIkBR0bw5zMACPlUcf2YXv-eamgg-GmGdv4cSU,3083
cattrs/preconf/ujson.py,sha256=ev1fdMvQdP-PSDqDbJ714Oy3Xxi2lbc6qVSZ-LGdgKg,2438
cattrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs/strategies/__init__.py,sha256=nkZWCzSRYcS-75FMfk52mioZSuWykaN8hB39Vig5Xkg,339
cattrs/strategies/_class_methods.py,sha256=O5xhQCzNpuFiDNDMlbcyeOVqyrV65NhMZNRsG3jnoBU,2591
cattrs/strategies/_subclasses.py,sha256=8fTr7CLbNlV2Nobi0yXkQ9OKpGyUENmEzyYAsy4SDJw,9852
cattrs/strategies/_unions.py,sha256=XQwrnPEE7KhBqOy5vyUZ06reQPfqcO85jdVjQ7i3HFk,10070
cattrs/subclasses.py,sha256=SyAaJ84sO3aen_dBQkuSAIg0eGOXYX-aF151d7IbiNk,783
cattrs/typealiases.py,sha256=toHavC2kJsIcxThwvATPO5JShzKeC8kIl9KqteFohbw,1619
cattrs/types.py,sha256=cqvfmzliYfrvPswxlW_tN4DmhQ2xpAKQvVbNBJaxiWs,278
cattrs/v.py,sha256=IqUajgJFCKJYf-4S9TCKRtJcmmK4c3En69TGuf2FKOs,4126
+41 -43
View File
@@ -29,6 +29,7 @@ from typing import (
_AnnotatedAlias,
_GenericAlias,
_SpecialGenericAlias,
_UnionGenericAlias,
get_args,
get_origin,
get_type_hints,
@@ -208,6 +209,9 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet
signature = _signature
if sys.version_info >= (3, 10):
signature = partial(_signature, eval_str=True)
@@ -252,25 +256,10 @@ def is_tuple(type):
)
if sys.version_info >= (3, 14):
if sys.version_info >= (3, 10):
def is_union_type(obj):
from types import UnionType # noqa: PLC0415
return obj is Union or isinstance(obj, UnionType)
def get_newtype_base(typ: Any) -> Optional[type]:
if typ is NewType or isinstance(typ, NewType):
return typ.__supertype__
return None
from typing import NotRequired, Required
else:
from typing import _UnionGenericAlias
def is_union_type(obj):
from types import UnionType # noqa: PLC0415
from types import UnionType
return (
obj is Union
@@ -288,6 +277,25 @@ else:
else:
from typing_extensions import NotRequired, Required
else:
# 3.9
from typing_extensions import NotRequired, Required
def is_union_type(obj):
return obj is Union or (
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
)
def get_newtype_base(typ: Any) -> Optional[type]:
supertype = getattr(typ, "__supertype__", None)
if (
supertype is not None
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
and typ.__module__ in ("typing", "typing_extensions")
):
return supertype
return None
def get_notrequired_base(type) -> Union[Any, NothingType]:
if is_annotated(type):
@@ -298,25 +306,6 @@ def get_notrequired_base(type) -> Union[Any, NothingType]:
return NOTHING
def is_mutable_sequence(type: Any) -> bool:
"""A predicate function for mutable sequences.
Matches lists, mutable sequences, and deques.
"""
origin = getattr(type, "__origin__", None)
return (
type in (List, list, TypingMutableSequence, AbcMutableSequence, deque, Deque)
or (
type.__class__ is _GenericAlias
and (
((origin is not tuple) and is_subclass(origin, TypingMutableSequence))
or (origin is tuple and type.__args__[1] is ...)
)
)
or (origin in (list, deque, AbcMutableSequence))
)
def is_sequence(type: Any) -> bool:
"""A predicate function for sequences.
@@ -324,8 +313,19 @@ def is_sequence(type: Any) -> bool:
tuples.
"""
origin = getattr(type, "__origin__", None)
return is_mutable_sequence(type) or (
type in (TypingSequence, tuple, Tuple)
return (
type
in (
List,
list,
TypingSequence,
TypingMutableSequence,
AbcMutableSequence,
tuple,
Tuple,
deque,
Deque,
)
or (
type.__class__ is _GenericAlias
and (
@@ -333,7 +333,7 @@ def is_sequence(type: Any) -> bool:
or (origin is tuple and type.__args__[1] is ...)
)
)
or (origin is AbcSequence)
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
or (origin is tuple and type.__args__[1] is ...)
)
@@ -403,10 +403,8 @@ def is_generic(type) -> bool:
"""Whether `type` is a generic type."""
# Inheriting from protocol will inject `Generic` into the MRO
# without `__orig_bases__`.
return (
isinstance(type, (_GenericAlias, GenericAlias))
or (is_subclass(type, Generic) and hasattr(type, "__orig_bases__"))
or type.__class__ is Union # On 3.14, unions are no longer typing._GenericAlias
return isinstance(type, (_GenericAlias, GenericAlias)) or (
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
)
+2 -2
View File
@@ -1,10 +1,10 @@
from collections.abc import Mapping
from typing import Any, get_args
from typing import Any
from attrs import NOTHING
from typing_extensions import Self
from ._compat import copy_with, is_annotated, is_generic
from ._compat import copy_with, get_args, is_annotated, is_generic
def deep_copy_with(t, mapping: Mapping[str, Any], self_is=NOTHING):
+10 -54
View File
@@ -5,20 +5,25 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable, Iterable
from functools import partial
from typing import TYPE_CHECKING, Any, DefaultDict, Literal, NamedTuple, TypeVar
from typing import (
TYPE_CHECKING,
Any,
DefaultDict,
Literal,
NamedTuple,
TypeVar,
get_type_hints,
)
from attrs import NOTHING, Attribute, NothingType
from ._compat import (
ANIES,
AbcSet,
get_args,
get_full_type_hints,
get_origin,
is_bare,
is_frozenset,
is_mapping,
is_mutable_sequence,
is_sequence,
is_subclass,
)
@@ -42,13 +47,10 @@ if TYPE_CHECKING:
__all__ = [
"defaultdict_structure_factory",
"homogenous_tuple_structure_factory",
"is_abstract_set",
"is_any_set",
"is_defaultdict",
"is_frozenset",
"is_mapping",
"is_mutable_sequence",
"is_namedtuple",
"is_sequence",
"is_set",
@@ -68,11 +70,6 @@ def is_any_set(type) -> bool:
return is_set(type) or is_frozenset(type)
def is_abstract_set(type) -> bool:
"""A predicate function for abstract (collection.abc) sets."""
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)
def is_namedtuple(type: Any) -> bool:
"""A predicate function for named tuples."""
@@ -154,47 +151,6 @@ def list_structure_factory(type: type, converter: BaseConverter) -> StructureHoo
return structure_list
def homogenous_tuple_structure_factory(
type: type, converter: BaseConverter
) -> StructureHook:
"""A hook factory for homogenous (all elements the same, indeterminate length) tuples.
Converts any given iterable into a tuple.
"""
if is_bare(type) or type.__args__[0] in ANIES:
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
return tuple(obj)
return structure_tuple
elem_type = type.__args__[0]
try:
handler = converter.get_structure_hook(elem_type)
except RecursionError:
# Break the cycle by using late binding.
handler = converter.structure
if converter.detailed_validation:
# We have to structure into a list first anyway.
list_structure = list_structure_factory(type, converter)
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
return tuple(list_structure(obj, _))
else:
def structure_tuple(
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
) -> tuple[T, ...]:
return tuple([_handler(e, _elem_type) for e in obj])
return structure_tuple
def namedtuple_unstructure_factory(
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
) -> UnstructureHook:
@@ -239,7 +195,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
type=a,
alias=name,
)
for name, a in get_full_type_hints(cl).items()
for name, a in get_type_hints(cl).items()
]
+13 -34
View File
@@ -43,11 +43,10 @@ from ._compat import (
is_hetero_tuple,
is_literal,
is_mapping,
is_mutable_sequence,
is_mutable_set,
is_optional,
is_protocol,
is_subclass,
is_sequence,
is_tuple,
is_typeddict,
is_union_type,
@@ -55,11 +54,8 @@ from ._compat import (
)
from .cols import (
defaultdict_structure_factory,
homogenous_tuple_structure_factory,
is_abstract_set,
is_defaultdict,
is_namedtuple,
is_sequence,
iterable_unstructure_factory,
list_structure_factory,
mapping_structure_factory,
@@ -77,7 +73,6 @@ from .dispatch import (
UnstructuredValue,
UnstructureHook,
)
from .enums import enum_structure_factory, enum_unstructure_factory
from .errors import (
IterableValidationError,
IterableValidationNote,
@@ -230,10 +225,6 @@ class BaseConverter:
)
self._unstructure_func.register_func_list(
[
(
lambda t: get_newtype_base(t) is not None,
lambda o: self.unstructure(o, unstructure_as=o.__class__),
),
(
is_protocol,
lambda o: self.unstructure(o, unstructure_as=o.__class__),
@@ -248,12 +239,12 @@ class BaseConverter:
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
True,
),
(is_literal_containing_enums, self.unstructure),
(is_mapping, self._unstructure_mapping),
(is_sequence, self._unstructure_seq),
(is_mutable_set, self._unstructure_seq),
(is_frozenset, self._unstructure_seq),
(is_literal_containing_enums, self.unstructure),
(lambda t: is_subclass(t, Enum), enum_unstructure_factory, "extended"),
(lambda t: issubclass(t, Enum), self._unstructure_enum),
(has, self._unstructure_attrs),
(is_union_type, self._unstructure_union),
(lambda t: t in ANIES, self.unstructure),
@@ -280,27 +271,20 @@ class BaseConverter:
),
(is_literal, self._structure_simple_literal),
(is_literal_containing_enums, self._structure_enum_literal),
(is_sequence, homogenous_tuple_structure_factory, "extended"),
(is_mutable_sequence, list_structure_factory, "extended"),
(is_sequence, list_structure_factory, "extended"),
(is_deque, self._structure_deque),
(is_mutable_set, self._structure_set),
(is_abstract_set, self._structure_frozenset),
(is_frozenset, self._structure_frozenset),
(is_tuple, self._structure_tuple),
(is_namedtuple, namedtuple_structure_factory, "extended"),
(is_mapping, self._structure_dict),
*(
[(is_supported_union, self._gen_attrs_union_structure, True)]
if unstruct_strat is UnstructureStrategy.AS_DICT
else []
),
(is_supported_union, self._gen_attrs_union_structure, True),
(is_optional, self._structure_optional),
(
lambda t: is_union_type(t) and t in self._union_struct_registry,
self._union_struct_registry.__getitem__,
True,
),
(lambda t: is_subclass(t, Enum), enum_structure_factory, "extended"),
(has, self._structure_attrs),
]
)
@@ -311,6 +295,7 @@ class BaseConverter:
(bytes, self._structure_call),
(int, self._structure_call),
(float, self._structure_call),
(Enum, self._structure_call),
(Path, self._structure_call),
]
)
@@ -632,6 +617,10 @@ class BaseConverter:
res.append(dispatch(a.type or v.__class__)(v))
return tuple(res)
def _unstructure_enum(self, obj: Enum) -> Any:
"""Convert an enum to its value."""
return obj.value
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
"""Convert a sequence to primitive equivalents."""
# We can reuse the sequence class, so tuples stay tuples.
@@ -1045,7 +1034,6 @@ class Converter(BaseConverter):
"forbid_extra_keys",
"omit_if_default",
"type_overrides",
"use_alias",
)
def __init__(
@@ -1062,7 +1050,6 @@ class Converter(BaseConverter):
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
None, t
),
use_alias: bool = False,
):
"""
:param detailed_validation: Whether to use a slightly slower mode for detailed
@@ -1071,15 +1058,12 @@ class Converter(BaseConverter):
registered unstructuring hooks match.
:param structure_fallback_factory: A hook factory to be called when no
registered structuring hooks match.
:param use_alias: Whether to use the field alias instead of the field name as
the un/structured dictionary key by default.
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
.. versionadded:: 23.2.0 *structure_fallback_factory*
.. versionchanged:: 24.2.0
The default `structure_fallback_factory` now raises errors for missing handlers
more eagerly, surfacing problems earlier.
.. versionadded:: 25.2.0 *use_alias*
"""
super().__init__(
dict_factory=dict_factory,
@@ -1092,7 +1076,6 @@ class Converter(BaseConverter):
self.omit_if_default = omit_if_default
self.forbid_extra_keys = forbid_extra_keys
self.type_overrides = dict(type_overrides)
self.use_alias = use_alias
unstruct_collection_overrides = {
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
@@ -1263,7 +1246,7 @@ class Converter(BaseConverter):
attribs = fields(origin or cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved.
resolve_types(origin or cl)
resolve_types(cl)
attrib_overrides = {
a.name: self.type_overrides[a.type]
for a in attribs
@@ -1301,11 +1284,10 @@ class Converter(BaseConverter):
def gen_structure_attrs_fromdict(
self, cl: type[T]
) -> Callable[[Mapping[str, Any], Any], T]:
origin = get_origin(cl)
attribs = fields(origin or cl if is_generic(cl) else cl)
attribs = fields(get_origin(cl) or cl if is_generic(cl) else cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved.
resolve_types(origin or cl)
resolve_types(cl)
attrib_overrides = {
a.name: self.type_overrides[a.type]
for a in attribs
@@ -1317,7 +1299,6 @@ class Converter(BaseConverter):
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
_cattrs_detailed_validation=self.detailed_validation,
_cattrs_use_alias=self.use_alias,
**attrib_overrides,
)
@@ -1396,7 +1377,6 @@ class Converter(BaseConverter):
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
prefer_attrib_converters: bool | None = None,
detailed_validation: bool | None = None,
use_alias: bool | None = None,
) -> Self:
"""Create a copy of the converter, keeping all existing custom hooks.
@@ -1436,7 +1416,6 @@ class Converter(BaseConverter):
if detailed_validation is not None
else self.detailed_validation
),
use_alias=(use_alias if use_alias is not None else self.use_alias),
)
self._unstructure_func.copy_to(
+2 -1
View File
@@ -7,7 +7,7 @@ from collections.abc import Mapping
from dataclasses import MISSING
from functools import reduce
from operator import or_
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, get_origin
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
from attrs import NOTHING, Attribute, AttrsInstance
@@ -16,6 +16,7 @@ from ._compat import (
adapted_fields,
fields_dict,
get_args,
get_origin,
has,
is_literal,
is_union_type,
-36
View File
@@ -1,36 +0,0 @@
from collections.abc import Callable
from enum import Enum
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from .converters import BaseConverter
def enum_unstructure_factory(
type: type[Enum], converter: "BaseConverter"
) -> Callable[[Enum], Any]:
"""A factory for generating enum unstructure hooks.
If the enum is a typed enum (has `_value_`), we use the underlying value's hook.
Otherwise, we use the value directly.
"""
if "_value_" in type.__annotations__:
return lambda e: converter.unstructure(e.value)
return lambda e: e.value
def enum_structure_factory(
type: type[Enum], converter: "BaseConverter"
) -> Callable[[Any, type[Enum]], Enum]:
"""A factory for generating enum structure hooks.
If the enum is a typed enum (has `_value_`), we structure the value first.
Otherwise, we use the value directly.
"""
if "_value_" in type.__annotations__:
val_type = type.__annotations__["_value_"]
val_hook = converter.get_structure_hook(val_type)
return lambda v, _: type(val_hook(v, val_type))
return lambda v, _: type(v)
+10 -16
View File
@@ -13,18 +13,14 @@ class StructureHandlerNotFoundError(Exception):
"""
def __init__(self, message: str, type_: type) -> None:
super().__init__(message, type_)
self.message = message
super().__init__(message)
self.type_ = type_
def __str__(self) -> str:
return self.message
class BaseValidationError(ExceptionGroup):
cl: type
def __new__(cls, message: str, excs: Sequence[Exception], cl: type) -> Self:
def __new__(cls, message: str, excs: Sequence[Exception], cl: type):
obj = super().__new__(cls, message, excs)
obj.cl = cl
return obj
@@ -39,7 +35,9 @@ class IterableValidationNote(str):
index: Union[int, str] # Ints for list indices, strs for dict keys
type: Any
def __new__(cls, string: str, index: Union[int, str], type: Any) -> Self:
def __new__(
cls, string: str, index: Union[int, str], type: Any
) -> "IterableValidationNote":
instance = str.__new__(cls, string)
instance.index = index
instance.type = type
@@ -78,7 +76,7 @@ class AttributeValidationNote(str):
name: str
type: Any
def __new__(cls, string: str, name: str, type: Any) -> Self:
def __new__(cls, string: str, name: str, type: Any) -> "AttributeValidationNote":
instance = str.__new__(cls, string)
instance.name = name
instance.type = type
@@ -124,15 +122,11 @@ class ForbiddenExtraKeysError(Exception):
def __init__(
self, message: Optional[str], cl: type, extra_fields: set[str]
) -> None:
self.message = message
self.cl = cl
self.extra_fields = extra_fields
cln = cl.__name__
super().__init__(message, cl, extra_fields)
def __str__(self) -> str:
return (
self.message
or f"Extra fields in constructor for {self.cl.__name__}: "
f"{', '.join(sorted(self.extra_fields))}"
super().__init__(
message
or f"Extra fields in constructor for {cln}: {', '.join(extra_fields)}"
)
+15 -99
View File
@@ -4,7 +4,7 @@ import re
from collections.abc import Callable, Iterable, Mapping
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
from attrs import NOTHING, Attribute, Converter, Factory, evolve
from attrs import NOTHING, Attribute, Factory
from typing_extensions import NoDefault
from .._compat import (
@@ -33,7 +33,7 @@ from ..types import SimpleStructureHook
from ._consts import AttributeOverride, already_generating, neutral
from ._generics import generate_mapping
from ._lc import generate_unique_filename
from ._shared import _annotated_override_or_default, find_structure_handler
from ._shared import find_structure_handler
if TYPE_CHECKING:
from ..converters import BaseConverter
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
typevar_map: dict[str, Any] = {},
_cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]:
@@ -96,16 +96,6 @@ def make_dict_unstructure_fn_from_attrs(
will be included.
.. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
.. versionchanged:: 26.1.0
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
specified, the converter is applied to the default value before checking if it
is equal to the attribute's value.
"""
fn_name = "unstructure_" + cl.__name__
@@ -114,27 +104,15 @@ def make_dict_unstructure_fn_from_attrs(
invocation_lines = []
internal_arg_parts = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
for a in attrs:
attr_name = a.name
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[attr_name] = override
override = kwargs.get(attr_name, neutral)
if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
continue
if override.rename is None:
kn = attr_name if not _cattrs_use_alias else a.alias
if kn != attr_name:
kwargs[attr_name] = evolve(override, rename=kn)
else:
kn = override.rename
d = a.default
@@ -192,31 +170,15 @@ def make_dict_unstructure_fn_from_attrs(
if isinstance(d, Factory):
globs[def_name] = d.factory
internal_arg_parts[def_name] = d.factory
def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()"
if d.takes_self:
lines.append(f" if instance.{attr_name} != {def_name}(instance):")
else:
lines.append(f" if instance.{attr_name} != {def_name}():")
lines.append(f" res['{kn}'] = {invoke}")
else:
globs[def_name] = d
internal_arg_parts[def_name] = d
def_str = def_name
c = a.converter
if c is not None:
conv_name = f"__c_conv_{attr_name}"
if isinstance(c, Converter):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
field_name = f"__c_field_{attr_name}"
globs[field_name] = a
internal_arg_parts[field_name] = a
def_str = f"{conv_name}({def_str}, instance, {field_name})"
elif isinstance(d, Factory):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
def_str = f"{conv_name}({def_str})"
else:
globs[def_name] = c(d)
internal_arg_parts[def_name] = c(d)
lines.append(f" if instance.{attr_name} != {def_str}:")
lines.append(f" if instance.{attr_name} != {def_name}:")
lines.append(f" res['{kn}'] = {invoke}")
else:
@@ -255,7 +217,7 @@ def make_dict_unstructure_fn(
converter: BaseConverter,
_cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]:
@@ -275,20 +237,11 @@ def make_dict_unstructure_fn(
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""
origin = get_origin(cl)
attrs = adapted_fields(origin or cl) # type: ignore
mapping = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if is_generic(cl):
mapping = generate_mapping(cl, mapping)
@@ -336,7 +289,7 @@ def make_dict_structure_fn_from_attrs(
bool | Literal["from_converter"]
) = "from_converter",
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -362,12 +315,6 @@ def make_dict_structure_fn_from_attrs(
will be included.
.. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""
cl_name = cl.__name__
@@ -403,9 +350,6 @@ def make_dict_structure_fn_from_attrs(
if _cattrs_forbid_extra_keys == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if _cattrs_detailed_validation == "from_converter":
_cattrs_detailed_validation = converter.detailed_validation
if _cattrs_prefer_attrib_converters == "from_converter":
@@ -423,13 +367,7 @@ def make_dict_structure_fn_from_attrs(
internal_arg_parts["__c_avn"] = AttributeValidationNote
for a in attrs:
an = a.name
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
override = kwargs.get(an, neutral)
if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
@@ -458,8 +396,6 @@ def make_dict_structure_fn_from_attrs(
ian = a.alias
if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else:
kn = override.rename
@@ -560,24 +496,14 @@ def make_dict_structure_fn_from_attrs(
# The first loop deals with required args.
for a in attrs:
an = a.name
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
override = kwargs.get(an, neutral)
if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
continue
if a.default is not NOTHING:
non_required.append(a)
# The next loop will handle it.
continue
t = a.type
if isinstance(t, TypeVar):
t = typevar_map.get(t.__name__, t)
@@ -597,8 +523,6 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else:
kn = override.rename
allowed_fields.add(kn)
@@ -668,8 +592,6 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else:
kn = override.rename
allowed_fields.add(kn)
@@ -760,7 +682,7 @@ def make_dict_structure_fn(
bool | Literal["from_converter"]
) = "from_converter",
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -792,12 +714,6 @@ def make_dict_structure_fn(
.. versionchanged:: 24.1.0
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
converter by default.
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""
mapping = {}
+1 -17
View File
@@ -4,31 +4,15 @@ from typing import TYPE_CHECKING, Any
from attrs import NOTHING, Attribute, Factory
from .._compat import get_args, is_annotated, is_bare_final
from .._compat import is_bare_final
from ..dispatch import StructureHook
from ..errors import StructureHandlerNotFoundError
from ..fns import raise_error
from ._consts import AttributeOverride
if TYPE_CHECKING:
from ..converters import BaseConverter
def _annotated_override_or_default(
type: Any, default: AttributeOverride
) -> AttributeOverride:
"""
If the type is Annotated containing an AttributeOverride, return it.
Otherwise, return the default.
"""
if is_annotated(type):
for arg in get_args(type):
if isinstance(arg, AttributeOverride):
return arg
return default
def find_structure_handler(
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
) -> StructureHook | None:
+35 -61
View File
@@ -3,12 +3,23 @@ from __future__ import annotations
import re
import sys
from collections.abc import Mapping
from inspect import get_annotations
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
from attrs import NOTHING, Attribute
from typing_extensions import _TypedDictMeta
try:
from inspect import get_annotations
def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)
except ImportError:
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
def get_annots(cl) -> dict[str, Any]:
return cl.__dict__.get("__annotations__", {})
from .._compat import (
get_full_type_hints,
get_notrequired_base,
@@ -29,7 +40,7 @@ from . import AttributeOverride
from ._consts import already_generating, neutral
from ._generics import generate_mapping
from ._lc import generate_unique_filename
from ._shared import _annotated_override_or_default, find_structure_handler
from ._shared import find_structure_handler
if TYPE_CHECKING:
from ..converters import BaseConverter
@@ -39,10 +50,6 @@ __all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
T = TypeVar("T")
def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)
def make_dict_unstructure_fn(
cl: type[T],
converter: BaseConverter,
@@ -102,20 +109,11 @@ def make_dict_unstructure_fn(
# * all attributes resolve to `converter._unstructure_identity`
for a in attrs:
attr_name = a.name
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[attr_name] = override
override = kwargs.get(attr_name, neutral)
if override != neutral:
break
handler = None
t = a.type
if isinstance(t, TypeVar):
if t.__name__ in mapping:
@@ -127,6 +125,9 @@ def make_dict_unstructure_fn(
t = deep_copy_with(t, mapping, cl)
if handler is None:
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
try:
handler = converter.get_unstructure_hook(t)
except RecursionError:
@@ -141,22 +142,10 @@ def make_dict_unstructure_fn(
for ix, a in enumerate(attrs):
attr_name = a.name
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[attr_name] = override
override = kwargs.get(attr_name, neutral)
if override.omit:
lines.append(f" res.pop('{attr_name}', None)")
continue
if override.rename is not None:
# We also need to pop when renaming, since we're copying
# the original.
@@ -171,6 +160,8 @@ def make_dict_unstructure_fn(
if override.unstruct_hook is not None:
handler = override.unstruct_hook
else:
t = a.type
if isinstance(t, TypeVar):
if t.__name__ in mapping:
t = mapping[t.__name__]
@@ -180,6 +171,9 @@ def make_dict_unstructure_fn(
t = deep_copy_with(t, mapping, cl)
if handler is None:
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
try:
handler = converter.get_unstructure_hook(t)
except RecursionError:
@@ -226,15 +220,12 @@ def make_dict_unstructure_fn(
)
eval(compile(script, fname, "exec"), globs)
res = globs[fn_name]
res.overrides = kwargs
finally:
working_set.remove(cl)
if not working_set:
del already_generating.working_set
return res
return globs[fn_name]
def make_dict_structure_fn(
@@ -335,25 +326,20 @@ def make_dict_structure_fn(
for ix, a in enumerate(attrs):
an = a.name
attr_required = an in req_keys
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[an] = override
override = kwargs.get(an, neutral)
if override.omit:
continue
t = a.type
if isinstance(t, TypeVar):
t = mapping.get(t.__name__, t)
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
t = deep_copy_with(t, mapping, cl)
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if is_generic(t) and not is_bare(t) and not is_annotated(t):
t = deep_copy_with(t, mapping, cl)
@@ -413,12 +399,7 @@ def make_dict_structure_fn(
for ix, a in enumerate(attrs):
an = a.name
attr_required = an in req_keys
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
override = kwargs.get(an, neutral)
if override.omit:
continue
if not attr_required:
@@ -467,18 +448,13 @@ def make_dict_structure_fn(
if non_required:
for ix, a in non_required:
an = a.name
override = kwargs.get(an, neutral)
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[an] = override
if isinstance(t, TypeVar):
t = mapping.get(t.__name__, t)
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
@@ -538,9 +514,7 @@ def make_dict_structure_fn(
)
eval(compile(script, fname, "exec"), globs)
res = globs[fn_name]
res.overrides = kwargs
return res
return globs[fn_name]
def _adapted_fields(cls: Any) -> list[Attribute]:
+7 -2
View File
@@ -1,12 +1,17 @@
from collections.abc import Callable
import sys
from datetime import datetime
from enum import Enum
from typing import Any, ParamSpec, TypeVar, get_args
from typing import Any, Callable, TypeVar, get_args
from .._compat import is_subclass
from ..converters import Converter, UnstructureHook
from ..fns import identity
if sys.version_info[:2] < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec
def validate_datetime(v, _):
if not isinstance(v, datetime):
+2 -2
View File
@@ -99,11 +99,11 @@ def configure_converter(converter: BaseConverter):
# datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running.
converter.register_unstructure_hook(datetime, identity)
converter.register_unstructure_hook(datetime, lambda v: v)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+1 -1
View File
@@ -37,7 +37,7 @@ def configure_converter(converter: BaseConverter):
)
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+1 -1
View File
@@ -52,7 +52,7 @@ def configure_converter(converter: BaseConverter) -> None:
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda _: identity)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
configure_union_passthrough(Union[str, bool, int, float, None], converter)
+1 -1
View File
@@ -46,7 +46,7 @@ def configure_converter(converter: BaseConverter) -> None:
converter.register_structure_hook(
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+3 -14
View File
@@ -3,27 +3,18 @@
from __future__ import annotations
from base64 import b64decode
from collections.abc import Callable
from dataclasses import is_dataclass
from datetime import date, datetime
from enum import Enum
from functools import partial
from typing import Any, TypeVar, Union, get_type_hints
from typing import Any, Callable, TypeVar, Union, get_type_hints
from attrs import has as attrs_has
from attrs import resolve_types
from msgspec import Struct, convert, to_builtins
from msgspec.json import Encoder, decode
from .._compat import (
fields,
get_args,
get_origin,
is_bare,
is_mapping,
is_sequence,
is_subclass,
)
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
from ..cols import is_namedtuple
from ..converters import BaseConverter, Converter
from ..dispatch import UnstructureHook
@@ -83,9 +74,7 @@ def configure_converter(converter: Converter) -> None:
configure_passthroughs(converter)
converter.register_unstructure_hook(Struct, to_builtins)
converter.register_unstructure_hook_factory(
lambda t: is_subclass(t, Enum), lambda t, c: identity
)
converter.register_unstructure_hook(Enum, identity)
converter.register_structure_hook(Struct, convert)
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
+2 -2
View File
@@ -87,8 +87,8 @@ def configure_converter(converter: Converter) -> None:
),
]
)
converter.register_unstructure_hook_factory(
partial(is_primitive_enum, include_bare_enums=True), lambda t: identity
converter.register_unstructure_hook_func(
partial(is_primitive_enum, include_bare_enums=True), identity
)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
+3 -9
View File
@@ -12,7 +12,6 @@ from tomlkit.items import Float, Integer, String
from .._compat import is_mapping, is_subclass
from ..converters import BaseConverter, Converter
from ..fns import identity
from ..strategies import configure_union_passthrough
from . import validate_datetime, wrap
@@ -38,9 +37,6 @@ def configure_converter(converter: BaseConverter):
* sets are serialized as lists
* tuples are serializas as lists
* mapping keys are coerced into strings when unstructuring
.. versionchanged:: 26.1.0
date objects are now passed through to tomlkit without unstructuring.
"""
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
converter.register_unstructure_hook(
@@ -71,12 +67,10 @@ def configure_converter(converter: BaseConverter):
# datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running.
converter.register_unstructure_hook(datetime, identity)
converter.register_unstructure_hook(datetime, lambda v: v)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, identity)
converter.register_structure_hook(
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
)
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
configure_union_passthrough(
Union[str, String, bool, int, Integer, float, Float], converter
)
-94
View File
@@ -1,94 +0,0 @@
"""Preconfigured converters for tomllib."""
from base64 import b85decode, b85encode
from collections.abc import Set
from datetime import date, datetime
from enum import Enum
from operator import attrgetter
from typing import Any, TypeVar, Union
try:
from tomllib import loads
except ImportError:
from tomli import loads
try:
from tomli_w import dumps
except ImportError: # pragma: nocover
dumps = None
from .._compat import is_mapping, is_subclass
from ..converters import BaseConverter, Converter
from ..fns import identity
from ..strategies import configure_union_passthrough
from . import validate_datetime, wrap
__all__ = ["TomllibConverter", "configure_converter", "make_converter"]
T = TypeVar("T")
_enum_value_getter = attrgetter("_value_")
class TomllibConverter(Converter):
"""A converter subclass specialized for tomllib."""
if dumps is not None:
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
def loads(self, data: str, cl: type[T], **kwargs: Any) -> T:
return self.structure(loads(data, **kwargs), cl)
def configure_converter(converter: BaseConverter):
"""
Configure the converter for use with the tomllib library.
* bytes are serialized as base85 strings
* sets are serialized as lists
* tuples are serializas as lists
* mapping keys are coerced into strings when unstructuring
* dates and datetimes are left for tomllib to handle
"""
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
converter.register_unstructure_hook(
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
)
@converter.register_unstructure_hook_factory(is_mapping)
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
key_handler = str
args = getattr(cl, "__args__", None)
if args:
if is_subclass(args[0], str):
key_handler = _enum_value_getter if is_subclass(args[0], Enum) else None
elif is_subclass(args[0], bytes):
def key_handler(k: bytes):
return b85encode(k).decode("utf8")
return converter.gen_unstructure_mapping(
cl, unstructure_to=unstructure_to, key_handler=key_handler
)
converter.register_unstructure_hook(datetime, identity)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, identity)
converter.register_structure_hook(
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
)
configure_union_passthrough(Union[str, int, float, bool], converter)
@wrap(TomllibConverter)
def make_converter(*args: Any, **kwargs: Any) -> TomllibConverter:
kwargs["unstruct_collection_overrides"] = {
Set: list,
tuple: list,
**kwargs.get("unstruct_collection_overrides", {}),
}
res = TomllibConverter(*args, **kwargs)
configure_converter(res)
return res
+1 -1
View File
@@ -47,7 +47,7 @@ def configure_converter(converter: BaseConverter):
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+7 -20
View File
@@ -9,25 +9,21 @@ from typing import Any, Callable, TypeVar, Union
from ..converters import BaseConverter
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
from ..gen._consts import already_generating
from ..subclasses import subclasses
def _make_subclasses_tree(cl: type) -> list[type]:
# get class origin for accessing subclasses (see #648 for more info)
cls_origin = typing.get_origin(cl) or cl
# Use a dict to deduplicate and keep insertion order.
seen = {cl: None}
for scl in subclasses(cls_origin):
for sscl in _make_subclasses_tree(scl):
seen[sscl] = None
return list(seen)
return [cl] + [
sscl
for scl in cls_origin.__subclasses__()
for sscl in _make_subclasses_tree(scl)
]
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
"""Whether the given class has subclasses from `given_subclasses`."""
cls_origin = typing.get_origin(cl) or cl
actual = set(subclasses(cls_origin))
actual = set(cl.__subclasses__())
given = set(given_subclasses)
return bool(actual & given)
@@ -72,9 +68,6 @@ def include_subclasses(
.. versionchanged:: 24.1.0
When overrides are not provided, hooks for individual classes are retrieved from
the converter instead of generated with no overrides, using converter defaults.
.. versionchanged:: 25.2.0
Slotted dataclasses work on Python 3.14 via :func:`cattrs.subclasses.subclasses`,
which filters out duplicate classes caused by slotting.
"""
# Due to https://github.com/python-attrs/attrs/issues/1047
collect()
@@ -238,13 +231,7 @@ def _include_subclasses_with_union_strategy(
return cls is _cl
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
subclasses = tuple(
[
c
for c in union_classes
if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)
]
)
subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
if len(subclasses) > 1:
u = Union[subclasses] # type: ignore
union_strategy(u, converter)
+45 -65
View File
@@ -52,10 +52,23 @@ def configure_tagged_union(
if is_type_alias(union):
union = union.__value__
args = union.__args__
tag_to_hook = {}
exact_cl_unstruct_hooks = {}
cl_to_tag = {}
for cl in args:
tag = tag_generator(cl)
struct_handler = converter.get_structure_hook(cl)
unstruct_handler = converter.get_unstructure_hook(cl)
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
return _h(val, _cl)
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
return _h(val)
tag_to_hook[tag] = structure_union_member
exact_cl_unstruct_hooks[cl] = unstructure_union_member
cl_to_tag = {cl: tag_generator(cl) for cl in args}
if default is not NOTHING:
default_handler = converter.get_structure_hook(default)
@@ -63,9 +76,36 @@ def configure_tagged_union(
def structure_default(val: dict, _cl=default, _h=default_handler):
return _h(val, _cl)
tag_to_hook = defaultdict(lambda: structure_default)
cl_to_tag = defaultdict(lambda: default)
tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
cl_to_tag = defaultdict(lambda: default, cl_to_tag)
def unstructure_tagged_union(
val: union,
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
_cl_to_tag=cl_to_tag,
_tag_name=tag_name,
) -> dict:
res = _exact_cl_unstruct_hooks[val.__class__](val)
res[_tag_name] = _cl_to_tag[val.__class__]
return res
if default is NOTHING:
if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
val = val.copy()
return _tag_to_cl[val.pop(_tag_name)](val)
else:
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
return _tag_to_cl[val[_tag_name]](val)
else:
if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union(
@@ -95,54 +135,11 @@ def configure_tagged_union(
return _tag_to_hook[val[_tag_name]](val)
return _dh(val, _default)
else:
if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
val = val.copy()
return _tag_to_cl[val.pop(_tag_name)](val)
else:
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
return _tag_to_cl[val[_tag_name]](val)
def unstructure_tagged_union(
val: union,
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
_cl_to_tag=cl_to_tag,
_tag_name=tag_name,
) -> dict:
res = _exact_cl_unstruct_hooks[val.__class__](val)
res[_tag_name] = _cl_to_tag[val.__class__]
return res
converter.register_unstructure_hook(union, unstructure_tagged_union)
converter.register_structure_hook(union, structure_tagged_union)
for cl in args:
tag = tag_generator(cl)
struct_handler = converter.get_structure_hook(cl)
unstruct_handler = converter.get_unstructure_hook(cl)
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
return _h(val, _cl)
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
return _h(val)
tag_to_hook[tag] = structure_union_member
exact_cl_unstruct_hooks[cl] = unstructure_union_member
cl_to_tag[cl] = tag
def configure_union_passthrough(
union: Any, converter: BaseConverter, accept_ints_as_floats: bool = True
) -> None:
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
"""
Configure the converter to support validating and passing through unions of the
provided types and their subsets.
@@ -165,14 +162,7 @@ def configure_union_passthrough(
If the union contains a class and one or more of its subclasses, the subclasses
will also be included when validating the superclass.
:param accept_ints_as_floats: When set (the default), if the provided union
contains both ints and floats, actual unions containing only floats will also accept
ints. See https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
for more information.
.. versionadded:: 23.2.0
.. versionchanged:: 25.2.0
Introduced the `accept_ints_as_floats` parameter.
"""
args = set(union.__args__)
@@ -215,16 +205,6 @@ def configure_union_passthrough(
and not is_literal(a)
}
# By default, when floats are part of the union, accept ints too.
if (
accept_ints_as_floats
and int in args
and float in args
and float in non_literal_classes
and int not in non_literal_classes
):
non_literal_classes.add(int)
if spillover:
spillover_type = (
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
-27
View File
@@ -1,27 +0,0 @@
import sys
if sys.version_info <= (3, 13):
def subclasses(cls: type) -> list[type]:
"""A proxy for `cls.__subclasses__()` on older Pythons."""
return cls.__subclasses__()
else:
def subclasses(cls: type) -> list[type]:
"""A helper for getting subclasses of a class.
Filters out duplicate subclasses of slot dataclasses and attrs classes.
"""
return [
cl
for cl in cls.__subclasses__()
if (
not (
"__slots__" not in cl.__dict__
and hasattr(cls, "__dataclass_params__")
and cls.__dataclass_params__.slots
)
and not hasattr(cls, "__attrs_base_of_slotted__")
)
]
-19
View File
@@ -1,19 +0,0 @@
from __future__ import annotations
class ExceptionGroup(Exception):
"""Minimal backport used by bundled libs on Python < 3.11."""
def __new__(cls, message, exceptions):
obj = super().__new__(cls, message)
obj.message = message
obj.exceptions = tuple(exceptions)
return obj
def __init__(self, message, exceptions):
super().__init__(message)
self.message = message
self.exceptions = tuple(exceptions)
def derive(self, exceptions):
return self.__class__(self.message, exceptions)
@@ -1 +1 @@
uv
pip
@@ -1,4 +1,4 @@
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
importlib_metadata-6.8.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
importlib_metadata-6.8.0.dist-info/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
importlib_metadata-6.8.0.dist-info/RECORD,,
@@ -6,6 +6,15 @@ importlib_metadata-6.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
importlib_metadata-6.8.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
importlib_metadata/__init__.py,sha256=EiH0qTKP_6oa6pRGJgPrq0kvjnL3hJ18BJH8VaAYSBA,30749
importlib_metadata/__pycache__/__init__.cpython-311.pyc,,
importlib_metadata/__pycache__/_adapters.cpython-311.pyc,,
importlib_metadata/__pycache__/_collections.cpython-311.pyc,,
importlib_metadata/__pycache__/_compat.cpython-311.pyc,,
importlib_metadata/__pycache__/_functools.cpython-311.pyc,,
importlib_metadata/__pycache__/_itertools.cpython-311.pyc,,
importlib_metadata/__pycache__/_meta.cpython-311.pyc,,
importlib_metadata/__pycache__/_py39compat.cpython-311.pyc,,
importlib_metadata/__pycache__/_text.cpython-311.pyc,,
importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554
@@ -1 +1 @@
uv
pip
@@ -1,10 +1,15 @@
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
lsprotocol-2023.0.1.dist-info/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
lsprotocol-2023.0.1.dist-info/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
lsprotocol-2023.0.1.dist-info/RECORD,,
lsprotocol-2023.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
lsprotocol/__pycache__/__init__.cpython-311.pyc,,
lsprotocol/__pycache__/_hooks.cpython-311.pyc,,
lsprotocol/__pycache__/converters.cpython-311.pyc,,
lsprotocol/__pycache__/types.cpython-311.pyc,,
lsprotocol/__pycache__/validators.cpython-311.pyc,,
lsprotocol/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -0,0 +1 @@
pip
@@ -1,13 +1,14 @@
Metadata-Version: 2.4
Name: packaging
Version: 26.2
Version: 25.0
Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-Expression: Apache-2.0 OR BSD-2-Clause
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
@@ -17,10 +18,8 @@ Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
Classifier: Typing :: Typed
License-File: LICENSE
License-File: LICENSE.APACHE
@@ -43,7 +42,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`).
.. end-intro
The ``packaging`` project includes the following: version handling, specifiers,
markers, requirements, tags, metadata, lockfiles, utilities.
markers, requirements, tags, utilities.
Documentation
-------------
@@ -53,14 +52,8 @@ The `documentation`_ provides information and the API for the following:
- Version Handling
- Specifiers
- Markers
- Licenses
- Requirements
- Metadata
- Tags
- Lockfiles (pylock)
- Direct URL helpers
- Dependency groups
- Errors
- Utilities
Installation
@@ -77,11 +70,11 @@ Discussion
If you run into bugs, you can file them in our `issue tracker`_.
You can also join discussions on `GitHub Discussions`_ to ask questions or get involved.
You can also join ``#pypa`` on Freenode to ask questions or get involved.
.. _`documentation`: https://packaging.pypa.io/
.. _`issue tracker`: https://github.com/pypa/packaging/issues
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
Code of Conduct
@@ -0,0 +1,41 @@
packaging-25.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
packaging-25.0.dist-info/METADATA,sha256=W2EaYJw4_vw9YWv0XSCuyY-31T8kXayp4sMPyFx6woI,3281
packaging-25.0.dist-info/RECORD,,
packaging-25.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging-25.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
packaging-25.0.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
packaging-25.0.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
packaging-25.0.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
packaging/__init__.py,sha256=_0cDiPVf2S-bNfVmZguxxzmrIYWlyASxpqph4qsJWUc,494
packaging/__pycache__/__init__.cpython-311.pyc,,
packaging/__pycache__/_elffile.cpython-311.pyc,,
packaging/__pycache__/_manylinux.cpython-311.pyc,,
packaging/__pycache__/_musllinux.cpython-311.pyc,,
packaging/__pycache__/_parser.cpython-311.pyc,,
packaging/__pycache__/_structures.cpython-311.pyc,,
packaging/__pycache__/_tokenizer.cpython-311.pyc,,
packaging/__pycache__/markers.cpython-311.pyc,,
packaging/__pycache__/metadata.cpython-311.pyc,,
packaging/__pycache__/requirements.cpython-311.pyc,,
packaging/__pycache__/specifiers.cpython-311.pyc,,
packaging/__pycache__/tags.cpython-311.pyc,,
packaging/__pycache__/utils.cpython-311.pyc,,
packaging/__pycache__/version.cpython-311.pyc,,
packaging/_elffile.py,sha256=UkrbDtW7aeq3qqoAfU16ojyHZ1xsTvGke_WqMTKAKd0,3286
packaging/_manylinux.py,sha256=t4y_-dTOcfr36gLY-ztiOpxxJFGO2ikC11HgfysGxiM,9596
packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694
packaging/_parser.py,sha256=gYfnj0pRHflVc4RHZit13KNTyN9iiVcU2RUCGi22BwM,10221
packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
packaging/_tokenizer.py,sha256=OYzt7qKxylOAJ-q0XyK1qAycyPRYLfMPdGQKRXkZWyI,5310
packaging/licenses/__init__.py,sha256=VsK4o27CJXWfTi8r2ybJmsBoCdhpnBWuNrskaCVKP7U,5715
packaging/licenses/__pycache__/__init__.cpython-311.pyc,,
packaging/licenses/__pycache__/_spdx.cpython-311.pyc,,
packaging/licenses/_spdx.py,sha256=oAm1ztPFwlsmCKe7lAAsv_OIOfS1cWDu9bNBkeu-2ns,48398
packaging/markers.py,sha256=P0we27jm1xUzgGMJxBjtUFCIWeBxTsMeJTOJ6chZmAY,12049
packaging/metadata.py,sha256=8IZErqQQnNm53dZZuYq4FGU4_dpyinMeH1QFBIWIkfE,34739
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947
packaging/specifiers.py,sha256=gtPu5DTc-F9baLq3FTGEK6dPhHGCuwwZetaY0PSV2gs,40055
packaging/tags.py,sha256=41s97W9Zatrq2Ed7Rc3qeBDaHe8pKKvYq2mGjwahfXk,22745
packaging/utils.py,sha256=0F3Hh9OFuRgrhTgGZUl5K22Fv1YP2tZl1z_2gO6kJiA,5050
packaging/version.py,sha256=olfyuk_DPbflNkJ4wBWetXQ17c74x3DB501degUv7DY,16676
@@ -1 +0,0 @@
uv
@@ -1,29 +0,0 @@
packaging-26.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
packaging-26.2.dist-info/METADATA,sha256=T5y815M0FaR5P3dnyYoralEsgj_IHIczeBVwXyMOyr8,3543
packaging-26.2.dist-info/RECORD,,
packaging-26.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging-26.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
packaging-26.2.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
packaging-26.2.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
packaging-26.2.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
packaging/__init__.py,sha256=QhMEdPu2XogrJzV3S0KWS6t7l0I9k8EeDRJl4fnw87s,494
packaging/_elffile.py,sha256=-sKkptYqzYw2-x3QByJa5mB4rfPWu1pxkZHRx1WAFCY,3211
packaging/_manylinux.py,sha256=Hf6nB0cOrayEs96-p3oIXAgGnFquv20DO5l-o2_Xnv0,9559
packaging/_musllinux.py,sha256=Z6swjH3MA7XS3qXnmMN7QPhqP3fnoYI0eQ18e9-HgAE,2707
packaging/_parser.py,sha256=Kf2nsDw4c54X82pY8ba4F02Bve6OygGMAjL-Begqcew,11698
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
packaging/_tokenizer.py,sha256=tFU2Wr-ZZJdAbkXLEJo7qUQDJaIkfft9DqaifiEND7A,5391
packaging/dependency_groups.py,sha256=XZIAVFK9uHG4RCGprmJn3VInUWMesxha_kytJuMO9eY,10218
packaging/direct_url.py,sha256=eKmbDiPP1sLV4Mj_kCSZqqknrIyVO9Sr7JpF8KCjp4U,10917
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
packaging/licenses/__init__.py,sha256=_Jx0XRiD_58palsWnyLrLuh59ZpGCPIPXLKdZo9OJvQ,7293
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
packaging/markers.py,sha256=8fDIUhAF6YMnCNB5FSiwh9pEIusiFzAF73J-0OB8bTk,17055
packaging/metadata.py,sha256=crAh0E3GVGVqPlu6EdRFsaG-Y6UYznTUqjuGKRGPv6c,38770
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging/pylock.py,sha256=G_1gncTmDbRLY1jo4VDI9Uw-b5IErh_Q9V_BbVJTmD8,33890
packaging/requirements.py,sha256=dd1c9aa1gp5NI6btF6UFRQjPn1nxQXnE_T34yDDTEpc,4383
packaging/specifiers.py,sha256=Mfp8avQg0lVot17to9lVKBtZD1FsWBTItoGwFUZ3wtg,71514
packaging/tags.py,sha256=NQ1weo69_Sjte3xBZ1I_G63CIgCmaN0C24mz-z3hGYo,34224
packaging/utils.py,sha256=M7-JMKic2sP1YtV_8aW7eVGB-x3ADuKCiSrsVeCd2Uo,9848
packaging/version.py,sha256=Y1aTtxe3sn2xOMa5BdI85-AcHuybbanOVkEvvSRRC8I,38369
+1 -1
View File
@@ -6,7 +6,7 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
__version__ = "26.2"
__version__ = "25.0"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
+1
View File
@@ -4,6 +4,7 @@ ELF file parser.
This provides a class ``ELFFile`` that parses an ELF executable in a similar
interface to ``ZipFile``. Only the read interface is implemented.
Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
+32 -32
View File
@@ -15,16 +15,6 @@ EF_ARM_ABIMASK = 0xFF000000
EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400
_ALLOWED_ARCHS = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then.
@@ -67,7 +57,16 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
return _is_linux_armhf(executable)
if "i686" in archs:
return _is_linux_i686(executable)
return any(arch in _ALLOWED_ARCHS for arch in archs)
allowed_archs = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
return any(arch in allowed_archs for arch in archs)
# If glibc ever changes its major version, we need to know what the last
@@ -107,7 +106,7 @@ def _glibc_version_string_ctypes() -> str | None:
Fallback implementation of glibc_version_string using ctypes.
"""
try:
import ctypes # noqa: PLC0415
import ctypes
except ImportError:
return None
@@ -151,7 +150,7 @@ def _glibc_version_string() -> str | None:
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
def _parse_glibc_version(version_str: str) -> tuple[int, int]:
"""Parse glibc version.
We use a regexp instead of str.split because we want to discard any
@@ -166,15 +165,15 @@ def _parse_glibc_version(version_str: str) -> _GLibCVersion:
RuntimeWarning,
stacklevel=2,
)
return _GLibCVersion(-1, -1)
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
return -1, -1
return int(m.group("major")), int(m.group("minor"))
@functools.lru_cache
def _get_glibc_version() -> _GLibCVersion:
def _get_glibc_version() -> tuple[int, int]:
version_str = _glibc_version_string()
if version_str is None:
return _GLibCVersion(-1, -1)
return (-1, -1)
return _parse_glibc_version(version_str)
@@ -185,7 +184,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
return False
# Check for presence of _manylinux module.
try:
import _manylinux # noqa: PLC0415
import _manylinux
except ImportError:
return True
if hasattr(_manylinux, "manylinux_compatible"):
@@ -193,26 +192,25 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
if result is not None:
return bool(result)
return True
if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"):
if version == _GLibCVersion(2, 5):
if hasattr(_manylinux, "manylinux1_compatible"):
return bool(_manylinux.manylinux1_compatible)
if version == _GLibCVersion(2, 12) and hasattr(
_manylinux, "manylinux2010_compatible"
):
if version == _GLibCVersion(2, 12):
if hasattr(_manylinux, "manylinux2010_compatible"):
return bool(_manylinux.manylinux2010_compatible)
if version == _GLibCVersion(2, 17) and hasattr(
_manylinux, "manylinux2014_compatible"
):
if version == _GLibCVersion(2, 17):
if hasattr(_manylinux, "manylinux2014_compatible"):
return bool(_manylinux.manylinux2014_compatible)
return True
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
_LEGACY_MANYLINUX_MAP = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
_GLibCVersion(2, 17): "manylinux2014",
(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
_GLibCVersion(2, 12): "manylinux2010",
(2, 12): "manylinux2010",
# CentOS 5 w/ glibc 2.5 (PEP 513)
_GLibCVersion(2, 5): "manylinux1",
(2, 5): "manylinux1",
}
@@ -254,9 +252,11 @@ def platform_tags(archs: Sequence[str]) -> Iterator[str]:
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, glibc_version):
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
yield f"{tag}_{arch}"
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(arch, glibc_version):
yield f"{legacy_tag}_{arch}"
+1 -1
View File
@@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None:
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
return _parse_musl_version(proc.stderr)
+10 -50
View File
@@ -7,14 +7,12 @@ the implementation.
from __future__ import annotations
import ast
from typing import List, Literal, NamedTuple, Sequence, Tuple, Union
from typing import NamedTuple, Sequence, Tuple, Union
from ._tokenizer import DEFAULT_RULES, Tokenizer
class Node:
__slots__ = ("value",)
def __init__(self, value: str) -> None:
self.value = value
@@ -22,66 +20,31 @@ class Node:
return self.value
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({self.value!r})>"
return f"<{self.__class__.__name__}('{self}')>"
def serialize(self) -> str:
raise NotImplementedError
def __getstate__(self) -> str:
# Return just the value string for compactness and stability.
return self.value
def _restore_value(self, value: object) -> None:
if not isinstance(value, str):
raise TypeError(
f"Cannot restore {self.__class__.__name__} value from {value!r}"
)
self.value = value
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the value string.
self._restore_value(state)
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "value" in slot_dict:
self._restore_value(slot_dict["value"])
return
if isinstance(state, dict) and "value" in state:
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
self._restore_value(state["value"])
return
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
class Variable(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
class Value(Node):
__slots__ = ()
def serialize(self) -> str:
return f'"{self}"'
class Op(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
MarkerLogical = Literal["and", "or"]
MarkerVar = Union[Variable, Value]
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]
MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
class ParsedRequirement(NamedTuple):
@@ -148,9 +111,7 @@ def _parse_requirement_details(
return (url, specifier, marker)
marker = _parse_requirement_marker(
tokenizer,
span_start=url_start,
expected="semicolon (after URL and whitespace)",
tokenizer, span_start=url_start, after="URL and whitespace"
)
else:
specifier_start = tokenizer.position
@@ -163,10 +124,10 @@ def _parse_requirement_details(
marker = _parse_requirement_marker(
tokenizer,
span_start=specifier_start,
expected=(
"comma (within version specifier), semicolon (after version specifier)"
after=(
"version specifier"
if specifier
else "semicolon (after name with no version specifier)"
else "name and no valid version specifier"
),
)
@@ -174,7 +135,7 @@ def _parse_requirement_details(
def _parse_requirement_marker(
tokenizer: Tokenizer, *, span_start: int, expected: str
tokenizer: Tokenizer, *, span_start: int, after: str
) -> MarkerList:
"""
requirement_marker = SEMICOLON marker WS?
@@ -182,9 +143,8 @@ def _parse_requirement_marker(
if not tokenizer.check("SEMICOLON"):
tokenizer.raise_syntax_error(
f"Expected {expected} or end",
f"Expected end or semicolon (after {after})",
span_start=span_start,
span_end=None,
)
tokenizer.read()
@@ -347,7 +307,7 @@ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
return (marker_var_left, marker_op, marker_var_right)
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:
"""
marker_var = VARIABLE | QUOTED_STRING
"""
+46 -18
View File
@@ -2,32 +2,60 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""Backward-compatibility shim for unpickling Version objects serialized before
packaging 26.1.
Old pickles reference ``packaging._structures.InfinityType`` and
``packaging._structures.NegativeInfinityType``. This module provides minimal
stand-in classes so that ``pickle.loads()`` can resolve those references.
The deserialized objects are not used for comparisons ``Version.__setstate__``
discards the stale ``_key`` cache and recomputes it from the core version fields.
"""
from __future__ import annotations
class InfinityType:
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
def __repr__(self) -> str:
return "Infinity"
def __hash__(self) -> int:
return hash(repr(self))
class NegativeInfinityType:
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
def __lt__(self, other: object) -> bool:
return False
def __repr__(self) -> str:
return "-Infinity"
def __le__(self, other: object) -> bool:
return False
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__)
def __gt__(self, other: object) -> bool:
return True
def __ge__(self, other: object) -> bool:
return True
def __neg__(self: object) -> "NegativeInfinityType":
return NegativeInfinity
Infinity = InfinityType()
class NegativeInfinityType:
def __repr__(self) -> str:
return "-Infinity"
def __hash__(self) -> int:
return hash(repr(self))
def __lt__(self, other: object) -> bool:
return True
def __le__(self, other: object) -> bool:
return True
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__)
def __gt__(self, other: object) -> bool:
return False
def __ge__(self, other: object) -> bool:
return False
def __neg__(self: object) -> InfinityType:
return Infinity
NegativeInfinity = NegativeInfinityType()
+26 -24
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import contextlib
import re
from dataclasses import dataclass
from typing import Generator, Mapping, NoReturn
from typing import Iterator, NoReturn
from .specifiers import Specifier
@@ -33,16 +33,16 @@ class ParserSyntaxError(Exception):
def __str__(self) -> str:
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
return f"{self.message}\n {self.source}\n {marker}"
return "\n ".join([self.message, self.source, marker])
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
"LEFT_PARENTHESIS": re.compile(r"\("),
"RIGHT_PARENTHESIS": re.compile(r"\)"),
"LEFT_BRACKET": re.compile(r"\["),
"RIGHT_BRACKET": re.compile(r"\]"),
"SEMICOLON": re.compile(r";"),
"COMMA": re.compile(r","),
DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
"LEFT_PARENTHESIS": r"\(",
"RIGHT_PARENTHESIS": r"\)",
"LEFT_BRACKET": r"\[",
"RIGHT_BRACKET": r"\]",
"SEMICOLON": r";",
"COMMA": r",",
"QUOTED_STRING": re.compile(
r"""
(
@@ -53,10 +53,10 @@ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
""",
re.VERBOSE,
),
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
"BOOLOP": re.compile(r"\b(or|and)\b"),
"IN": re.compile(r"\bin\b"),
"NOT": re.compile(r"\bnot\b"),
"OP": r"(===|==|~=|!=|<=|>=|<|>)",
"BOOLOP": r"\b(or|and)\b",
"IN": r"\bin\b",
"NOT": r"\bnot\b",
"VARIABLE": re.compile(
r"""
\b(
@@ -75,16 +75,16 @@ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
re.VERBOSE,
),
"SPECIFIER": re.compile(
Specifier._specifier_regex_str,
Specifier._operator_regex_str + Specifier._version_regex_str,
re.VERBOSE | re.IGNORECASE,
),
"AT": re.compile(r"\@"),
"URL": re.compile(r"[^ \t]+"),
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
"WS": re.compile(r"[ \t]+"),
"END": re.compile(r"$"),
"AT": r"\@",
"URL": r"[^ \t]+",
"IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
"VERSION_PREFIX_TRAIL": r"\.\*",
"VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
"WS": r"[ \t]+",
"END": r"$",
}
@@ -99,10 +99,12 @@ class Tokenizer:
self,
source: str,
*,
rules: Mapping[str, re.Pattern[str]],
rules: dict[str, str | re.Pattern[str]],
) -> None:
self.source = source
self.rules = rules
self.rules: dict[str, re.Pattern[str]] = {
name: re.compile(pattern) for name, pattern in rules.items()
}
self.next_token: Token | None = None
self.position = 0
@@ -172,7 +174,7 @@ class Tokenizer:
@contextlib.contextmanager
def enclosing_tokens(
self, open_token: str, close_token: str, *, around: str
) -> Generator[None, None, None]:
) -> Iterator[None]:
if self.check(open_token):
open_position = self.position
self.read()
-302
View File
@@ -1,302 +0,0 @@
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from .errors import _ErrorCollector
from .requirements import Requirement
__all__ = [
"CyclicDependencyGroup",
"DependencyGroupInclude",
"DependencyGroupResolver",
"DuplicateGroupNames",
"InvalidDependencyGroupObject",
"resolve_dependency_groups",
]
def __dir__() -> list[str]:
return __all__
# -----------
# Error Types
# -----------
class DuplicateGroupNames(ValueError):
"""
The same dependency groups were defined twice, with different non-normalized names.
"""
class CyclicDependencyGroup(ValueError):
"""
The dependency group includes form a cycle.
"""
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
self.requested_group = requested_group
self.group = group
self.include_group = include_group
if include_group == group:
reason = f"{group} includes itself"
else:
reason = f"{include_group} -> {group}, {group} -> {include_group}"
super().__init__(
"Cyclic dependency group include while resolving "
f"{requested_group}: {reason}"
)
# in the PEP 735 spec, the tables in dependency group lists were described as
# "Dependency Object Specifiers", but the only defined type of object was a
# "Dependency Group Include" -- hence the naming of this error as "Object"
class InvalidDependencyGroupObject(ValueError):
"""
A member of a dependency group was identified as a dict, but was not in a valid
format.
"""
# ------------------------
# Object Model & Interface
# ------------------------
class DependencyGroupInclude:
__slots__ = ("include_group",)
def __init__(self, include_group: str) -> None:
"""
Initialize a DependencyGroupInclude.
:param include_group: The name of the group referred to by this include.
"""
self.include_group = include_group
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.include_group!r})"
class DependencyGroupResolver:
"""
A resolver for Dependency Group data.
This class handles caching, name normalization, cycle detection, and other
parsing requirements. There are only two public methods for exploring the data:
``lookup()`` and ``resolve()``.
:param dependency_groups: A mapping, as provided via pyproject
``[dependency-groups]``.
"""
def __init__(
self,
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
) -> None:
errors = _ErrorCollector()
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
# a map of group names to parsed data
self._parsed_groups: dict[
str, tuple[Requirement | DependencyGroupInclude, ...]
] = {}
# a map of group names to their ancestors, used for cycle detection
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
# a cache of completed resolutions to Requirement lists
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
errors.finalize("[dependency-groups] data was invalid")
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
"""
Lookup a group name, returning the parsed dependency data for that group.
This will not resolve includes.
:param group: the name of the group to lookup
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._parse_group(group, errors)
def resolve(self, group: str) -> tuple[Requirement, ...]:
"""
Resolve a dependency group to a list of requirements.
:param group: the name of the group to resolve
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._resolve(group, group, errors)
def _resolve(
self, group: str, requested_group: str, errors: _ErrorCollector
) -> tuple[Requirement, ...]:
"""
This is a helper for cached resolution to strings. It preserves the name of the
group which the user initially requested in order to present a clearer error in
the event that a cycle is detected.
:param group: The normalized name of the group to resolve.
:param requested_group: The group which was used in the original, user-facing
request.
"""
if group in self._resolve_cache:
return self._resolve_cache[group]
parsed = self._parse_group(group, errors)
resolved_group = []
for item in parsed:
if isinstance(item, Requirement):
resolved_group.append(item)
elif isinstance(item, DependencyGroupInclude):
include_group = _normalize_name(item.include_group)
# if a group is cyclic, record the error
# otherwise, follow the include_group reference
#
# this allows us to examine all includes in a group, even in the
# presence of errors
if include_group in self._include_graph_ancestors.get(group, ()):
errors.error(
CyclicDependencyGroup(
requested_group, group, item.include_group
)
)
else:
self._include_graph_ancestors[include_group] = (
*self._include_graph_ancestors.get(group, ()),
group,
)
resolved_group.extend(
self._resolve(include_group, requested_group, errors)
)
else: # pragma: no cover
raise NotImplementedError(
f"Invalid dependency group item after parse: {item}"
)
# in the event that errors were detected, present the group as empty and do not
# cache the result
# this ensures that repeated access to a cyclic group will raise multiple errors
if errors.errors:
return ()
self._resolve_cache[group] = tuple(resolved_group)
return self._resolve_cache[group]
def _parse_group(
self, group: str, errors: _ErrorCollector
) -> tuple[Requirement | DependencyGroupInclude, ...]:
# short circuit -- never do the work twice
if group in self._parsed_groups:
return self._parsed_groups[group]
if group not in self.dependency_groups:
errors.error(LookupError(f"Dependency group '{group}' not found"))
return ()
raw_group = self.dependency_groups[group]
if isinstance(raw_group, str):
errors.error(
TypeError(
f"Dependency group {group!r} contained a string rather than a list."
)
)
return ()
if not isinstance(raw_group, Sequence):
errors.error(
TypeError(f"Dependency group {group!r} is not a sequence type.")
)
return ()
elements: list[Requirement | DependencyGroupInclude] = []
for item in raw_group:
if isinstance(item, str):
# packaging.requirements.Requirement parsing ensures that this is a
# valid PEP 508 Dependency Specifier
# raises InvalidRequirement on failure
elements.append(Requirement(item))
elif isinstance(item, Mapping):
if tuple(item.keys()) != ("include-group",):
errors.error(
InvalidDependencyGroupObject(
f"Invalid dependency group item: {item!r}"
)
)
else:
include_group = item["include-group"]
elements.append(DependencyGroupInclude(include_group=include_group))
else:
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
self._parsed_groups[group] = tuple(elements)
return self._parsed_groups[group]
# --------------------
# Functional Interface
# --------------------
def resolve_dependency_groups(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
) -> tuple[str, ...]:
"""
Resolve a dependency group to a tuple of requirements, as strings.
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
from ``pyproject.toml``
:param groups: the name of the group(s) to resolve
"""
resolver = DependencyGroupResolver(dependency_groups)
return tuple(str(r) for group in groups for r in resolver.resolve(group))
# ----------------
# internal helpers
# ----------------
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
def _normalize_name(name: str) -> str:
return _NORMALIZE_PATTERN.sub("-", name).lower()
def _normalize_group_names(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
errors: _ErrorCollector,
) -> dict[str, Sequence[str | Mapping[str, str]]]:
original_names: dict[str, list[str]] = {}
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
for group_name, value in dependency_groups.items():
normed_group_name = _normalize_name(group_name)
original_names.setdefault(normed_group_name, []).append(group_name)
normalized_groups[normed_group_name] = value
for normed_name, names in original_names.items():
if len(names) > 1:
errors.error(
DuplicateGroupNames(
"Duplicate dependency group names: "
f"{normed_name} ({', '.join(names)})"
)
)
return normalized_groups
-325
View File
@@ -1,325 +0,0 @@
from __future__ import annotations
import dataclasses
import re
import urllib.parse
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
if TYPE_CHECKING: # pragma: no cover
import sys
from collections.abc import Collection
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
__all__ = [
"ArchiveInfo",
"DirInfo",
"DirectUrl",
"DirectUrlValidationError",
"VcsInfo",
]
def __dir__() -> list[str]:
return __all__
_T = TypeVar("_T")
class _FromMappingProtocol(Protocol): # pragma: no cover
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
return {key: value for key, value in data if value is not None}
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
"""Get a value from the dictionary and verify it's the expected type."""
if (value := d.get(key)) is None:
return None
if not isinstance(value, expected_type):
raise DirectUrlValidationError(
f"Unexpected type {type(value).__name__} "
f"(expected {expected_type.__name__})",
context=key,
)
return value
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
"""Get a required value from the dictionary and verify it's the expected type."""
if (value := _get(d, expected_type, key)) is None:
raise _DirectUrlRequiredKeyError(key)
return value
def _get_object(
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
) -> _FromMappingProtocolT | None:
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
try:
return target_type._from_dict(value)
except Exception as e:
raise DirectUrlValidationError(e, context=key) from e
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
)
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
if "@" not in netloc:
return netloc
user_pass, netloc_no_user_pass = netloc.split("@", 1)
if user_pass in safe_user_passwords:
return netloc
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
return netloc
return netloc_no_user_pass
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
"""url with user:password part removed unless it is formed with
environment variables as specified in PEP 610, or it is a safe user:password
such as `git`.
"""
parsed_url = urllib.parse.urlsplit(url)
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
return urllib.parse.urlunsplit(
(
parsed_url.scheme,
netloc,
parsed_url.path,
parsed_url.query,
parsed_url.fragment,
)
)
class DirectUrlValidationError(Exception):
"""Raised when when input data is not spec-compliant."""
context: str | None = None
message: str
def __init__(
self,
cause: str | Exception,
*,
context: str | None = None,
) -> None:
if isinstance(cause, DirectUrlValidationError):
if cause.context:
self.context = (
f"{context}.{cause.context}" if context else cause.context
)
else:
self.context = context # pragma: no cover
self.message = cause.message
else:
self.context = context
self.message = str(cause)
def __str__(self) -> str:
if self.context:
return f"{self.message} in {self.context!r}"
return self.message
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
def __init__(self, key: str) -> None:
super().__init__("Missing required value", context=key)
@dataclasses.dataclass(frozen=True, init=False)
class VcsInfo:
vcs: str
commit_id: str
requested_revision: str | None = None
def __init__(
self,
*,
vcs: str,
commit_id: str,
requested_revision: str | None = None,
) -> None:
object.__setattr__(self, "vcs", vcs)
object.__setattr__(self, "commit_id", commit_id)
object.__setattr__(self, "requested_revision", requested_revision)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
# We can't validate vcs value because is not closed.
return cls(
vcs=_get_required(d, str, "vcs"),
requested_revision=_get(d, str, "requested_revision"),
commit_id=_get_required(d, str, "commit_id"),
)
@dataclasses.dataclass(frozen=True, init=False)
class ArchiveInfo:
hashes: Mapping[str, str] | None = None
def __init__(
self,
*,
hashes: Mapping[str, str] | None = None,
) -> None:
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
raise DirectUrlValidationError(
"Hash values must be strings", context="hashes"
)
legacy_hash = _get(d, str, "hash")
if legacy_hash is not None:
if "=" not in legacy_hash:
raise DirectUrlValidationError(
"Invalid hash format (expected '<algorithm>=<hash>')",
context="hash",
)
hash_algorithm, hash_value = legacy_hash.split("=", 1)
if hashes is None:
# if `hashes` are not present, we can derive it from the legacy `hash`
hashes = {hash_algorithm: hash_value}
else:
# if `hashes` are present, the legacy `hash` must match one of them
if hash_algorithm not in hashes:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"is not present in hashes field",
context="hashes",
)
if hashes[hash_algorithm] != hash_value:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"has different value in hashes field",
context="hash",
)
return cls(hashes=hashes)
@dataclasses.dataclass(frozen=True, init=False)
class DirInfo:
editable: bool | None = None
def __init__(
self,
*,
editable: bool | None = None,
) -> None:
object.__setattr__(self, "editable", editable)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
return cls(
editable=_get(d, bool, "editable"),
)
@dataclasses.dataclass(frozen=True, init=False)
class DirectUrl:
"""A class representing a direct URL."""
url: str
archive_info: ArchiveInfo | None = None
vcs_info: VcsInfo | None = None
dir_info: DirInfo | None = None
subdirectory: str | None = None # XXX Path or str?
def __init__(
self,
*,
url: str,
archive_info: ArchiveInfo | None = None,
vcs_info: VcsInfo | None = None,
dir_info: DirInfo | None = None,
subdirectory: str | None = None,
) -> None:
object.__setattr__(self, "url", url)
object.__setattr__(self, "archive_info", archive_info)
object.__setattr__(self, "vcs_info", vcs_info)
object.__setattr__(self, "dir_info", dir_info)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
direct_url = cls(
url=_get_required(d, str, "url"),
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
dir_info=_get_object(d, DirInfo, "dir_info"),
subdirectory=_get(d, str, "subdirectory"),
)
if (
bool(direct_url.vcs_info)
+ bool(direct_url.archive_info)
+ bool(direct_url.dir_info)
) != 1:
raise DirectUrlValidationError(
"Exactly one of vcs_info, archive_info, dir_info must be present"
)
if direct_url.dir_info is not None and not direct_url.url.startswith("file://"):
raise DirectUrlValidationError(
"URL scheme must be file:// when dir_info is present",
context="url",
)
# XXX subdirectory must be relative, can we, should we validate that here?
return direct_url
@classmethod
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
"""Create and validate a DirectUrl instance from a JSON dictionary."""
return cls._from_dict(d)
def to_dict(
self,
*,
generate_legacy_hash: bool = False,
strip_user_password: bool = True,
safe_user_passwords: Collection[str] = ("git",),
) -> Mapping[str, Any]:
"""Convert the DirectUrl instance to a JSON dictionary.
:param generate_legacy_hash: If True, include a legacy `hash` field in
`archive_info` for backward compatibility with tools that don't
support the `hashes` field.
:param strip_user_password: If True, strip user:password from the URL
unless it is formed with environment variables as specified in PEP
610, or it is a safe user:password such as `git`.
:param safe_user_passwords: A collection of user:password strings that
should not be stripped from the URL even if `strip_user_password` is
True.
"""
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
if strip_user_password:
res["url"] = _strip_url(self.url, safe_user_passwords)
return res
def validate(self) -> None:
"""Validate the DirectUrl instance against the specification.
Raises :class:`DirectUrlValidationError` if invalid.
"""
self.from_dict(self.to_dict())
-94
View File
@@ -1,94 +0,0 @@
from __future__ import annotations
import contextlib
import dataclasses
import sys
import typing
__all__ = ["ExceptionGroup"]
def __dir__() -> list[str]:
return __all__
if sys.version_info >= (3, 11): # pragma: no cover
from builtins import ExceptionGroup
else: # pragma: no cover
class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
If :external:exc:`ExceptionGroup` is already defined by Python itself,
that version is used instead.
"""
message: str
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
@dataclasses.dataclass
class _ErrorCollector:
"""
Collect errors into ExceptionGroups.
Used like this:
collector = _ErrorCollector()
# Add a single exception
collector.error(ValueError("one"))
# Supports nesting, including combining ExceptionGroups
with collector.collect():
raise ValueError("two")
collector.finalize("Found some errors")
Since making a collector and then calling finalize later is a common pattern,
a convenience method ``on_exit`` is provided.
"""
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
def finalize(self, msg: str) -> None:
"""Raise a group exception if there are any errors."""
if self.errors:
raise ExceptionGroup(msg, self.errors)
@contextlib.contextmanager
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
"""
Calls finalize if no uncollected errors were present.
Uncollected errors are raised normally.
"""
yield self
self.finalize(msg)
@contextlib.contextmanager
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
"""
Context manager to collect errors into the error list.
Must be inside loops, as only one error can be collected at a time.
"""
error_classes = err_cls or (Exception,)
try:
yield
except ExceptionGroup as error:
self.errors.extend(error.exceptions)
except error_classes as error:
self.errors.append(error)
def error(
self,
error: Exception,
) -> None:
"""Add an error to the list."""
self.errors.append(error)
+11 -52
View File
@@ -34,7 +34,7 @@ from __future__ import annotations
import re
from typing import NewType, cast
from ._spdx import EXCEPTIONS, LICENSES
from packaging.licenses._spdx import EXCEPTIONS, LICENSES
__all__ = [
"InvalidLicenseExpression",
@@ -42,25 +42,14 @@ __all__ = [
"canonicalize_license_expression",
]
# Simple __dir__ implementation since there are no public submodules
def __dir__() -> list[str]:
return __all__
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized
License-Expression.
"""
class InvalidLicenseExpression(ValueError):
"""Raised when a license-expression string is invalid
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("invalid")
Traceback (most recent call last):
...
@@ -71,34 +60,6 @@ class InvalidLicenseExpression(ValueError):
def canonicalize_license_expression(
raw_license_expression: str,
) -> NormalizedLicenseExpression:
"""
This function takes a valid License-Expression, and returns the normalized
form of it.
The return type is typed as :class:`NormalizedLicenseExpression`. This
allows type checkers to help require that a string has passed through this
function before use.
:param str raw_license_expression: The License-Expression to canonicalize.
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
invalid/unknown license identifier or invalid syntax.
.. doctest::
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("mit")
'MIT'
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
>>> canonicalize_license_expression("(mit")
Traceback (most recent call last):
...
InvalidLicenseExpression: Invalid license expression: '(mit'
>>> canonicalize_license_expression("Use-it-after-midnight")
Traceback (most recent call last):
...
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
"""
if not raw_license_expression:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
@@ -119,21 +80,16 @@ def canonicalize_license_expression(
tokens = license_expression.split()
# Rather than implementing a parenthesis/boolean logic parser, create an
# expression that Python can parse. Everything that is not involved with the
# grammar itself is replaced with the placeholder `False` and the resultant
# expression should become a valid Python expression.
# Rather than implementing boolean logic, we create an expression that Python can
# parse. Everything that is not involved with the grammar itself is treated as
# `False` and the expression should evaluate as such.
python_tokens = []
for token in tokens:
if token not in {"or", "and", "with", "(", ")"}:
python_tokens.append("False")
elif token == "with":
python_tokens.append("or")
elif (
token == "("
and python_tokens
and python_tokens[-1] not in {"or", "and", "("}
) or (token == ")" and python_tokens and python_tokens[-1] == "("):
elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
else:
@@ -141,8 +97,11 @@ def canonicalize_license_expression(
python_expression = " ".join(python_tokens)
try:
compile(python_expression, "", "eval")
except SyntaxError:
invalid = eval(python_expression, globals(), locals())
except Exception:
invalid = True
if invalid is not False:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) from None
@@ -181,6 +140,6 @@ def canonicalize_license_expression(
normalized_expression = " ".join(normalized_tokens)
return cast(
"NormalizedLicenseExpression",
NormalizedLicenseExpression,
normalized_expression.replace("( ", "(").replace(" )", ")"),
)
+1 -41
View File
@@ -12,7 +12,7 @@ class SPDXException(TypedDict):
deprecated: bool
VERSION = '3.27.0'
VERSION = '3.25.0'
LICENSES: dict[str, SPDXLicense] = {
'0bsd': {'id': '0BSD', 'deprecated': False},
@@ -46,7 +46,6 @@ LICENSES: dict[str, SPDXLicense] = {
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
'any-osi': {'id': 'any-OSI', 'deprecated': False},
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
@@ -62,8 +61,6 @@ LICENSES: dict[str, SPDXLicense] = {
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
@@ -78,7 +75,6 @@ LICENSES: dict[str, SPDXLicense] = {
'blessing': {'id': 'blessing', 'deprecated': False},
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
'borceux': {'id': 'Borceux', 'deprecated': False},
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
@@ -89,7 +85,6 @@ LICENSES: dict[str, SPDXLicense] = {
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
@@ -181,8 +176,6 @@ LICENSES: dict[str, SPDXLicense] = {
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
@@ -222,7 +215,6 @@ LICENSES: dict[str, SPDXLicense] = {
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
'cronyx': {'id': 'Cronyx', 'deprecated': False},
'crossword': {'id': 'Crossword', 'deprecated': False},
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
'cube': {'id': 'Cube', 'deprecated': False},
@@ -234,9 +226,7 @@ LICENSES: dict[str, SPDXLicense] = {
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
'doc': {'id': 'DOC', 'deprecated': False},
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
@@ -273,17 +263,12 @@ LICENSES: dict[str, SPDXLicense] = {
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
'fsful': {'id': 'FSFUL', 'deprecated': False},
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
'ftl': {'id': 'FTL', 'deprecated': False},
'furuseth': {'id': 'Furuseth', 'deprecated': False},
'fwlw': {'id': 'fwlw', 'deprecated': False},
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
'gd': {'id': 'GD', 'deprecated': False},
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
@@ -335,7 +320,6 @@ LICENSES: dict[str, SPDXLicense] = {
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
'gutmann': {'id': 'Gutmann', 'deprecated': False},
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
'hdf5': {'id': 'HDF5', 'deprecated': False},
'hdparm': {'id': 'hdparm', 'deprecated': False},
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
@@ -376,7 +360,6 @@ LICENSES: dict[str, SPDXLicense] = {
'imlib2': {'id': 'Imlib2', 'deprecated': False},
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
'intel': {'id': 'Intel', 'deprecated': False},
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
@@ -386,7 +369,6 @@ LICENSES: dict[str, SPDXLicense] = {
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
'jam': {'id': 'Jam', 'deprecated': False},
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
'jove': {'id': 'jove', 'deprecated': False},
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
'jpnic': {'id': 'JPNIC', 'deprecated': False},
'json': {'id': 'JSON', 'deprecated': False},
@@ -412,7 +394,6 @@ LICENSES: dict[str, SPDXLicense] = {
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
'libpng': {'id': 'Libpng', 'deprecated': False},
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
'libtiff': {'id': 'libtiff', 'deprecated': False},
@@ -443,17 +424,14 @@ LICENSES: dict[str, SPDXLicense] = {
'magaz': {'id': 'magaz', 'deprecated': False},
'mailprio': {'id': 'mailprio', 'deprecated': False},
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
'man2html': {'id': 'man2html', 'deprecated': False},
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
'metamail': {'id': 'metamail', 'deprecated': False},
'minpack': {'id': 'Minpack', 'deprecated': False},
'mips': {'id': 'MIPS', 'deprecated': False},
'miros': {'id': 'MirOS', 'deprecated': False},
'mit': {'id': 'MIT', 'deprecated': False},
'mit-0': {'id': 'MIT-0', 'deprecated': False},
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
@@ -494,7 +472,6 @@ LICENSES: dict[str, SPDXLicense] = {
'netcdf': {'id': 'NetCDF', 'deprecated': False},
'newsletr': {'id': 'Newsletr', 'deprecated': False},
'ngpl': {'id': 'NGPL', 'deprecated': False},
'ngrep': {'id': 'ngrep', 'deprecated': False},
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
@@ -509,7 +486,6 @@ LICENSES: dict[str, SPDXLicense] = {
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
'nrl': {'id': 'NRL', 'deprecated': False},
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
'ntp': {'id': 'NTP', 'deprecated': False},
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
'nunit': {'id': 'Nunit', 'deprecated': True},
@@ -604,7 +580,6 @@ LICENSES: dict[str, SPDXLicense] = {
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
'sendmail': {'id': 'Sendmail', 'deprecated': False},
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
@@ -617,12 +592,10 @@ LICENSES: dict[str, SPDXLicense] = {
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
'sl': {'id': 'SL', 'deprecated': False},
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
'smppl': {'id': 'SMPPL', 'deprecated': False},
'snia': {'id': 'SNIA', 'deprecated': False},
'snprintf': {'id': 'snprintf', 'deprecated': False},
'sofa': {'id': 'SOFA', 'deprecated': False},
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
'soundex': {'id': 'Soundex', 'deprecated': False},
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
@@ -636,7 +609,6 @@ LICENSES: dict[str, SPDXLicense] = {
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
'sunpro': {'id': 'SunPro', 'deprecated': False},
@@ -648,14 +620,12 @@ LICENSES: dict[str, SPDXLicense] = {
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
'tmate': {'id': 'TMate', 'deprecated': False},
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
'tosl': {'id': 'TOSL', 'deprecated': False},
'tpdl': {'id': 'TPDL', 'deprecated': False},
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
'ttwl': {'id': 'TTWL', 'deprecated': False},
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
@@ -671,8 +641,6 @@ LICENSES: dict[str, SPDXLicense] = {
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
'unlicense': {'id': 'Unlicense', 'deprecated': False},
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
'vim': {'id': 'Vim', 'deprecated': False},
@@ -686,7 +654,6 @@ LICENSES: dict[str, SPDXLicense] = {
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
'wwl': {'id': 'wwl', 'deprecated': False},
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
'x11': {'id': 'X11', 'deprecated': False},
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
@@ -728,11 +695,9 @@ EXCEPTIONS: dict[str, SPDXException] = {
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
@@ -749,16 +714,13 @@ EXCEPTIONS: dict[str, SPDXException] = {
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
@@ -768,14 +730,12 @@ EXCEPTIONS: dict[str, SPDXException] = {
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
+51 -181
View File
@@ -8,7 +8,7 @@ import operator
import os
import platform
import sys
from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast
from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
from ._parser import parse_marker as _parse_marker
@@ -17,7 +17,6 @@ from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
__all__ = [
"Environment",
"EvaluateContext",
"InvalidMarker",
"Marker",
@@ -26,58 +25,31 @@ __all__ = [
"default_environment",
]
def __dir__() -> list[str]:
return __all__
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
* ``"metadata"`` (for core metadata; default)
* ``"lock_file"`` (for lock files)
* ``"requirement"`` (i.e. all other situations)
"""
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
MARKERS_REQUIRING_VERSION = {
"implementation_version",
"platform_release",
"python_full_version",
"python_version",
}
class InvalidMarker(ValueError):
"""Raised when attempting to create a :class:`Marker` from invalid input.
This error indicates that the given marker string does not conform to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
"""
An invalid marker was found, users should refer to PEP 508.
"""
class UndefinedComparison(ValueError):
"""Raised when evaluating an unsupported marker comparison.
This can happen when marker values are compared as versions but do not
conform to the :ref:`specification of version specifiers
<pypug:version-specifiers>`.
"""
An invalid operation was attempted on a value that doesn't support it.
"""
class UndefinedEnvironmentName(ValueError):
"""Raised when evaluating a marker that references a missing environment key."""
"""
A name was attempted to be used that does not exist inside of the
environment.
"""
class Environment(TypedDict):
"""
A dictionary that represents a Python environment as captured by
:func:`default_environment`. All fields are required.
"""
implementation_name: str
"""The implementation's identifier, e.g. ``'cpython'``."""
@@ -149,28 +121,20 @@ class Environment(TypedDict):
"""
def _normalize_extras(
result: MarkerList | MarkerAtom | str,
) -> MarkerList | MarkerAtom | str:
if not isinstance(result, tuple):
return result
lhs, op, rhs = result
def _normalize_extra_values(results: Any) -> Any:
"""
Normalize extra values.
"""
if isinstance(results[0], tuple):
lhs, op, rhs = results[0]
if isinstance(lhs, Variable) and lhs.value == "extra":
normalized_extra = canonicalize_name(rhs.value)
rhs = Value(normalized_extra)
elif isinstance(rhs, Variable) and rhs.value == "extra":
normalized_extra = canonicalize_name(lhs.value)
lhs = Value(normalized_extra)
return lhs, op, rhs
def _normalize_extra_values(results: MarkerList) -> MarkerList:
"""
Normalize extra values.
"""
return [_normalize_extras(r) for r in results]
results[0] = lhs, op, rhs
return results
def _format_marker(
@@ -204,26 +168,25 @@ def _format_marker(
_operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": lambda _lhs, _rhs: False,
"<=": operator.eq,
"<": operator.lt,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
">=": operator.eq,
">": lambda _lhs, _rhs: False,
">=": operator.ge,
">": operator.gt,
}
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
op_str = op.serialize()
if key in MARKERS_REQUIRING_VERSION:
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
if isinstance(rhs, str):
try:
spec = Specifier(f"{op_str}{rhs}")
spec = Specifier("".join([op.serialize(), rhs]))
except InvalidSpecifier:
pass
else:
return spec.contains(lhs, prereleases=True)
oper: Operator | None = _operators.get(op_str)
oper: Operator | None = _operators.get(op.serialize())
if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
@@ -233,14 +196,13 @@ def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool
def _normalize(
lhs: str, rhs: str | AbstractSet[str], key: str
) -> tuple[str, str | AbstractSet[str]]:
# PEP 685 - Comparison of extra names for optional distribution dependencies
# PEP 685 Comparison of extra names for optional distribution dependencies
# https://peps.python.org/pep-0685/
# > When comparing extra names, tools MUST normalize the names being
# > compared using the semantics outlined in PEP 503 for names
if key == "extra":
assert isinstance(rhs, str), "extra value must be a string"
# Both sides are normalized at this point already
return (lhs, rhs)
return (canonicalize_name(lhs), canonicalize_name(rhs))
if key in MARKERS_ALLOWING_SET:
if isinstance(rhs, str): # pragma: no cover
return (canonicalize_name(lhs), canonicalize_name(rhs))
@@ -257,6 +219,8 @@ def _evaluate_markers(
groups: list[list[bool]] = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, str))
if isinstance(marker, list):
groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple):
@@ -270,21 +234,18 @@ def _evaluate_markers(
lhs_value = lhs.value
environment_key = rhs.value
rhs_value = environment[environment_key]
assert isinstance(lhs_value, str), "lhs must be a string"
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
elif marker == "or":
groups[-1].append(_eval_op(lhs_value, op, rhs_value))
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
elif marker == "and":
pass
else: # pragma: nocover
raise TypeError(f"Unexpected marker {marker!r}")
return any(all(item) for item in groups)
def _format_full_version(info: sys._version_info) -> str:
def format_full_version(info: sys._version_info) -> str:
version = f"{info.major}.{info.minor}.{info.micro}"
kind = info.releaselevel
if kind != "final":
@@ -293,11 +254,7 @@ def _format_full_version(info: sys._version_info) -> str:
def default_environment() -> Environment:
"""Return the default marker environment for the current Python process.
This is the base environment used by :meth:`Marker.evaluate`.
"""
iver = _format_full_version(sys.implementation.version)
iver = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
return {
"implementation_name": implementation_name,
@@ -315,36 +272,10 @@ def default_environment() -> Environment:
class Marker:
"""Represents a parsed dependency marker expression.
Marker expressions are parsed according to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
:param marker: The string representation of a marker expression.
:raises InvalidMarker: If ``marker`` cannot be parsed.
Instances are safe to serialize with :mod:`pickle`. They use a stable
format so the same pickle can be loaded in future packaging releases.
.. versionchanged:: 26.2
Added a stable pickle format. Pickles created with packaging 26.2+ can
be unpickled with future releases. Backward compatibility with pickles
from packaging < 26.2 is supported but may be removed in a future
release.
"""
__slots__ = ("_markers",)
def __init__(self, marker: str) -> None:
# Note: We create a Marker object without calling this constructor in
# packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement.
# If this fails and throws an error, the repr still expects _markers to
# be defined.
self._markers: MarkerList = []
try:
self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a recursive type:
@@ -366,94 +297,36 @@ class Marker:
except ParserSyntaxError as e:
raise InvalidMarker(str(e)) from e
@classmethod
def _from_markers(cls, markers: MarkerList) -> Marker:
"""Create a Marker instance from a pre-parsed marker tree.
This avoids re-parsing serialised marker strings when combining markers.
"""
new = cls.__new__(cls)
new._markers = markers
return new
def __str__(self) -> str:
return _format_marker(self._markers)
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({str(self)!r})>"
return f"<Marker('{self}')>"
def __hash__(self) -> int:
return hash(str(self))
return hash((self.__class__.__name__, str(self)))
def __eq__(self, other: object) -> bool:
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Marker):
return NotImplemented
return str(self) == str(other)
def __getstate__(self) -> str:
# Return the marker expression string for compactness and stability.
# Internal Node objects are excluded; the string is re-parsed on load.
return str(self)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the marker expression string.
try:
self._markers = _normalize_extra_values(_parse_marker(state))
except ParserSyntaxError as exc:
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
return
if isinstance(state, dict) and "_markers" in state:
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
markers = state["_markers"]
if isinstance(markers, list):
self._markers = markers
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
markers = slot_dict["_markers"]
if isinstance(markers, list):
self._markers = markers
return
raise TypeError(f"Cannot restore Marker from {state!r}")
def __and__(self, other: Marker) -> Marker:
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "and", other._markers])
def __or__(self, other: Marker) -> Marker:
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "or", other._markers])
def evaluate(
self,
environment: Mapping[str, str | AbstractSet[str]] | None = None,
environment: dict[str, str] | None = None,
context: EvaluateContext = "metadata",
) -> bool:
"""Evaluate a marker.
Return the boolean from evaluating this marker against the environment.
The environment is determined from the current Python process unless
passed in explicitly.
:param environment: Mapping containing keys and values to override the
detected environment.
:param EvaluateContext context: The context in which the marker is
evaluated, which influences what marker names are considered valid.
Accepted values are ``"metadata"`` (for core metadata; default),
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
:raises UndefinedComparison: If the marker uses a comparison on values
that are not valid versions per the :ref:`specification of version
specifiers <pypug:version-specifiers>`.
:raises UndefinedEnvironmentName: If the marker references a value that
is missing from the evaluation environment.
:returns: ``True`` if the marker matches, otherwise ``False``.
Return the boolean from evaluating the given marker against the
environment. environment is an optional argument to override all or
part of the determined environment. The *context* parameter specifies what
context the markers are being evaluated for, which influences what markers
are considered valid. Acceptable values are "metadata" (for core metadata;
default), "lock_file", and "requirement" (i.e. all other situations).
The environment is determined from the current Python process.
"""
current_environment = cast(
"dict[str, str | AbstractSet[str]]", default_environment()
@@ -464,15 +337,12 @@ class Marker:
)
elif context == "metadata":
current_environment["extra"] = ""
if environment is not None:
current_environment.update(environment)
if "extra" in current_environment:
# The API used to allow setting extra to None. We need to handle
# this case for backwards compatibility. Also skip running
# normalize name if extra is empty.
extra = cast("str | None", current_environment["extra"])
current_environment["extra"] = canonicalize_name(extra) if extra else ""
# The API used to allow setting extra to None. We need to handle this
# case for backwards compatibility.
if "extra" in current_environment and current_environment["extra"] is None:
current_environment["extra"] = ""
return _evaluate_markers(
self._markers, _repair_python_full_version(current_environment)
@@ -486,7 +356,7 @@ def _repair_python_full_version(
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
python_full_version = cast("str", env["python_full_version"])
python_full_version = cast(str, env["python_full_version"])
if python_full_version.endswith("+"):
env["python_full_version"] = f"{python_full_version}local"
return env
+63 -165
View File
@@ -1,11 +1,12 @@
from __future__ import annotations
import email.feedparser
import email.header
import email.message
import email.parser
import email.policy
import keyword
import pathlib
import sys
import typing
from typing import (
Any,
@@ -18,27 +19,31 @@ from typing import (
from . import licenses, requirements, specifiers, utils
from . import version as version_module
from .errors import ExceptionGroup, _ErrorCollector
if typing.TYPE_CHECKING:
from .licenses import NormalizedLicenseExpression
T = typing.TypeVar("T")
__all__ = [
"ExceptionGroup", # Keep this for a bit (makes mypy happy w/ 26.0 compat)
"InvalidMetadata",
"Metadata",
"RFC822Message",
"RFC822Policy",
"RawMetadata",
"parse_email",
]
if sys.version_info >= (3, 11): # pragma: no cover
ExceptionGroup = ExceptionGroup
else: # pragma: no cover
class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
def __dir__() -> list[str]:
return __all__
If :external:exc:`ExceptionGroup` is already defined by Python itself,
that version is used instead.
"""
message: str
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
class InvalidMetadata(ValueError):
@@ -68,8 +73,8 @@ class RawMetadata(TypedDict, total=False):
Core metadata fields that can be specified multiple times are stored as a
list or dict depending on which is appropriate for the field. Any fields
which hold multiple values in a single field are stored as a list. All fields
are considered optional.
which hold multiple values in a single field are stored as a list.
"""
# Metadata 1.0 - PEP 241
@@ -121,19 +126,13 @@ class RawMetadata(TypedDict, total=False):
# Metadata 2.3 - PEP 685
# No new fields were added in PEP 685, just some edge case were
# tightened up to provide better interoperability.
# tightened up to provide better interoptability.
# Metadata 2.4 - PEP 639
license_expression: str
license_files: list[str]
# Metadata 2.5 - PEP 794
import_names: list[str]
import_namespaces: list[str]
# 'keywords' is special as it's a string in the core metadata spec, but we
# represent it as a list.
_STRING_FIELDS = {
"author",
"author_email",
@@ -166,8 +165,6 @@ _LIST_FIELDS = {
"requires_dist",
"requires_external",
"supported_platforms",
"import_names",
"import_namespaces",
}
_DICT_FIELDS = {
@@ -196,23 +193,24 @@ def _parse_project_urls(data: list[str]) -> dict[str, str]:
# be the missing value, then they'd have multiple '' values that
# overwrite each other in a accumulating dict.
#
# The other potential issue is that it's possible to have the
# The other potentional issue is that it's possible to have the
# same label multiple times in the metadata, with no solid "right"
# answer with what to do in that case. As such, we'll do the only
# thing we can, which is treat the field as unparsable and add it
# thing we can, which is treat the field as unparseable and add it
# to our list of unparsed fields.
#
parts = [p.strip() for p in pair.split(",", 1)]
parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items
# TODO: The spec doesn't say anything about if the keys should be
# considered case sensitive or not... logically they should
# be case-preserving and case-insensitive, but doing that
# would open up more cases where we might have duplicate
# entries.
label, _, url = (s.strip() for s in pair.partition(","))
label, url = parts
if label in urls:
# The label already exists in our set of urls, so this field
# is unparsable, and we can just add the whole thing to our
# unparsable data and stop processing it.
# is unparseable, and we can just add the whole thing to our
# unparseable data and stop processing it.
raise KeyError("duplicate labels in project urls")
urls[label] = url
@@ -259,8 +257,6 @@ _EMAIL_TO_RAW_MAPPING = {
"download-url": "download_url",
"dynamic": "dynamic",
"home-page": "home_page",
"import-name": "import_names",
"import-namespace": "import_namespaces",
"keywords": "keywords",
"license": "license",
"license-expression": "license_expression",
@@ -287,45 +283,6 @@ _EMAIL_TO_RAW_MAPPING = {
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
# This class is for writing RFC822 messages
class RFC822Policy(email.policy.EmailPolicy):
"""
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
implementation that handles multi-line values, and some nice defaults.
"""
utf8 = True
mangle_from_ = False
max_line_length = 0
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
size = len(name) + 2
value = value.replace("\n", "\n" + " " * size)
return (name, value)
# This class is for writing RFC822 messages
class RFC822Message(email.message.EmailMessage):
"""
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
our `RFC822Policy`, and it correctly writes unicode when being called
with `bytes()`.
"""
def __init__(self) -> None:
super().__init__(policy=RFC822Policy())
def as_bytes(
self, unixfrom: bool = False, policy: email.policy.Policy | None = None
) -> bytes:
"""
Return the bytes representation of the message.
This handles unicode encoding.
"""
return self.as_string(unixfrom, policy=policy).encode("utf-8")
def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
@@ -353,10 +310,10 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
# We have to wrap parsed.keys() in a set, because in the case of multiple
# values for a key (a list), the key will appear multiple times in the
# list of keys, but we're avoiding that by using get_all().
for name_with_case in frozenset(parsed.keys()):
for name in frozenset(parsed.keys()):
# Header names in RFC are case insensitive, so we'll normalize to all
# lower case to make comparisons easier.
name = name_with_case.lower()
name = name.lower()
# We use get_all() here, even for fields that aren't multiple use,
# because otherwise someone could have e.g. two Name fields, and we
@@ -392,16 +349,16 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
# can be independently encoded, so we'll need to check each
# of them.
chunks: list[tuple[bytes, str | None]] = []
for binary, _encoding in email.header.decode_header(h):
for bin, encoding in email.header.decode_header(h):
try:
binary.decode("utf8", "strict")
bin.decode("utf8", "strict")
except UnicodeDecodeError:
# Enable mojibake.
encoding = "latin1"
valid_encoding = False
else:
encoding = "utf8"
chunks.append((binary, encoding))
chunks.append((bin, encoding))
# Turn our chunks back into a Header object, then let that
# Header object do the right thing to turn them into a
@@ -440,11 +397,6 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
# of unparsed stuff.
if raw_name in _STRING_FIELDS and len(value) == 1:
raw[raw_name] = value[0]
# If this is import_names, we need to special case the empty field
# case, which converts to an empty list instead of None. We can't let
# the empty case slip through, as it will fail validation.
elif raw_name == "import_names" and value == [""]:
raw[raw_name] = []
# If this is one of our list of string fields, then we can just assign
# the value, since email *only* has strings, and our get_all() call
# above ensures that this is a list.
@@ -472,7 +424,7 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
except KeyError:
unparsed[name] = value
# Nothing that we've done has managed to parse this, so it'll just
# throw it in our unparsable data and move on.
# throw it in our unparseable data and move on.
else:
unparsed[name] = value
@@ -489,9 +441,9 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
else:
if payload:
# Check to see if we've already got a description, if so then both
# it, and this body move to unparsable.
# it, and this body move to unparseable.
if "description" in raw:
description_header = cast("str", raw.pop("description"))
description_header = cast(str, raw.pop("description"))
unparsed.setdefault("description", []).extend(
[description_header, payload]
)
@@ -504,15 +456,15 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
# literal key names, but we're computing our key names on purpose, but the
# way this function is implemented, our `TypedDict` can only have valid key
# names.
return cast("RawMetadata", raw), unparsed
return cast(RawMetadata, raw), unparsed
_NOT_FOUND = object()
# Keep the two values in sync.
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
@@ -567,7 +519,7 @@ class _Validator(Generic[T]):
except KeyError:
pass
return cast("T", value)
return cast(T, value)
def _invalid_metadata(
self, msg: str, cause: Exception | None = None
@@ -582,7 +534,7 @@ class _Validator(Generic[T]):
# Implicitly makes Metadata-Version required.
if value not in _VALID_METADATA_VERSIONS:
raise self._invalid_metadata(f"{value!r} is not a valid metadata version")
return cast("_MetadataVersion", value)
return cast(_MetadataVersion, value)
def _process_name(self, value: str) -> str:
if not value:
@@ -633,7 +585,7 @@ class _Validator(Generic[T]):
charset = parameters.get("charset", "UTF-8")
if charset != "UTF-8":
raise self._invalid_metadata(
f"{{field}} can only specify the UTF-8 charset, not {charset!r}"
f"{{field}} can only specify the UTF-8 charset, not {list(charset)}"
)
markdown_variants = {"GFM", "CommonMark"}
@@ -695,7 +647,9 @@ class _Validator(Generic[T]):
else:
return reqs
def _process_license_expression(self, value: str) -> NormalizedLicenseExpression:
def _process_license_expression(
self, value: str
) -> NormalizedLicenseExpression | None:
try:
return licenses.canonicalize_license_expression(value)
except ValueError as exc:
@@ -729,30 +683,6 @@ class _Validator(Generic[T]):
paths.append(path)
return paths
def _process_import_names(self, value: list[str]) -> list[str]:
for import_name in value:
name, semicolon, private = import_name.partition(";")
name = name.rstrip()
for identifier in name.split("."):
if not identifier.isidentifier():
raise self._invalid_metadata(
f"{name!r} is invalid for {{field}}; "
f"{identifier!r} is not a valid identifier"
)
elif keyword.iskeyword(identifier):
raise self._invalid_metadata(
f"{name!r} is invalid for {{field}}; "
f"{identifier!r} is a keyword"
)
if semicolon and private.lstrip() != "private":
raise self._invalid_metadata(
f"{import_name!r} is invalid for {{field}}; "
"the only valid option is 'private'"
)
return value
_process_import_namespaces = _process_import_names
class Metadata:
"""Representation of distribution metadata.
@@ -776,11 +706,13 @@ class Metadata:
ins._raw = data.copy() # Mutations occur due to caching enriched values.
if validate:
collector = _ErrorCollector()
metadata_version = None
with collector.collect(InvalidMetadata):
exceptions: list[Exception] = []
try:
metadata_version = ins.metadata_version
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
except InvalidMetadata as metadata_version_exc:
exceptions.append(metadata_version_exc)
metadata_version = None
# Make sure to check for the fields that are present, the required
# fields (so their absence can be reported).
@@ -797,7 +729,7 @@ class Metadata:
field_metadata_version = cls.__dict__[key].added
except KeyError:
exc = InvalidMetadata(key, f"unrecognized field: {key!r}")
collector.error(exc)
exceptions.append(exc)
continue
field_age = _VALID_METADATA_VERSIONS.index(
field_metadata_version
@@ -809,13 +741,14 @@ class Metadata:
f"{field} introduced in metadata version "
f"{field_metadata_version}, not {metadata_version}",
)
collector.error(exc)
exceptions.append(exc)
continue
getattr(ins, key)
except InvalidMetadata as exc:
collector.error(exc)
exceptions.append(exc)
collector.finalize("invalid metadata")
if exceptions:
raise ExceptionGroup("invalid metadata", exceptions)
return ins
@@ -829,13 +762,16 @@ class Metadata:
raw, unparsed = parse_email(data)
if validate:
with _ErrorCollector().on_exit("unparsed") as collector:
exceptions: list[Exception] = []
for unparsed_key in unparsed:
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
message = f"{unparsed_key!r} has invalid data"
else:
message = f"unrecognized field: {unparsed_key!r}"
collector.error(InvalidMetadata(unparsed_key, message))
exceptions.append(InvalidMetadata(unparsed_key, message))
if exceptions:
raise ExceptionGroup("unparsed", exceptions)
try:
return cls.from_raw(raw, validate=validate)
@@ -918,47 +854,9 @@ class Metadata:
""":external:ref:`core-metadata-provides-dist`"""
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
""":external:ref:`core-metadata-obsoletes-dist`"""
import_names: _Validator[list[str] | None] = _Validator(added="2.5")
""":external:ref:`core-metadata-import-name`"""
import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5")
""":external:ref:`core-metadata-import-namespace`"""
requires: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Requires`` (deprecated)"""
provides: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Provides`` (deprecated)"""
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
"""``Obsoletes`` (deprecated)"""
def as_rfc822(self) -> RFC822Message:
"""
Return an RFC822 message with the metadata.
"""
message = RFC822Message()
self._write_metadata(message)
return message
def _write_metadata(self, message: RFC822Message) -> None:
"""
Return an RFC822 message with the metadata.
"""
for name, validator in self.__class__.__dict__.items():
if isinstance(validator, _Validator) and name != "description":
value = getattr(self, name)
email_name = _RAW_TO_EMAIL_MAPPING[name]
if value is not None:
if email_name == "project-url":
for label, url in value.items():
message[email_name] = f"{label}, {url}"
elif email_name == "keywords":
message[email_name] = ",".join(value)
elif email_name == "import-name" and value == []:
message[email_name] = ""
elif isinstance(value, list):
for item in value:
message[email_name] = str(item)
else:
message[email_name] = str(value)
# The description is a special case because it is in the body of the message.
if self.description is not None:
message.set_payload(self.description)

Some files were not shown because too many files have changed in this diff Show More