Compare commits

..
11 Commits
Author SHA1 Message Date
Christoph b9d12555b5 allow same version when update the version
/ build_and_publish (release) Successful in 40s
2025-08-16 18:33:17 +02:00
Christoph fe8d60cf64 2025.9.100 2025-08-16 18:31:21 +02:00
Christoph 3679a808c5 update readme
/ build_and_publish (release) Failing after 37s
2025-08-16 18:26:07 +02:00
Christoph 91fb97a7c0 Update README.md 2025-08-16 11:45:26 +02:00
Christoph 727ea1fa0d Relicense to AGPL-3.0-or-later 2025-08-16 11:37:54 +02:00
Christoph Brandau 4a94f1aea8 fix if var exits for incr 2025-08-13 12:45:46 +02:00
Christoph Brandau 781b240128 add signature help 2025-08-13 11:00:22 +02:00
Christoph Brandau 1a542cc9e9 add new semantic tokens 2025-08-13 09:26:35 +02:00
Christoph Brandau 5b2bf39cba fix foreach var 2025-08-13 09:26:22 +02:00
Christoph Brandau db8f849a06 add variable check 2025-08-13 07:41:54 +02:00
Christoph Brandau 2ce48ad558 add: Outline support for DEF and CDL files 2025-08-12 18:43:41 +02:00
194 changed files with 3546 additions and 12753 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Build Package Files - name: Build Package Files
run: npm run package run: npm run package
- name: Update package.json Version - name: Update package.json Version
run: npm version ${{ github.ref_name }} --no-git-tag-version run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version
- name: Commit and Push Changes - name: Commit and Push Changes
run: | run: |
git config user.name "${{ vars.USERNAME_GIT }}" git config user.name "${{ vars.USERNAME_GIT }}"
@@ -40,7 +40,7 @@ jobs:
# Get current branch; if detached, resolve to a remote branch that contains this commit # Get current branch; if detached, resolve to a remote branch that contains this commit
BRANCH="$(git rev-parse --abbrev-ref HEAD)" BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then
# If target_commitish is a branch, use it; if its a SHA, find a branch that contains it # If target_commitish is a branch, use it; if it's a SHA, find a branch that contains it
CANDIDATE='${{ github.event.release.target_commitish }}' CANDIDATE='${{ github.event.release.target_commitish }}'
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)" BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
+1 -2
View File
@@ -8,5 +8,4 @@ __pycache__
*.pyc *.pyc
.nox .nox
*.g4 *.g4
.antlr .antlr
.claude
+12 -35
View File
@@ -10,21 +10,13 @@
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"runtimeExecutable": "${execPath}", "runtimeExecutable": "${execPath}",
"args": [ "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug", "outFiles": ["${workspaceFolder}/client/**/*.js"],
"${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>/**"],
"autoAttachChildProcesses": true, "autoAttachChildProcesses": true,
"preLaunchTask": "NX Post Support: Compile Debug" "preLaunchTask": {
"type": "npm",
"script": "watch"
}
}, },
{ {
"name": "Python Attach", "name": "Python Attach",
@@ -42,24 +34,10 @@
"name": "Debug Extension (hidden)", "name": "Debug Extension (hidden)",
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"args": [ "outFiles": ["${workspaceFolder}/client/**/*.js"],
"--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>/**"],
"env": { "env": {
"USE_DEBUGPY": "True", "USE_DEBUGPY": "True"
"NXPS_DEBUG_HOST": "127.0.0.1",
"NXPS_DEBUG_PORT": "5678"
}, },
"presentation": { "presentation": {
"hidden": true, "hidden": true,
@@ -71,9 +49,8 @@
"name": "Python debug server (hidden)", "name": "Python debug server (hidden)",
"type": "debugpy", "type": "debugpy",
"request": "attach", "request": "attach",
"listen": { "host": "127.0.0.1", "port": 5678 }, "listen": { "host": "localhost", "port": 5678 },
"justMyCode": false, "justMyCode": true,
"logToFile": true,
"presentation": { "presentation": {
"hidden": true, "hidden": true,
"group": "", "group": "",
@@ -86,7 +63,7 @@
"name": "Debug Extension and Python", "name": "Debug Extension and Python",
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"], "configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
"stopAll": true, "stopAll": true,
"preLaunchTask": "NX Post Support: Compile Debug", "preLaunchTask": "npm: watch",
"presentation": { "presentation": {
"hidden": false, "hidden": false,
"group": "", "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
}
}
]
}
+1 -20
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] ## [0.0.1]
- Initial release - Initial release
@@ -23,12 +12,4 @@
## [0.2.0] ## [0.2.0]
- Add DEF File Support - 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 - **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code - **Intelligent Code Analysis** - Linting and error detection for postprocessor code
- **Auto-completion** - Context-aware code completion for faster development - **Auto-completion** - Context-aware code completion for faster development
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
## Supported File Types ## Supported File Types
@@ -20,9 +19,8 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
## Installation ## Installation
1. Install from the VS Code Marketplace 1. Install from the VS Code Marketplace
2. Install Python 3.11 or higher 2. Open any `.cdl`, `.tcl`, or `.def` file
3. Open any `.cdl`, `.tcl`, or `.def` file 3. The extension will automatically activate and provide language support
4. The extension will automatically activate and provide language support
## Configuration ## 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.interpreter` - Specify custom Python interpreter path for the language server
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false) - `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true) - `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 ## Usage
@@ -47,30 +39,6 @@ Simply open any supported file type and enjoy:
- Code completion - Code completion
- Code formatting (Format Document command) - Code formatting (Format Document command)
- Hover information - 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 ## Contributing
-22
View File
@@ -14,7 +14,6 @@
"vscode-languageclient": "^9.0.1" "vscode-languageclient": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/vscode": "^1.96.0" "@types/vscode": "^1.96.0"
}, },
@@ -22,27 +21,6 @@
"vscode": "^1.96.0" "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": { "node_modules/@types/node": {
"version": "22.10.5", "version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
-1
View File
@@ -12,7 +12,6 @@
"vscode-languageclient": "^9.0.1" "vscode-languageclient": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/vscode": "^1.96.0" "@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 * as vscode from "vscode"
import {
cdlEventHandlerAtLine,
createCdlEventHandlerSnippet
} from "./cdlEventHandler"
const MACHINE_HEADER_REGEX = /^MACHINE\s+\S+/
export function formatCdlFile(content: string): string { export function formatCdlFile(content: string): string {
let indentLevel = 0 let indentLevel = 0
@@ -43,19 +37,15 @@ export function formatDefFile(content: string): string {
} }
export function isFirstLineMachine(content: string): boolean { export function isFirstLineMachine(content: string): boolean {
let lineStart = 0 const lines = content.split("\n").map((line) => line.trim())
while (lineStart <= content.length) { for (const line of lines) {
const newline = content.indexOf("\n", lineStart) console.log(line)
const lineEnd = newline === -1 ? content.length : newline
const line = content.slice(lineStart, lineEnd).trim()
if (line === "" || line.startsWith("#")) { if (line === "" || line.startsWith("#")) {
if (newline === -1) { console.log("skipping line")
return false
}
lineStart = newline + 1
continue continue
} }
return MACHINE_HEADER_REGEX.test(line) const machineRegex = /^MACHINE\s+\S+/
return machineRegex.test(line)
} }
return false return false
} }
@@ -63,11 +53,10 @@ export function isFirstLineMachine(content: string): boolean {
export function diagnosticHandler(document: vscode.TextDocument) { export function diagnosticHandler(document: vscode.TextDocument) {
const diagnostics: vscode.Diagnostic[] = [] const diagnostics: vscode.Diagnostic[] = []
if (document.languageId === "cdl" || document.languageId === "def") { if (document.languageId === "cdl" || document.languageId === "def") {
const text = document.getText() if (!isFirstLineMachine(document.getText())) {
if (!isFirstLineMachine(text)) {
const range = new vscode.Range( const range = new vscode.Range(
document.positionAt(0), document.positionAt(0),
document.positionAt(text.length) document.positionAt(document.getText().length)
) )
const diagnostic = new vscode.Diagnostic( const diagnostic = new vscode.Diagnostic(
range, range,
@@ -121,132 +110,33 @@ export function completionHandlerCdl(document: vscode.TextDocument, position: vs
} }
export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.Position) { export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.Position) {
const line = document.lineAt(position.line).text const wordRange = document.getWordRangeAtPosition(position)
const eventMatch = /^\s*EVENT\s+([^\s{]+)/.exec(line) const word = document.getText(wordRange)
if (eventMatch) { const text = document.getText()
const eventStart = line.indexOf(eventMatch[1], eventMatch.index) const lines = text.split("\n")
const declarationEnd = eventStart + eventMatch[1].length
if (position.character <= declarationEnd) { let hoverText: string | undefined
const handler = cdlEventHandlerAtLine(document.getText(), position.line)
if (handler) { for (const line of lines) {
const markdown = new vscode.MarkdownString() const words = line.split(/\s+/)
markdown.appendCodeblock(createCdlEventHandlerSnippet(handler), "tcl") const wordIndex = words.indexOf(word)
return new vscode.Hover(
markdown, if (wordIndex > 0) {
new vscode.Range(position.line, eventMatch.index, position.line, declarationEnd) 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 (hoverText) {
if (parameterMatch) { return new vscode.Hover(hoverText)
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, "")}`)
}
} }
return undefined 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[] { export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const symbols: vscode.DocumentSymbol[] = [] const symbols: vscode.DocumentSymbol[] = []
const lines = document.getText().split("\n") const lines = document.getText().split("\n")
+34 -66
View File
@@ -2,7 +2,7 @@
// Licensed under the MIT License. // Licensed under the MIT License.
import * as fsapi from "fs-extra" 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 { State } from "vscode-languageclient"
import { import {
LanguageClient, LanguageClient,
@@ -24,13 +24,6 @@ import { isVirtualWorkspace } from "./vscodeapi"
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings } export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
let _disposables: Disposable[] = []
export function disposeServerResources(): void {
_disposables.forEach((disposable) => disposable.dispose())
_disposables = []
}
async function createServer( async function createServer(
settings: ISettings, settings: ISettings,
serverId: string, serverId: string,
@@ -39,26 +32,13 @@ async function createServer(
initializationOptions: IInitOptions initializationOptions: IInitOptions
): Promise<LanguageClient> { ): Promise<LanguageClient> {
const command = settings.interpreter[0] const command = settings.interpreter[0]
if (!command) {
throw new Error("No Python interpreter is configured for the language server.")
}
const cwd = settings.cwd const cwd = settings.cwd
// Set debugger path needed for debugging python code. // Set debugger path needed for debugging python code.
const newEnv = { ...process.env } const newEnv = { ...process.env }
const debuggerPath = await getDebuggerPath()
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH) const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
const debugRequested = newEnv.USE_DEBUGPY?.toLowerCase() === "true" if (newEnv.USE_DEBUGPY && debuggerPath) {
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) {
newEnv.DEBUGPY_PATH = debuggerPath newEnv.DEBUGPY_PATH = debuggerPath
} else { } else {
newEnv.USE_DEBUGPY = "False" newEnv.USE_DEBUGPY = "False"
@@ -70,13 +50,10 @@ async function createServer(
// Set notification type // Set notification type
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH const args =
const interpreterArgs = settings.interpreter.slice(1) newEnv.USE_DEBUGPY === "False" || !isDebugScript
if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) { ? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH])
interpreterArgs.push("-Xfrozen_modules=off") : settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH])
}
const args = interpreterArgs.concat([serverScript])
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
traceInfo(`Server run command: ${[command, ...args].join(" ")}`) traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
const serverOptions: ServerOptions = { const serverOptions: ServerOptions = {
@@ -86,7 +63,6 @@ async function createServer(
} }
// Options to control the language client // Options to control the language client
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
const clientOptions: LanguageClientOptions = { const clientOptions: LanguageClientOptions = {
// Register the server for python documents // Register the server for python documents
documentSelector: isVirtualWorkspace() documentSelector: isVirtualWorkspace()
@@ -100,16 +76,13 @@ async function createServer(
outputChannel: outputChannel, outputChannel: outputChannel,
traceOutputChannel: outputChannel, traceOutputChannel: outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never, revealOutputChannelOn: RevealOutputChannelOn.Never,
synchronize: {
fileEvents: tclFileWatcher
},
initializationOptions initializationOptions
} }
_disposables.push(tclFileWatcher)
return new LanguageClient(serverId, serverName, serverOptions, clientOptions) return new LanguageClient(serverId, serverName, serverOptions, clientOptions)
} }
let _disposables: Disposable[] = []
export async function restartServer( export async function restartServer(
serverId: string, serverId: string,
serverName: string, serverName: string,
@@ -119,45 +92,40 @@ export async function restartServer(
if (lsClient) { if (lsClient) {
traceInfo(`Server: Stop requested`) traceInfo(`Server: Stop requested`)
await lsClient.stop() await lsClient.stop()
disposeServerResources() _disposables.forEach((d) => d.dispose())
_disposables = []
} }
const projectRoot = await getProjectRoot() const projectRoot = await getProjectRoot()
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true) const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
try { const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, {
const newLSClient = await createServer( settings: await getExtensionSettings(serverId, true),
workspaceSetting, globalSettings: await getGlobalSettings(serverId, false)
serverId, })
serverName, traceInfo(`Server: Start requested.`)
outputChannel, _disposables.push(
{ newLSClient.onDidChangeState((e) => {
settings: await getExtensionSettings(serverId, true), switch (e.newState) {
globalSettings: await getGlobalSettings(serverId, false) case State.Stopped:
traceVerbose(`Server State: Stopped`)
break
case State.Starting:
traceVerbose(`Server State: Starting`)
break
case State.Running:
traceVerbose(`Server State: Running`)
break
} }
) })
traceInfo(`Server: Start requested.`) )
_disposables.push( try {
newLSClient.onDidChangeState((e) => {
switch (e.newState) {
case State.Stopped:
traceVerbose(`Server State: Stopped`)
break
case State.Starting:
traceVerbose(`Server State: Starting`)
break
case State.Running:
traceVerbose(`Server State: Running`)
break
}
})
)
await newLSClient.start() await newLSClient.start()
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
} catch (ex) { } catch (ex) {
traceError(`Server: Start failed: ${ex}`) traceError(`Server: Start failed: ${ex}`)
disposeServerResources()
return undefined return undefined
} }
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
} }
+3 -23
View File
@@ -18,12 +18,6 @@ export interface ISettings {
interpreter: string[] interpreter: string[]
importStrategy: string importStrategy: string
showNotifications: string showNotifications: string
formatter: boolean
inlayHint: boolean
inlayHints: {
parameterNames: "all" | "literals" | "none"
suppressWhenArgumentMatchesName: boolean
}
} }
export function getExtensionSettings( export function getExtensionSettings(
@@ -86,13 +80,7 @@ export async function getWorkspaceSettings(
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled", importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
showNotifications: config.get<string>(`showNotifications`) ?? "off", showNotifications: config.get<string>(`showNotifications`) ?? "off",
formatter: config.get<boolean>(`formatter`) ?? true, formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true, inlayHint: config.get<boolean>(`inlayHint`) ?? true
inlayHints: {
parameterNames:
config.get<"all" | "literals" | "none">(`inlayHints.parameterNames`) ?? "all",
suppressWhenArgumentMatchesName:
config.get<boolean>(`inlayHints.suppressWhenArgumentMatchesName`) ?? true
}
} }
return workspaceSetting return workspaceSetting
} }
@@ -125,13 +113,7 @@ export async function getGlobalSettings(
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"), importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"), showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
formatter: config.get<boolean>(`formatter`) ?? true, formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true, inlayHint: config.get<boolean>(`inlayHint`) ?? true
inlayHints: {
parameterNames:
config.get<"all" | "literals" | "none">(`inlayHints.parameterNames`) ?? "all",
suppressWhenArgumentMatchesName:
config.get<boolean>(`inlayHints.suppressWhenArgumentMatchesName`) ?? true
}
} }
return setting return setting
} }
@@ -147,9 +129,7 @@ export function checkIfConfigurationChanged(
`${namespace}.importStrategy`, `${namespace}.importStrategy`,
`${namespace}.showNotifications`, `${namespace}.showNotifications`,
`${namespace}.formatter`, `${namespace}.formatter`,
`${namespace}.inlayHint`, `${namespace}.inlayHint`
`${namespace}.inlayHints.parameterNames`,
`${namespace}.inlayHints.suppressWhenArgumentMatchesName`
] ]
const changed = settings.map((s) => e.affectsConfiguration(s)) const changed = settings.map((s) => e.affectsConfiguration(s))
return changed.includes(true) return changed.includes(true)
+26 -107
View File
@@ -13,8 +13,7 @@ import {
isFirstLineMachine, isFirstLineMachine,
diagnosticHandler, diagnosticHandler,
cdlDocumentSymbolProvider, cdlDocumentSymbolProvider,
defDocumentSymbolProvider, defDocumentSymbolProvider
definitionCdlEventHandler
} from "./common/handlers" } from "./common/handlers"
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging" import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
import { import {
@@ -24,13 +23,13 @@ import {
onDidChangePythonInterpreter, onDidChangePythonInterpreter,
resolveInterpreter resolveInterpreter
} from "./common/python" } from "./common/python"
import { disposeServerResources, restartServer } from "./common/server" import { restartServer } from "./common/server"
import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings" import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings"
import { loadServerDefaults } from "./common/setup" import { loadServerDefaults } from "./common/setup"
import { getLSClientTraceLevel } from "./common/utilities" import { getLSClientTraceLevel } from "./common/utilities"
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi" import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
let client: LanguageClient | undefined let client: LanguageClient
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
// This is required to get server name and module. This should be // 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 serverInfo = loadServerDefaults()
const serverName = serverInfo.name const serverName = serverInfo.name
const serverId = serverInfo.module const serverId = serverInfo.module
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
// Setup logging // Setup logging
const outputChannel = createOutputChannel(serverName) const outputChannel = createOutputChannel(serverName)
@@ -63,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext) {
traceLog(`Module: ${serverInfo.module}`) traceLog(`Module: ${serverInfo.module}`)
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`) traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
const runServerImpl = async () => { const runServer = async () => {
const interpreter = getInterpreterFromSetting(serverId) const interpreter = getInterpreterFromSetting(serverId)
if (interpreter && interpreter.length > 0) { if (interpreter && interpreter.length > 0) {
if (checkVersion(await resolveInterpreter(interpreter))) { 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( context.subscriptions.push(
onDidChangePythonInterpreter(async () => {
await runServer()
}),
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
if (checkIfConfigurationChanged(e, serverId)) { if (checkIfConfigurationChanged(e, serverId)) {
await runServer() await runServer()
@@ -122,15 +105,6 @@ export async function activate(context: vscode.ExtensionContext) {
) )
setImmediate(async () => { 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) const interpreter = getInterpreterFromSetting(serverId)
if (interpreter === undefined || interpreter.length === 0) { if (interpreter === undefined || interpreter.length === 0) {
traceLog(`Python extension loading`) 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( const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "cdl" }, { scheme: "file", language: "cdl" },
@@ -193,16 +163,6 @@ export async function activate(context: vscode.ExtensionContext) {
) )
context.subscriptions.push(hoverCdlProvider) 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( const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "def" }, { scheme: "file", language: "def" },
{ {
@@ -246,73 +206,32 @@ export async function activate(context: vscode.ExtensionContext) {
const diagnosticCollectionDef = vscode.languages.createDiagnosticCollection("def") const diagnosticCollectionDef = vscode.languages.createDiagnosticCollection("def")
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef) context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
const diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>()
const updateDiagnostics = (document: vscode.TextDocument) => {
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" // Check if the first line of the CDL file contains "MACHINE"
context.subscriptions.push( vscode.workspace.onDidOpenTextDocument((document) => {
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") { 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))
}
} }
} })
vscode.workspace.onDidChangeTextDocument((event) => {
const document = event.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))
}
}
})
} }
export function deactivate(): Thenable<void> | undefined { export function deactivate(): Thenable<void> | undefined {
if (!client) { if (!client) {
disposeServerResources()
return undefined return undefined
} }
return client.stop().finally(disposeServerResources) return client.stop()
} }
+2 -4
View File
@@ -1,21 +1,19 @@
const esbuild = require("esbuild") const esbuild = require("esbuild")
const path = require("path")
const production = process.argv.includes("--production") const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch") const watch = process.argv.includes("--watch")
async function main() { async function main() {
const ctx = await esbuild.context({ const ctx = await esbuild.context({
absWorkingDir: __dirname,
entryPoints: ["client/src/extension.ts"], entryPoints: ["client/src/extension.ts"],
bundle: true, bundle: true,
format: "cjs", format: "cjs",
minify: production, minify: production,
sourcemap: !production, sourcemap: !production,
sourcesContent: !production, sourcesContent: false,
platform: "node", platform: "node",
// outdir: "out", // outdir: "out",
outfile: path.join(__dirname, "dist", "extension.js"), outfile: "./dist/extension.js",
external: ["vscode"], external: ["vscode"],
logLevel: "silent", logLevel: "silent",
plugins: [ plugins: [
+26 -33
View File
@@ -1,35 +1,28 @@
{ {
"comments": { "comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments // symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "#" "lineComment": "#"
}, },
"indentationRules": { // symbols used as brackets
// Ignore pure comment lines so they don't terminate folds inside Tcl brace blocks. "brackets": [
"unIndentedLinePattern": "^\\s*#.*$", ["{", "}"],
// Opening braces on their own line are common in NX Tcl. ["[", "]"],
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*)$", ["(", ")"]
"decreaseIndentPattern": "^\\s*[\\}\\]\\)].*$" ],
}, // symbols that are auto closed when typing
// symbols used as brackets "autoClosingPairs": [
"brackets": [ ["{", "}"],
["{", "}"], ["[", "]"],
["[", "]"], ["(", ")"],
["(", ")"] ["\"", "\""],
], ["'", "'"]
// symbols that are auto closed when typing ],
"autoClosingPairs": [ // symbols that can be used to surround a selection
["{", "}"], "surroundingPairs": [
["[", "]"], ["{", "}"],
["(", ")"], ["[", "]"],
["\"", "\""], ["(", ")"],
["'", "'"] ["\"", "\""],
], ["'", "'"]
// symbols that can be used to surround a selection ]
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "nx-post-support", "name": "nx-post-support",
"version": "2025.9.200", "version": "2025.9.100",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nx-post-support", "name": "nx-post-support",
"version": "2025.9.200", "version": "2025.9.100",
"devDependencies": { "devDependencies": {
"@types/vscode": "^1.96.0", "@types/vscode": "^1.96.0",
"@vscode/vsce": "^3.2.1", "@vscode/vsce": "^3.2.1",
+2 -31
View File
@@ -1,13 +1,10 @@
{ {
"name": "nx-post-support", "name": "nx-post-support",
"displayName": "NX Postprocessor 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", "description": "",
"version": "2026.8.100", "version": "2025.9.100",
"publisher": "Christoph", "publisher": "Christoph",
"icon": "images/nx-1.png", "icon": "images/nx-1.png",
"extensionDependencies": [
"ms-python.python"
],
"serverInfo": { "serverInfo": {
"name": "NX Postprocessor Support", "name": "NX Postprocessor Support",
"module": "nx-post-support" "module": "nx-post-support"
@@ -96,26 +93,6 @@
"default": true, "default": true,
"description": "Use the Inlay Hints in from `NX Postprocessor Support`" "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": { "nx-post-support.importStrategy": {
"default": "useBundled", "default": "useBundled",
"description": "Defines where `NX Postprocessor Support` is imported from.", "description": "Defines where `NX Postprocessor Support` is imported from.",
@@ -140,16 +117,10 @@
"type": "array" "type": "array"
} }
} }
},
"configurationDefaults": {
"[tcl]": {
"editor.inlayHints.maximumLength": 0
}
} }
}, },
"scripts": { "scripts": {
"compile": "node esbuild.js --production", "compile": "node esbuild.js --production",
"compile:debug": "node esbuild.js",
"watch": "node esbuild.js --watch", "watch": "node esbuild.js --watch",
"package": "node esbuild.js --production" "package": "node esbuild.js --production"
}, },
View File
+5 -5
View File
@@ -176,7 +176,7 @@ def attrib(
type: None = ..., type: None = ...,
converter: None = ..., converter: None = ...,
factory: None = ..., factory: None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -200,7 +200,7 @@ def attrib(
| tuple[_ConverterType] | tuple[_ConverterType]
| None = ..., | None = ...,
factory: Callable[[], _T] | None = ..., factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -223,7 +223,7 @@ def attrib(
| tuple[_ConverterType] | tuple[_ConverterType]
| None = ..., | None = ...,
factory: Callable[[], _T] | None = ..., factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -246,7 +246,7 @@ def attrib(
| tuple[_ConverterType] | tuple[_ConverterType]
| None = ..., | None = ...,
factory: Callable[[], _T] | None = ..., factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -308,7 +308,7 @@ def attrs(
match_args: bool = ..., match_args: bool = ...,
unsafe_hash: bool | None = ..., unsafe_hash: bool | None = ...,
) -> Callable[[_C], _C]: ... ) -> 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 fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
def validate(inst: AttrsInstance) -> None: ... def validate(inst: AttrsInstance) -> None: ...
def resolve_types( def resolve_types(
+3 -8
View File
@@ -10,6 +10,7 @@ from typing import _GenericAlias
PYPY = platform.python_implementation() == "PyPy" 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_10_PLUS = sys.version_info[:2] >= (3, 10)
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) 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) 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 import annotationlib
# We request forward-ref annotations to not break in the presence of _get_annotations = annotationlib.get_annotations
# forward references.
def _get_annotations(cls):
return annotationlib.get_annotations(
cls, format=annotationlib.Format.FORWARDREF
)
else: else:
+16 -45
View File
@@ -3,28 +3,11 @@
import copy 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 ._make import _OBJ_SETATTR, NOTHING, fields
from .exceptions import AttrsAttributeNotFoundError from .exceptions import AttrsAttributeNotFoundError
_ATOMIC_TYPES = frozenset(
{
type(None),
bool,
int,
float,
str,
complex,
bytes,
type(...),
type,
range,
property,
}
)
def asdict( def asdict(
inst, inst,
recurse=True, recurse=True,
@@ -88,10 +71,7 @@ def asdict(
v = value_serializer(inst, a, v) v = value_serializer(inst, a, v)
if recurse is True: if recurse is True:
value_type = type(v) if has(v.__class__):
if value_type in _ATOMIC_TYPES:
rv[a.name] = v
elif has(value_type):
rv[a.name] = asdict( rv[a.name] = asdict(
v, v,
recurse=True, recurse=True,
@@ -100,8 +80,8 @@ def asdict(
retain_collection_types=retain_collection_types, retain_collection_types=retain_collection_types,
value_serializer=value_serializer, value_serializer=value_serializer,
) )
elif issubclass(value_type, (tuple, list, set, frozenset)): elif isinstance(v, (tuple, list, set, frozenset)):
cf = value_type if retain_collection_types is True else list cf = v.__class__ if retain_collection_types is True else list
items = [ items = [
_asdict_anything( _asdict_anything(
i, i,
@@ -121,7 +101,7 @@ def asdict(
# Workaround for TypeError: cf.__new__() missing 1 required # Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle) # positional argument (which appears, for a namedturle)
rv[a.name] = cf(*items) rv[a.name] = cf(*items)
elif issubclass(value_type, dict): elif isinstance(v, dict):
df = dict_factory df = dict_factory
rv[a.name] = df( rv[a.name] = df(
( (
@@ -162,12 +142,7 @@ def _asdict_anything(
""" """
``asdict`` only works on attrs instances, this works on anything. ``asdict`` only works on attrs instances, this works on anything.
""" """
val_type = type(val) if getattr(val.__class__, "__attrs_attrs__", None) is not None:
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:
# Attrs class. # Attrs class.
rv = asdict( rv = asdict(
val, val,
@@ -177,7 +152,7 @@ def _asdict_anything(
retain_collection_types=retain_collection_types, retain_collection_types=retain_collection_types,
value_serializer=value_serializer, 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: if retain_collection_types is True:
cf = val.__class__ cf = val.__class__
elif is_key: elif is_key:
@@ -198,7 +173,7 @@ def _asdict_anything(
for i in val for i in val
] ]
) )
elif issubclass(val_type, dict): elif isinstance(val, dict):
df = dict_factory df = dict_factory
rv = df( rv = df(
( (
@@ -278,11 +253,8 @@ def astuple(
v = getattr(inst, a.name) v = getattr(inst, a.name)
if filter is not None and not filter(a, v): if filter is not None and not filter(a, v):
continue continue
value_type = type(v)
if recurse is True: if recurse is True:
if value_type in _ATOMIC_TYPES: if has(v.__class__):
rv.append(v)
elif has(value_type):
rv.append( rv.append(
astuple( astuple(
v, v,
@@ -292,7 +264,7 @@ def astuple(
retain_collection_types=retain, 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 cf = v.__class__ if retain is True else list
items = [ items = [
( (
@@ -316,8 +288,8 @@ def astuple(
# Workaround for TypeError: cf.__new__() missing 1 required # Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle) # positional argument (which appears, for a namedturle)
rv.append(cf(*items)) rv.append(cf(*items))
elif issubclass(value_type, dict): elif isinstance(v, dict):
df = value_type if retain is True else dict df = v.__class__ if retain is True else dict
rv.append( rv.append(
df( df(
( (
@@ -478,11 +450,10 @@ def resolve_types(
if getattr(cls, "__attrs_types_resolved__", None) != cls: if getattr(cls, "__attrs_types_resolved__", None) != cls:
import typing import typing
kwargs = { kwargs = {"globalns": globalns, "localns": localns}
"globalns": globalns,
"localns": localns, if PY_3_9_PLUS:
"include_extras": include_extras, kwargs["include_extras"] = include_extras
}
hints = typing.get_type_hints(cls, **kwargs) hints = typing.get_type_hints(cls, **kwargs)
for field in fields(cls) if attribs is None else attribs: for field in fields(cls) if attribs is None else attribs:
+113 -396
View File
@@ -12,7 +12,6 @@ import linecache
import sys import sys
import types import types
import unicodedata import unicodedata
import weakref
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from functools import cached_property from functools import cached_property
@@ -114,7 +113,7 @@ def attrib(
type=None, type=None,
converter=None, converter=None,
factory=None, factory=None,
kw_only=None, kw_only=False,
eq=None, eq=None,
order=None, order=None,
on_setattr=None, on_setattr=None,
@@ -157,9 +156,6 @@ def attrib(
*eq*, *order*, and *cmp* also accept a custom callable *eq*, *order*, and *cmp* also accept a custom callable
.. versionchanged:: 21.1.0 *cmp* undeprecated .. versionchanged:: 21.1.0 *cmp* undeprecated
.. versionadded:: 22.2.0 *alias* .. 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( eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq, order, True cmp, eq, order, True
@@ -377,12 +373,7 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
def _transform_attrs( def _transform_attrs(
cls, cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
these,
auto_attribs,
kw_only,
collect_by_mro,
field_transformer,
) -> _Attributes: ) -> _Attributes:
""" """
Transform all `_CountingAttr`s on a class into `Attribute`s. Transform all `_CountingAttr`s on a class into `Attribute`s.
@@ -437,15 +428,8 @@ def _transform_attrs(
) )
fca = Attribute.from_counting_attr fca = Attribute.from_counting_attr
no = ClassProps.KeywordOnly.NO
own_attrs = [ own_attrs = [
fca( fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list
attr_name,
ca,
kw_only is not no,
anns.get(attr_name),
)
for attr_name, ca in ca_list
] ]
if collect_by_mro: if collect_by_mro:
@@ -457,21 +441,12 @@ def _transform_attrs(
cls, {a.name for a in own_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] own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
base_attrs = [a.evolve(kw_only=True) for a in base_attrs] base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
attrs = base_attrs + own_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: if field_transformer is not None:
attrs = tuple(field_transformer(cls, attrs)) attrs = tuple(field_transformer(cls, attrs))
@@ -489,12 +464,13 @@ def _transform_attrs(
if had_default is False and a.default is not NOTHING: if had_default is False and a.default is not NOTHING:
had_default = True had_default = True
# Resolve default field alias for any new attributes that the # Resolve default field alias after executing field_transformer.
# field_transformer may have added without setting an alias. # This allows field_transformer to differentiate between explicit vs
# default aliases and supply their own defaults.
for a in attrs: for a in attrs:
if not a.alias: 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", _default_init_alias_for(a.name))
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
# Create AttrsClass *after* applying the field_transformer since it may # Create AttrsClass *after* applying the field_transformer since it may
# add or remove attributes! # add or remove attributes!
@@ -577,7 +553,7 @@ def _frozen_delattrs(self, name):
""" """
Attached to frozen classes as __delattr__. 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) BaseException.__delattr__(self, name)
return return
@@ -675,31 +651,38 @@ class _ClassBuilder:
self, self,
cls: type, cls: type,
these, these,
auto_attribs: bool, slots,
props: ClassProps, frozen,
has_custom_setattr: bool, 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( attrs, base_attrs, base_map = _transform_attrs(
cls, cls,
these, these,
auto_attribs, auto_attribs,
props.kw_only, kw_only,
props.collected_fields_by_mro, collect_by_mro,
props.field_transformer, field_transformer,
) )
self._cls = cls 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._attrs = attrs
self._base_names = {a.name for a in base_attrs} self._base_names = {a.name for a in base_attrs}
self._base_attr_map = base_map self._base_attr_map = base_map
self._attr_names = tuple(a.name for a in attrs) self._attr_names = tuple(a.name for a in attrs)
self._slots = props.is_slotted self._slots = slots
self._frozen = props.is_frozen self._frozen = frozen
self._weakref_slot = props.has_weakref_slot self._weakref_slot = weakref_slot
self._cache_hash = ( self._cache_hash = cache_hash
props.hashability is ClassProps.Hashability.HASHABLE_CACHED
)
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
self._pre_init_has_args = False self._pre_init_has_args = False
if self._has_pre_init: if self._has_pre_init:
@@ -710,21 +693,20 @@ class _ClassBuilder:
self._pre_init_has_args = len(pre_init_signature.parameters) > 1 self._pre_init_has_args = len(pre_init_signature.parameters) > 1
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these) self._delete_attribs = not bool(these)
self._is_exc = props.is_exception self._is_exc = is_exc
self._on_setattr = props.on_setattr_hook self._on_setattr = on_setattr
self._has_custom_setattr = has_custom_setattr self._has_custom_setattr = has_custom_setattr
self._wrote_own_setattr = False self._wrote_own_setattr = False
self._cls_dict["__attrs_attrs__"] = self._attrs 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["__setattr__"] = _frozen_setattrs
self._cls_dict["__delattr__"] = _frozen_delattrs self._cls_dict["__delattr__"] = _frozen_delattrs
self._wrote_own_setattr = True self._wrote_own_setattr = True
elif self._on_setattr in ( elif on_setattr in (
_DEFAULT_ON_SETATTR, _DEFAULT_ON_SETATTR,
setters.validate, setters.validate,
setters.convert, setters.convert,
@@ -740,18 +722,18 @@ class _ClassBuilder:
break break
if ( if (
( (
self._on_setattr == _DEFAULT_ON_SETATTR on_setattr == _DEFAULT_ON_SETATTR
and not (has_validator or has_converter) and not (has_validator or has_converter)
) )
or (self._on_setattr == setters.validate and not has_validator) or (on_setattr == setters.validate and not has_validator)
or (self._on_setattr == setters.convert and not has_converter) or (on_setattr == setters.convert and not has_converter)
): ):
# If class-level on_setattr is set to convert + validate, but # If class-level on_setattr is set to convert + validate, but
# there's no field to convert or validate, pretend like there's # there's no field to convert or validate, pretend like there's
# no on_setattr. # no on_setattr.
self._on_setattr = None self._on_setattr = None
if props.added_pickling: if getstate_setstate:
( (
self._cls_dict["__getstate__"], self._cls_dict["__getstate__"],
self._cls_dict["__setstate__"], self._cls_dict["__setstate__"],
@@ -802,7 +784,6 @@ class _ClassBuilder:
self._eval_snippets() self._eval_snippets()
if self._slots is True: if self._slots is True:
cls = self._create_slots_class() cls = self._create_slots_class()
self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)
else: else:
cls = self._patch_original_class() cls = self._patch_original_class()
if PY_3_10_PLUS: if PY_3_10_PLUS:
@@ -864,10 +845,6 @@ class _ClassBuilder:
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") 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__ # 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 # (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 # an attrs-made __setattr__, that needs to be reset. We don't walk the
@@ -1104,7 +1081,9 @@ class _ClassBuilder:
return self return self
def add_replace(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 return self
def add_match_args(self): def add_match_args(self):
@@ -1347,7 +1326,6 @@ def attrs(
field_transformer=None, field_transformer=None,
match_args=True, match_args=True,
unsafe_hash=None, unsafe_hash=None,
force_kw_only=True,
): ):
r""" r"""
A class decorator that adds :term:`dunder methods` according to the 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 If a class has an *inherited* classmethod called
``__attrs_init_subclass__``, it is executed after the class is created. ``__attrs_init_subclass__``, it is executed after the class is created.
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. .. 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: if repr_ns is not None:
import warnings import warnings
@@ -1439,7 +1413,6 @@ def attrs(
on_setattr = setters.pipe(*on_setattr) on_setattr = setters.pipe(*on_setattr)
def wrap(cls): def wrap(cls):
nonlocal hash
is_frozen = frozen or _has_frozen_base_class(cls) is_frozen = frozen or _has_frozen_base_class(cls)
is_exc = auto_exc is True and issubclass(cls, BaseException) is_exc = auto_exc is True and issubclass(cls, BaseException)
has_own_setattr = auto_detect and _has_own_attribute( 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__." msg = "Can't freeze a class with a custom __setattr__."
raise ValueError(msg) raise ValueError(msg)
eq = not is_exc and _determine_whether_to_implement( builder = _ClassBuilder(
cls, eq_, auto_detect, ("__eq__", "__ne__") cls,
) these,
slots,
Hashability = ClassProps.Hashability is_frozen,
weakref_slot,
if is_exc: _determine_whether_to_implement(
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(
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(
cls, cls,
getstate_setstate, getstate_setstate,
auto_detect, auto_detect,
("__getstate__", "__setstate__"), ("__getstate__", "__setstate__"),
default=slots, default=slots,
), ),
on_setattr_hook=on_setattr, auto_attribs,
field_transformer=field_transformer, kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_own_setattr,
field_transformer,
) )
if not props.is_hashable and cache_hash: if _determine_whether_to_implement(
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." cls, repr, auto_detect, ("__repr__",)
raise TypeError(msg) ):
builder = _ClassBuilder(
cls,
these,
auto_attribs=auto_attribs,
props=props,
has_custom_setattr=has_own_setattr,
)
if props.added_repr:
builder.add_repr(repr_ns) builder.add_repr(repr_ns)
if props.added_str: if str is True:
builder.add_str() 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() 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() builder.add_order()
if not frozen: if not frozen:
builder.add_setattr() 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() 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() builder.make_unhashable()
if props.added_init: if _determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
):
builder.add_init() builder.add_init()
else: else:
builder.add_attrs_init() builder.add_attrs_init()
@@ -1890,16 +1835,16 @@ def _add_repr(cls, ns=None, attrs=None):
def fields(cls): 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 The tuple also allows accessing the fields by their names (see below for
examples). examples).
Args: Args:
cls (type): Class or instance to introspect. cls (type): Class to introspect.
Raises: Raises:
TypeError: If *cls* is neither a class nor an *attrs* instance. TypeError: If *cls* is not a class.
attrs.exceptions.NotAnAttrsClassError: attrs.exceptions.NotAnAttrsClassError:
If *cls* is not an *attrs* class. If *cls* is not an *attrs* class.
@@ -1910,17 +1855,12 @@ def fields(cls):
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
by name. by name.
.. versionchanged:: 23.1.0 Add support for generic classes. .. versionchanged:: 23.1.0 Add support for generic classes.
.. versionchanged:: 26.1.0 Add support for instances.
""" """
generic_base = get_generic_base(cls) generic_base = get_generic_base(cls)
if generic_base is None and not isinstance(cls, type): if generic_base is None and not isinstance(cls, type):
type_ = type(cls) msg = "Passed object must be a class."
if getattr(type_, "__attrs_attrs__", None) is None: raise TypeError(msg)
msg = "Passed object must be a class or attrs instance."
raise TypeError(msg)
return fields(type_)
attrs = getattr(cls, "__attrs_attrs__", None) attrs = getattr(cls, "__attrs_attrs__", None)
@@ -2027,7 +1967,7 @@ def _make_init_script(
attr_dict[a.name] = a attr_dict[a.name] = a
if a.on_setattr is not None: 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." msg = "Frozen classes can't use on_setattr."
raise ValueError(msg) raise ValueError(msg)
@@ -2186,9 +2126,8 @@ def _attrs_to_init_script(
) )
lines.extend(extra_lines) lines.extend(extra_lines)
args = [] # Parameters in the definition of __init__ args = []
pre_init_args = [] # Parameters in the call to __attrs_pre_init__ kw_only_args = []
kw_only_args = [] # Used for both 'args' and 'pre_init_args' above
attrs_to_validate = [] attrs_to_validate = []
# This is a dictionary of names to validator and converter callables. # 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) kw_only_args.append(arg)
else: else:
args.append(arg) args.append(arg)
pre_init_args.append(arg_name)
if converter is not None: if converter is not None:
lines.append( lines.append(
@@ -2286,7 +2224,6 @@ def _attrs_to_init_script(
kw_only_args.append(arg) kw_only_args.append(arg)
else: else:
args.append(arg) args.append(arg)
pre_init_args.append(arg_name)
lines.append(f"if {arg_name} is not NOTHING:") lines.append(f"if {arg_name} is not NOTHING:")
init_factory_name = _INIT_FACTORY_PAT % (a.name,) init_factory_name = _INIT_FACTORY_PAT % (a.name,)
@@ -2329,7 +2266,6 @@ def _attrs_to_init_script(
kw_only_args.append(arg_name) kw_only_args.append(arg_name)
else: else:
args.append(arg_name) args.append(arg_name)
pre_init_args.append(arg_name)
if converter is not None: if converter is not None:
lines.append( lines.append(
@@ -2386,7 +2322,7 @@ def _attrs_to_init_script(
lines.append(f"BaseException.__init__(self, {vals})") lines.append(f"BaseException.__init__(self, {vals})")
args = ", ".join(args) args = ", ".join(args)
pre_init_args = ", ".join(pre_init_args) pre_init_args = args
if kw_only_args: if kw_only_args:
# leading comma & kw_only args # leading comma & kw_only args
args += f"{', ' if args else ''}*, {', '.join(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 pre_init_args += pre_init_kw_only_args
if call_pre_init and pre_init_has_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})" lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
# Python <3.12 doesn't allow backslashes in f-strings. # Python <3.12 doesn't allow backslashes in f-strings.
@@ -2440,8 +2376,6 @@ class Attribute:
- ``name`` (`str`): The name of the attribute. - ``name`` (`str`): The name of the attribute.
- ``alias`` (`str`): The __init__ parameter name of the attribute, after - ``alias`` (`str`): The __init__ parameter name of the attribute, after
any explicit overrides and default private-attribute-name handling. 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 - ``inherited`` (`bool`): Whether or not that attribute has been inherited
from a base class. from a base class.
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
@@ -2467,7 +2401,6 @@ class Attribute:
equality checks and hashing anymore. equality checks and hashing anymore.
.. versionadded:: 21.1.0 *eq_key* and *order_key* .. versionadded:: 21.1.0 *eq_key* and *order_key*
.. versionadded:: 22.2.0 *alias* .. versionadded:: 22.2.0 *alias*
.. versionadded:: 26.1.0 *alias_is_default*
For the full version history of the fields, see `attr.ib`. For the full version history of the fields, see `attr.ib`.
""" """
@@ -2492,7 +2425,6 @@ class Attribute:
"inherited", "inherited",
"on_setattr", "on_setattr",
"alias", "alias",
"alias_is_default",
) )
def __init__( def __init__(
@@ -2515,7 +2447,6 @@ class Attribute:
order_key=None, order_key=None,
on_setattr=None, on_setattr=None,
alias=None, alias=None,
alias_is_default=None,
): ):
eq, eq_key, order, order_key = _determine_attrib_eq_order( eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq_key or eq, order_key or order, True cmp, eq_key or eq, order_key or order, True
@@ -2550,20 +2481,12 @@ class Attribute:
bound_setattr("inherited", inherited) bound_setattr("inherited", inherited)
bound_setattr("on_setattr", on_setattr) bound_setattr("on_setattr", on_setattr)
bound_setattr("alias", alias) 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): def __setattr__(self, name, value):
raise FrozenInstanceError raise FrozenInstanceError
@classmethod @classmethod
def from_counting_attr( def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
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'.
# type holds the annotated value. deal with conflicts: # type holds the annotated value. deal with conflicts:
if type is None: if type is None:
type = ca.type type = ca.type
@@ -2582,14 +2505,13 @@ class Attribute:
ca.metadata, ca.metadata,
type, type,
ca.converter, ca.converter,
kw_only if ca.kw_only is None else ca.kw_only, ca.kw_only,
ca.eq, ca.eq,
ca.eq_key, ca.eq_key,
ca.order, ca.order,
ca.order_key, ca.order_key,
ca.on_setattr, ca.on_setattr,
ca.alias, ca.alias,
ca.alias is None,
) )
# Don't use attrs.evolve since fields(Attribute) doesn't work # Don't use attrs.evolve since fields(Attribute) doesn't work
@@ -2608,20 +2530,6 @@ class Attribute:
new._setattrs(changes.items()) 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 return new
# Don't use _add_pickle since fields(Attribute) doesn't work # Don't use _add_pickle since fields(Attribute) doesn't work
@@ -2638,17 +2546,6 @@ class Attribute:
""" """
Play nice with pickle. 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)) self._setattrs(zip(self.__slots__, state))
def _setattrs(self, name_values_pairs): def _setattrs(self, name_values_pairs):
@@ -2672,7 +2569,7 @@ _a = [
name=name, name=name,
default=NOTHING, default=NOTHING,
validator=None, validator=None,
repr=(name != "alias_is_default"), repr=True,
cmp=None, cmp=None,
eq=True, eq=True,
order=False, order=False,
@@ -2844,188 +2741,6 @@ class _CountingAttr:
_CountingAttr = _add_eq(_add_repr(_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: class Factory:
""" """
Stores a factory callable. Stores a factory callable.
@@ -3133,7 +2848,9 @@ class Converter:
value, field value, field
) )
else: else:
self.__call__ = self.converter self.__call__ = lambda value, instance, field: self.converter(
value, instance, field
)
rt = ex.get_return_type() rt = ex.get_return_type()
if rt is not None: if rt is not None:
+7 -58
View File
@@ -17,7 +17,7 @@ from ._make import (
attrib, attrib,
attrs, attrs,
) )
from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError from .exceptions import UnannotatedAttributeError
def define( def define(
@@ -43,7 +43,6 @@ def define(
on_setattr=None, on_setattr=None,
field_transformer=None, field_transformer=None,
match_args=True, match_args=True,
force_kw_only=False,
): ):
r""" r"""
A class decorator that adds :term:`dunder methods` according to 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 sensible ``__ne__`` by default, so it *should* be enough to only
implement ``__eq__`` in most cases). 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. overrides whatever *auto_detect* would determine.
auto_exc (bool): auto_exc (bool):
@@ -215,12 +214,8 @@ def define(
5. Subclasses of a frozen class are frozen too. 5. Subclasses of a frozen class are frozen too.
kw_only (bool): kw_only (bool):
Make attributes keyword-only in the generated ``__init__`` (if Make all attributes keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored). Attributes that *init* is False, this parameter is ignored).
explicitly set ``kw_only=False`` are not affected; base class
attributes are also not affected.
Also see *force_kw_only*.
weakref_slot (bool): weakref_slot (bool):
Make instances weak-referenceable. This has no effect unless Make instances weak-referenceable. This has no effect unless
@@ -249,15 +244,6 @@ def define(
See also `issue #428 See also `issue #428
<https://github.com/python-attrs/attrs/issues/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): getstate_setstate (bool | None):
.. note:: .. note::
@@ -333,11 +319,6 @@ def define(
.. versionadded:: 24.3.0 .. versionadded:: 24.3.0
Unless already present, a ``__replace__`` method is automatically Unless already present, a ``__replace__`` method is automatically
created for `copy.replace` (Python 3.13+ only). 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:: .. note::
@@ -356,7 +337,6 @@ def define(
- *auto_exc=True* - *auto_exc=True*
- *auto_detect=True* - *auto_detect=True*
- *order=False* - *order=False*
- *force_kw_only=False*
- Some options that were only relevant on Python 2 or were kept around - Some options that were only relevant on Python 2 or were kept around
for backwards-compatibility have been removed. for backwards-compatibility have been removed.
@@ -386,7 +366,6 @@ def define(
on_setattr=on_setattr, on_setattr=on_setattr,
field_transformer=field_transformer, field_transformer=field_transformer,
match_args=match_args, match_args=match_args,
force_kw_only=force_kw_only,
) )
def wrap(cls): def wrap(cls):
@@ -445,7 +424,7 @@ def field(
type=None, type=None,
converter=None, converter=None,
factory=None, factory=None,
kw_only=None, kw_only=False,
eq=None, eq=None,
order=None, order=None,
on_setattr=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 itself. You can use it as part of your own code or for `static type
checking <types>`. checking <types>`.
kw_only (bool | None): kw_only (bool):
Make this attribute keyword-only in the generated ``__init__`` (if Make this attribute keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored). If None (default), ``init`` is False, this parameter is ignored).
mirror the setting from `attrs.define`.
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): 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 Allows to overwrite the *on_setattr* setting from `attr.s`. If left
@@ -594,9 +572,6 @@ def field(
.. versionadded:: 23.1.0 .. versionadded:: 23.1.0
The *type* parameter has been re-added; mostly for `attrs.make_class`. The *type* parameter has been re-added; mostly for `attrs.make_class`.
Please note that type checkers ignore this metadata. 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:: .. seealso::
@@ -646,29 +621,3 @@ def astuple(inst, *, recurse=True, filter=None):
return _astuple( return _astuple(
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True 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 # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
# have to do anything special with releaselevel for now. # have to do anything special with releaselevel for now.
return us < them return us < them
def __hash__(self):
return hash((self.year, self.minor, self.micro, self.releaselevel))
+4 -4
View File
@@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from typing import ClassVar
class FrozenError(AttributeError): class FrozenError(AttributeError):
""" """
@@ -14,10 +16,8 @@ class FrozenError(AttributeError):
.. versionadded:: 20.1.0 .. versionadded:: 20.1.0
""" """
def __init__(self): msg = "can't set attribute"
msg = "can't set attribute" args: ClassVar[tuple[str]] = [msg]
super().__init__(msg)
self.msg = msg
class FrozenInstanceError(FrozenError): class FrozenInstanceError(FrozenError):
+14 -54
View File
@@ -79,14 +79,12 @@ def disabled():
This context manager is not thread-safe! This context manager is not thread-safe!
.. versionadded:: 21.3.0 .. versionadded:: 21.3.0
.. versionchanged:: 26.1.0 The contextmanager is nestable.
""" """
prev = get_run_validators()
set_run_validators(False) set_run_validators(False)
try: try:
yield yield
finally: finally:
set_run_validators(prev) set_run_validators(True)
@attrs(repr=False, slots=True, unsafe_hash=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. A validator that performs deep validation of an iterable.
Args: Args:
member_validator: Validator(s) to apply to iterable members. member_validator: Validator to apply to iterable members.
iterable_validator: iterable_validator:
Validator(s) to apply to iterable itself (optional). Validator to apply to iterable itself (optional).
Raises Raises
TypeError: if any sub-validators fail TypeError: if any sub-validators fail
.. versionadded:: 19.1.0 .. 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)): if isinstance(member_validator, (list, tuple)):
member_validator = and_(*member_validator) member_validator = and_(*member_validator)
if isinstance(iterable_validator, (list, tuple)):
iterable_validator = and_(*iterable_validator)
return _DeepIterable(member_validator, iterable_validator) return _DeepIterable(member_validator, iterable_validator)
@attrs(repr=False, slots=True, unsafe_hash=True) @attrs(repr=False, slots=True, unsafe_hash=True)
class _DeepMapping: class _DeepMapping:
key_validator = attrib(validator=optional(is_callable())) key_validator = attrib(validator=is_callable())
value_validator = attrib(validator=optional(is_callable())) value_validator = attrib(validator=is_callable())
mapping_validator = attrib(validator=optional(is_callable())) mapping_validator = attrib(default=None, validator=optional(is_callable()))
def __call__(self, inst, attr, value): def __call__(self, inst, attr, value):
""" """
@@ -398,62 +390,30 @@ class _DeepMapping:
self.mapping_validator(inst, attr, value) self.mapping_validator(inst, attr, value)
for key in value: for key in value:
if self.key_validator is not None: self.key_validator(inst, attr, key)
self.key_validator(inst, attr, key) self.value_validator(inst, attr, value[key])
if self.value_validator is not None:
self.value_validator(inst, attr, value[key])
def __repr__(self): def __repr__(self):
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>" return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
def deep_mapping( def deep_mapping(key_validator, value_validator, mapping_validator=None):
key_validator=None, value_validator=None, mapping_validator=None
):
""" """
A validator that performs deep validation of a dictionary. 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: 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: mapping_validator:
Validator(s) to apply to top-level mapping attribute. Validator to apply to top-level mapping attribute (optional).
.. versionadded:: 19.1.0 .. 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: Raises:
TypeError: If any sub-validator fails on validation. TypeError: if any sub-validators fail
ValueError:
If neither *key_validator* nor *value_validator* is provided on
instantiation.
""" """
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) 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 A validator that raises `ValueError` if the initializer is called with a
number smaller or equal to *val*. 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: Args:
val: Exclusive lower bound for values val: Exclusive lower bound for values
+6 -60
View File
@@ -20,9 +20,6 @@ _T = TypeVar("_T")
_T1 = TypeVar("_T1") _T1 = TypeVar("_T1")
_T2 = TypeVar("_T2") _T2 = TypeVar("_T2")
_T3 = TypeVar("_T3") _T3 = TypeVar("_T3")
_T4 = TypeVar("_T4")
_T5 = TypeVar("_T5")
_T6 = TypeVar("_T6")
_I = TypeVar("_I", bound=Iterable) _I = TypeVar("_I", bound=Iterable)
_K = TypeVar("_K") _K = TypeVar("_K")
_V = TypeVar("_V") _V = TypeVar("_V")
@@ -54,7 +51,7 @@ def optional(
validator: ( validator: (
_ValidatorType[_T] _ValidatorType[_T]
| list[_ValidatorType[_T]] | list[_ValidatorType[_T]]
| tuple[_ValidatorType[_T], ...] | tuple[_ValidatorType[_T]]
), ),
) -> _ValidatorType[_T | None]: ... ) -> _ValidatorType[_T | None]: ...
def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
@@ -66,19 +63,12 @@ def matches_re(
) -> _ValidatorType[AnyStr]: ... ) -> _ValidatorType[AnyStr]: ...
def deep_iterable( def deep_iterable(
member_validator: _ValidatorArgType[_T], member_validator: _ValidatorArgType[_T],
iterable_validator: _ValidatorArgType[_I] | None = ..., iterable_validator: _ValidatorType[_I] | None = ...,
) -> _ValidatorType[_I]: ... ) -> _ValidatorType[_I]: ...
@overload
def deep_mapping( def deep_mapping(
key_validator: _ValidatorArgType[_K], key_validator: _ValidatorType[_K],
value_validator: _ValidatorArgType[_V] | None = ..., value_validator: _ValidatorType[_V],
mapping_validator: _ValidatorArgType[_M] | None = ..., mapping_validator: _ValidatorType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
@overload
def deep_mapping(
key_validator: _ValidatorArgType[_K] | None = ...,
value_validator: _ValidatorArgType[_V] = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
) -> _ValidatorType[_M]: ... ) -> _ValidatorType[_M]: ...
def is_callable() -> _ValidatorType[_T]: ... def is_callable() -> _ValidatorType[_T]: ...
def lt(val: _T) -> _ValidatorType[_T]: ... def lt(val: _T) -> _ValidatorType[_T]: ...
@@ -93,48 +83,4 @@ def not_(
msg: str | None = None, msg: str | None = None,
exc_types: type[Exception] | Iterable[type[Exception]] = ..., exc_types: type[Exception] | Iterable[type[Exception]] = ...,
) -> _ValidatorType[_T]: ... ) -> _ValidatorType[_T]: ...
@overload def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
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]: ...
@@ -0,0 +1 @@
pip
@@ -1,6 +1,6 @@
Metadata-Version: 2.4 Metadata-Version: 2.4
Name: attrs Name: attrs
Version: 26.1.0 Version: 25.3.0
Summary: Classes Without Boilerplate Summary: Classes Without Boilerplate
Project-URL: Documentation, https://www.attrs.org/ Project-URL: Documentation, https://www.attrs.org/
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
@@ -12,16 +12,62 @@ License-Expression: MIT
License-File: LICENSE License-File: LICENSE
Keywords: attribute,boilerplate,class Keywords: attribute,boilerplate,class
Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed 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 Description-Content-Type: text/markdown
<p align="center"> <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)). *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. 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 import pathlib, tomllib
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]: 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://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/26.1.0/_static/sponsors/Tidelift.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://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://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/26.1.0/_static/sponsors/Privacy-Solutions.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://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.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://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://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/26.1.0/_static/sponsors/Polar.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]]] --> <!-- [[[end]]] -->
</p> </p>
@@ -174,23 +220,10 @@ Save time, reduce risk, and improve code health, while paying the maintainers of
## Release Information ## 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 ### Changes
- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator. - Restore support for generator-based `field_transformer`s.
[#1496](https://github.com/python-attrs/attrs/issues/1496) [#1417](https://github.com/python-attrs/attrs/issues/1417)
- 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)
+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 Wheel-Version: 1.0
Generator: hatchling 1.29.0 Generator: hatchling 1.27.0
Root-Is-Purelib: true Root-Is-Purelib: true
Tag: py3-none-any 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, resolve_types,
validate, validate,
) )
from attr._make import ClassProps from attr._next_gen import asdict, astuple
from attr._next_gen import asdict, astuple, inspect
from . import converters, exceptions, filters, setters, validators from . import converters, exceptions, filters, setters, validators
@@ -32,7 +31,6 @@ __all__ = [
"NOTHING", "NOTHING",
"Attribute", "Attribute",
"AttrsInstance", "AttrsInstance",
"ClassProps",
"Converter", "Converter",
"Factory", "Factory",
"NothingType", "NothingType",
@@ -60,7 +58,6 @@ __all__ = [
"filters", "filters",
"frozen", "frozen",
"has", "has",
"inspect",
"make_class", "make_class",
"mutable", "mutable",
"resolve_types", "resolve_types",
+7 -58
View File
@@ -77,7 +77,7 @@ def field(
metadata: Mapping[Any, Any] | None = ..., metadata: Mapping[Any, Any] | None = ...,
converter: None = ..., converter: None = ...,
factory: None = ..., factory: None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: bool | None = ..., eq: bool | None = ...,
order: bool | None = ..., order: bool | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -98,10 +98,10 @@ def field(
metadata: Mapping[Any, Any] | None = ..., metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType converter: _ConverterType
| list[_ConverterType] | list[_ConverterType]
| tuple[_ConverterType, ...] | tuple[_ConverterType]
| None = ..., | None = ...,
factory: Callable[[], _T] | None = ..., factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -121,10 +121,10 @@ def field(
metadata: Mapping[Any, Any] | None = ..., metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType converter: _ConverterType
| list[_ConverterType] | list[_ConverterType]
| tuple[_ConverterType, ...] | tuple[_ConverterType]
| None = ..., | None = ...,
factory: Callable[[], _T] | None = ..., factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -144,10 +144,10 @@ def field(
metadata: Mapping[Any, Any] | None = ..., metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType converter: _ConverterType
| list[_ConverterType] | list[_ConverterType]
| tuple[_ConverterType, ...] | tuple[_ConverterType]
| None = ..., | None = ...,
factory: Callable[[], _T] | None = ..., factory: Callable[[], _T] | None = ...,
kw_only: bool | None = ..., kw_only: bool = ...,
eq: _EqOrderType | None = ..., eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ..., order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ..., on_setattr: _OnSetAttrArgType | None = ...,
@@ -261,54 +261,3 @@ def frozen(
field_transformer: _FieldTransformer | None = ..., field_transformer: _FieldTransformer | None = ...,
match_args: bool = ..., match_args: bool = ...,
) -> Callable[[_C], _C]: ... ) -> 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 Metadata-Version: 2.4
Name: cattrs Name: cattrs
Version: 26.1.0 Version: 25.1.1
Summary: Composable complex class support for attrs and dataclasses. Summary: Composable complex class support for attrs and dataclasses.
Project-URL: Homepage, https://catt.rs Project-URL: Homepage, https://catt.rs
Project-URL: Changelog, https://catt.rs/en/latest/history.html 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: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed Classifier: Typing :: Typed
Requires-Python: >=3.10 Requires-Python: >=3.9
Requires-Dist: attrs>=25.4.0 Requires-Dist: attrs>=24.3.0
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11' 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 Provides-Extra: bson
Requires-Dist: pymongo>=4.4.0; extra == 'bson' Requires-Dist: pymongo>=4.4.0; extra == 'bson'
Provides-Extra: cbor2 Provides-Extra: cbor2
@@ -35,14 +35,11 @@ Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
Provides-Extra: msgspec Provides-Extra: msgspec
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec' Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
Provides-Extra: orjson 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 Provides-Extra: pyyaml
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml' Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
Provides-Extra: tomlkit Provides-Extra: tomlkit
Requires-Dist: tomlkit>=0.11.8; 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 Provides-Extra: ujson
Requires-Dist: ujson>=5.10.0; extra == 'ujson' Requires-Dist: ujson>=5.10.0; extra == 'ujson'
Description-Content-Type: text/markdown 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 Wheel-Version: 1.0
Generator: hatchling 1.28.0 Generator: hatchling 1.27.0
Root-Is-Purelib: true Root-Is-Purelib: true
Tag: py3-none-any 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
+42 -44
View File
@@ -29,6 +29,7 @@ from typing import (
_AnnotatedAlias, _AnnotatedAlias,
_GenericAlias, _GenericAlias,
_SpecialGenericAlias, _SpecialGenericAlias,
_UnionGenericAlias,
get_args, get_args,
get_origin, get_origin,
get_type_hints, get_type_hints,
@@ -208,7 +209,10 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet OriginMutableSet = AbcMutableSet
signature = partial(_signature, eval_str=True) signature = _signature
if sys.version_info >= (3, 10):
signature = partial(_signature, eval_str=True)
try: try:
@@ -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): def is_union_type(obj):
from types import UnionType # noqa: PLC0415 from types import UnionType
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
return ( return (
obj is Union obj is Union
@@ -288,6 +277,25 @@ else:
else: else:
from typing_extensions import NotRequired, Required 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]: def get_notrequired_base(type) -> Union[Any, NothingType]:
if is_annotated(type): if is_annotated(type):
@@ -298,25 +306,6 @@ def get_notrequired_base(type) -> Union[Any, NothingType]:
return NOTHING 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: def is_sequence(type: Any) -> bool:
"""A predicate function for sequences. """A predicate function for sequences.
@@ -324,8 +313,19 @@ def is_sequence(type: Any) -> bool:
tuples. tuples.
""" """
origin = getattr(type, "__origin__", None) origin = getattr(type, "__origin__", None)
return is_mutable_sequence(type) or ( return (
type in (TypingSequence, tuple, Tuple) type
in (
List,
list,
TypingSequence,
TypingMutableSequence,
AbcMutableSequence,
tuple,
Tuple,
deque,
Deque,
)
or ( or (
type.__class__ is _GenericAlias type.__class__ is _GenericAlias
and ( and (
@@ -333,7 +333,7 @@ def is_sequence(type: Any) -> bool:
or (origin is tuple and type.__args__[1] is ...) 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 ...) or (origin is tuple and type.__args__[1] is ...)
) )
@@ -403,10 +403,8 @@ def is_generic(type) -> bool:
"""Whether `type` is a generic type.""" """Whether `type` is a generic type."""
# Inheriting from protocol will inject `Generic` into the MRO # Inheriting from protocol will inject `Generic` into the MRO
# without `__orig_bases__`. # without `__orig_bases__`.
return ( return isinstance(type, (_GenericAlias, GenericAlias)) or (
isinstance(type, (_GenericAlias, GenericAlias)) is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
or (is_subclass(type, Generic) and hasattr(type, "__orig_bases__"))
or type.__class__ is Union # On 3.14, unions are no longer typing._GenericAlias
) )
+2 -2
View File
@@ -1,10 +1,10 @@
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any, get_args from typing import Any
from attrs import NOTHING from attrs import NOTHING
from typing_extensions import Self 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): 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 import defaultdict
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from functools import partial 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 attrs import NOTHING, Attribute, NothingType
from ._compat import ( from ._compat import (
ANIES, ANIES,
AbcSet,
get_args, get_args,
get_full_type_hints,
get_origin, get_origin,
is_bare, is_bare,
is_frozenset, is_frozenset,
is_mapping, is_mapping,
is_mutable_sequence,
is_sequence, is_sequence,
is_subclass, is_subclass,
) )
@@ -42,13 +47,10 @@ if TYPE_CHECKING:
__all__ = [ __all__ = [
"defaultdict_structure_factory", "defaultdict_structure_factory",
"homogenous_tuple_structure_factory",
"is_abstract_set",
"is_any_set", "is_any_set",
"is_defaultdict", "is_defaultdict",
"is_frozenset", "is_frozenset",
"is_mapping", "is_mapping",
"is_mutable_sequence",
"is_namedtuple", "is_namedtuple",
"is_sequence", "is_sequence",
"is_set", "is_set",
@@ -68,11 +70,6 @@ def is_any_set(type) -> bool:
return is_set(type) or is_frozenset(type) 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: def is_namedtuple(type: Any) -> bool:
"""A predicate function for named tuples.""" """A predicate function for named tuples."""
@@ -154,47 +151,6 @@ def list_structure_factory(type: type, converter: BaseConverter) -> StructureHoo
return structure_list 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( def namedtuple_unstructure_factory(
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
) -> UnstructureHook: ) -> UnstructureHook:
@@ -239,7 +195,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
type=a, type=a,
alias=name, 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_hetero_tuple,
is_literal, is_literal,
is_mapping, is_mapping,
is_mutable_sequence,
is_mutable_set, is_mutable_set,
is_optional, is_optional,
is_protocol, is_protocol,
is_subclass, is_sequence,
is_tuple, is_tuple,
is_typeddict, is_typeddict,
is_union_type, is_union_type,
@@ -55,11 +54,8 @@ from ._compat import (
) )
from .cols import ( from .cols import (
defaultdict_structure_factory, defaultdict_structure_factory,
homogenous_tuple_structure_factory,
is_abstract_set,
is_defaultdict, is_defaultdict,
is_namedtuple, is_namedtuple,
is_sequence,
iterable_unstructure_factory, iterable_unstructure_factory,
list_structure_factory, list_structure_factory,
mapping_structure_factory, mapping_structure_factory,
@@ -77,7 +73,6 @@ from .dispatch import (
UnstructuredValue, UnstructuredValue,
UnstructureHook, UnstructureHook,
) )
from .enums import enum_structure_factory, enum_unstructure_factory
from .errors import ( from .errors import (
IterableValidationError, IterableValidationError,
IterableValidationNote, IterableValidationNote,
@@ -230,10 +225,6 @@ class BaseConverter:
) )
self._unstructure_func.register_func_list( 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, is_protocol,
lambda o: self.unstructure(o, unstructure_as=o.__class__), 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)), lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
True, True,
), ),
(is_literal_containing_enums, self.unstructure),
(is_mapping, self._unstructure_mapping), (is_mapping, self._unstructure_mapping),
(is_sequence, self._unstructure_seq), (is_sequence, self._unstructure_seq),
(is_mutable_set, self._unstructure_seq), (is_mutable_set, self._unstructure_seq),
(is_frozenset, self._unstructure_seq), (is_frozenset, self._unstructure_seq),
(is_literal_containing_enums, self.unstructure), (lambda t: issubclass(t, Enum), self._unstructure_enum),
(lambda t: is_subclass(t, Enum), enum_unstructure_factory, "extended"),
(has, self._unstructure_attrs), (has, self._unstructure_attrs),
(is_union_type, self._unstructure_union), (is_union_type, self._unstructure_union),
(lambda t: t in ANIES, self.unstructure), (lambda t: t in ANIES, self.unstructure),
@@ -280,27 +271,20 @@ class BaseConverter:
), ),
(is_literal, self._structure_simple_literal), (is_literal, self._structure_simple_literal),
(is_literal_containing_enums, self._structure_enum_literal), (is_literal_containing_enums, self._structure_enum_literal),
(is_sequence, homogenous_tuple_structure_factory, "extended"), (is_sequence, list_structure_factory, "extended"),
(is_mutable_sequence, list_structure_factory, "extended"),
(is_deque, self._structure_deque), (is_deque, self._structure_deque),
(is_mutable_set, self._structure_set), (is_mutable_set, self._structure_set),
(is_abstract_set, self._structure_frozenset),
(is_frozenset, self._structure_frozenset), (is_frozenset, self._structure_frozenset),
(is_tuple, self._structure_tuple), (is_tuple, self._structure_tuple),
(is_namedtuple, namedtuple_structure_factory, "extended"), (is_namedtuple, namedtuple_structure_factory, "extended"),
(is_mapping, self._structure_dict), (is_mapping, self._structure_dict),
*( (is_supported_union, self._gen_attrs_union_structure, True),
[(is_supported_union, self._gen_attrs_union_structure, True)]
if unstruct_strat is UnstructureStrategy.AS_DICT
else []
),
(is_optional, self._structure_optional), (is_optional, self._structure_optional),
( (
lambda t: is_union_type(t) and t in self._union_struct_registry, lambda t: is_union_type(t) and t in self._union_struct_registry,
self._union_struct_registry.__getitem__, self._union_struct_registry.__getitem__,
True, True,
), ),
(lambda t: is_subclass(t, Enum), enum_structure_factory, "extended"),
(has, self._structure_attrs), (has, self._structure_attrs),
] ]
) )
@@ -311,6 +295,7 @@ class BaseConverter:
(bytes, self._structure_call), (bytes, self._structure_call),
(int, self._structure_call), (int, self._structure_call),
(float, self._structure_call), (float, self._structure_call),
(Enum, self._structure_call),
(Path, self._structure_call), (Path, self._structure_call),
] ]
) )
@@ -632,6 +617,10 @@ class BaseConverter:
res.append(dispatch(a.type or v.__class__)(v)) res.append(dispatch(a.type or v.__class__)(v))
return tuple(res) 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]: def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
"""Convert a sequence to primitive equivalents.""" """Convert a sequence to primitive equivalents."""
# We can reuse the sequence class, so tuples stay tuples. # We can reuse the sequence class, so tuples stay tuples.
@@ -1045,7 +1034,6 @@ class Converter(BaseConverter):
"forbid_extra_keys", "forbid_extra_keys",
"omit_if_default", "omit_if_default",
"type_overrides", "type_overrides",
"use_alias",
) )
def __init__( def __init__(
@@ -1062,7 +1050,6 @@ class Converter(BaseConverter):
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error( structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
None, t None, t
), ),
use_alias: bool = False,
): ):
""" """
:param detailed_validation: Whether to use a slightly slower mode for detailed :param detailed_validation: Whether to use a slightly slower mode for detailed
@@ -1071,15 +1058,12 @@ class Converter(BaseConverter):
registered unstructuring hooks match. registered unstructuring hooks match.
:param structure_fallback_factory: A hook factory to be called when no :param structure_fallback_factory: A hook factory to be called when no
registered structuring hooks match. 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 *unstructure_fallback_factory*
.. versionadded:: 23.2.0 *structure_fallback_factory* .. versionadded:: 23.2.0 *structure_fallback_factory*
.. versionchanged:: 24.2.0 .. versionchanged:: 24.2.0
The default `structure_fallback_factory` now raises errors for missing handlers The default `structure_fallback_factory` now raises errors for missing handlers
more eagerly, surfacing problems earlier. more eagerly, surfacing problems earlier.
.. versionadded:: 25.2.0 *use_alias*
""" """
super().__init__( super().__init__(
dict_factory=dict_factory, dict_factory=dict_factory,
@@ -1092,7 +1076,6 @@ class Converter(BaseConverter):
self.omit_if_default = omit_if_default self.omit_if_default = omit_if_default
self.forbid_extra_keys = forbid_extra_keys self.forbid_extra_keys = forbid_extra_keys
self.type_overrides = dict(type_overrides) self.type_overrides = dict(type_overrides)
self.use_alias = use_alias
unstruct_collection_overrides = { unstruct_collection_overrides = {
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items() 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) attribs = fields(origin or cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs): if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved. # PEP 563 annotations - need to be resolved.
resolve_types(origin or cl) resolve_types(cl)
attrib_overrides = { attrib_overrides = {
a.name: self.type_overrides[a.type] a.name: self.type_overrides[a.type]
for a in attribs for a in attribs
@@ -1301,11 +1284,10 @@ class Converter(BaseConverter):
def gen_structure_attrs_fromdict( def gen_structure_attrs_fromdict(
self, cl: type[T] self, cl: type[T]
) -> Callable[[Mapping[str, Any], Any], T]: ) -> Callable[[Mapping[str, Any], Any], T]:
origin = get_origin(cl) attribs = fields(get_origin(cl) or cl if is_generic(cl) else cl)
attribs = fields(origin or cl if is_generic(cl) else cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs): if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved. # PEP 563 annotations - need to be resolved.
resolve_types(origin or cl) resolve_types(cl)
attrib_overrides = { attrib_overrides = {
a.name: self.type_overrides[a.type] a.name: self.type_overrides[a.type]
for a in attribs for a in attribs
@@ -1317,7 +1299,6 @@ class Converter(BaseConverter):
_cattrs_forbid_extra_keys=self.forbid_extra_keys, _cattrs_forbid_extra_keys=self.forbid_extra_keys,
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters, _cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
_cattrs_detailed_validation=self.detailed_validation, _cattrs_detailed_validation=self.detailed_validation,
_cattrs_use_alias=self.use_alias,
**attrib_overrides, **attrib_overrides,
) )
@@ -1396,7 +1377,6 @@ class Converter(BaseConverter):
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None, unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
prefer_attrib_converters: bool | None = None, prefer_attrib_converters: bool | None = None,
detailed_validation: bool | None = None, detailed_validation: bool | None = None,
use_alias: bool | None = None,
) -> Self: ) -> Self:
"""Create a copy of the converter, keeping all existing custom hooks. """Create a copy of the converter, keeping all existing custom hooks.
@@ -1436,7 +1416,6 @@ class Converter(BaseConverter):
if detailed_validation is not None if detailed_validation is not None
else self.detailed_validation else self.detailed_validation
), ),
use_alias=(use_alias if use_alias is not None else self.use_alias),
) )
self._unstructure_func.copy_to( self._unstructure_func.copy_to(
+2 -1
View File
@@ -7,7 +7,7 @@ from collections.abc import Mapping
from dataclasses import MISSING from dataclasses import MISSING
from functools import reduce from functools import reduce
from operator import or_ 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 from attrs import NOTHING, Attribute, AttrsInstance
@@ -16,6 +16,7 @@ from ._compat import (
adapted_fields, adapted_fields,
fields_dict, fields_dict,
get_args, get_args,
get_origin,
has, has,
is_literal, is_literal,
is_union_type, 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: def __init__(self, message: str, type_: type) -> None:
super().__init__(message, type_) super().__init__(message)
self.message = message
self.type_ = type_ self.type_ = type_
def __str__(self) -> str:
return self.message
class BaseValidationError(ExceptionGroup): class BaseValidationError(ExceptionGroup):
cl: type 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 = super().__new__(cls, message, excs)
obj.cl = cl obj.cl = cl
return obj return obj
@@ -39,7 +35,9 @@ class IterableValidationNote(str):
index: Union[int, str] # Ints for list indices, strs for dict keys index: Union[int, str] # Ints for list indices, strs for dict keys
type: Any 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 = str.__new__(cls, string)
instance.index = index instance.index = index
instance.type = type instance.type = type
@@ -78,7 +76,7 @@ class AttributeValidationNote(str):
name: str name: str
type: Any 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 = str.__new__(cls, string)
instance.name = name instance.name = name
instance.type = type instance.type = type
@@ -124,15 +122,11 @@ class ForbiddenExtraKeysError(Exception):
def __init__( def __init__(
self, message: Optional[str], cl: type, extra_fields: set[str] self, message: Optional[str], cl: type, extra_fields: set[str]
) -> None: ) -> None:
self.message = message
self.cl = cl self.cl = cl
self.extra_fields = extra_fields self.extra_fields = extra_fields
cln = cl.__name__
super().__init__(message, cl, extra_fields) super().__init__(
message
def __str__(self) -> str: or f"Extra fields in constructor for {cln}: {', '.join(extra_fields)}"
return (
self.message
or f"Extra fields in constructor for {self.cl.__name__}: "
f"{', '.join(sorted(self.extra_fields))}"
) )
+24 -108
View File
@@ -4,7 +4,7 @@ import re
from collections.abc import Callable, Iterable, Mapping from collections.abc import Callable, Iterable, Mapping
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar 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 typing_extensions import NoDefault
from .._compat import ( from .._compat import (
@@ -33,7 +33,7 @@ from ..types import SimpleStructureHook
from ._consts import AttributeOverride, already_generating, neutral from ._consts import AttributeOverride, already_generating, neutral
from ._generics import generate_mapping from ._generics import generate_mapping
from ._lc import generate_unique_filename 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: if TYPE_CHECKING:
from ..converters import BaseConverter from ..converters import BaseConverter
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
typevar_map: dict[str, Any] = {}, typevar_map: dict[str, Any] = {},
_cattrs_omit_if_default: bool = False, _cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True, _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, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]: ) -> Callable[[T], dict[str, Any]]:
@@ -95,17 +95,7 @@ def make_dict_unstructure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 24.1.0 .. 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__ fn_name = "unstructure_" + cl.__name__
@@ -114,27 +104,15 @@ def make_dict_unstructure_fn_from_attrs(
invocation_lines = [] invocation_lines = []
internal_arg_parts = {} 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: for a in attrs:
attr_name = a.name attr_name = a.name
if attr_name in kwargs: override = kwargs.get(attr_name, neutral)
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[attr_name] = override
if override.omit: if override.omit:
continue continue
if override.omit is None and not a.init and not _cattrs_include_init_false: if override.omit is None and not a.init and not _cattrs_include_init_false:
continue continue
if override.rename is None: if override.rename is None:
kn = attr_name if not _cattrs_use_alias else a.alias kn = attr_name if not _cattrs_use_alias else a.alias
if kn != attr_name:
kwargs[attr_name] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
d = a.default d = a.default
@@ -192,32 +170,16 @@ def make_dict_unstructure_fn_from_attrs(
if isinstance(d, Factory): if isinstance(d, Factory):
globs[def_name] = d.factory globs[def_name] = d.factory
internal_arg_parts[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: else:
globs[def_name] = d globs[def_name] = d
internal_arg_parts[def_name] = d internal_arg_parts[def_name] = d
def_str = def_name lines.append(f" if instance.{attr_name} != {def_name}:")
lines.append(f" res['{kn}'] = {invoke}")
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" res['{kn}'] = {invoke}")
else: else:
# No default or no override. # No default or no override.
@@ -255,7 +217,7 @@ def make_dict_unstructure_fn(
converter: BaseConverter, converter: BaseConverter,
_cattrs_omit_if_default: bool = False, _cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True, _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, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]: ) -> Callable[[T], dict[str, Any]]:
@@ -273,22 +235,13 @@ def make_dict_unstructure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 23.2.0 *_cattrs_use_alias* .. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false* .. 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) origin = get_origin(cl)
attrs = adapted_fields(origin or cl) # type: ignore attrs = adapted_fields(origin or cl) # type: ignore
mapping = {} 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): if is_generic(cl):
mapping = generate_mapping(cl, mapping) mapping = generate_mapping(cl, mapping)
@@ -336,7 +289,7 @@ def make_dict_structure_fn_from_attrs(
bool | Literal["from_converter"] bool | Literal["from_converter"]
) = "from_converter", ) = "from_converter",
_cattrs_detailed_validation: 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, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]: ) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -361,13 +314,7 @@ def make_dict_structure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 24.1.0 .. 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__ cl_name = cl.__name__
@@ -403,9 +350,6 @@ def make_dict_structure_fn_from_attrs(
if _cattrs_forbid_extra_keys == "from_converter": if _cattrs_forbid_extra_keys == "from_converter":
# BaseConverter doesn't have it so we're careful. # BaseConverter doesn't have it so we're careful.
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False) _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": if _cattrs_detailed_validation == "from_converter":
_cattrs_detailed_validation = converter.detailed_validation _cattrs_detailed_validation = converter.detailed_validation
if _cattrs_prefer_attrib_converters == "from_converter": if _cattrs_prefer_attrib_converters == "from_converter":
@@ -423,13 +367,7 @@ def make_dict_structure_fn_from_attrs(
internal_arg_parts["__c_avn"] = AttributeValidationNote internal_arg_parts["__c_avn"] = AttributeValidationNote
for a in attrs: for a in attrs:
an = a.name an = a.name
if an in kwargs: override = kwargs.get(an, neutral)
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
if override.omit: if override.omit:
continue continue
if override.omit is None and not a.init and not _cattrs_include_init_false: 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 ian = a.alias
if override.rename is None: if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
@@ -560,24 +496,14 @@ def make_dict_structure_fn_from_attrs(
# The first loop deals with required args. # The first loop deals with required args.
for a in attrs: for a in attrs:
an = a.name an = a.name
override = kwargs.get(an, neutral)
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
if override.omit: if override.omit:
continue continue
if override.omit is None and not a.init and not _cattrs_include_init_false: if override.omit is None and not a.init and not _cattrs_include_init_false:
continue continue
if a.default is not NOTHING: if a.default is not NOTHING:
non_required.append(a) non_required.append(a)
# The next loop will handle it.
continue continue
t = a.type t = a.type
if isinstance(t, TypeVar): if isinstance(t, TypeVar):
t = typevar_map.get(t.__name__, t) t = typevar_map.get(t.__name__, t)
@@ -597,8 +523,6 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None: if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
allowed_fields.add(kn) allowed_fields.add(kn)
@@ -668,8 +592,6 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None: if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
allowed_fields.add(kn) allowed_fields.add(kn)
@@ -760,7 +682,7 @@ def make_dict_structure_fn(
bool | Literal["from_converter"] bool | Literal["from_converter"]
) = "from_converter", ) = "from_converter",
_cattrs_detailed_validation: 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, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]: ) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -784,20 +706,14 @@ def make_dict_structure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 23.2.0 *_cattrs_use_alias* .. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false* .. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0 .. versionchanged:: 23.2.0
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
take their values from the given converter by default. take their values from the given converter by default.
.. versionchanged:: 24.1.0 .. versionchanged:: 24.1.0
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
converter by default. 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 = {} mapping = {}
+1 -17
View File
@@ -4,31 +4,15 @@ from typing import TYPE_CHECKING, Any
from attrs import NOTHING, Attribute, Factory 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 ..dispatch import StructureHook
from ..errors import StructureHandlerNotFoundError from ..errors import StructureHandlerNotFoundError
from ..fns import raise_error from ..fns import raise_error
from ._consts import AttributeOverride
if TYPE_CHECKING: if TYPE_CHECKING:
from ..converters import BaseConverter 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( def find_structure_handler(
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
) -> StructureHook | None: ) -> StructureHook | None:
+35 -61
View File
@@ -3,12 +3,23 @@ from __future__ import annotations
import re import re
import sys import sys
from collections.abc import Mapping from collections.abc import Mapping
from inspect import get_annotations
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
from attrs import NOTHING, Attribute from attrs import NOTHING, Attribute
from typing_extensions import _TypedDictMeta 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 ( from .._compat import (
get_full_type_hints, get_full_type_hints,
get_notrequired_base, get_notrequired_base,
@@ -29,7 +40,7 @@ from . import AttributeOverride
from ._consts import already_generating, neutral from ._consts import already_generating, neutral
from ._generics import generate_mapping from ._generics import generate_mapping
from ._lc import generate_unique_filename 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: if TYPE_CHECKING:
from ..converters import BaseConverter from ..converters import BaseConverter
@@ -39,10 +50,6 @@ __all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
T = TypeVar("T") T = TypeVar("T")
def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)
def make_dict_unstructure_fn( def make_dict_unstructure_fn(
cl: type[T], cl: type[T],
converter: BaseConverter, converter: BaseConverter,
@@ -102,20 +109,11 @@ def make_dict_unstructure_fn(
# * all attributes resolve to `converter._unstructure_identity` # * all attributes resolve to `converter._unstructure_identity`
for a in attrs: for a in attrs:
attr_name = a.name attr_name = a.name
t = a.type override = kwargs.get(attr_name, neutral)
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
if override != neutral: if override != neutral:
break break
handler = None handler = None
t = a.type
if isinstance(t, TypeVar): if isinstance(t, TypeVar):
if t.__name__ in mapping: if t.__name__ in mapping:
@@ -127,6 +125,9 @@ def make_dict_unstructure_fn(
t = deep_copy_with(t, mapping, cl) t = deep_copy_with(t, mapping, cl)
if handler is None: if handler is None:
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
try: try:
handler = converter.get_unstructure_hook(t) handler = converter.get_unstructure_hook(t)
except RecursionError: except RecursionError:
@@ -141,22 +142,10 @@ def make_dict_unstructure_fn(
for ix, a in enumerate(attrs): for ix, a in enumerate(attrs):
attr_name = a.name attr_name = a.name
t = a.type override = kwargs.get(attr_name, neutral)
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
if override.omit: if override.omit:
lines.append(f" res.pop('{attr_name}', None)") lines.append(f" res.pop('{attr_name}', None)")
continue continue
if override.rename is not None: if override.rename is not None:
# We also need to pop when renaming, since we're copying # We also need to pop when renaming, since we're copying
# the original. # the original.
@@ -171,6 +160,8 @@ def make_dict_unstructure_fn(
if override.unstruct_hook is not None: if override.unstruct_hook is not None:
handler = override.unstruct_hook handler = override.unstruct_hook
else: else:
t = a.type
if isinstance(t, TypeVar): if isinstance(t, TypeVar):
if t.__name__ in mapping: if t.__name__ in mapping:
t = mapping[t.__name__] t = mapping[t.__name__]
@@ -180,6 +171,9 @@ def make_dict_unstructure_fn(
t = deep_copy_with(t, mapping, cl) t = deep_copy_with(t, mapping, cl)
if handler is None: if handler is None:
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
try: try:
handler = converter.get_unstructure_hook(t) handler = converter.get_unstructure_hook(t)
except RecursionError: except RecursionError:
@@ -226,15 +220,12 @@ def make_dict_unstructure_fn(
) )
eval(compile(script, fname, "exec"), globs) eval(compile(script, fname, "exec"), globs)
res = globs[fn_name]
res.overrides = kwargs
finally: finally:
working_set.remove(cl) working_set.remove(cl)
if not working_set: if not working_set:
del already_generating.working_set del already_generating.working_set
return res return globs[fn_name]
def make_dict_structure_fn( def make_dict_structure_fn(
@@ -335,25 +326,20 @@ def make_dict_structure_fn(
for ix, a in enumerate(attrs): for ix, a in enumerate(attrs):
an = a.name an = a.name
attr_required = an in req_keys attr_required = an in req_keys
t = a.type override = kwargs.get(an, neutral)
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 override.omit: if override.omit:
continue continue
t = a.type
if isinstance(t, TypeVar): if isinstance(t, TypeVar):
t = mapping.get(t.__name__, t) t = mapping.get(t.__name__, t)
elif is_generic(t) and not is_bare(t) and not is_annotated(t): elif is_generic(t) and not is_bare(t) and not is_annotated(t):
t = deep_copy_with(t, mapping, cl) 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): if is_generic(t) and not is_bare(t) and not is_annotated(t):
t = deep_copy_with(t, mapping, cl) t = deep_copy_with(t, mapping, cl)
@@ -413,12 +399,7 @@ def make_dict_structure_fn(
for ix, a in enumerate(attrs): for ix, a in enumerate(attrs):
an = a.name an = a.name
attr_required = an in req_keys attr_required = an in req_keys
if an in kwargs: override = kwargs.get(an, neutral)
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
if override.omit: if override.omit:
continue continue
if not attr_required: if not attr_required:
@@ -467,18 +448,13 @@ def make_dict_structure_fn(
if non_required: if non_required:
for ix, a in non_required: for ix, a in non_required:
an = a.name an = a.name
override = kwargs.get(an, neutral)
t = a.type t = a.type
nrb = get_notrequired_base(t) nrb = get_notrequired_base(t)
if nrb is not NOTHING: if nrb is not NOTHING:
t = nrb 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): if isinstance(t, TypeVar):
t = mapping.get(t.__name__, t) t = mapping.get(t.__name__, t)
elif is_generic(t) and not is_bare(t) and not is_annotated(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) eval(compile(script, fname, "exec"), globs)
res = globs[fn_name] return globs[fn_name]
res.overrides = kwargs
return res
def _adapted_fields(cls: Any) -> list[Attribute]: 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 datetime import datetime
from enum import Enum 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 .._compat import is_subclass
from ..converters import Converter, UnstructureHook from ..converters import Converter, UnstructureHook
from ..fns import identity 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, _): def validate_datetime(v, _):
if not isinstance(v, datetime): 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 # datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running. # 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_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, lambda v: v.isoformat()) converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) 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( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_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_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) 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( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_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( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_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) 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( converter.register_structure_hook(
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date() 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( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory is_literal_containing_enums, literals_with_enums_unstructure_factory
) )
+3 -14
View File
@@ -3,27 +3,18 @@
from __future__ import annotations from __future__ import annotations
from base64 import b64decode from base64 import b64decode
from collections.abc import Callable
from dataclasses import is_dataclass from dataclasses import is_dataclass
from datetime import date, datetime from datetime import date, datetime
from enum import Enum from enum import Enum
from functools import partial 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 has as attrs_has
from attrs import resolve_types from attrs import resolve_types
from msgspec import Struct, convert, to_builtins from msgspec import Struct, convert, to_builtins
from msgspec.json import Encoder, decode from msgspec.json import Encoder, decode
from .._compat import ( from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
fields,
get_args,
get_origin,
is_bare,
is_mapping,
is_sequence,
is_subclass,
)
from ..cols import is_namedtuple from ..cols import is_namedtuple
from ..converters import BaseConverter, Converter from ..converters import BaseConverter, Converter
from ..dispatch import UnstructureHook from ..dispatch import UnstructureHook
@@ -83,9 +74,7 @@ def configure_converter(converter: Converter) -> None:
configure_passthroughs(converter) configure_passthroughs(converter)
converter.register_unstructure_hook(Struct, to_builtins) converter.register_unstructure_hook(Struct, to_builtins)
converter.register_unstructure_hook_factory( converter.register_unstructure_hook(Enum, identity)
lambda t: is_subclass(t, Enum), lambda t, c: identity
)
converter.register_structure_hook(Struct, convert) converter.register_structure_hook(Struct, convert)
converter.register_structure_hook(bytes, lambda v, _: b64decode(v)) 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( converter.register_unstructure_hook_func(
partial(is_primitive_enum, include_bare_enums=True), lambda t: identity partial(is_primitive_enum, include_bare_enums=True), identity
) )
converter.register_unstructure_hook_factory( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_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 .._compat import is_mapping, is_subclass
from ..converters import BaseConverter, Converter from ..converters import BaseConverter, Converter
from ..fns import identity
from ..strategies import configure_union_passthrough from ..strategies import configure_union_passthrough
from . import validate_datetime, wrap from . import validate_datetime, wrap
@@ -38,9 +37,6 @@ def configure_converter(converter: BaseConverter):
* sets are serialized as lists * sets are serialized as lists
* tuples are serializas as lists * tuples are serializas as lists
* mapping keys are coerced into strings when unstructuring * 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_structure_hook(bytes, lambda v, _: b85decode(v))
converter.register_unstructure_hook( converter.register_unstructure_hook(
@@ -71,12 +67,10 @@ def configure_converter(converter: BaseConverter):
# datetime inherits from date, so identity unstructure hook used # datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running. # 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_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, identity) converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook( converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
)
configure_union_passthrough( configure_union_passthrough(
Union[str, String, bool, int, Integer, float, Float], converter 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_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
converter.register_unstructure_hook(date, lambda v: v.isoformat()) converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) 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( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_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 ..converters import BaseConverter
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
from ..gen._consts import already_generating from ..gen._consts import already_generating
from ..subclasses import subclasses
def _make_subclasses_tree(cl: type) -> list[type]: def _make_subclasses_tree(cl: type) -> list[type]:
# get class origin for accessing subclasses (see #648 for more info) # get class origin for accessing subclasses (see #648 for more info)
cls_origin = typing.get_origin(cl) or cl cls_origin = typing.get_origin(cl) or cl
return [cl] + [
# Use a dict to deduplicate and keep insertion order. sscl
seen = {cl: None} for scl in cls_origin.__subclasses__()
for scl in subclasses(cls_origin): for sscl in _make_subclasses_tree(scl)
for sscl in _make_subclasses_tree(scl): ]
seen[sscl] = None
return list(seen)
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool: def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
"""Whether the given class has subclasses from `given_subclasses`.""" """Whether the given class has subclasses from `given_subclasses`."""
cls_origin = typing.get_origin(cl) or cl actual = set(cl.__subclasses__())
actual = set(subclasses(cls_origin))
given = set(given_subclasses) given = set(given_subclasses)
return bool(actual & given) return bool(actual & given)
@@ -72,9 +68,6 @@ def include_subclasses(
.. versionchanged:: 24.1.0 .. versionchanged:: 24.1.0
When overrides are not provided, hooks for individual classes are retrieved from When overrides are not provided, hooks for individual classes are retrieved from
the converter instead of generated with no overrides, using converter defaults. 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 # Due to https://github.com/python-attrs/attrs/issues/1047
collect() collect()
@@ -238,13 +231,7 @@ def _include_subclasses_with_union_strategy(
return cls is _cl return cls is _cl
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook) converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
subclasses = tuple( subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
[
c
for c in union_classes
if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)
]
)
if len(subclasses) > 1: if len(subclasses) > 1:
u = Union[subclasses] # type: ignore u = Union[subclasses] # type: ignore
union_strategy(u, converter) union_strategy(u, converter)
+45 -65
View File
@@ -52,10 +52,23 @@ def configure_tagged_union(
if is_type_alias(union): if is_type_alias(union):
union = union.__value__ union = union.__value__
args = union.__args__ args = union.__args__
tag_to_hook = {} tag_to_hook = {}
exact_cl_unstruct_hooks = {} 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: if default is not NOTHING:
default_handler = converter.get_structure_hook(default) 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): def structure_default(val: dict, _cl=default, _h=default_handler):
return _h(val, _cl) return _h(val, _cl)
tag_to_hook = defaultdict(lambda: structure_default) tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
cl_to_tag = defaultdict(lambda: default) 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): if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union( def structure_tagged_union(
@@ -95,54 +135,11 @@ def configure_tagged_union(
return _tag_to_hook[val[_tag_name]](val) return _tag_to_hook[val[_tag_name]](val)
return _dh(val, _default) 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_unstructure_hook(union, unstructure_tagged_union)
converter.register_structure_hook(union, structure_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: def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
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:
""" """
Configure the converter to support validating and passing through unions of the Configure the converter to support validating and passing through unions of the
provided types and their subsets. 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 If the union contains a class and one or more of its subclasses, the subclasses
will also be included when validating the superclass. 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 .. versionadded:: 23.2.0
.. versionchanged:: 25.2.0
Introduced the `accept_ints_as_floats` parameter.
""" """
args = set(union.__args__) args = set(union.__args__)
@@ -215,16 +205,6 @@ def configure_union_passthrough(
and not is_literal(a) 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: if spillover:
spillover_type = ( spillover_type = (
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover)) 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/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/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
importlib_metadata-6.8.0.dist-info/RECORD,, 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/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19 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/__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/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743 importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554 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/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/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
lsprotocol-2023.0.1.dist-info/RECORD,, 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/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94 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/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433 lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -0,0 +1 @@
pip
@@ -1,13 +1,14 @@
Metadata-Version: 2.4 Metadata-Version: 2.4
Name: packaging Name: packaging
Version: 26.2 Version: 25.0
Summary: Core utilities for Python packages Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io> Author-email: Donald Stufft <donald@stufft.io>
Requires-Python: >=3.8 Requires-Python: >=3.8
Description-Content-Type: text/x-rst Description-Content-Type: text/x-rst
License-Expression: Apache-2.0 OR BSD-2-Clause
Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers 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
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only 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.11
Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
Classifier: Typing :: Typed Classifier: Typing :: Typed
License-File: LICENSE License-File: LICENSE
License-File: LICENSE.APACHE License-File: LICENSE.APACHE
@@ -43,7 +42,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`).
.. end-intro .. end-intro
The ``packaging`` project includes the following: version handling, specifiers, The ``packaging`` project includes the following: version handling, specifiers,
markers, requirements, tags, metadata, lockfiles, utilities. markers, requirements, tags, utilities.
Documentation Documentation
------------- -------------
@@ -53,14 +52,8 @@ The `documentation`_ provides information and the API for the following:
- Version Handling - Version Handling
- Specifiers - Specifiers
- Markers - Markers
- Licenses
- Requirements - Requirements
- Metadata
- Tags - Tags
- Lockfiles (pylock)
- Direct URL helpers
- Dependency groups
- Errors
- Utilities - Utilities
Installation Installation
@@ -77,11 +70,11 @@ Discussion
If you run into bugs, you can file them in our `issue tracker`_. 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/ .. _`documentation`: https://packaging.pypa.io/
.. _`issue tracker`: https://github.com/pypa/packaging/issues .. _`issue tracker`: https://github.com/pypa/packaging/issues
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
Code of Conduct 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" __summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging" __uri__ = "https://github.com/pypa/packaging"
__version__ = "26.2" __version__ = "25.0"
__author__ = "Donald Stufft and individual contributors" __author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io" __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 This provides a class ``ELFFile`` that parses an ELF executable in a similar
interface to ``ZipFile``. Only the read interface is implemented. 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 ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
""" """
+36 -36
View File
@@ -15,16 +15,6 @@ EF_ARM_ABIMASK = 0xFF000000
EF_ARM_ABI_VER5 = 0x05000000 EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400 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` # `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then. # 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) return _is_linux_armhf(executable)
if "i686" in archs: if "i686" in archs:
return _is_linux_i686(executable) 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 # 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. Fallback implementation of glibc_version_string using ctypes.
""" """
try: try:
import ctypes # noqa: PLC0415 import ctypes
except ImportError: except ImportError:
return None return None
@@ -151,7 +150,7 @@ def _glibc_version_string() -> str | None:
return _glibc_version_string_confstr() or _glibc_version_string_ctypes() 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. """Parse glibc version.
We use a regexp instead of str.split because we want to discard any 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, RuntimeWarning,
stacklevel=2, stacklevel=2,
) )
return _GLibCVersion(-1, -1) return -1, -1
return _GLibCVersion(int(m.group("major")), int(m.group("minor"))) return int(m.group("major")), int(m.group("minor"))
@functools.lru_cache @functools.lru_cache
def _get_glibc_version() -> _GLibCVersion: def _get_glibc_version() -> tuple[int, int]:
version_str = _glibc_version_string() version_str = _glibc_version_string()
if version_str is None: if version_str is None:
return _GLibCVersion(-1, -1) return (-1, -1)
return _parse_glibc_version(version_str) return _parse_glibc_version(version_str)
@@ -185,7 +184,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
return False return False
# Check for presence of _manylinux module. # Check for presence of _manylinux module.
try: try:
import _manylinux # noqa: PLC0415 import _manylinux
except ImportError: except ImportError:
return True return True
if hasattr(_manylinux, "manylinux_compatible"): if hasattr(_manylinux, "manylinux_compatible"):
@@ -193,26 +192,25 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
if result is not None: if result is not None:
return bool(result) return bool(result)
return True return True
if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"): if version == _GLibCVersion(2, 5):
return bool(_manylinux.manylinux1_compatible) if hasattr(_manylinux, "manylinux1_compatible"):
if version == _GLibCVersion(2, 12) and hasattr( return bool(_manylinux.manylinux1_compatible)
_manylinux, "manylinux2010_compatible" if version == _GLibCVersion(2, 12):
): if hasattr(_manylinux, "manylinux2010_compatible"):
return bool(_manylinux.manylinux2010_compatible) return bool(_manylinux.manylinux2010_compatible)
if version == _GLibCVersion(2, 17) and hasattr( if version == _GLibCVersion(2, 17):
_manylinux, "manylinux2014_compatible" if hasattr(_manylinux, "manylinux2014_compatible"):
): return bool(_manylinux.manylinux2014_compatible)
return bool(_manylinux.manylinux2014_compatible)
return True return True
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = { _LEGACY_MANYLINUX_MAP = {
# CentOS 7 w/ glibc 2.17 (PEP 599) # CentOS 7 w/ glibc 2.17 (PEP 599)
_GLibCVersion(2, 17): "manylinux2014", (2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571) # CentOS 6 w/ glibc 2.12 (PEP 571)
_GLibCVersion(2, 12): "manylinux2010", (2, 12): "manylinux2010",
# CentOS 5 w/ glibc 2.5 (PEP 513) # 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 min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1): for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, 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.
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. if glibc_version in _LEGACY_MANYLINUX_MAP:
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version): legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(arch, glibc_version):
yield f"{legacy_tag}_{arch}" yield f"{legacy_tag}_{arch}"
+1 -1
View File
@@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None:
return None return None
if ld is None or "musl" not in ld: if ld is None or "musl" not in ld:
return None 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) return _parse_musl_version(proc.stderr)
+10 -50
View File
@@ -7,14 +7,12 @@ the implementation.
from __future__ import annotations from __future__ import annotations
import ast import ast
from typing import List, Literal, NamedTuple, Sequence, Tuple, Union from typing import NamedTuple, Sequence, Tuple, Union
from ._tokenizer import DEFAULT_RULES, Tokenizer from ._tokenizer import DEFAULT_RULES, Tokenizer
class Node: class Node:
__slots__ = ("value",)
def __init__(self, value: str) -> None: def __init__(self, value: str) -> None:
self.value = value self.value = value
@@ -22,66 +20,31 @@ class Node:
return self.value return self.value
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__.__name__}({self.value!r})>" return f"<{self.__class__.__name__}('{self}')>"
def serialize(self) -> str: def serialize(self) -> str:
raise NotImplementedError 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): class Variable(Node):
__slots__ = ()
def serialize(self) -> str: def serialize(self) -> str:
return str(self) return str(self)
class Value(Node): class Value(Node):
__slots__ = ()
def serialize(self) -> str: def serialize(self) -> str:
return f'"{self}"' return f'"{self}"'
class Op(Node): class Op(Node):
__slots__ = ()
def serialize(self) -> str: def serialize(self) -> str:
return str(self) return str(self)
MarkerLogical = Literal["and", "or"]
MarkerVar = Union[Variable, Value] MarkerVar = Union[Variable, Value]
MarkerItem = Tuple[MarkerVar, Op, MarkerVar] MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]] MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]] MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
class ParsedRequirement(NamedTuple): class ParsedRequirement(NamedTuple):
@@ -148,9 +111,7 @@ def _parse_requirement_details(
return (url, specifier, marker) return (url, specifier, marker)
marker = _parse_requirement_marker( marker = _parse_requirement_marker(
tokenizer, tokenizer, span_start=url_start, after="URL and whitespace"
span_start=url_start,
expected="semicolon (after URL and whitespace)",
) )
else: else:
specifier_start = tokenizer.position specifier_start = tokenizer.position
@@ -163,10 +124,10 @@ def _parse_requirement_details(
marker = _parse_requirement_marker( marker = _parse_requirement_marker(
tokenizer, tokenizer,
span_start=specifier_start, span_start=specifier_start,
expected=( after=(
"comma (within version specifier), semicolon (after version specifier)" "version specifier"
if 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( def _parse_requirement_marker(
tokenizer: Tokenizer, *, span_start: int, expected: str tokenizer: Tokenizer, *, span_start: int, after: str
) -> MarkerList: ) -> MarkerList:
""" """
requirement_marker = SEMICOLON marker WS? requirement_marker = SEMICOLON marker WS?
@@ -182,9 +143,8 @@ def _parse_requirement_marker(
if not tokenizer.check("SEMICOLON"): if not tokenizer.check("SEMICOLON"):
tokenizer.raise_syntax_error( tokenizer.raise_syntax_error(
f"Expected {expected} or end", f"Expected end or semicolon (after {after})",
span_start=span_start, span_start=span_start,
span_end=None,
) )
tokenizer.read() tokenizer.read()
@@ -347,7 +307,7 @@ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
return (marker_var_left, marker_op, marker_var_right) 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 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 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details. # 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: class InfinityType:
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
def __repr__(self) -> str: def __repr__(self) -> str:
return "Infinity" return "Infinity"
def __hash__(self) -> int:
return hash(repr(self))
class NegativeInfinityType: def __lt__(self, other: object) -> bool:
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys.""" return False
def __repr__(self) -> str: def __le__(self, other: object) -> bool:
return "-Infinity" 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() 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() NegativeInfinity = NegativeInfinityType()
+26 -24
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import contextlib import contextlib
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Generator, Mapping, NoReturn from typing import Iterator, NoReturn
from .specifiers import Specifier from .specifiers import Specifier
@@ -33,16 +33,16 @@ class ParserSyntaxError(Exception):
def __str__(self) -> str: def __str__(self) -> str:
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" 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]] = { DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
"LEFT_PARENTHESIS": re.compile(r"\("), "LEFT_PARENTHESIS": r"\(",
"RIGHT_PARENTHESIS": re.compile(r"\)"), "RIGHT_PARENTHESIS": r"\)",
"LEFT_BRACKET": re.compile(r"\["), "LEFT_BRACKET": r"\[",
"RIGHT_BRACKET": re.compile(r"\]"), "RIGHT_BRACKET": r"\]",
"SEMICOLON": re.compile(r";"), "SEMICOLON": r";",
"COMMA": re.compile(r","), "COMMA": r",",
"QUOTED_STRING": re.compile( "QUOTED_STRING": re.compile(
r""" r"""
( (
@@ -53,10 +53,10 @@ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
""", """,
re.VERBOSE, re.VERBOSE,
), ),
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"), "OP": r"(===|==|~=|!=|<=|>=|<|>)",
"BOOLOP": re.compile(r"\b(or|and)\b"), "BOOLOP": r"\b(or|and)\b",
"IN": re.compile(r"\bin\b"), "IN": r"\bin\b",
"NOT": re.compile(r"\bnot\b"), "NOT": r"\bnot\b",
"VARIABLE": re.compile( "VARIABLE": re.compile(
r""" r"""
\b( \b(
@@ -75,16 +75,16 @@ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
re.VERBOSE, re.VERBOSE,
), ),
"SPECIFIER": re.compile( "SPECIFIER": re.compile(
Specifier._specifier_regex_str, Specifier._operator_regex_str + Specifier._version_regex_str,
re.VERBOSE | re.IGNORECASE, re.VERBOSE | re.IGNORECASE,
), ),
"AT": re.compile(r"\@"), "AT": r"\@",
"URL": re.compile(r"[^ \t]+"), "URL": r"[^ \t]+",
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"), "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"), "VERSION_PREFIX_TRAIL": r"\.\*",
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"), "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
"WS": re.compile(r"[ \t]+"), "WS": r"[ \t]+",
"END": re.compile(r"$"), "END": r"$",
} }
@@ -99,10 +99,12 @@ class Tokenizer:
self, self,
source: str, source: str,
*, *,
rules: Mapping[str, re.Pattern[str]], rules: dict[str, str | re.Pattern[str]],
) -> None: ) -> None:
self.source = source 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.next_token: Token | None = None
self.position = 0 self.position = 0
@@ -172,7 +174,7 @@ class Tokenizer:
@contextlib.contextmanager @contextlib.contextmanager
def enclosing_tokens( def enclosing_tokens(
self, open_token: str, close_token: str, *, around: str self, open_token: str, close_token: str, *, around: str
) -> Generator[None, None, None]: ) -> Iterator[None]:
if self.check(open_token): if self.check(open_token):
open_position = self.position open_position = self.position
self.read() 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 import re
from typing import NewType, cast from typing import NewType, cast
from ._spdx import EXCEPTIONS, LICENSES from packaging.licenses._spdx import EXCEPTIONS, LICENSES
__all__ = [ __all__ = [
"InvalidLicenseExpression", "InvalidLicenseExpression",
@@ -42,25 +42,14 @@ __all__ = [
"canonicalize_license_expression", "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.-]*$") license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str) NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized
License-Expression.
"""
class InvalidLicenseExpression(ValueError): class InvalidLicenseExpression(ValueError):
"""Raised when a license-expression string is invalid """Raised when a license-expression string is invalid
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("invalid") >>> canonicalize_license_expression("invalid")
Traceback (most recent call last): Traceback (most recent call last):
... ...
@@ -71,34 +60,6 @@ class InvalidLicenseExpression(ValueError):
def canonicalize_license_expression( def canonicalize_license_expression(
raw_license_expression: str, raw_license_expression: str,
) -> NormalizedLicenseExpression: ) -> 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: if not raw_license_expression:
message = f"Invalid license expression: {raw_license_expression!r}" message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) raise InvalidLicenseExpression(message)
@@ -119,21 +80,16 @@ def canonicalize_license_expression(
tokens = license_expression.split() tokens = license_expression.split()
# Rather than implementing a parenthesis/boolean logic parser, create an # Rather than implementing boolean logic, we create an expression that Python can
# expression that Python can parse. Everything that is not involved with the # parse. Everything that is not involved with the grammar itself is treated as
# grammar itself is replaced with the placeholder `False` and the resultant # `False` and the expression should evaluate as such.
# expression should become a valid Python expression.
python_tokens = [] python_tokens = []
for token in tokens: for token in tokens:
if token not in {"or", "and", "with", "(", ")"}: if token not in {"or", "and", "with", "(", ")"}:
python_tokens.append("False") python_tokens.append("False")
elif token == "with": elif token == "with":
python_tokens.append("or") python_tokens.append("or")
elif ( elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}:
token == "("
and python_tokens
and python_tokens[-1] not in {"or", "and", "("}
) or (token == ")" and python_tokens and python_tokens[-1] == "("):
message = f"Invalid license expression: {raw_license_expression!r}" message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) raise InvalidLicenseExpression(message)
else: else:
@@ -141,8 +97,11 @@ def canonicalize_license_expression(
python_expression = " ".join(python_tokens) python_expression = " ".join(python_tokens)
try: try:
compile(python_expression, "", "eval") invalid = eval(python_expression, globals(), locals())
except SyntaxError: except Exception:
invalid = True
if invalid is not False:
message = f"Invalid license expression: {raw_license_expression!r}" message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) from None raise InvalidLicenseExpression(message) from None
@@ -181,6 +140,6 @@ def canonicalize_license_expression(
normalized_expression = " ".join(normalized_tokens) normalized_expression = " ".join(normalized_tokens)
return cast( return cast(
"NormalizedLicenseExpression", NormalizedLicenseExpression,
normalized_expression.replace("( ", "(").replace(" )", ")"), normalized_expression.replace("( ", "(").replace(" )", ")"),
) )
+1 -41
View File
@@ -12,7 +12,7 @@ class SPDXException(TypedDict):
deprecated: bool deprecated: bool
VERSION = '3.27.0' VERSION = '3.25.0'
LICENSES: dict[str, SPDXLicense] = { LICENSES: dict[str, SPDXLicense] = {
'0bsd': {'id': '0BSD', 'deprecated': False}, '0bsd': {'id': '0BSD', 'deprecated': False},
@@ -46,7 +46,6 @@ LICENSES: dict[str, SPDXLicense] = {
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False}, 'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False}, 'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
'any-osi': {'id': 'any-OSI', '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.0': {'id': 'Apache-1.0', 'deprecated': False},
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False}, 'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
'apache-2.0': {'id': 'Apache-2.0', '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-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False}, 'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
'artistic-2.0': {'id': 'Artistic-2.0', '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.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False}, 'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
'baekmuk': {'id': 'Baekmuk', 'deprecated': False}, 'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
@@ -78,7 +75,6 @@ LICENSES: dict[str, SPDXLicense] = {
'blessing': {'id': 'blessing', 'deprecated': False}, 'blessing': {'id': 'blessing', 'deprecated': False},
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False}, 'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
'boehm-gc': {'id': 'Boehm-GC', '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}, 'borceux': {'id': 'Borceux', 'deprecated': False},
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False}, 'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-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-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', '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-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-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False}, 'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', '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-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-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
'cc-pddc': {'id': 'CC-PDDC', '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}, 'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False}, 'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
'cddl-1.1': {'id': 'CDDL-1.1', '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}, 'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
'cronyx': {'id': 'Cronyx', 'deprecated': False}, 'cronyx': {'id': 'Cronyx', 'deprecated': False},
'crossword': {'id': 'Crossword', 'deprecated': False}, 'crossword': {'id': 'Crossword', 'deprecated': False},
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False}, 'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False}, 'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
'cube': {'id': 'Cube', '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-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False}, 'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
'doc': {'id': 'DOC', 'deprecated': False}, 'doc': {'id': 'DOC', 'deprecated': False},
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False}, 'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False}, 'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
'dotseqn': {'id': 'Dotseqn', 'deprecated': False}, 'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
'drl-1.0': {'id': 'DRL-1.0', '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}, 'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
'fsful': {'id': 'FSFUL', 'deprecated': False}, 'fsful': {'id': 'FSFUL', 'deprecated': False},
'fsfullr': {'id': 'FSFULLR', 'deprecated': False}, 'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
'fsfullrwd': {'id': 'FSFULLRWD', '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}, 'ftl': {'id': 'FTL', 'deprecated': False},
'furuseth': {'id': 'Furuseth', 'deprecated': False}, 'furuseth': {'id': 'Furuseth', 'deprecated': False},
'fwlw': {'id': 'fwlw', 'deprecated': False}, 'fwlw': {'id': 'fwlw', 'deprecated': False},
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False}, 'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
'gd': {'id': 'GD', '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': {'id': 'GFDL-1.1', 'deprecated': True},
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False}, '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}, '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}, 'gtkbook': {'id': 'gtkbook', 'deprecated': False},
'gutmann': {'id': 'Gutmann', 'deprecated': False}, 'gutmann': {'id': 'Gutmann', 'deprecated': False},
'haskellreport': {'id': 'HaskellReport', 'deprecated': False}, 'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
'hdf5': {'id': 'HDF5', 'deprecated': False},
'hdparm': {'id': 'hdparm', 'deprecated': False}, 'hdparm': {'id': 'hdparm', 'deprecated': False},
'hidapi': {'id': 'HIDAPI', 'deprecated': False}, 'hidapi': {'id': 'HIDAPI', 'deprecated': False},
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False}, 'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
@@ -376,7 +360,6 @@ LICENSES: dict[str, SPDXLicense] = {
'imlib2': {'id': 'Imlib2', 'deprecated': False}, 'imlib2': {'id': 'Imlib2', 'deprecated': False},
'info-zip': {'id': 'Info-ZIP', 'deprecated': False}, 'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False}, 'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
'intel': {'id': 'Intel', 'deprecated': False}, 'intel': {'id': 'Intel', 'deprecated': False},
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False}, 'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
'interbase-1.0': {'id': 'Interbase-1.0', '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}, 'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
'jam': {'id': 'Jam', 'deprecated': False}, 'jam': {'id': 'Jam', 'deprecated': False},
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False}, 'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
'jove': {'id': 'jove', 'deprecated': False},
'jpl-image': {'id': 'JPL-image', 'deprecated': False}, 'jpl-image': {'id': 'JPL-image', 'deprecated': False},
'jpnic': {'id': 'JPNIC', 'deprecated': False}, 'jpnic': {'id': 'JPNIC', 'deprecated': False},
'json': {'id': 'JSON', '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}, 'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
'lgpllr': {'id': 'LGPLLR', 'deprecated': False}, 'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
'libpng': {'id': 'Libpng', '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}, 'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False}, 'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
'libtiff': {'id': 'libtiff', 'deprecated': False}, 'libtiff': {'id': 'libtiff', 'deprecated': False},
@@ -443,17 +424,14 @@ LICENSES: dict[str, SPDXLicense] = {
'magaz': {'id': 'magaz', 'deprecated': False}, 'magaz': {'id': 'magaz', 'deprecated': False},
'mailprio': {'id': 'mailprio', 'deprecated': False}, 'mailprio': {'id': 'mailprio', 'deprecated': False},
'makeindex': {'id': 'MakeIndex', 'deprecated': False}, 'makeindex': {'id': 'MakeIndex', 'deprecated': False},
'man2html': {'id': 'man2html', 'deprecated': False},
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False}, 'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False}, 'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
'metamail': {'id': 'metamail', 'deprecated': False}, 'metamail': {'id': 'metamail', 'deprecated': False},
'minpack': {'id': 'Minpack', 'deprecated': False}, 'minpack': {'id': 'Minpack', 'deprecated': False},
'mips': {'id': 'MIPS', 'deprecated': False},
'miros': {'id': 'MirOS', 'deprecated': False}, 'miros': {'id': 'MirOS', 'deprecated': False},
'mit': {'id': 'MIT', 'deprecated': False}, 'mit': {'id': 'MIT', 'deprecated': False},
'mit-0': {'id': 'MIT-0', 'deprecated': False}, 'mit-0': {'id': 'MIT-0', 'deprecated': False},
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False}, 'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False}, 'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
'mit-enna': {'id': 'MIT-enna', 'deprecated': False}, 'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
'mit-feh': {'id': 'MIT-feh', 'deprecated': False}, 'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
@@ -494,7 +472,6 @@ LICENSES: dict[str, SPDXLicense] = {
'netcdf': {'id': 'NetCDF', 'deprecated': False}, 'netcdf': {'id': 'NetCDF', 'deprecated': False},
'newsletr': {'id': 'Newsletr', 'deprecated': False}, 'newsletr': {'id': 'Newsletr', 'deprecated': False},
'ngpl': {'id': 'NGPL', 'deprecated': False}, 'ngpl': {'id': 'NGPL', 'deprecated': False},
'ngrep': {'id': 'ngrep', 'deprecated': False},
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False}, 'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
'nist-pd': {'id': 'NIST-PD', 'deprecated': False}, 'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
'nist-pd-fallback': {'id': 'NIST-PD-fallback', '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}, 'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False}, 'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
'nrl': {'id': 'NRL', 'deprecated': False}, 'nrl': {'id': 'NRL', 'deprecated': False},
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
'ntp': {'id': 'NTP', 'deprecated': False}, 'ntp': {'id': 'NTP', 'deprecated': False},
'ntp-0': {'id': 'NTP-0', 'deprecated': False}, 'ntp-0': {'id': 'NTP-0', 'deprecated': False},
'nunit': {'id': 'Nunit', 'deprecated': True}, 'nunit': {'id': 'Nunit', 'deprecated': True},
@@ -604,7 +580,6 @@ LICENSES: dict[str, SPDXLicense] = {
'schemereport': {'id': 'SchemeReport', 'deprecated': False}, 'schemereport': {'id': 'SchemeReport', 'deprecated': False},
'sendmail': {'id': 'Sendmail', 'deprecated': False}, 'sendmail': {'id': 'Sendmail', 'deprecated': False},
'sendmail-8.23': {'id': 'Sendmail-8.23', '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.0': {'id': 'SGI-B-1.0', 'deprecated': False},
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False}, 'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
'sgi-b-2.0': {'id': 'SGI-B-2.0', '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}, 'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
'sl': {'id': 'SL', 'deprecated': False}, 'sl': {'id': 'SL', 'deprecated': False},
'sleepycat': {'id': 'Sleepycat', 'deprecated': False}, 'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
'smlnj': {'id': 'SMLNJ', 'deprecated': False}, 'smlnj': {'id': 'SMLNJ', 'deprecated': False},
'smppl': {'id': 'SMPPL', 'deprecated': False}, 'smppl': {'id': 'SMPPL', 'deprecated': False},
'snia': {'id': 'SNIA', 'deprecated': False}, 'snia': {'id': 'SNIA', 'deprecated': False},
'snprintf': {'id': 'snprintf', 'deprecated': False}, 'snprintf': {'id': 'snprintf', 'deprecated': False},
'sofa': {'id': 'SOFA', 'deprecated': False},
'softsurfer': {'id': 'softSurfer', 'deprecated': False}, 'softsurfer': {'id': 'softSurfer', 'deprecated': False},
'soundex': {'id': 'Soundex', 'deprecated': False}, 'soundex': {'id': 'Soundex', 'deprecated': False},
'spencer-86': {'id': 'Spencer-86', '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}, 'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True}, 'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False}, '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': {'id': 'Sun-PPP', 'deprecated': False},
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False}, 'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
'sunpro': {'id': 'SunPro', 'deprecated': False}, 'sunpro': {'id': 'SunPro', 'deprecated': False},
@@ -648,14 +620,12 @@ LICENSES: dict[str, SPDXLicense] = {
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False}, 'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
'termreadkey': {'id': 'TermReadKey', 'deprecated': False}, 'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False}, 'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
'threeparttable': {'id': 'threeparttable', 'deprecated': False}, 'threeparttable': {'id': 'threeparttable', 'deprecated': False},
'tmate': {'id': 'TMate', 'deprecated': False}, 'tmate': {'id': 'TMate', 'deprecated': False},
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False}, 'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
'tosl': {'id': 'TOSL', 'deprecated': False}, 'tosl': {'id': 'TOSL', 'deprecated': False},
'tpdl': {'id': 'TPDL', 'deprecated': False}, 'tpdl': {'id': 'TPDL', 'deprecated': False},
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False}, 'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
'ttwl': {'id': 'TTWL', 'deprecated': False}, 'ttwl': {'id': 'TTWL', 'deprecated': False},
'ttyp0': {'id': 'TTYP0', 'deprecated': False}, 'ttyp0': {'id': 'TTYP0', 'deprecated': False},
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', '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}, 'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False}, 'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
'unlicense': {'id': 'Unlicense', '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}, 'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
'urt-rle': {'id': 'URT-RLE', 'deprecated': False}, 'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
'vim': {'id': 'Vim', 'deprecated': False}, 'vim': {'id': 'Vim', 'deprecated': False},
@@ -686,7 +654,6 @@ LICENSES: dict[str, SPDXLicense] = {
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False}, 'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
'wsuipa': {'id': 'Wsuipa', 'deprecated': False}, 'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
'wtfpl': {'id': 'WTFPL', 'deprecated': False}, 'wtfpl': {'id': 'WTFPL', 'deprecated': False},
'wwl': {'id': 'wwl', 'deprecated': False},
'wxwindows': {'id': 'wxWindows', 'deprecated': True}, 'wxwindows': {'id': 'wxWindows', 'deprecated': True},
'x11': {'id': 'X11', 'deprecated': False}, 'x11': {'id': 'X11', 'deprecated': False},
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', '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-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False}, 'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
'bootloader-exception': {'id': 'Bootloader-exception', '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}, 'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
'clisp-exception-2.0': {'id': 'CLISP-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}, '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}, 'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False}, 'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', '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}, 'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False}, 'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
'gnu-javamail-exception': {'id': 'gnu-javamail-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-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-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-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}, 'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False}, 'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', '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}, '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}, 'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-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}, 'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
@@ -768,14 +730,12 @@ EXCEPTIONS: dict[str, SPDXException] = {
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False}, 'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False}, 'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
'mif-exception': {'id': 'mif-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}, 'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False}, 'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', '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}, 'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False}, 'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
'pcre2-exception': {'id': 'PCRE2-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}, '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}, '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}, 'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
+55 -185
View File
@@ -8,7 +8,7 @@ import operator
import os import os
import platform import platform
import sys 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 MarkerAtom, MarkerList, Op, Value, Variable
from ._parser import parse_marker as _parse_marker from ._parser import parse_marker as _parse_marker
@@ -17,7 +17,6 @@ from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name from .utils import canonicalize_name
__all__ = [ __all__ = [
"Environment",
"EvaluateContext", "EvaluateContext",
"InvalidMarker", "InvalidMarker",
"Marker", "Marker",
@@ -26,58 +25,31 @@ __all__ = [
"default_environment", "default_environment",
] ]
def __dir__() -> list[str]:
return __all__
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool] Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
EvaluateContext = Literal["metadata", "lock_file", "requirement"] 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_ALLOWING_SET = {"extras", "dependency_groups"}
MARKERS_REQUIRING_VERSION = {
"implementation_version",
"platform_release",
"python_full_version",
"python_version",
}
class InvalidMarker(ValueError): class InvalidMarker(ValueError):
"""Raised when attempting to create a :class:`Marker` from invalid input. """
An invalid marker was found, users should refer to PEP 508.
This error indicates that the given marker string does not conform to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
""" """
class UndefinedComparison(ValueError): class UndefinedComparison(ValueError):
"""Raised when evaluating an unsupported marker comparison. """
An invalid operation was attempted on a value that doesn't support it.
This can happen when marker values are compared as versions but do not
conform to the :ref:`specification of version specifiers
<pypug:version-specifiers>`.
""" """
class UndefinedEnvironmentName(ValueError): 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): class Environment(TypedDict):
"""
A dictionary that represents a Python environment as captured by
:func:`default_environment`. All fields are required.
"""
implementation_name: str implementation_name: str
"""The implementation's identifier, e.g. ``'cpython'``.""" """The implementation's identifier, e.g. ``'cpython'``."""
@@ -149,28 +121,20 @@ class Environment(TypedDict):
""" """
def _normalize_extras( def _normalize_extra_values(results: Any) -> Any:
result: MarkerList | MarkerAtom | str,
) -> MarkerList | MarkerAtom | str:
if not isinstance(result, tuple):
return result
lhs, op, rhs = result
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. Normalize extra values.
""" """
if isinstance(results[0], tuple):
return [_normalize_extras(r) for r in results] 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)
results[0] = lhs, op, rhs
return results
def _format_marker( def _format_marker(
@@ -204,26 +168,25 @@ def _format_marker(
_operators: dict[str, Operator] = { _operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs, "in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs, "not in": lambda lhs, rhs: lhs not in rhs,
"<": lambda _lhs, _rhs: False, "<": operator.lt,
"<=": operator.eq, "<=": operator.le,
"==": operator.eq, "==": operator.eq,
"!=": operator.ne, "!=": operator.ne,
">=": operator.eq, ">=": operator.ge,
">": lambda _lhs, _rhs: False, ">": operator.gt,
} }
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool: def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
op_str = op.serialize() if isinstance(rhs, str):
if key in MARKERS_REQUIRING_VERSION:
try: try:
spec = Specifier(f"{op_str}{rhs}") spec = Specifier("".join([op.serialize(), rhs]))
except InvalidSpecifier: except InvalidSpecifier:
pass pass
else: else:
return spec.contains(lhs, prereleases=True) return spec.contains(lhs, prereleases=True)
oper: Operator | None = _operators.get(op_str) oper: Operator | None = _operators.get(op.serialize())
if oper is None: if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") 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( def _normalize(
lhs: str, rhs: str | AbstractSet[str], key: str lhs: str, rhs: str | AbstractSet[str], key: str
) -> tuple[str, str | AbstractSet[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/ # https://peps.python.org/pep-0685/
# > When comparing extra names, tools MUST normalize the names being # > When comparing extra names, tools MUST normalize the names being
# > compared using the semantics outlined in PEP 503 for names # > compared using the semantics outlined in PEP 503 for names
if key == "extra": if key == "extra":
assert isinstance(rhs, str), "extra value must be a string" assert isinstance(rhs, str), "extra value must be a string"
# Both sides are normalized at this point already return (canonicalize_name(lhs), canonicalize_name(rhs))
return (lhs, rhs)
if key in MARKERS_ALLOWING_SET: if key in MARKERS_ALLOWING_SET:
if isinstance(rhs, str): # pragma: no cover if isinstance(rhs, str): # pragma: no cover
return (canonicalize_name(lhs), canonicalize_name(rhs)) return (canonicalize_name(lhs), canonicalize_name(rhs))
@@ -257,6 +219,8 @@ def _evaluate_markers(
groups: list[list[bool]] = [[]] groups: list[list[bool]] = [[]]
for marker in markers: for marker in markers:
assert isinstance(marker, (list, tuple, str))
if isinstance(marker, list): if isinstance(marker, list):
groups[-1].append(_evaluate_markers(marker, environment)) groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple): elif isinstance(marker, tuple):
@@ -270,21 +234,18 @@ def _evaluate_markers(
lhs_value = lhs.value lhs_value = lhs.value
environment_key = rhs.value environment_key = rhs.value
rhs_value = environment[environment_key] rhs_value = environment[environment_key]
assert isinstance(lhs_value, str), "lhs must be a string" assert isinstance(lhs_value, str), "lhs must be a string"
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) 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)) groups[-1].append(_eval_op(lhs_value, op, rhs_value))
elif marker == "or": else:
groups.append([]) assert marker in ["and", "or"]
elif marker == "and": if marker == "or":
pass groups.append([])
else: # pragma: nocover
raise TypeError(f"Unexpected marker {marker!r}")
return any(all(item) for item in groups) 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}" version = f"{info.major}.{info.minor}.{info.micro}"
kind = info.releaselevel kind = info.releaselevel
if kind != "final": if kind != "final":
@@ -293,11 +254,7 @@ def _format_full_version(info: sys._version_info) -> str:
def default_environment() -> Environment: def default_environment() -> Environment:
"""Return the default marker environment for the current Python process. iver = format_full_version(sys.implementation.version)
This is the base environment used by :meth:`Marker.evaluate`.
"""
iver = _format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name implementation_name = sys.implementation.name
return { return {
"implementation_name": implementation_name, "implementation_name": implementation_name,
@@ -315,36 +272,10 @@ def default_environment() -> Environment:
class Marker: 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: def __init__(self, marker: str) -> None:
# Note: We create a Marker object without calling this constructor in # Note: We create a Marker object without calling this constructor in
# packaging.requirements.Requirement. If any additional logic is # packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement. # 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: try:
self._markers = _normalize_extra_values(_parse_marker(marker)) self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a recursive type: # The attribute `_markers` can be described in terms of a recursive type:
@@ -366,94 +297,36 @@ class Marker:
except ParserSyntaxError as e: except ParserSyntaxError as e:
raise InvalidMarker(str(e)) from 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: def __str__(self) -> str:
return _format_marker(self._markers) return _format_marker(self._markers)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__.__name__}({str(self)!r})>" return f"<Marker('{self}')>"
def __hash__(self) -> int: 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): if not isinstance(other, Marker):
return NotImplemented return NotImplemented
return str(self) == str(other) 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( def evaluate(
self, self,
environment: Mapping[str, str | AbstractSet[str]] | None = None, environment: dict[str, str] | None = None,
context: EvaluateContext = "metadata", context: EvaluateContext = "metadata",
) -> bool: ) -> bool:
"""Evaluate a marker. """Evaluate a marker.
Return the boolean from evaluating this marker against the environment. Return the boolean from evaluating the given marker against the
The environment is determined from the current Python process unless environment. environment is an optional argument to override all or
passed in explicitly. part of the determined environment. The *context* parameter specifies what
context the markers are being evaluated for, which influences what markers
:param environment: Mapping containing keys and values to override the are considered valid. Acceptable values are "metadata" (for core metadata;
detected environment. default), "lock_file", and "requirement" (i.e. all other situations).
: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``.
The environment is determined from the current Python process.
""" """
current_environment = cast( current_environment = cast(
"dict[str, str | AbstractSet[str]]", default_environment() "dict[str, str | AbstractSet[str]]", default_environment()
@@ -464,15 +337,12 @@ class Marker:
) )
elif context == "metadata": elif context == "metadata":
current_environment["extra"] = "" current_environment["extra"] = ""
if environment is not None: if environment is not None:
current_environment.update(environment) current_environment.update(environment)
if "extra" in current_environment: # The API used to allow setting extra to None. We need to handle this
# The API used to allow setting extra to None. We need to handle # case for backwards compatibility.
# this case for backwards compatibility. Also skip running if "extra" in current_environment and current_environment["extra"] is None:
# normalize name if extra is empty. current_environment["extra"] = ""
extra = cast("str | None", current_environment["extra"])
current_environment["extra"] = canonicalize_name(extra) if extra else ""
return _evaluate_markers( return _evaluate_markers(
self._markers, _repair_python_full_version(current_environment) 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 Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds. 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("+"): if python_full_version.endswith("+"):
env["python_full_version"] = f"{python_full_version}local" env["python_full_version"] = f"{python_full_version}local"
return env return env

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