Compare commits

...
73 Commits
Author SHA1 Message Date
Christoph Brandau 33cf282b0a feat(debug): enable Python debug workflow and debug server integration
Adds an end-to-end Python debug workflow for the extension.
Includes a new prepare-debug.ps1 script and a VS Code task.
Extends the Python server and extension to coordinate a debug session and safe startup.

- Add prepare-debug.ps1 and a VS Code task to build the debug bundle
- Enable Python debug wiring in the server and tests
- Ensure a single stable Python debug session during startup
2026-08-17 11:02:22 +02:00
Christoph Brandau 35a4357551 feat(navigation): add symbol index and LSP navigation features
The changes introduce a Tcl symbol index powering LSP navigation
features across the workspace. A navigation API exposes
snapshots and update hooks, enabling goto-definition,
references, and rename using the index. Background indexing
now watches Tcl files and rebuilds the index to stay in sync.

- Add Tcl symbol index and navigation snapshot API
- Wire go-to-definition, references, and rename using the index
- Watch Tcl files and refresh the index in the background
2026-08-17 09:24:45 +02:00
Christoph Brandau f5bd79f067 feat(lsp): add incremental indexing and file ops support
Adds a thread-safe incremental index and snapshot API for LSP.
Introduces cache invalidation and file operation hooks for delete
and rename. This keeps indices in sync with disk changes.
Supports reindexing TCL files from disk when needed.

- Adds workspace file change handlers to sync indices on delete/rename.
- Introduces locking and snapshot helpers to safely access shared state.
- Refactors to invalidate caches on edits and reindex TCL files.
2026-08-17 08:45:09 +02:00
Christoph Brandau a39aee1b9d feat(server): add TCL signature help support
Adds a signature help system for TCL commands.
The LSP server now exposes signature help for custom and
built-in MOM procedures, enabling parameter hints while editing.
Tests and documentation were added to cover common usage.

- Adds signature_help module to parse and present signatures
- Integrates with LSP server to provide signature help on the client
- Adds tests for custom and built-in procedures
2026-08-17 08:31:15 +02:00
Christoph 4ca1b0cce9 Update version to 2026.6.201 2026-06-22 06:50:56 +00:00
Christoph 6051260a8d Merge pull request 'Fix single-line brace formatting to preserve internal whitespace' (#31) from bug_fixing into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #31
2026-06-22 06:50:16 +00:00
Christoph Brandau dfe15dd3db Fix single-line brace formatting to preserve internal whitespace
The formatter now calculates and passes explicit leading and trailing whitespace around expression content to the `_brace` method. This ensures correct rendering of single-line expressions that include braces.
2026-06-22 08:49:22 +02:00
Christoph 5d1c402d2a Update version to 2026.6.200 2026-06-22 06:34:00 +00:00
Christoph 88f834ae79 Merge pull request 'Fix Bug for Formatting' (#30) from bug_fixing into main
/ build_and_publish (release) Successful in 37s
Reviewed-on: #30
2026-06-22 06:32:26 +00:00
Christoph Brandau 3b233389c8 Fix Tcl formatting bug by adjusting formatter options 2026-06-22 08:29:48 +02:00
Christoph 67bd9de18a Update version to 2026.6.100 2026-06-18 19:03:03 +00:00
Christoph 495b7b7a07 Update package.json
/ build_and_publish (release) Failing after 14m37s
2026-06-18 19:01:47 +00:00
Christoph 8b39c4d250 Update version to 2026.6.100 2026-06-18 18:41:28 +00:00
Christoph 49e8cdd7df Merge pull request 'Bug fixing' (#29) from bug_fixing into main
/ build_and_publish (release) Has been cancelled
Reviewed-on: #29
2026-06-18 18:39:23 +00:00
Christoph Brandau f60c4563e4 Delegate TCL folding to server, fix bugs, and enhance stability
- Move TCL folding range computation from client to language server, avoiding duplicate regions.
- Serialize language server restarts to prevent multiple server instances running concurrently.
- Pin server-side Python dependencies to specific minor versions for improved stability and predictability.
- Make debug server connection non-fatal, allowing the language server to start even if the debugger isn't attached.
- Optimize semantic token generation by caching Tclint plugin commands.
2026-06-18 20:38:47 +02:00
Christoph Brandau 3ad3847045 add new features and fix bugs 2026-06-18 17:31:51 +02:00
Christoph Brandau 41f331d4c4 add updates libs 2026-06-18 17:21:58 +02:00
Christoph 91ce762570 Update version to 2025.9.200 2025-09-01 13:24:13 +00:00
Christoph d60d3ec942 Update package.json
/ build_and_publish (release) Successful in 40s
2025-09-01 13:22:58 +00:00
Christoph e52722e353 Update README.md 2025-09-01 13:21:36 +00:00
Christoph a8bf55882e Merge pull request 'feat: use uv instead of pip' (#28) from change_to_uv_ into main
Reviewed-on: #28
2025-08-16 20:23:34 +00:00
Christoph 377bddf579 feat: use uv instead of pip 2025-08-16 22:22:28 +02:00
Christoph cdc1cb4805 Update version to 2025.9.100 2025-08-16 16:35:58 +00:00
Christoph 34430097d8 Update README.md
/ build_and_publish (release) Failing after 25s
2025-08-16 09:43:44 +00:00
Christoph dcf447ed4f Merge branch 'relicense-agpl3' 2025-08-16 11:33:32 +02:00
Christoph 783a9aa7d7 Relicense to AGPL-3.0-or-later 2025-08-16 11:29:09 +02:00
Christoph 63638d6b3b Update LICENSE 2025-08-16 09:16:37 +00:00
Christoph Brandau af8cd58fda Merge branch 'main' of https://git.cbsk-tech.de/Christoph/nx_post_support 2025-08-15 13:13:15 +02:00
Christoph Brandau df0f3d7a61 add description 2025-08-15 13:12:41 +02:00
Christoph 72906a1930 Update version to 2025.8.400 2025-08-15 07:34:53 +00:00
Christoph 068cf2d1c3 Update README.md
/ build_and_publish (release) Successful in 42s
2025-08-15 07:33:26 +00:00
Christoph ad18a3dcf3 Update README.md 2025-08-15 07:32:58 +00:00
Christoph Brandau bd4b17825c add: Outline support for DEF and CDL files 2025-08-15 07:29:01 +02:00
Christoph Brandau 5d6182e91f double value MOM_add_to_block_buffer rename to MOM_add_to_line_buffer 2025-08-15 07:18:38 +02:00
Christoph Brandau 63e9c429bd update ReadMe 2025-08-14 07:57:40 +02:00
Christoph 7fdafe484e Update version to 2025.8.300 2025-08-12 11:26:06 +00:00
Christoph Brandau 5cba6763f1 Merge branch 'new_features'
/ build_and_publish (release) Successful in 36s
2025-08-12 13:21:27 +02:00
Christoph Brandau e20bb85ac6 fix: wrong highligthing 2025-08-12 13:18:08 +02:00
Christoph 98a0b2197d Update version to 2025.8.202 2025-08-12 09:38:34 +00:00
Christoph Brandau 579847b56d fix formatter
/ build_and_publish (release) Successful in 33s
2025-08-12 11:35:54 +02:00
Christoph Brandau 618d6b478b fix: formatter when \ was deleting 2025-08-12 11:28:10 +02:00
Christoph Brandau 480978d6bb Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features
/ build_and_publish (release) Failing after 31s
2025-08-12 11:27:20 +02:00
Christoph Brandau 04798328bd fix: formatter 2025-08-12 11:27:14 +02:00
Christoph Brandau ae835089b6 fix: formatter when \ was deleting 2025-08-12 11:26:52 +02:00
Christoph 0ed1e887fc Update version to 2025.9.100 2025-08-12 07:21:23 +00:00
Christoph Brandau 12b7b62aa5 add variable indexing keep track of postion
/ build_and_publish (release) Successful in 39s
2025-08-12 09:07:04 +02:00
Christoph Brandau 7263b6f530 optimze Server 2025-08-12 07:48:05 +02:00
Christoph Brandau c0635e48ea Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features 2025-08-12 07:15:21 +02:00
Christoph Brandau 8fb98d2f40 refacxtor the code 2025-08-12 07:15:18 +02:00
Christoph f24b280edf mod: Keywords 2025-08-11 22:06:49 +02:00
Christoph b61cff3b3b Update version to 2025.9.4 2025-08-11 19:46:20 +00:00
Christoph cf62044d3e Update version to 2025.8.201 2025-08-11 19:37:32 +00:00
Christoph feba88212f fix: some highlight error
/ build_and_publish (release) Successful in 29s
2025-08-11 21:36:04 +02:00
Christoph aeebb5c964 fix: some highlight error
/ build_and_publish (release) Successful in 38s
2025-08-11 21:35:14 +02:00
Christoph b141818251 Update version to 2025.9.3 2025-08-11 16:41:00 +00:00
Christoph Brandau 72f894b05d add goto
/ build_and_publish (release) Successful in 31s
2025-08-11 18:40:06 +02:00
Christoph Brandau 0a26cb5c0f add outline to lsp 2025-08-11 18:28:23 +02:00
Christoph b3cac5c292 Update version to 2025.9.2 2025-08-11 15:20:46 +00:00
Christoph Brandau ef558c3aa3 update pipline
/ build_and_publish (release) Successful in 28s
2025-08-11 17:19:47 +02:00
Christoph Brandau 9d5ef23162 update pipline
/ build_and_publish (release) Failing after 26s
2025-08-11 17:16:27 +02:00
Christoph f659db1147 Update package.json 2025-08-11 15:12:55 +00:00
Christoph Brandau c25bf79427 update pipline
/ build_and_publish (release) Failing after 32s
2025-08-11 17:00:14 +02:00
Christoph b059a75205 Update version to 2025.9.001 2025-08-11 14:24:17 +00:00
Christoph a90e15514b Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (release) Failing after 31s
2025-08-11 14:23:08 +00:00
Christoph Brandau 2c2579319d update hover feature
/ build_and_publish (release) Failing after 27s
2025-08-11 16:20:14 +02:00
Christoph Brandau b38fab1428 Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features 2025-08-11 16:05:58 +02:00
Christoph Brandau bc745622b9 add first proc doc 2025-08-11 15:59:09 +02:00
Christoph Brandau 0bb4ef731e add first proc doc 2025-08-11 10:15:58 +02:00
Christoph 27e2b3bf6e Merge pull request 'update' (#25) from tcl_language_support into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #25
2025-08-09 21:35:09 +00:00
Christoph f55c3ba904 Update version to 2025.8.200 2025-08-09 12:09:07 +00:00
Christoph cb430a5553 Update version to 2025.8.2
/ build_and_publish (release) Successful in 33s
2025-08-09 12:06:50 +00:00
Christoph 637a28ad82 Update version to 2025.08.1
/ build_and_publish (release) Successful in 43s
2025-08-09 11:57:08 +00:00
Christoph Brandau 31ccae9a40 update 2025-08-08 09:18:22 +02:00
196 changed files with 13649 additions and 3350 deletions
+23 -3
View File
@@ -5,8 +5,11 @@ jobs:
build_and_publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- name: Checkout branch from release
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.target_commitish }}
fetch-depth: 0
- name: GitHub Tag Name example
run: |
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
@@ -32,8 +35,25 @@ jobs:
git config user.name "${{ vars.USERNAME_GIT }}"
git config user.email "${{ vars.EMAIL_GIT }}"
git add package.json
git commit -m "Update version to ${{ github.ref_name }}"
git push origin HEAD:main
git commit -m "Update version to ${{ github.ref_name }}" || echo "No changes to commit."
# Get current branch; if detached, resolve to a remote branch that contains this commit
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then
# If target_commitish is a branch, use it; if its a SHA, find a branch that contains it
CANDIDATE='${{ github.event.release.target_commitish }}'
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
else
BRANCH="$CANDIDATE"
fi
fi
if [[ -z "$BRANCH" ]]; then
echo "Could not determine branch for push."; git branch -a; exit 1
fi
git push origin HEAD:refs/heads/$BRANCH
- name: Publish to Visual Studio Marketplace (Pre-Release)
if: ${{ github.event.release.prerelease }}
run: vsce publish --pre-release
+1
View File
@@ -9,3 +9,4 @@ __pycache__
.nox
*.g4
.antlr
.claude
+35 -12
View File
@@ -10,13 +10,21 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/client/**/*.js"],
"args": [
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
"!**/node_modules/**"
],
"skipFiles": ["<node_internals>/**"],
"autoAttachChildProcesses": true,
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
"preLaunchTask": "NX Post Support: Compile Debug"
},
{
"name": "Python Attach",
@@ -34,10 +42,24 @@
"name": "Debug Extension (hidden)",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/client/**/*.js"],
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
"!**/node_modules/**"
],
"skipFiles": ["<node_internals>/**"],
"env": {
"USE_DEBUGPY": "True"
"USE_DEBUGPY": "True",
"NXPS_DEBUG_HOST": "127.0.0.1",
"NXPS_DEBUG_PORT": "5678"
},
"presentation": {
"hidden": true,
@@ -49,8 +71,9 @@
"name": "Python debug server (hidden)",
"type": "debugpy",
"request": "attach",
"listen": { "host": "localhost", "port": 5678 },
"justMyCode": true,
"listen": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": false,
"logToFile": true,
"presentation": {
"hidden": true,
"group": "",
@@ -63,7 +86,7 @@
"name": "Debug Extension and Python",
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
"stopAll": true,
"preLaunchTask": "npm: watch",
"preLaunchTask": "NX Post Support: Compile Debug",
"presentation": {
"hidden": false,
"group": "",
+57
View File
@@ -0,0 +1,57 @@
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
@@ -0,0 +1,26 @@
{
"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
}
}
]
}
+14
View File
@@ -1,3 +1,9 @@
## Unreleased
- 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
## [0.0.1]
- Initial release
@@ -13,3 +19,11 @@
## [0.2.0]
- Add DEF File Support
## [2026.6.100]
- Fix several bugs
## [2026.6.200]
- Fix foramtting bug
+657 -17
View File
@@ -1,21 +1,661 @@
MIT License
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (c) 2025 Christoph
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
nx-post-support
Copyright (C) 2025 Christoph
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+31 -5
View File
@@ -1,6 +1,6 @@
# NX Postprocessor Support
A comprehensive VS Code extension providing language support for NX CAD postprocessor development, including CDL (Cutter Definition Language), TCL, and DEF files.
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
## Features
@@ -9,18 +9,20 @@ A comprehensive VS Code extension providing language support for NX CAD postproc
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
- **Auto-completion** - Context-aware code completion for faster development
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
## Supported File Types
- `.cdl` - NX Cutter Definition Language files
- `.cdl` - NX User Defined Event File
- `.tcl` - NX TCL postprocessor files
- `.def` - NX Definition files
## Installation
1. Install from the VS Code Marketplace
2. Open any `.cdl`, `.tcl`, or `.def` file
3. The extension will automatically activate and provide language support
2. Install Python 3.11 or higher
3. Open any `.cdl`, `.tcl`, or `.def` file
4. The extension will automatically activate and provide language support
## Configuration
@@ -39,6 +41,30 @@ Simply open any supported file type and enjoy:
- Code completion
- Code formatting (Format Document command)
- Hover information
- Signature help while entering procedure arguments
## Development and debugging
Install the root and client dependencies before the first debug session:
```powershell
npm install
npm install --prefix client
```
Use one of the checked-in VS Code launch configurations:
- **Run Extension** debugs the TypeScript extension host.
- **Debug Extension and Python** debugs both the TypeScript extension and the
Python language server. This is the recommended configuration for LSP work.
- **Python Attach** attaches manually to an already running Python process.
The launch configuration creates a fresh non-minified bundle with embedded
source maps and opens `test/test.tcl` so the extension activates immediately.
For combined debugging, the Python adapter listens on `127.0.0.1:5678`; the
language server waits for that adapter before initialization. The NX
Postprocessor Support output channel reports `Python debug mode: enabled` and
shows `_debug_server.py` in the server command when the debug path is active.
## Contributing
@@ -46,4 +72,4 @@ This extension is actively maintained. For issues or feature requests, please vi
## License
MIT License - see LICENSE file for details.
AGPL-3.0 License - see LICENSE file for details.
+22
View File
@@ -14,6 +14,7 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@types/vscode": "^1.96.0"
},
@@ -21,6 +22,27 @@
"vscode": "^1.96.0"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
+1
View File
@@ -12,6 +12,7 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@types/vscode": "^1.96.0"
}
+273
View File
@@ -137,6 +137,102 @@ export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.
return undefined
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export function cdlEventAtPosition(
document: vscode.TextDocument,
position: vscode.Position
): string | undefined {
const line = document.lineAt(position.line).text
const match = /^\s*EVENT\s+([^\s{]+)/.exec(line)
if (!match) {
return undefined
}
const declarationStart = match.index
const eventEnd = line.indexOf(match[1], match.index) + match[1].length
if (position.character < declarationStart || position.character > eventEnd) {
return undefined
}
return match[1]
}
export async function definitionCdlEventHandler(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Location[] | undefined> {
const eventName = cdlEventAtPosition(document, position)
if (!eventName) {
return undefined
}
const handlerName = `MOM_${eventName}`
try {
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
"vscode.executeWorkspaceSymbolProvider",
handlerName
)
const indexedLocations = (symbols || [])
.filter(
(symbol) =>
symbol.kind === vscode.SymbolKind.Function &&
(symbol.name === handlerName ||
symbol.name.endsWith(`::${handlerName}`))
)
.map((symbol) => symbol.location)
if (indexedLocations.length > 0) {
return indexedLocations
}
} catch {
// The Tcl language server may still be starting; use the file fallback below.
}
const declaration = new RegExp(
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
)
const tclFiles = await vscode.workspace.findFiles(
"**/*.tcl",
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
)
const locations: vscode.Location[] = []
for (const uri of tclFiles) {
if (token.isCancellationRequested) {
return undefined
}
let tclDocument: vscode.TextDocument
try {
tclDocument = await vscode.workspace.openTextDocument(uri)
} catch {
continue
}
for (let lineNumber = 0; lineNumber < tclDocument.lineCount; lineNumber++) {
const line = tclDocument.lineAt(lineNumber).text
const match = declaration.exec(line)
if (!match) {
continue
}
const start = line.indexOf(handlerName, match.index)
locations.push(
new vscode.Location(
uri,
new vscode.Range(
lineNumber,
start,
lineNumber,
start + handlerName.length
)
)
)
}
}
return locations.length > 0 ? locations : undefined
}
export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const symbols: vscode.DocumentSymbol[] = []
const lines = document.getText().split("\n")
@@ -271,3 +367,180 @@ export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode
return collectSymbols(rootScope)
}
export function cdlDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const lines = document.getText().split("\n")
interface Scope {
symbol: vscode.DocumentSymbol
startLine: number
}
const root = new vscode.DocumentSymbol(
"root",
"",
vscode.SymbolKind.Namespace,
new vscode.Range(0, 0, lines.length, 0),
new vscode.Range(0, 0, 0, 0)
)
root.children = []
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()
const createSymbol = () => {
if (!pending) {
return
}
const startRange = new vscode.Range(
pending.startLine,
0,
pending.startLine,
lines[pending.startLine].length
)
const symbol = new vscode.DocumentSymbol(
pending.name,
"",
pending.kind,
startRange,
startRange
)
symbol.children = []
stack[stack.length - 1].symbol.children?.push(symbol)
stack.push({ symbol, startLine: pending.startLine })
pending = null
}
if (pending && trimmed.includes("{")) {
createSymbol()
}
if (trimmed.startsWith("}")) {
const scope = stack.pop()
if (scope) {
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
}
continue
}
let match
if ((match = /^\s*EVENT\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Event,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
} else if ((match = /^\s*PARAM\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Field,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
}
}
return root.children || []
}
export function defDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const lines = document.getText().split("\n")
interface Scope {
symbol: vscode.DocumentSymbol
startLine: number
}
const root = new vscode.DocumentSymbol(
"root",
"",
vscode.SymbolKind.Namespace,
new vscode.Range(0, 0, lines.length, 0),
new vscode.Range(0, 0, 0, 0)
)
root.children = []
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()
const createSymbol = () => {
if (!pending) {
return
}
const startRange = new vscode.Range(
pending.startLine,
0,
pending.startLine,
lines[pending.startLine].length
)
const symbol = new vscode.DocumentSymbol(
pending.name,
"",
pending.kind,
startRange,
startRange
)
symbol.children = []
stack[stack.length - 1].symbol.children?.push(symbol)
stack.push({ symbol, startLine: pending.startLine })
pending = null
}
if (pending && trimmed.includes("{")) {
createSymbol()
}
if (trimmed.startsWith("}")) {
const scope = stack.pop()
if (scope) {
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
}
continue
}
let match
if ((match = /^\s*FORMATTING\b/.exec(line))) {
pending = {
name: "FORMATTING",
kind: vscode.SymbolKind.Module,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
} else if ((match = /^\s*ADDRESS\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Field,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
} else if ((match = /^\s*BLOCK_TEMPLATE\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Function,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
}
}
return root.children || []
}
+66 -34
View File
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
import * as fsapi from "fs-extra"
import { Disposable, env, LogOutputChannel } from "vscode"
import { Disposable, env, LogOutputChannel, workspace } from "vscode"
import { State } from "vscode-languageclient"
import {
LanguageClient,
@@ -24,6 +24,13 @@ import { isVirtualWorkspace } from "./vscodeapi"
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
let _disposables: Disposable[] = []
export function disposeServerResources(): void {
_disposables.forEach((disposable) => disposable.dispose())
_disposables = []
}
async function createServer(
settings: ISettings,
serverId: string,
@@ -32,13 +39,26 @@ async function createServer(
initializationOptions: IInitOptions
): Promise<LanguageClient> {
const command = settings.interpreter[0]
if (!command) {
throw new Error("No Python interpreter is configured for the language server.")
}
const cwd = settings.cwd
// Set debugger path needed for debugging python code.
const newEnv = { ...process.env }
const debuggerPath = await getDebuggerPath()
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
if (newEnv.USE_DEBUGPY && debuggerPath) {
const debugRequested = newEnv.USE_DEBUGPY?.toLowerCase() === "true"
if (debugRequested && !isDebugScript) {
throw new Error(`Python debug bootstrap not found: ${DEBUG_SERVER_SCRIPT_PATH}`)
}
const debuggerPath = debugRequested ? await getDebuggerPath() : undefined
if (debugRequested && !debuggerPath) {
throw new Error(
"Python debugging was requested, but the Python Debugger extension did not provide debugpy."
)
}
if (debugRequested && debuggerPath) {
newEnv.DEBUGPY_PATH = debuggerPath
} else {
newEnv.USE_DEBUGPY = "False"
@@ -50,10 +70,13 @@ async function createServer(
// Set notification type
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
const args =
newEnv.USE_DEBUGPY === "False" || !isDebugScript
? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH])
: settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH])
const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH
const interpreterArgs = settings.interpreter.slice(1)
if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) {
interpreterArgs.push("-Xfrozen_modules=off")
}
const args = interpreterArgs.concat([serverScript])
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
const serverOptions: ServerOptions = {
@@ -63,6 +86,7 @@ async function createServer(
}
// Options to control the language client
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
const clientOptions: LanguageClientOptions = {
// Register the server for python documents
documentSelector: isVirtualWorkspace()
@@ -76,13 +100,16 @@ async function createServer(
outputChannel: outputChannel,
traceOutputChannel: outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never,
synchronize: {
fileEvents: tclFileWatcher
},
initializationOptions
}
_disposables.push(tclFileWatcher)
return new LanguageClient(serverId, serverName, serverOptions, clientOptions)
}
let _disposables: Disposable[] = []
export async function restartServer(
serverId: string,
serverName: string,
@@ -92,40 +119,45 @@ export async function restartServer(
if (lsClient) {
traceInfo(`Server: Stop requested`)
await lsClient.stop()
_disposables.forEach((d) => d.dispose())
_disposables = []
disposeServerResources()
}
const projectRoot = await getProjectRoot()
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, {
settings: await getExtensionSettings(serverId, true),
globalSettings: await getGlobalSettings(serverId, false)
})
traceInfo(`Server: Start requested.`)
_disposables.push(
newLSClient.onDidChangeState((e) => {
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
}
})
)
try {
const newLSClient = await createServer(
workspaceSetting,
serverId,
serverName,
outputChannel,
{
settings: await getExtensionSettings(serverId, true),
globalSettings: await getGlobalSettings(serverId, false)
}
)
traceInfo(`Server: Start requested.`)
_disposables.push(
newLSClient.onDidChangeState((e) => {
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()
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
} catch (ex) {
traceError(`Server: Start failed: ${ex}`)
disposeServerResources()
return undefined
}
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
}
+6 -3
View File
@@ -79,7 +79,8 @@ export async function getWorkspaceSettings(
interpreter: resolveVariables(interpreter, workspace),
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
showNotifications: config.get<string>(`showNotifications`) ?? "off",
formatter: config.get<boolean>(`formatter`) ?? true
formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true
}
return workspaceSetting
}
@@ -111,7 +112,8 @@ export async function getGlobalSettings(
interpreter: interpreter,
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
formatter: config.get<boolean>(`formatter`) ?? true
formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true
}
return setting
}
@@ -126,7 +128,8 @@ export function checkIfConfigurationChanged(
`${namespace}.interpreter`,
`${namespace}.importStrategy`,
`${namespace}.showNotifications`,
`${namespace}.formatter`
`${namespace}.formatter`,
`${namespace}.inlayHint`
]
const changed = settings.map((s) => e.affectsConfiguration(s))
return changed.includes(true)
+88 -30
View File
@@ -12,7 +12,9 @@ import {
formatDefFile,
isFirstLineMachine,
diagnosticHandler,
tclDocumentSymbolProvider
cdlDocumentSymbolProvider,
defDocumentSymbolProvider,
definitionCdlEventHandler
} from "./common/handlers"
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
import {
@@ -22,13 +24,13 @@ import {
onDidChangePythonInterpreter,
resolveInterpreter
} from "./common/python"
import { restartServer } from "./common/server"
import { disposeServerResources, restartServer } from "./common/server"
import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings"
import { loadServerDefaults } from "./common/setup"
import { getLSClientTraceLevel } from "./common/utilities"
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
let client: LanguageClient
let client: LanguageClient | undefined
export async function activate(context: vscode.ExtensionContext) {
// This is required to get server name and module. This should be
@@ -36,6 +38,7 @@ export async function activate(context: vscode.ExtensionContext) {
const serverInfo = loadServerDefaults()
const serverName = serverInfo.name
const serverId = serverInfo.module
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
// Setup logging
const outputChannel = createOutputChannel(serverName)
@@ -60,7 +63,7 @@ export async function activate(context: vscode.ExtensionContext) {
traceLog(`Module: ${serverInfo.module}`)
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
const runServer = async () => {
const runServerImpl = async () => {
const interpreter = getInterpreterFromSetting(serverId)
if (interpreter && interpreter.length > 0) {
if (checkVersion(await resolveInterpreter(interpreter))) {
@@ -89,10 +92,25 @@ 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(
onDidChangePythonInterpreter(async () => {
await runServer()
}),
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
if (checkIfConfigurationChanged(e, serverId)) {
await runServer()
@@ -104,6 +122,15 @@ export async function activate(context: vscode.ExtensionContext) {
)
setImmediate(async () => {
if (pythonDebugMode) {
// A debugpy listen session is attached to exactly one process. Do not
// subscribe to interpreter changes during startup, as the Python
// extension can emit a duplicate event and restart that process.
traceLog("Python debug mode: starting one stable server session")
await runServer()
return
}
const interpreter = getInterpreterFromSetting(serverId)
if (interpreter === undefined || interpreter.length === 0) {
traceLog(`Python extension loading`)
@@ -114,6 +141,10 @@ export async function activate(context: vscode.ExtensionContext) {
}
})
// Folding ranges for TCL are provided by the language server
// (folding_range_provider in lsp_server.py). No client-side provider here to
// avoid duplicate folding regions.
//
const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "cdl" },
@@ -162,6 +193,16 @@ export async function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(hoverCdlProvider)
const definitionCdlEventProvider = vscode.languages.registerDefinitionProvider(
{ scheme: "file", language: "cdl" },
{
provideDefinition(document, position, token) {
return definitionCdlEventHandler(document, position, token)
}
}
)
context.subscriptions.push(definitionCdlEventProvider)
const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "def" },
{
@@ -179,11 +220,26 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(formatDefProvider)
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
{ scheme: "file", language: "tcl" },
{ provideDocumentSymbols: tclDocumentSymbolProvider }
const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
{ scheme: "file", language: "cdl" },
{
provideDocumentSymbols(document: vscode.TextDocument) {
return cdlDocumentSymbolProvider(document)
}
}
)
context.subscriptions.push(tclOutlineProvider)
context.subscriptions.push(cdlSymbolProvider)
// Outline now provided by the language server (Document Symbols). Removing TS provider.
const defSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
{ scheme: "file", language: "def" },
{
provideDocumentSymbols(document: vscode.TextDocument) {
return defDocumentSymbolProvider(document)
}
}
)
context.subscriptions.push(defSymbolProvider)
// Diagnostics collection
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
@@ -191,31 +247,33 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
// Check if the first line of the CDL file contains "MACHINE"
vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl" || document.languageId === "def") {
if (document.languageId === "cdl") {
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
} else if (document.languageId === "def") {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl" || document.languageId === "def") {
if (document.languageId === "cdl") {
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
} else if (document.languageId === "def") {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
}
}
}
})
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))
}),
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 {
if (!client) {
disposeServerResources()
return undefined
}
return client.stop()
return client.stop().finally(disposeServerResources)
}
+4 -2
View File
@@ -1,19 +1,21 @@
const esbuild = require("esbuild")
const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
async function main() {
const ctx = await esbuild.context({
absWorkingDir: __dirname,
entryPoints: ["client/src/extension.ts"],
bundle: true,
format: "cjs",
minify: production,
sourcemap: !production,
sourcesContent: false,
sourcesContent: !production,
platform: "node",
// outdir: "out",
outfile: "./dist/extension.js",
outfile: path.join(__dirname, "dist", "extension.js"),
external: ["vscode"],
logLevel: "silent",
plugins: [
+33 -26
View File
@@ -1,28 +1,35 @@
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "#"
},
// symbols used as brackets
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
// symbols that can be used to surround a selection
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "#"
},
"indentationRules": {
// Ignore pure comment lines so they don't terminate folds inside Tcl brace blocks.
"unIndentedLinePattern": "^\\s*#.*$",
// Opening braces on their own line are common in NX Tcl.
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*)$",
"decreaseIndentPattern": "^\\s*[\\}\\]\\)].*$"
},
// symbols used as brackets
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
// symbols that can be used to surround a selection
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "nx-post-support",
"version": "0.3.0",
"version": "2025.9.200",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nx-post-support",
"version": "0.3.0",
"version": "2025.9.200",
"devDependencies": {
"@types/vscode": "^1.96.0",
"@vscode/vsce": "^3.2.1",
+15 -3
View File
@@ -1,10 +1,13 @@
{
"name": "nx-post-support",
"displayName": "NX Postprocessor Support",
"description": "",
"version": "2025.8.101",
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
"version": "2026.6.201",
"publisher": "Christoph",
"icon": "images/nx-1.png",
"extensionDependencies": [
"ms-python.python"
],
"serverInfo": {
"name": "NX Postprocessor Support",
"module": "nx-post-support"
@@ -17,7 +20,10 @@
"keywords": [
"cdl",
"NX CDL",
"UDE"
"UDE",
"Postprocessor",
"NX CAM",
"Siemens NX"
],
"engines": {
"vscode": "^1.96.0"
@@ -85,6 +91,11 @@
"default": false,
"description": "Use the TCL formatter from `NX Postprocessor Support`"
},
"nx-post-support.inlayHint": {
"type": "boolean",
"default": true,
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
},
"nx-post-support.importStrategy": {
"default": "useBundled",
"description": "Defines where `NX Postprocessor Support` is imported from.",
@@ -113,6 +124,7 @@
},
"scripts": {
"compile": "node esbuild.js --production",
"compile:debug": "node esbuild.js",
"watch": "node esbuild.js --watch",
"package": "node esbuild.js --production"
},
+5 -5
View File
@@ -176,7 +176,7 @@ def attrib(
type: None = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -200,7 +200,7 @@ def attrib(
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -223,7 +223,7 @@ def attrib(
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -246,7 +246,7 @@ def attrib(
| tuple[_ConverterType]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -308,7 +308,7 @@ def attrs(
match_args: bool = ...,
unsafe_hash: bool | None = ...,
) -> Callable[[_C], _C]: ...
def fields(cls: type[AttrsInstance]) -> Any: ...
def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ...
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
def validate(inst: AttrsInstance) -> None: ...
def resolve_types(
+8 -3
View File
@@ -10,7 +10,6 @@ from typing import _GenericAlias
PYPY = platform.python_implementation() == "PyPy"
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
@@ -18,10 +17,16 @@ PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
if PY_3_14_PLUS: # pragma: no cover
if PY_3_14_PLUS:
import annotationlib
_get_annotations = annotationlib.get_annotations
# We request forward-ref annotations to not break in the presence of
# forward references.
def _get_annotations(cls):
return annotationlib.get_annotations(
cls, format=annotationlib.Format.FORWARDREF
)
else:
+45 -16
View File
@@ -3,11 +3,28 @@
import copy
from ._compat import PY_3_9_PLUS, get_generic_base
from ._compat import get_generic_base
from ._make import _OBJ_SETATTR, NOTHING, fields
from .exceptions import AttrsAttributeNotFoundError
_ATOMIC_TYPES = frozenset(
{
type(None),
bool,
int,
float,
str,
complex,
bytes,
type(...),
type,
range,
property,
}
)
def asdict(
inst,
recurse=True,
@@ -71,7 +88,10 @@ def asdict(
v = value_serializer(inst, a, v)
if recurse is True:
if has(v.__class__):
value_type = type(v)
if value_type in _ATOMIC_TYPES:
rv[a.name] = v
elif has(value_type):
rv[a.name] = asdict(
v,
recurse=True,
@@ -80,8 +100,8 @@ def asdict(
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain_collection_types is True else list
elif issubclass(value_type, (tuple, list, set, frozenset)):
cf = value_type if retain_collection_types is True else list
items = [
_asdict_anything(
i,
@@ -101,7 +121,7 @@ def asdict(
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv[a.name] = cf(*items)
elif isinstance(v, dict):
elif issubclass(value_type, dict):
df = dict_factory
rv[a.name] = df(
(
@@ -142,7 +162,12 @@ def _asdict_anything(
"""
``asdict`` only works on attrs instances, this works on anything.
"""
if getattr(val.__class__, "__attrs_attrs__", None) is not None:
val_type = type(val)
if val_type in _ATOMIC_TYPES:
rv = val
if value_serializer is not None:
rv = value_serializer(None, None, rv)
elif getattr(val_type, "__attrs_attrs__", None) is not None:
# Attrs class.
rv = asdict(
val,
@@ -152,7 +177,7 @@ def _asdict_anything(
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
elif isinstance(val, (tuple, list, set, frozenset)):
elif issubclass(val_type, (tuple, list, set, frozenset)):
if retain_collection_types is True:
cf = val.__class__
elif is_key:
@@ -173,7 +198,7 @@ def _asdict_anything(
for i in val
]
)
elif isinstance(val, dict):
elif issubclass(val_type, dict):
df = dict_factory
rv = df(
(
@@ -253,8 +278,11 @@ def astuple(
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
value_type = type(v)
if recurse is True:
if has(v.__class__):
if value_type in _ATOMIC_TYPES:
rv.append(v)
elif has(value_type):
rv.append(
astuple(
v,
@@ -264,7 +292,7 @@ def astuple(
retain_collection_types=retain,
)
)
elif isinstance(v, (tuple, list, set, frozenset)):
elif issubclass(value_type, (tuple, list, set, frozenset)):
cf = v.__class__ if retain is True else list
items = [
(
@@ -288,8 +316,8 @@ def astuple(
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv.append(cf(*items))
elif isinstance(v, dict):
df = v.__class__ if retain is True else dict
elif issubclass(value_type, dict):
df = value_type if retain is True else dict
rv.append(
df(
(
@@ -450,10 +478,11 @@ def resolve_types(
if getattr(cls, "__attrs_types_resolved__", None) != cls:
import typing
kwargs = {"globalns": globalns, "localns": localns}
if PY_3_9_PLUS:
kwargs["include_extras"] = include_extras
kwargs = {
"globalns": globalns,
"localns": localns,
"include_extras": include_extras,
}
hints = typing.get_type_hints(cls, **kwargs)
for field in fields(cls) if attribs is None else attribs:
+396 -113
View File
@@ -12,6 +12,7 @@ import linecache
import sys
import types
import unicodedata
import weakref
from collections.abc import Callable, Mapping
from functools import cached_property
@@ -113,7 +114,7 @@ def attrib(
type=None,
converter=None,
factory=None,
kw_only=False,
kw_only=None,
eq=None,
order=None,
on_setattr=None,
@@ -156,6 +157,9 @@ def attrib(
*eq*, *order*, and *cmp* also accept a custom callable
.. versionchanged:: 21.1.0 *cmp* undeprecated
.. versionadded:: 22.2.0 *alias*
.. versionchanged:: 25.4.0
*kw_only* can now be None, and its default is also changed from False to
None.
"""
eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq, order, True
@@ -373,7 +377,12 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
def _transform_attrs(
cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
cls,
these,
auto_attribs,
kw_only,
collect_by_mro,
field_transformer,
) -> _Attributes:
"""
Transform all `_CountingAttr`s on a class into `Attribute`s.
@@ -428,8 +437,15 @@ def _transform_attrs(
)
fca = Attribute.from_counting_attr
no = ClassProps.KeywordOnly.NO
own_attrs = [
fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list
fca(
attr_name,
ca,
kw_only is not no,
anns.get(attr_name),
)
for attr_name, ca in ca_list
]
if collect_by_mro:
@@ -441,12 +457,21 @@ def _transform_attrs(
cls, {a.name for a in own_attrs}
)
if kw_only:
if kw_only is ClassProps.KeywordOnly.FORCE:
own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
attrs = base_attrs + own_attrs
# Resolve default field alias before executing field_transformer, so that
# the transformer receives fully populated Attribute objects with usable
# alias values.
for a in attrs:
if not a.alias:
# Evolve is very slow, so we hold our nose and do it dirty.
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
if field_transformer is not None:
attrs = tuple(field_transformer(cls, attrs))
@@ -464,13 +489,12 @@ def _transform_attrs(
if had_default is False and a.default is not NOTHING:
had_default = True
# Resolve default field alias after executing field_transformer.
# This allows field_transformer to differentiate between explicit vs
# default aliases and supply their own defaults.
# Resolve default field alias for any new attributes that the
# field_transformer may have added without setting an alias.
for a in attrs:
if not a.alias:
# Evolve is very slow, so we hold our nose and do it dirty.
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
# Create AttrsClass *after* applying the field_transformer since it may
# add or remove attributes!
@@ -553,7 +577,7 @@ def _frozen_delattrs(self, name):
"""
Attached to frozen classes as __delattr__.
"""
if isinstance(self, BaseException) and name in ("__notes__",):
if isinstance(self, BaseException) and name == "__notes__":
BaseException.__delattr__(self, name)
return
@@ -651,38 +675,31 @@ class _ClassBuilder:
self,
cls: type,
these,
slots,
frozen,
weakref_slot,
getstate_setstate,
auto_attribs,
kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_custom_setattr,
field_transformer,
auto_attribs: bool,
props: ClassProps,
has_custom_setattr: bool,
):
attrs, base_attrs, base_map = _transform_attrs(
cls,
these,
auto_attribs,
kw_only,
collect_by_mro,
field_transformer,
props.kw_only,
props.collected_fields_by_mro,
props.field_transformer,
)
self._cls = cls
self._cls_dict = dict(cls.__dict__) if slots else {}
self._cls_dict = dict(cls.__dict__) if props.is_slotted else {}
self._attrs = attrs
self._base_names = {a.name for a in base_attrs}
self._base_attr_map = base_map
self._attr_names = tuple(a.name for a in attrs)
self._slots = slots
self._frozen = frozen
self._weakref_slot = weakref_slot
self._cache_hash = cache_hash
self._slots = props.is_slotted
self._frozen = props.is_frozen
self._weakref_slot = props.has_weakref_slot
self._cache_hash = (
props.hashability is ClassProps.Hashability.HASHABLE_CACHED
)
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
self._pre_init_has_args = False
if self._has_pre_init:
@@ -693,20 +710,21 @@ class _ClassBuilder:
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)
self._is_exc = is_exc
self._on_setattr = on_setattr
self._is_exc = props.is_exception
self._on_setattr = props.on_setattr_hook
self._has_custom_setattr = has_custom_setattr
self._wrote_own_setattr = False
self._cls_dict["__attrs_attrs__"] = self._attrs
self._cls_dict["__attrs_props__"] = props
if frozen:
if props.is_frozen:
self._cls_dict["__setattr__"] = _frozen_setattrs
self._cls_dict["__delattr__"] = _frozen_delattrs
self._wrote_own_setattr = True
elif on_setattr in (
elif self._on_setattr in (
_DEFAULT_ON_SETATTR,
setters.validate,
setters.convert,
@@ -722,18 +740,18 @@ class _ClassBuilder:
break
if (
(
on_setattr == _DEFAULT_ON_SETATTR
self._on_setattr == _DEFAULT_ON_SETATTR
and not (has_validator or has_converter)
)
or (on_setattr == setters.validate and not has_validator)
or (on_setattr == setters.convert and not has_converter)
or (self._on_setattr == setters.validate and not has_validator)
or (self._on_setattr == setters.convert and not has_converter)
):
# If class-level on_setattr is set to convert + validate, but
# there's no field to convert or validate, pretend like there's
# no on_setattr.
self._on_setattr = None
if getstate_setstate:
if props.added_pickling:
(
self._cls_dict["__getstate__"],
self._cls_dict["__setstate__"],
@@ -784,6 +802,7 @@ class _ClassBuilder:
self._eval_snippets()
if self._slots is True:
cls = self._create_slots_class()
self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)
else:
cls = self._patch_original_class()
if PY_3_10_PLUS:
@@ -845,6 +864,10 @@ class _ClassBuilder:
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")
}
# 3.14.0rc2+
if hasattr(sys, "_clear_type_descriptors"):
sys._clear_type_descriptors(self._cls)
# If our class doesn't have its own implementation of __setattr__
# (either from the user or by us), check the bases, if one of them has
# an attrs-made __setattr__, that needs to be reset. We don't walk the
@@ -1081,9 +1104,7 @@ class _ClassBuilder:
return self
def add_replace(self):
self._cls_dict["__replace__"] = self._add_method_dunders(
lambda self, **changes: evolve(self, **changes)
)
self._cls_dict["__replace__"] = self._add_method_dunders(evolve)
return self
def add_match_args(self):
@@ -1326,6 +1347,7 @@ def attrs(
field_transformer=None,
match_args=True,
unsafe_hash=None,
force_kw_only=True,
):
r"""
A class decorator that adds :term:`dunder methods` according to the
@@ -1392,6 +1414,10 @@ def attrs(
If a class has an *inherited* classmethod called
``__attrs_init_subclass__``, it is executed after the class is created.
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
.. versionchanged:: 25.4.0
*kw_only* now only applies to attributes defined in the current class,
and respects attribute-level ``kw_only=False`` settings.
.. versionadded:: 25.4.0 *force_kw_only*
"""
if repr_ns is not None:
import warnings
@@ -1413,6 +1439,7 @@ def attrs(
on_setattr = setters.pipe(*on_setattr)
def wrap(cls):
nonlocal hash
is_frozen = frozen or _has_frozen_base_class(cls)
is_exc = auto_exc is True and issubclass(cls, BaseException)
has_own_setattr = auto_detect and _has_own_attribute(
@@ -1423,84 +1450,112 @@ def attrs(
msg = "Can't freeze a class with a custom __setattr__."
raise ValueError(msg)
builder = _ClassBuilder(
cls,
these,
slots,
is_frozen,
weakref_slot,
_determine_whether_to_implement(
eq = not is_exc and _determine_whether_to_implement(
cls, eq_, auto_detect, ("__eq__", "__ne__")
)
Hashability = ClassProps.Hashability
if is_exc:
hashability = Hashability.LEAVE_ALONE
elif hash is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif hash is False:
hashability = Hashability.LEAVE_ALONE
elif hash is None:
if auto_detect is True and _has_own_attribute(cls, "__hash__"):
hashability = Hashability.LEAVE_ALONE
elif eq is True and is_frozen is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif eq is False:
hashability = Hashability.LEAVE_ALONE
else:
hashability = Hashability.UNHASHABLE
else:
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)
KeywordOnly = ClassProps.KeywordOnly
if kw_only:
kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES
else:
kwo = KeywordOnly.NO
props = ClassProps(
is_exception=is_exc,
is_frozen=is_frozen,
is_slotted=slots,
collected_fields_by_mro=collect_by_mro,
added_init=_determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
),
added_repr=_determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
),
added_eq=eq,
added_ordering=not is_exc
and _determine_whether_to_implement(
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,
getstate_setstate,
auto_detect,
("__getstate__", "__setstate__"),
default=slots,
),
auto_attribs,
kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_own_setattr,
field_transformer,
on_setattr_hook=on_setattr,
field_transformer=field_transformer,
)
if _determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
):
if not props.is_hashable and cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
builder = _ClassBuilder(
cls,
these,
auto_attribs=auto_attribs,
props=props,
has_custom_setattr=has_own_setattr,
)
if props.added_repr:
builder.add_repr(repr_ns)
if str is True:
if props.added_str:
builder.add_str()
eq = _determine_whether_to_implement(
cls, eq_, auto_detect, ("__eq__", "__ne__")
)
if not is_exc and eq is True:
if props.added_eq:
builder.add_eq()
if not is_exc and _determine_whether_to_implement(
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
):
if props.added_ordering:
builder.add_order()
if not frozen:
builder.add_setattr()
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.
if props.is_hashable:
builder.add_hash()
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)
elif props.hashability is Hashability.UNHASHABLE:
builder.make_unhashable()
if _determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
):
if props.added_init:
builder.add_init()
else:
builder.add_attrs_init()
@@ -1835,16 +1890,16 @@ def _add_repr(cls, ns=None, attrs=None):
def fields(cls):
"""
Return the tuple of *attrs* attributes for a class.
Return the tuple of *attrs* attributes for a class or instance.
The tuple also allows accessing the fields by their names (see below for
examples).
Args:
cls (type): Class to introspect.
cls (type): Class or instance to introspect.
Raises:
TypeError: If *cls* is not a class.
TypeError: If *cls* is neither a class nor an *attrs* instance.
attrs.exceptions.NotAnAttrsClassError:
If *cls* is not an *attrs* class.
@@ -1855,12 +1910,17 @@ def fields(cls):
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
by name.
.. versionchanged:: 23.1.0 Add support for generic classes.
.. versionchanged:: 26.1.0 Add support for instances.
"""
generic_base = get_generic_base(cls)
if generic_base is None and not isinstance(cls, type):
msg = "Passed object must be a class."
raise TypeError(msg)
type_ = type(cls)
if getattr(type_, "__attrs_attrs__", None) is None:
msg = "Passed object must be a class or attrs instance."
raise TypeError(msg)
return fields(type_)
attrs = getattr(cls, "__attrs_attrs__", None)
@@ -1967,7 +2027,7 @@ def _make_init_script(
attr_dict[a.name] = a
if a.on_setattr is not None:
if frozen is True:
if frozen is True and a.on_setattr is not setters.NO_OP:
msg = "Frozen classes can't use on_setattr."
raise ValueError(msg)
@@ -2126,8 +2186,9 @@ def _attrs_to_init_script(
)
lines.extend(extra_lines)
args = []
kw_only_args = []
args = [] # Parameters in the definition of __init__
pre_init_args = [] # Parameters in the call to __attrs_pre_init__
kw_only_args = [] # Used for both 'args' and 'pre_init_args' above
attrs_to_validate = []
# This is a dictionary of names to validator and converter callables.
@@ -2205,6 +2266,7 @@ def _attrs_to_init_script(
kw_only_args.append(arg)
else:
args.append(arg)
pre_init_args.append(arg_name)
if converter is not None:
lines.append(
@@ -2224,6 +2286,7 @@ def _attrs_to_init_script(
kw_only_args.append(arg)
else:
args.append(arg)
pre_init_args.append(arg_name)
lines.append(f"if {arg_name} is not NOTHING:")
init_factory_name = _INIT_FACTORY_PAT % (a.name,)
@@ -2266,6 +2329,7 @@ def _attrs_to_init_script(
kw_only_args.append(arg_name)
else:
args.append(arg_name)
pre_init_args.append(arg_name)
if converter is not None:
lines.append(
@@ -2322,7 +2386,7 @@ def _attrs_to_init_script(
lines.append(f"BaseException.__init__(self, {vals})")
args = ", ".join(args)
pre_init_args = args
pre_init_args = ", ".join(pre_init_args)
if kw_only_args:
# leading comma & kw_only args
args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}"
@@ -2337,7 +2401,7 @@ def _attrs_to_init_script(
pre_init_args += pre_init_kw_only_args
if call_pre_init and pre_init_has_args:
# If pre init method has arguments, pass same arguments as `__init__`.
# If pre init method has arguments, pass the values given to __init__.
lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
# Python <3.12 doesn't allow backslashes in f-strings.
@@ -2376,6 +2440,8 @@ class Attribute:
- ``name`` (`str`): The name of the attribute.
- ``alias`` (`str`): The __init__ parameter name of the attribute, after
any explicit overrides and default private-attribute-name handling.
- ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically
generated (``True``) or explicitly provided by the user (``False``).
- ``inherited`` (`bool`): Whether or not that attribute has been inherited
from a base class.
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
@@ -2401,6 +2467,7 @@ class Attribute:
equality checks and hashing anymore.
.. versionadded:: 21.1.0 *eq_key* and *order_key*
.. versionadded:: 22.2.0 *alias*
.. versionadded:: 26.1.0 *alias_is_default*
For the full version history of the fields, see `attr.ib`.
"""
@@ -2425,6 +2492,7 @@ class Attribute:
"inherited",
"on_setattr",
"alias",
"alias_is_default",
)
def __init__(
@@ -2447,6 +2515,7 @@ class Attribute:
order_key=None,
on_setattr=None,
alias=None,
alias_is_default=None,
):
eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq_key or eq, order_key or order, True
@@ -2481,12 +2550,20 @@ class Attribute:
bound_setattr("inherited", inherited)
bound_setattr("on_setattr", on_setattr)
bound_setattr("alias", alias)
bound_setattr(
"alias_is_default",
alias is None if alias_is_default is None else alias_is_default,
)
def __setattr__(self, name, value):
raise FrozenInstanceError
@classmethod
def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
def from_counting_attr(
cls, name: str, ca: _CountingAttr, kw_only: bool, type=None
):
# The 'kw_only' argument is the class-level setting, and is used if the
# attribute itself does not explicitly set 'kw_only'.
# type holds the annotated value. deal with conflicts:
if type is None:
type = ca.type
@@ -2505,13 +2582,14 @@ class Attribute:
ca.metadata,
type,
ca.converter,
ca.kw_only,
kw_only if ca.kw_only is None else ca.kw_only,
ca.eq,
ca.eq_key,
ca.order,
ca.order_key,
ca.on_setattr,
ca.alias,
ca.alias is None,
)
# Don't use attrs.evolve since fields(Attribute) doesn't work
@@ -2530,6 +2608,20 @@ class Attribute:
new._setattrs(changes.items())
if "alias" in changes and "alias_is_default" not in changes:
# Explicit alias provided -- no longer the default.
_OBJ_SETATTR.__get__(new)("alias_is_default", False)
elif (
"name" in changes
and "alias" not in changes
# Don't auto-generate alias if the user picked picked the old one.
and self.alias_is_default
):
# Name changed, alias was auto-generated -- update it.
_OBJ_SETATTR.__get__(new)(
"alias", _default_init_alias_for(new.name)
)
return new
# Don't use _add_pickle since fields(Attribute) doesn't work
@@ -2546,6 +2638,17 @@ class Attribute:
"""
Play nice with pickle.
"""
if len(state) < len(self.__slots__):
# Pre-26.1.0 pickle without alias_is_default -- infer it
# heuristically.
state_dict = dict(zip(self.__slots__, state))
alias_is_default = state_dict.get(
"alias"
) is None or state_dict.get("alias") == _default_init_alias_for(
state_dict["name"]
)
state = (*state, alias_is_default)
self._setattrs(zip(self.__slots__, state))
def _setattrs(self, name_values_pairs):
@@ -2569,7 +2672,7 @@ _a = [
name=name,
default=NOTHING,
validator=None,
repr=True,
repr=(name != "alias_is_default"),
cmp=None,
eq=True,
order=False,
@@ -2741,6 +2844,188 @@ class _CountingAttr:
_CountingAttr = _add_eq(_add_repr(_CountingAttr))
class ClassProps:
"""
Effective class properties as derived from parameters to `attr.s()` or
`define()` decorators.
This is the same data structure that *attrs* uses internally to decide how
to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
is_exception (bool):
Whether the class is treated as an exception class.
is_slotted (bool):
Whether the class is `slotted <slotted classes>`.
has_weakref_slot (bool):
Whether the class has a slot for weak references.
is_frozen (bool):
Whether the class is frozen.
kw_only (KeywordOnly):
Whether / how the class enforces keyword-only arguments on the
``__init__`` method.
collected_fields_by_mro (bool):
Whether the class fields were collected by method resolution order.
That is, correctly but unlike `dataclasses`.
added_init (bool):
Whether the class has an *attrs*-generated ``__init__`` method.
added_repr (bool):
Whether the class has an *attrs*-generated ``__repr__`` method.
added_eq (bool):
Whether the class has *attrs*-generated equality methods.
added_ordering (bool):
Whether the class has *attrs*-generated ordering methods.
hashability (Hashability): How `hashable <hashing>` the class is.
added_match_args (bool):
Whether the class supports positional `match <match>` over its
fields.
added_str (bool):
Whether the class has an *attrs*-generated ``__str__`` method.
added_pickling (bool):
Whether the class has *attrs*-generated ``__getstate__`` and
``__setstate__`` methods for `pickle`.
on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None):
The class's ``__setattr__`` hook.
field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None):
The class's `field transformers <transform-fields>`.
.. versionadded:: 25.4.0
"""
class Hashability(enum.Enum):
"""
The hashability of a class.
.. versionadded:: 25.4.0
"""
HASHABLE = "hashable"
"""Write a ``__hash__``."""
HASHABLE_CACHED = "hashable_cache"
"""Write a ``__hash__`` and cache the hash."""
UNHASHABLE = "unhashable"
"""Set ``__hash__`` to ``None``."""
LEAVE_ALONE = "leave_alone"
"""Don't touch ``__hash__``."""
class KeywordOnly(enum.Enum):
"""
How attributes should be treated regarding keyword-only parameters.
.. versionadded:: 25.4.0
"""
NO = "no"
"""Attributes are not keyword-only."""
YES = "yes"
"""Attributes in current class without kw_only=False are keyword-only."""
FORCE = "force"
"""All attributes are keyword-only."""
__slots__ = ( # noqa: RUF023 -- order matters for __init__
"is_exception",
"is_slotted",
"has_weakref_slot",
"is_frozen",
"kw_only",
"collected_fields_by_mro",
"added_init",
"added_repr",
"added_eq",
"added_ordering",
"hashability",
"added_match_args",
"added_str",
"added_pickling",
"on_setattr_hook",
"field_transformer",
)
def __init__(
self,
is_exception,
is_slotted,
has_weakref_slot,
is_frozen,
kw_only,
collected_fields_by_mro,
added_init,
added_repr,
added_eq,
added_ordering,
hashability,
added_match_args,
added_str,
added_pickling,
on_setattr_hook,
field_transformer,
):
self.is_exception = is_exception
self.is_slotted = is_slotted
self.has_weakref_slot = has_weakref_slot
self.is_frozen = is_frozen
self.kw_only = kw_only
self.collected_fields_by_mro = collected_fields_by_mro
self.added_init = added_init
self.added_repr = added_repr
self.added_eq = added_eq
self.added_ordering = added_ordering
self.hashability = hashability
self.added_match_args = added_match_args
self.added_str = added_str
self.added_pickling = added_pickling
self.on_setattr_hook = on_setattr_hook
self.field_transformer = field_transformer
@property
def is_hashable(self):
return (
self.hashability is ClassProps.Hashability.HASHABLE
or self.hashability is ClassProps.Hashability.HASHABLE_CACHED
)
_cas = [
Attribute(
name=name,
default=NOTHING,
validator=None,
repr=True,
cmp=None,
eq=True,
order=False,
hash=True,
init=True,
inherited=False,
alias=_default_init_alias_for(name),
)
for name in ClassProps.__slots__
]
ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas)
class Factory:
"""
Stores a factory callable.
@@ -2848,9 +3133,7 @@ class Converter:
value, field
)
else:
self.__call__ = lambda value, instance, field: self.converter(
value, instance, field
)
self.__call__ = self.converter
rt = ex.get_return_type()
if rt is not None:
+58 -7
View File
@@ -17,7 +17,7 @@ from ._make import (
attrib,
attrs,
)
from .exceptions import UnannotatedAttributeError
from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError
def define(
@@ -43,6 +43,7 @@ def define(
on_setattr=None,
field_transformer=None,
match_args=True,
force_kw_only=False,
):
r"""
A class decorator that adds :term:`dunder methods` according to
@@ -76,7 +77,7 @@ def define(
sensible ``__ne__`` by default, so it *should* be enough to only
implement ``__eq__`` in most cases).
Passing True or False` to *init*, *repr*, *eq*, or *hash*
Passing :data:`True` or :data:`False` to *init*, *repr*, *eq*, or *hash*
overrides whatever *auto_detect* would determine.
auto_exc (bool):
@@ -214,8 +215,12 @@ def define(
5. Subclasses of a frozen class are frozen too.
kw_only (bool):
Make all attributes keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored).
Make attributes keyword-only in the generated ``__init__`` (if
*init* is False, this parameter is ignored). Attributes that
explicitly set ``kw_only=False`` are not affected; base class
attributes are also not affected.
Also see *force_kw_only*.
weakref_slot (bool):
Make instances weak-referenceable. This has no effect unless
@@ -244,6 +249,15 @@ def define(
See also `issue #428
<https://github.com/python-attrs/attrs/issues/428>`_.
force_kw_only (bool):
A back-compat flag for restoring pre-25.4.0 behavior. If True and
``kw_only=True``, all attributes are made keyword-only, including
base class attributes, and those set to ``kw_only=False`` at the
attribute level. Defaults to False.
See also `issue #980
<https://github.com/python-attrs/attrs/issues/980>`_.
getstate_setstate (bool | None):
.. note::
@@ -319,6 +333,11 @@ def define(
.. versionadded:: 24.3.0
Unless already present, a ``__replace__`` method is automatically
created for `copy.replace` (Python 3.13+ only).
.. versionchanged:: 25.4.0
*kw_only* now only applies to attributes defined in the current class,
and respects attribute-level ``kw_only=False`` settings.
.. versionadded:: 25.4.0
Added *force_kw_only* to go back to the previous *kw_only* behavior.
.. note::
@@ -337,6 +356,7 @@ def define(
- *auto_exc=True*
- *auto_detect=True*
- *order=False*
- *force_kw_only=False*
- Some options that were only relevant on Python 2 or were kept around
for backwards-compatibility have been removed.
@@ -366,6 +386,7 @@ def define(
on_setattr=on_setattr,
field_transformer=field_transformer,
match_args=match_args,
force_kw_only=force_kw_only,
)
def wrap(cls):
@@ -424,7 +445,7 @@ def field(
type=None,
converter=None,
factory=None,
kw_only=False,
kw_only=None,
eq=None,
order=None,
on_setattr=None,
@@ -550,9 +571,10 @@ def field(
itself. You can use it as part of your own code or for `static type
checking <types>`.
kw_only (bool):
kw_only (bool | None):
Make this attribute keyword-only in the generated ``__init__`` (if
``init`` is False, this parameter is ignored).
*init* is False, this parameter is ignored). If None (default),
mirror the setting from `attrs.define`.
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
@@ -572,6 +594,9 @@ def field(
.. versionadded:: 23.1.0
The *type* parameter has been re-added; mostly for `attrs.make_class`.
Please note that type checkers ignore this metadata.
.. versionchanged:: 25.4.0
*kw_only* can now be None, and its default is also changed from False to
None.
.. seealso::
@@ -621,3 +646,29 @@ def astuple(inst, *, recurse=True, filter=None):
return _astuple(
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
)
def inspect(cls):
"""
Inspect the class and return its effective build parameters.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Args:
cls: The *attrs*-decorated class to inspect.
Returns:
The effective build parameters of the class.
Raises:
NotAnAttrsClassError: If the class is not an *attrs*-decorated class.
.. versionadded:: 25.4.0
"""
try:
return cls.__dict__["__attrs_props__"]
except KeyError:
msg = f"{cls!r} is not an attrs-decorated class."
raise NotAnAttrsClassError(msg) from None
+3
View File
@@ -84,3 +84,6 @@ class VersionInfo:
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
# have to do anything special with releaselevel for now.
return us < them
def __hash__(self):
return hash((self.year, self.minor, self.micro, self.releaselevel))
+4 -4
View File
@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import ClassVar
class FrozenError(AttributeError):
"""
@@ -16,8 +14,10 @@ class FrozenError(AttributeError):
.. versionadded:: 20.1.0
"""
msg = "can't set attribute"
args: ClassVar[tuple[str]] = [msg]
def __init__(self):
msg = "can't set attribute"
super().__init__(msg)
self.msg = msg
class FrozenInstanceError(FrozenError):
+55 -15
View File
@@ -79,12 +79,14 @@ def disabled():
This context manager is not thread-safe!
.. versionadded:: 21.3.0
.. versionchanged:: 26.1.0 The contextmanager is nestable.
"""
prev = get_run_validators()
set_run_validators(False)
try:
yield
finally:
set_run_validators(True)
set_run_validators(prev)
@attrs(repr=False, slots=True, unsafe_hash=True)
@@ -361,26 +363,32 @@ def deep_iterable(member_validator, iterable_validator=None):
A validator that performs deep validation of an iterable.
Args:
member_validator: Validator to apply to iterable members.
member_validator: Validator(s) to apply to iterable members.
iterable_validator:
Validator to apply to iterable itself (optional).
Validator(s) to apply to iterable itself (optional).
Raises
TypeError: if any sub-validators fail
.. versionadded:: 19.1.0
.. versionchanged:: 25.4.0
*member_validator* and *iterable_validator* can now be a list or tuple
of validators.
"""
if isinstance(member_validator, (list, tuple)):
member_validator = and_(*member_validator)
if isinstance(iterable_validator, (list, tuple)):
iterable_validator = and_(*iterable_validator)
return _DeepIterable(member_validator, iterable_validator)
@attrs(repr=False, slots=True, unsafe_hash=True)
class _DeepMapping:
key_validator = attrib(validator=is_callable())
value_validator = attrib(validator=is_callable())
mapping_validator = attrib(default=None, validator=optional(is_callable()))
key_validator = attrib(validator=optional(is_callable()))
value_validator = attrib(validator=optional(is_callable()))
mapping_validator = attrib(validator=optional(is_callable()))
def __call__(self, inst, attr, value):
"""
@@ -390,30 +398,62 @@ class _DeepMapping:
self.mapping_validator(inst, attr, value)
for key in value:
self.key_validator(inst, attr, key)
self.value_validator(inst, attr, value[key])
if self.key_validator is not None:
self.key_validator(inst, attr, key)
if self.value_validator is not None:
self.value_validator(inst, attr, value[key])
def __repr__(self):
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
def deep_mapping(key_validator, value_validator, mapping_validator=None):
def deep_mapping(
key_validator=None, value_validator=None, mapping_validator=None
):
"""
A validator that performs deep validation of a dictionary.
Args:
key_validator: Validator to apply to dictionary keys.
All validators are optional, but at least one of *key_validator* or
*value_validator* must be provided.
value_validator: Validator to apply to dictionary values.
Args:
key_validator: Validator(s) to apply to dictionary keys.
value_validator: Validator(s) to apply to dictionary values.
mapping_validator:
Validator to apply to top-level mapping attribute (optional).
Validator(s) to apply to top-level mapping attribute.
.. versionadded:: 19.1.0
.. versionchanged:: 25.4.0
*key_validator* and *value_validator* are now optional, but at least one
of them must be provided.
.. versionchanged:: 25.4.0
*key_validator*, *value_validator*, and *mapping_validator* can now be a
list or tuple of validators.
Raises:
TypeError: if any sub-validators fail
TypeError: If any sub-validator fails on validation.
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)
@@ -485,7 +525,7 @@ def gt(val):
A validator that raises `ValueError` if the initializer is called with a
number smaller or equal to *val*.
The validator uses `operator.ge` to compare the values.
The validator uses `operator.gt` to compare the values.
Args:
val: Exclusive lower bound for values
+60 -6
View File
@@ -20,6 +20,9 @@ _T = TypeVar("_T")
_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_T3 = TypeVar("_T3")
_T4 = TypeVar("_T4")
_T5 = TypeVar("_T5")
_T6 = TypeVar("_T6")
_I = TypeVar("_I", bound=Iterable)
_K = TypeVar("_K")
_V = TypeVar("_V")
@@ -51,7 +54,7 @@ def optional(
validator: (
_ValidatorType[_T]
| list[_ValidatorType[_T]]
| tuple[_ValidatorType[_T]]
| tuple[_ValidatorType[_T], ...]
),
) -> _ValidatorType[_T | None]: ...
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
@@ -63,12 +66,19 @@ def matches_re(
) -> _ValidatorType[AnyStr]: ...
def deep_iterable(
member_validator: _ValidatorArgType[_T],
iterable_validator: _ValidatorType[_I] | None = ...,
iterable_validator: _ValidatorArgType[_I] | None = ...,
) -> _ValidatorType[_I]: ...
@overload
def deep_mapping(
key_validator: _ValidatorType[_K],
value_validator: _ValidatorType[_V],
mapping_validator: _ValidatorType[_M] | None = ...,
key_validator: _ValidatorArgType[_K],
value_validator: _ValidatorArgType[_V] | None = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
@overload
def deep_mapping(
key_validator: _ValidatorArgType[_K] | None = ...,
value_validator: _ValidatorArgType[_V] = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
def is_callable() -> _ValidatorType[_T]: ...
def lt(val: _T) -> _ValidatorType[_T]: ...
@@ -83,4 +93,48 @@ def not_(
msg: str | None = None,
exc_types: type[Exception] | Iterable[type[Exception]] = ...,
) -> _ValidatorType[_T]: ...
def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
) -> _ValidatorType[_T1 | _T2]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
) -> _ValidatorType[_T1 | _T2 | _T3]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
__v4: _ValidatorType[_T4],
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
__v4: _ValidatorType[_T4],
__v5: _ValidatorType[_T5],
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ...
@overload
def or_(
__v1: _ValidatorType[_T1],
__v2: _ValidatorType[_T2],
__v3: _ValidatorType[_T3],
__v4: _ValidatorType[_T4],
__v5: _ValidatorType[_T5],
__v6: _ValidatorType[_T6],
) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ...
@overload
def or_(
__v1: _ValidatorType[Any],
__v2: _ValidatorType[Any],
__v3: _ValidatorType[Any],
__v4: _ValidatorType[Any],
__v5: _ValidatorType[Any],
__v6: _ValidatorType[Any],
*validators: _ValidatorType[Any],
) -> _ValidatorType[Any]: ...
@@ -1 +0,0 @@
pip
-56
View File
@@ -1,56 +0,0 @@
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
@@ -0,0 +1 @@
uv
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: attrs
Version: 25.3.0
Version: 26.1.0
Summary: Classes Without Boilerplate
Project-URL: Documentation, https://www.attrs.org/
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
@@ -12,62 +12,16 @@ License-Expression: MIT
License-File: LICENSE
Keywords: attribute,boilerplate,class
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
Requires-Python: >=3.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'
Requires-Python: >=3.9
Description-Content-Type: text/markdown
<p align="center">
@@ -78,7 +32,7 @@ Description-Content-Type: text/markdown
*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)).
[Trusted by NASA](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!
Trusted by NASA for [Mars missions since 2020](https://github.com/readme/featured/nasa-ingenuity-helicopter)!
Its main goal is to help you to write **concise** and **correct** software without slowing down your code.
@@ -96,15 +50,15 @@ Especially those generously supporting us at the *The Organization* tier and hig
import pathlib, tomllib
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]:
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/25.3.0/_static/sponsors/{sponsor["img"]}" width="190" /></a>')
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>')
]]] -->
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Variomedia.svg" width="190" /></a>
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Tidelift.svg" width="190" /></a>
<a href="https://klaviyo.com/"><img title="Klaviyo" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Klaviyo.svg" width="190" /></a>
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
<a href="https://www.emsys-renewables.com/"><img title="emsys renewables" src="https://www.attrs.org/en/25.3.0/_static/sponsors/emsys-renewables.svg" width="190" /></a>
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/25.3.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/25.3.0/_static/sponsors/Polar.svg" width="190" /></a>
<a href="https://www.variomedia.de/"><img title="Variomedia AG" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Variomedia.svg" width="190" /></a>
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Tidelift.svg" width="190" /></a>
<a href="https://kraken.tech/"><img title="Kraken Tech" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Kraken.svg" width="190" /></a>
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
<a href="https://filepreviews.io/"><img title="FilePreviews" src="https://www.attrs.org/en/26.1.0/_static/sponsors/FilePreviews.svg" width="190" /></a>
<a href="https://www.testmuai.com/?utm_medium=sponsor&utm_source=structlog"><img title="TestMu AI" src="https://www.attrs.org/en/26.1.0/_static/sponsors/TestMu-AI.svg" width="190" /></a>
<a href="https://polar.sh/"><img title="Polar" src="https://www.attrs.org/en/26.1.0/_static/sponsors/Polar.svg" width="190" /></a>
<!-- [[[end]]] -->
</p>
@@ -220,10 +174,23 @@ Save time, reduce risk, and improve code health, while paying the maintainers of
## Release Information
### Backwards-incompatible Changes
- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`.
The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`).
[#1509](https://github.com/python-attrs/attrs/issues/1509)
### Changes
- Restore support for generator-based `field_transformer`s.
[#1417](https://github.com/python-attrs/attrs/issues/1417)
- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator.
[#1496](https://github.com/python-attrs/attrs/issues/1496)
- The `attrs.validators.disabled()` contextmanager can now be nested.
[#1513](https://github.com/python-attrs/attrs/issues/1513)
- Frozen classes can set `on_setattr=attrs.setters.NO_OP` in addition to `None`.
[#1515](https://github.com/python-attrs/attrs/issues/1515)
- It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`.
[#1529](https://github.com/python-attrs/attrs/issues/1529)
+37
View File
@@ -0,0 +1,37 @@
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 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.27.0
Generator: hatchling 1.29.0
Root-Is-Purelib: true
Tag: py3-none-any
+4 -1
View File
@@ -22,7 +22,8 @@ from attr import (
resolve_types,
validate,
)
from attr._next_gen import asdict, astuple
from attr._make import ClassProps
from attr._next_gen import asdict, astuple, inspect
from . import converters, exceptions, filters, setters, validators
@@ -31,6 +32,7 @@ __all__ = [
"NOTHING",
"Attribute",
"AttrsInstance",
"ClassProps",
"Converter",
"Factory",
"NothingType",
@@ -58,6 +60,7 @@ __all__ = [
"filters",
"frozen",
"has",
"inspect",
"make_class",
"mutable",
"resolve_types",
+58 -7
View File
@@ -77,7 +77,7 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: bool | None = ...,
order: bool | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -98,10 +98,10 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType
| list[_ConverterType]
| tuple[_ConverterType]
| tuple[_ConverterType, ...]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -121,10 +121,10 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType
| list[_ConverterType]
| tuple[_ConverterType]
| tuple[_ConverterType, ...]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -144,10 +144,10 @@ def field(
metadata: Mapping[Any, Any] | None = ...,
converter: _ConverterType
| list[_ConverterType]
| tuple[_ConverterType]
| tuple[_ConverterType, ...]
| None = ...,
factory: Callable[[], _T] | None = ...,
kw_only: bool = ...,
kw_only: bool | None = ...,
eq: _EqOrderType | None = ...,
order: _EqOrderType | None = ...,
on_setattr: _OnSetAttrArgType | None = ...,
@@ -261,3 +261,54 @@ def frozen(
field_transformer: _FieldTransformer | None = ...,
match_args: bool = ...,
) -> Callable[[_C], _C]: ...
class ClassProps:
# XXX: somehow when defining/using enums Mypy starts looking at our own
# (untyped) code and causes tons of errors.
Hashability: Any
KeywordOnly: Any
is_exception: bool
is_slotted: bool
has_weakref_slot: bool
is_frozen: bool
# kw_only: ClassProps.KeywordOnly
kw_only: Any
collected_fields_by_mro: bool
added_init: bool
added_repr: bool
added_eq: bool
added_ordering: bool
# hashability: ClassProps.Hashability
hashability: Any
added_match_args: bool
added_str: bool
added_pickling: bool
on_setattr_hook: _OnSetAttrType | None
field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None
def __init__(
self,
is_exception: bool,
is_slotted: bool,
has_weakref_slot: bool,
is_frozen: bool,
# kw_only: ClassProps.KeywordOnly
kw_only: Any,
collected_fields_by_mro: bool,
added_init: bool,
added_repr: bool,
added_eq: bool,
added_ordering: bool,
# hashability: ClassProps.Hashability
hashability: Any,
added_match_args: bool,
added_str: bool,
added_pickling: bool,
on_setattr_hook: _OnSetAttrType,
field_transformer: Callable[[Attribute[Any]], Attribute[Any]],
) -> None: ...
@property
def is_hashable(self) -> bool: ...
def inspect(cls: type) -> ClassProps: ...
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
pip
-102
View File
@@ -1,102 +0,0 @@
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
@@ -0,0 +1 @@
uv
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: cattrs
Version: 25.1.1
Version: 26.1.0
Summary: Composable complex class support for attrs and dataclasses.
Project-URL: Homepage, https://catt.rs
Project-URL: Changelog, https://catt.rs/en/latest/history.html
@@ -14,18 +14,18 @@ Keywords: attrs,dataclasses,serialization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: attrs>=24.3.0
Requires-Python: >=3.10
Requires-Dist: attrs>=25.4.0
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11'
Requires-Dist: typing-extensions>=4.12.2
Requires-Dist: typing-extensions>=4.14.0
Provides-Extra: bson
Requires-Dist: pymongo>=4.4.0; extra == 'bson'
Provides-Extra: cbor2
@@ -35,11 +35,14 @@ Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
Provides-Extra: msgspec
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
Provides-Extra: orjson
Requires-Dist: orjson>=3.10.7; (implementation_name == 'cpython') and extra == 'orjson'
Requires-Dist: orjson>=3.11.3; (implementation_name == 'cpython') and extra == 'orjson'
Provides-Extra: pyyaml
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
Provides-Extra: tomlkit
Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
Provides-Extra: tomllib
Requires-Dist: tomli-w>=1.1.0; extra == 'tomllib'
Requires-Dist: tomli>=1.1.0; (python_version < '3.11') and extra == 'tomllib'
Provides-Extra: ujson
Requires-Dist: ujson>=5.10.0; extra == 'ujson'
Description-Content-Type: text/markdown
@@ -0,0 +1,58 @@
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
@@ -1,4 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.27.0
Generator: hatchling 1.28.0
Root-Is-Purelib: true
Tag: py3-none-any
+44 -42
View File
@@ -29,7 +29,6 @@ from typing import (
_AnnotatedAlias,
_GenericAlias,
_SpecialGenericAlias,
_UnionGenericAlias,
get_args,
get_origin,
get_type_hints,
@@ -209,10 +208,7 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet
signature = _signature
if sys.version_info >= (3, 10):
signature = partial(_signature, eval_str=True)
signature = partial(_signature, eval_str=True)
try:
@@ -256,10 +252,25 @@ def is_tuple(type):
)
if sys.version_info >= (3, 10):
if sys.version_info >= (3, 14):
def is_union_type(obj):
from types import UnionType
from types import UnionType # noqa: PLC0415
return obj is Union or isinstance(obj, UnionType)
def get_newtype_base(typ: Any) -> Optional[type]:
if typ is NewType or isinstance(typ, NewType):
return typ.__supertype__
return None
from typing import NotRequired, Required
else:
from typing import _UnionGenericAlias
def is_union_type(obj):
from types import UnionType # noqa: PLC0415
return (
obj is Union
@@ -277,25 +288,6 @@ if sys.version_info >= (3, 10):
else:
from typing_extensions import NotRequired, Required
else:
# 3.9
from typing_extensions import NotRequired, Required
def is_union_type(obj):
return obj is Union or (
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
)
def get_newtype_base(typ: Any) -> Optional[type]:
supertype = getattr(typ, "__supertype__", None)
if (
supertype is not None
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
and typ.__module__ in ("typing", "typing_extensions")
):
return supertype
return None
def get_notrequired_base(type) -> Union[Any, NothingType]:
if is_annotated(type):
@@ -306,6 +298,25 @@ def get_notrequired_base(type) -> Union[Any, NothingType]:
return NOTHING
def is_mutable_sequence(type: Any) -> bool:
"""A predicate function for mutable sequences.
Matches lists, mutable sequences, and deques.
"""
origin = getattr(type, "__origin__", None)
return (
type in (List, list, TypingMutableSequence, AbcMutableSequence, deque, Deque)
or (
type.__class__ is _GenericAlias
and (
((origin is not tuple) and is_subclass(origin, TypingMutableSequence))
or (origin is tuple and type.__args__[1] is ...)
)
)
or (origin in (list, deque, AbcMutableSequence))
)
def is_sequence(type: Any) -> bool:
"""A predicate function for sequences.
@@ -313,19 +324,8 @@ def is_sequence(type: Any) -> bool:
tuples.
"""
origin = getattr(type, "__origin__", None)
return (
type
in (
List,
list,
TypingSequence,
TypingMutableSequence,
AbcMutableSequence,
tuple,
Tuple,
deque,
Deque,
)
return is_mutable_sequence(type) or (
type in (TypingSequence, tuple, Tuple)
or (
type.__class__ is _GenericAlias
and (
@@ -333,7 +333,7 @@ def is_sequence(type: Any) -> bool:
or (origin is tuple and type.__args__[1] is ...)
)
)
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
or (origin is AbcSequence)
or (origin is tuple and type.__args__[1] is ...)
)
@@ -403,8 +403,10 @@ def is_generic(type) -> bool:
"""Whether `type` is a generic type."""
# Inheriting from protocol will inject `Generic` into the MRO
# without `__orig_bases__`.
return isinstance(type, (_GenericAlias, GenericAlias)) or (
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
return (
isinstance(type, (_GenericAlias, GenericAlias))
or (is_subclass(type, Generic) and hasattr(type, "__orig_bases__"))
or type.__class__ is Union # On 3.14, unions are no longer typing._GenericAlias
)
+2 -2
View File
@@ -1,10 +1,10 @@
from collections.abc import Mapping
from typing import Any
from typing import Any, get_args
from attrs import NOTHING
from typing_extensions import Self
from ._compat import copy_with, get_args, is_annotated, is_generic
from ._compat import copy_with, is_annotated, is_generic
def deep_copy_with(t, mapping: Mapping[str, Any], self_is=NOTHING):
+54 -10
View File
@@ -5,25 +5,20 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable, Iterable
from functools import partial
from typing import (
TYPE_CHECKING,
Any,
DefaultDict,
Literal,
NamedTuple,
TypeVar,
get_type_hints,
)
from typing import TYPE_CHECKING, Any, DefaultDict, Literal, NamedTuple, TypeVar
from attrs import NOTHING, Attribute, NothingType
from ._compat import (
ANIES,
AbcSet,
get_args,
get_full_type_hints,
get_origin,
is_bare,
is_frozenset,
is_mapping,
is_mutable_sequence,
is_sequence,
is_subclass,
)
@@ -47,10 +42,13 @@ if TYPE_CHECKING:
__all__ = [
"defaultdict_structure_factory",
"homogenous_tuple_structure_factory",
"is_abstract_set",
"is_any_set",
"is_defaultdict",
"is_frozenset",
"is_mapping",
"is_mutable_sequence",
"is_namedtuple",
"is_sequence",
"is_set",
@@ -70,6 +68,11 @@ def is_any_set(type) -> bool:
return is_set(type) or is_frozenset(type)
def is_abstract_set(type) -> bool:
"""A predicate function for abstract (collection.abc) sets."""
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)
def is_namedtuple(type: Any) -> bool:
"""A predicate function for named tuples."""
@@ -151,6 +154,47 @@ def list_structure_factory(type: type, converter: BaseConverter) -> StructureHoo
return structure_list
def homogenous_tuple_structure_factory(
type: type, converter: BaseConverter
) -> StructureHook:
"""A hook factory for homogenous (all elements the same, indeterminate length) tuples.
Converts any given iterable into a tuple.
"""
if is_bare(type) or type.__args__[0] in ANIES:
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
return tuple(obj)
return structure_tuple
elem_type = type.__args__[0]
try:
handler = converter.get_structure_hook(elem_type)
except RecursionError:
# Break the cycle by using late binding.
handler = converter.structure
if converter.detailed_validation:
# We have to structure into a list first anyway.
list_structure = list_structure_factory(type, converter)
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
return tuple(list_structure(obj, _))
else:
def structure_tuple(
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
) -> tuple[T, ...]:
return tuple([_handler(e, _elem_type) for e in obj])
return structure_tuple
def namedtuple_unstructure_factory(
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
) -> UnstructureHook:
@@ -195,7 +239,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
type=a,
alias=name,
)
for name, a in get_type_hints(cl).items()
for name, a in get_full_type_hints(cl).items()
]
+34 -13
View File
@@ -43,10 +43,11 @@ from ._compat import (
is_hetero_tuple,
is_literal,
is_mapping,
is_mutable_sequence,
is_mutable_set,
is_optional,
is_protocol,
is_sequence,
is_subclass,
is_tuple,
is_typeddict,
is_union_type,
@@ -54,8 +55,11 @@ from ._compat import (
)
from .cols import (
defaultdict_structure_factory,
homogenous_tuple_structure_factory,
is_abstract_set,
is_defaultdict,
is_namedtuple,
is_sequence,
iterable_unstructure_factory,
list_structure_factory,
mapping_structure_factory,
@@ -73,6 +77,7 @@ from .dispatch import (
UnstructuredValue,
UnstructureHook,
)
from .enums import enum_structure_factory, enum_unstructure_factory
from .errors import (
IterableValidationError,
IterableValidationNote,
@@ -225,6 +230,10 @@ class BaseConverter:
)
self._unstructure_func.register_func_list(
[
(
lambda t: get_newtype_base(t) is not None,
lambda o: self.unstructure(o, unstructure_as=o.__class__),
),
(
is_protocol,
lambda o: self.unstructure(o, unstructure_as=o.__class__),
@@ -239,12 +248,12 @@ class BaseConverter:
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
True,
),
(is_literal_containing_enums, self.unstructure),
(is_mapping, self._unstructure_mapping),
(is_sequence, self._unstructure_seq),
(is_mutable_set, self._unstructure_seq),
(is_frozenset, self._unstructure_seq),
(lambda t: issubclass(t, Enum), self._unstructure_enum),
(is_literal_containing_enums, self.unstructure),
(lambda t: is_subclass(t, Enum), enum_unstructure_factory, "extended"),
(has, self._unstructure_attrs),
(is_union_type, self._unstructure_union),
(lambda t: t in ANIES, self.unstructure),
@@ -271,20 +280,27 @@ class BaseConverter:
),
(is_literal, self._structure_simple_literal),
(is_literal_containing_enums, self._structure_enum_literal),
(is_sequence, list_structure_factory, "extended"),
(is_sequence, homogenous_tuple_structure_factory, "extended"),
(is_mutable_sequence, list_structure_factory, "extended"),
(is_deque, self._structure_deque),
(is_mutable_set, self._structure_set),
(is_abstract_set, self._structure_frozenset),
(is_frozenset, self._structure_frozenset),
(is_tuple, self._structure_tuple),
(is_namedtuple, namedtuple_structure_factory, "extended"),
(is_mapping, self._structure_dict),
(is_supported_union, self._gen_attrs_union_structure, True),
*(
[(is_supported_union, self._gen_attrs_union_structure, True)]
if unstruct_strat is UnstructureStrategy.AS_DICT
else []
),
(is_optional, self._structure_optional),
(
lambda t: is_union_type(t) and t in self._union_struct_registry,
self._union_struct_registry.__getitem__,
True,
),
(lambda t: is_subclass(t, Enum), enum_structure_factory, "extended"),
(has, self._structure_attrs),
]
)
@@ -295,7 +311,6 @@ class BaseConverter:
(bytes, self._structure_call),
(int, self._structure_call),
(float, self._structure_call),
(Enum, self._structure_call),
(Path, self._structure_call),
]
)
@@ -617,10 +632,6 @@ class BaseConverter:
res.append(dispatch(a.type or v.__class__)(v))
return tuple(res)
def _unstructure_enum(self, obj: Enum) -> Any:
"""Convert an enum to its value."""
return obj.value
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
"""Convert a sequence to primitive equivalents."""
# We can reuse the sequence class, so tuples stay tuples.
@@ -1034,6 +1045,7 @@ class Converter(BaseConverter):
"forbid_extra_keys",
"omit_if_default",
"type_overrides",
"use_alias",
)
def __init__(
@@ -1050,6 +1062,7 @@ class Converter(BaseConverter):
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
None, t
),
use_alias: bool = False,
):
"""
:param detailed_validation: Whether to use a slightly slower mode for detailed
@@ -1058,12 +1071,15 @@ class Converter(BaseConverter):
registered unstructuring hooks match.
:param structure_fallback_factory: A hook factory to be called when no
registered structuring hooks match.
:param use_alias: Whether to use the field alias instead of the field name as
the un/structured dictionary key by default.
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
.. versionadded:: 23.2.0 *structure_fallback_factory*
.. versionchanged:: 24.2.0
The default `structure_fallback_factory` now raises errors for missing handlers
more eagerly, surfacing problems earlier.
.. versionadded:: 25.2.0 *use_alias*
"""
super().__init__(
dict_factory=dict_factory,
@@ -1076,6 +1092,7 @@ class Converter(BaseConverter):
self.omit_if_default = omit_if_default
self.forbid_extra_keys = forbid_extra_keys
self.type_overrides = dict(type_overrides)
self.use_alias = use_alias
unstruct_collection_overrides = {
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
@@ -1246,7 +1263,7 @@ class Converter(BaseConverter):
attribs = fields(origin or cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved.
resolve_types(cl)
resolve_types(origin or cl)
attrib_overrides = {
a.name: self.type_overrides[a.type]
for a in attribs
@@ -1284,10 +1301,11 @@ class Converter(BaseConverter):
def gen_structure_attrs_fromdict(
self, cl: type[T]
) -> Callable[[Mapping[str, Any], Any], T]:
attribs = fields(get_origin(cl) or cl if is_generic(cl) else cl)
origin = get_origin(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):
# PEP 563 annotations - need to be resolved.
resolve_types(cl)
resolve_types(origin or cl)
attrib_overrides = {
a.name: self.type_overrides[a.type]
for a in attribs
@@ -1299,6 +1317,7 @@ class Converter(BaseConverter):
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
_cattrs_detailed_validation=self.detailed_validation,
_cattrs_use_alias=self.use_alias,
**attrib_overrides,
)
@@ -1377,6 +1396,7 @@ class Converter(BaseConverter):
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
prefer_attrib_converters: bool | None = None,
detailed_validation: bool | None = None,
use_alias: bool | None = None,
) -> Self:
"""Create a copy of the converter, keeping all existing custom hooks.
@@ -1416,6 +1436,7 @@ class Converter(BaseConverter):
if detailed_validation is not None
else self.detailed_validation
),
use_alias=(use_alias if use_alias is not None else self.use_alias),
)
self._unstructure_func.copy_to(
+1 -2
View File
@@ -7,7 +7,7 @@ from collections.abc import Mapping
from dataclasses import MISSING
from functools import reduce
from operator import or_
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, get_origin
from attrs import NOTHING, Attribute, AttrsInstance
@@ -16,7 +16,6 @@ from ._compat import (
adapted_fields,
fields_dict,
get_args,
get_origin,
has,
is_literal,
is_union_type,
+36
View File
@@ -0,0 +1,36 @@
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)
+16 -10
View File
@@ -13,14 +13,18 @@ class StructureHandlerNotFoundError(Exception):
"""
def __init__(self, message: str, type_: type) -> None:
super().__init__(message)
super().__init__(message, type_)
self.message = message
self.type_ = type_
def __str__(self) -> str:
return self.message
class BaseValidationError(ExceptionGroup):
cl: type
def __new__(cls, message: str, excs: Sequence[Exception], cl: type):
def __new__(cls, message: str, excs: Sequence[Exception], cl: type) -> Self:
obj = super().__new__(cls, message, excs)
obj.cl = cl
return obj
@@ -35,9 +39,7 @@ class IterableValidationNote(str):
index: Union[int, str] # Ints for list indices, strs for dict keys
type: Any
def __new__(
cls, string: str, index: Union[int, str], type: Any
) -> "IterableValidationNote":
def __new__(cls, string: str, index: Union[int, str], type: Any) -> Self:
instance = str.__new__(cls, string)
instance.index = index
instance.type = type
@@ -76,7 +78,7 @@ class AttributeValidationNote(str):
name: str
type: Any
def __new__(cls, string: str, name: str, type: Any) -> "AttributeValidationNote":
def __new__(cls, string: str, name: str, type: Any) -> Self:
instance = str.__new__(cls, string)
instance.name = name
instance.type = type
@@ -122,11 +124,15 @@ class ForbiddenExtraKeysError(Exception):
def __init__(
self, message: Optional[str], cl: type, extra_fields: set[str]
) -> None:
self.message = message
self.cl = cl
self.extra_fields = extra_fields
cln = cl.__name__
super().__init__(
message
or f"Extra fields in constructor for {cln}: {', '.join(extra_fields)}"
super().__init__(message, cl, extra_fields)
def __str__(self) -> str:
return (
self.message
or f"Extra fields in constructor for {self.cl.__name__}: "
f"{', '.join(sorted(self.extra_fields))}"
)
+108 -24
View File
@@ -4,7 +4,7 @@ import re
from collections.abc import Callable, Iterable, Mapping
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
from attrs import NOTHING, Attribute, Factory
from attrs import NOTHING, Attribute, Converter, Factory, evolve
from typing_extensions import NoDefault
from .._compat import (
@@ -33,7 +33,7 @@ from ..types import SimpleStructureHook
from ._consts import AttributeOverride, already_generating, neutral
from ._generics import generate_mapping
from ._lc import generate_unique_filename
from ._shared import find_structure_handler
from ._shared import _annotated_override_or_default, find_structure_handler
if TYPE_CHECKING:
from ..converters import BaseConverter
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
typevar_map: dict[str, Any] = {},
_cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool = False,
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]:
@@ -95,7 +95,17 @@ def make_dict_unstructure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
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__
@@ -104,15 +114,27 @@ def make_dict_unstructure_fn_from_attrs(
invocation_lines = []
internal_arg_parts = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
for a in attrs:
attr_name = a.name
override = kwargs.get(attr_name, neutral)
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[attr_name] = override
if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
continue
if override.rename is None:
kn = attr_name if not _cattrs_use_alias else a.alias
if kn != attr_name:
kwargs[attr_name] = evolve(override, rename=kn)
else:
kn = override.rename
d = a.default
@@ -170,16 +192,32 @@ def make_dict_unstructure_fn_from_attrs(
if isinstance(d, Factory):
globs[def_name] = d.factory
internal_arg_parts[def_name] = d.factory
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}")
def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()"
else:
globs[def_name] = d
internal_arg_parts[def_name] = d
lines.append(f" if instance.{attr_name} != {def_name}:")
lines.append(f" res['{kn}'] = {invoke}")
def_str = def_name
c = a.converter
if c is not None:
conv_name = f"__c_conv_{attr_name}"
if isinstance(c, Converter):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
field_name = f"__c_field_{attr_name}"
globs[field_name] = a
internal_arg_parts[field_name] = a
def_str = f"{conv_name}({def_str}, instance, {field_name})"
elif isinstance(d, Factory):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
def_str = f"{conv_name}({def_str})"
else:
globs[def_name] = c(d)
internal_arg_parts[def_name] = c(d)
lines.append(f" if instance.{attr_name} != {def_str}:")
lines.append(f" res['{kn}'] = {invoke}")
else:
# No default or no override.
@@ -217,7 +255,7 @@ def make_dict_unstructure_fn(
converter: BaseConverter,
_cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool = False,
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]:
@@ -235,13 +273,22 @@ def make_dict_unstructure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""
origin = get_origin(cl)
attrs = adapted_fields(origin or cl) # type: ignore
mapping = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if is_generic(cl):
mapping = generate_mapping(cl, mapping)
@@ -289,7 +336,7 @@ def make_dict_structure_fn_from_attrs(
bool | Literal["from_converter"]
) = "from_converter",
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -314,7 +361,13 @@ def make_dict_structure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
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__
@@ -350,6 +403,9 @@ def make_dict_structure_fn_from_attrs(
if _cattrs_forbid_extra_keys == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if _cattrs_detailed_validation == "from_converter":
_cattrs_detailed_validation = converter.detailed_validation
if _cattrs_prefer_attrib_converters == "from_converter":
@@ -367,7 +423,13 @@ def make_dict_structure_fn_from_attrs(
internal_arg_parts["__c_avn"] = AttributeValidationNote
for a in attrs:
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:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
@@ -396,6 +458,8 @@ def make_dict_structure_fn_from_attrs(
ian = a.alias
if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else:
kn = override.rename
@@ -496,14 +560,24 @@ def make_dict_structure_fn_from_attrs(
# The first loop deals with required args.
for a in attrs:
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:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
continue
if a.default is not NOTHING:
non_required.append(a)
# The next loop will handle it.
continue
t = a.type
if isinstance(t, TypeVar):
t = typevar_map.get(t.__name__, t)
@@ -523,6 +597,8 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else:
kn = override.rename
allowed_fields.add(kn)
@@ -592,6 +668,8 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else:
kn = override.rename
allowed_fields.add(kn)
@@ -682,7 +760,7 @@ def make_dict_structure_fn(
bool | Literal["from_converter"]
) = "from_converter",
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False,
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -706,14 +784,20 @@ def make_dict_structure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
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
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 = {}
+17 -1
View File
@@ -4,15 +4,31 @@ from typing import TYPE_CHECKING, Any
from attrs import NOTHING, Attribute, Factory
from .._compat import is_bare_final
from .._compat import get_args, is_annotated, is_bare_final
from ..dispatch import StructureHook
from ..errors import StructureHandlerNotFoundError
from ..fns import raise_error
from ._consts import AttributeOverride
if TYPE_CHECKING:
from ..converters import BaseConverter
def _annotated_override_or_default(
type: Any, default: AttributeOverride
) -> AttributeOverride:
"""
If the type is Annotated containing an AttributeOverride, return it.
Otherwise, return the default.
"""
if is_annotated(type):
for arg in get_args(type):
if isinstance(arg, AttributeOverride):
return arg
return default
def find_structure_handler(
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
) -> StructureHook | None:
+61 -35
View File
@@ -3,23 +3,12 @@ from __future__ import annotations
import re
import sys
from collections.abc import Mapping
from inspect import get_annotations
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
from attrs import NOTHING, Attribute
from typing_extensions import _TypedDictMeta
try:
from inspect import get_annotations
def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)
except ImportError:
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
def get_annots(cl) -> dict[str, Any]:
return cl.__dict__.get("__annotations__", {})
from .._compat import (
get_full_type_hints,
get_notrequired_base,
@@ -40,7 +29,7 @@ from . import AttributeOverride
from ._consts import already_generating, neutral
from ._generics import generate_mapping
from ._lc import generate_unique_filename
from ._shared import find_structure_handler
from ._shared import _annotated_override_or_default, find_structure_handler
if TYPE_CHECKING:
from ..converters import BaseConverter
@@ -50,6 +39,10 @@ __all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
T = TypeVar("T")
def get_annots(cl) -> dict[str, Any]:
return get_annotations(cl, eval_str=True)
def make_dict_unstructure_fn(
cl: type[T],
converter: BaseConverter,
@@ -109,11 +102,20 @@ def make_dict_unstructure_fn(
# * all attributes resolve to `converter._unstructure_identity`
for a in attrs:
attr_name = a.name
override = kwargs.get(attr_name, neutral)
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[attr_name] = override
if override != neutral:
break
handler = None
t = a.type
if isinstance(t, TypeVar):
if t.__name__ in mapping:
@@ -125,9 +127,6 @@ def make_dict_unstructure_fn(
t = deep_copy_with(t, mapping, cl)
if handler is None:
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
try:
handler = converter.get_unstructure_hook(t)
except RecursionError:
@@ -142,10 +141,22 @@ def make_dict_unstructure_fn(
for ix, a in enumerate(attrs):
attr_name = a.name
override = kwargs.get(attr_name, neutral)
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[attr_name] = override
if override.omit:
lines.append(f" res.pop('{attr_name}', None)")
continue
if override.rename is not None:
# We also need to pop when renaming, since we're copying
# the original.
@@ -160,8 +171,6 @@ def make_dict_unstructure_fn(
if override.unstruct_hook is not None:
handler = override.unstruct_hook
else:
t = a.type
if isinstance(t, TypeVar):
if t.__name__ in mapping:
t = mapping[t.__name__]
@@ -171,9 +180,6 @@ def make_dict_unstructure_fn(
t = deep_copy_with(t, mapping, cl)
if handler is None:
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
try:
handler = converter.get_unstructure_hook(t)
except RecursionError:
@@ -220,12 +226,15 @@ def make_dict_unstructure_fn(
)
eval(compile(script, fname, "exec"), globs)
res = globs[fn_name]
res.overrides = kwargs
finally:
working_set.remove(cl)
if not working_set:
del already_generating.working_set
return globs[fn_name]
return res
def make_dict_structure_fn(
@@ -326,20 +335,25 @@ def make_dict_structure_fn(
for ix, a in enumerate(attrs):
an = a.name
attr_required = an in req_keys
override = kwargs.get(an, neutral)
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[an] = override
if override.omit:
continue
t = a.type
if isinstance(t, TypeVar):
t = mapping.get(t.__name__, t)
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
t = deep_copy_with(t, mapping, cl)
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if is_generic(t) and not is_bare(t) and not is_annotated(t):
t = deep_copy_with(t, mapping, cl)
@@ -399,7 +413,12 @@ def make_dict_structure_fn(
for ix, a in enumerate(attrs):
an = a.name
attr_required = an in req_keys
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:
continue
if not attr_required:
@@ -448,13 +467,18 @@ def make_dict_structure_fn(
if non_required:
for ix, a in non_required:
an = a.name
override = kwargs.get(an, neutral)
t = a.type
nrb = get_notrequired_base(t)
if nrb is not NOTHING:
t = nrb
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(t, neutral)
if override != neutral:
kwargs[an] = override
if isinstance(t, TypeVar):
t = mapping.get(t.__name__, t)
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
@@ -514,7 +538,9 @@ def make_dict_structure_fn(
)
eval(compile(script, fname, "exec"), globs)
return globs[fn_name]
res = globs[fn_name]
res.overrides = kwargs
return res
def _adapted_fields(cls: Any) -> list[Attribute]:
+2 -7
View File
@@ -1,17 +1,12 @@
import sys
from collections.abc import Callable
from datetime import datetime
from enum import Enum
from typing import Any, Callable, TypeVar, get_args
from typing import Any, ParamSpec, TypeVar, get_args
from .._compat import is_subclass
from ..converters import Converter, UnstructureHook
from ..fns import identity
if sys.version_info[:2] < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec
def validate_datetime(v, _):
if not isinstance(v, datetime):
+2 -2
View File
@@ -99,11 +99,11 @@ def configure_converter(converter: BaseConverter):
# datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running.
converter.register_unstructure_hook(datetime, lambda v: v)
converter.register_unstructure_hook(datetime, identity)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+1 -1
View File
@@ -37,7 +37,7 @@ def configure_converter(converter: BaseConverter):
)
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+1 -1
View File
@@ -52,7 +52,7 @@ def configure_converter(converter: BaseConverter) -> None:
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda _: identity)
configure_union_passthrough(Union[str, bool, int, float, None], converter)
+1 -1
View File
@@ -46,7 +46,7 @@ def configure_converter(converter: BaseConverter) -> None:
converter.register_structure_hook(
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
)
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+14 -3
View File
@@ -3,18 +3,27 @@
from __future__ import annotations
from base64 import b64decode
from collections.abc import Callable
from dataclasses import is_dataclass
from datetime import date, datetime
from enum import Enum
from functools import partial
from typing import Any, Callable, TypeVar, Union, get_type_hints
from typing import Any, TypeVar, Union, get_type_hints
from attrs import has as attrs_has
from attrs import resolve_types
from msgspec import Struct, convert, to_builtins
from msgspec.json import Encoder, decode
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
from .._compat import (
fields,
get_args,
get_origin,
is_bare,
is_mapping,
is_sequence,
is_subclass,
)
from ..cols import is_namedtuple
from ..converters import BaseConverter, Converter
from ..dispatch import UnstructureHook
@@ -74,7 +83,9 @@ def configure_converter(converter: Converter) -> None:
configure_passthroughs(converter)
converter.register_unstructure_hook(Struct, to_builtins)
converter.register_unstructure_hook(Enum, identity)
converter.register_unstructure_hook_factory(
lambda t: is_subclass(t, Enum), lambda t, c: identity
)
converter.register_structure_hook(Struct, convert)
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
+2 -2
View File
@@ -87,8 +87,8 @@ def configure_converter(converter: Converter) -> None:
),
]
)
converter.register_unstructure_hook_func(
partial(is_primitive_enum, include_bare_enums=True), identity
converter.register_unstructure_hook_factory(
partial(is_primitive_enum, include_bare_enums=True), lambda t: identity
)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
+9 -3
View File
@@ -12,6 +12,7 @@ from tomlkit.items import Float, Integer, String
from .._compat import is_mapping, is_subclass
from ..converters import BaseConverter, Converter
from ..fns import identity
from ..strategies import configure_union_passthrough
from . import validate_datetime, wrap
@@ -37,6 +38,9 @@ def configure_converter(converter: BaseConverter):
* sets are serialized as lists
* tuples are serializas as lists
* mapping keys are coerced into strings when unstructuring
.. versionchanged:: 26.1.0
date objects are now passed through to tomlkit without unstructuring.
"""
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
converter.register_unstructure_hook(
@@ -67,10 +71,12 @@ def configure_converter(converter: BaseConverter):
# datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running.
converter.register_unstructure_hook(datetime, lambda v: v)
converter.register_unstructure_hook(datetime, identity)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook(date, identity)
converter.register_structure_hook(
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
)
configure_union_passthrough(
Union[str, String, bool, int, Integer, float, Float], converter
)
+94
View File
@@ -0,0 +1,94 @@
"""Preconfigured converters for tomllib."""
from base64 import b85decode, b85encode
from collections.abc import Set
from datetime import date, datetime
from enum import Enum
from operator import attrgetter
from typing import Any, TypeVar, Union
try:
from tomllib import loads
except ImportError:
from tomli import loads
try:
from tomli_w import dumps
except ImportError: # pragma: nocover
dumps = None
from .._compat import is_mapping, is_subclass
from ..converters import BaseConverter, Converter
from ..fns import identity
from ..strategies import configure_union_passthrough
from . import validate_datetime, wrap
__all__ = ["TomllibConverter", "configure_converter", "make_converter"]
T = TypeVar("T")
_enum_value_getter = attrgetter("_value_")
class TomllibConverter(Converter):
"""A converter subclass specialized for tomllib."""
if dumps is not None:
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
def loads(self, data: str, cl: type[T], **kwargs: Any) -> T:
return self.structure(loads(data, **kwargs), cl)
def configure_converter(converter: BaseConverter):
"""
Configure the converter for use with the tomllib library.
* bytes are serialized as base85 strings
* sets are serialized as lists
* tuples are serializas as lists
* mapping keys are coerced into strings when unstructuring
* dates and datetimes are left for tomllib to handle
"""
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
converter.register_unstructure_hook(
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
)
@converter.register_unstructure_hook_factory(is_mapping)
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
key_handler = str
args = getattr(cl, "__args__", None)
if args:
if is_subclass(args[0], str):
key_handler = _enum_value_getter if is_subclass(args[0], Enum) else None
elif is_subclass(args[0], bytes):
def key_handler(k: bytes):
return b85encode(k).decode("utf8")
return converter.gen_unstructure_mapping(
cl, unstructure_to=unstructure_to, key_handler=key_handler
)
converter.register_unstructure_hook(datetime, identity)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, identity)
converter.register_structure_hook(
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
)
configure_union_passthrough(Union[str, int, float, bool], converter)
@wrap(TomllibConverter)
def make_converter(*args: Any, **kwargs: Any) -> TomllibConverter:
kwargs["unstruct_collection_overrides"] = {
Set: list,
tuple: list,
**kwargs.get("unstruct_collection_overrides", {}),
}
res = TomllibConverter(*args, **kwargs)
configure_converter(res)
return res
+1 -1
View File
@@ -47,7 +47,7 @@ def configure_converter(converter: BaseConverter):
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_func(is_primitive_enum, identity)
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory
)
+20 -7
View File
@@ -9,21 +9,25 @@ from typing import Any, Callable, TypeVar, Union
from ..converters import BaseConverter
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
from ..gen._consts import already_generating
from ..subclasses import subclasses
def _make_subclasses_tree(cl: type) -> list[type]:
# get class origin for accessing subclasses (see #648 for more info)
cls_origin = typing.get_origin(cl) or cl
return [cl] + [
sscl
for scl in cls_origin.__subclasses__()
for sscl in _make_subclasses_tree(scl)
]
# Use a dict to deduplicate and keep insertion order.
seen = {cl: None}
for scl in subclasses(cls_origin):
for sscl in _make_subclasses_tree(scl):
seen[sscl] = None
return list(seen)
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
"""Whether the given class has subclasses from `given_subclasses`."""
actual = set(cl.__subclasses__())
cls_origin = typing.get_origin(cl) or cl
actual = set(subclasses(cls_origin))
given = set(given_subclasses)
return bool(actual & given)
@@ -68,6 +72,9 @@ def include_subclasses(
.. versionchanged:: 24.1.0
When overrides are not provided, hooks for individual classes are retrieved from
the converter instead of generated with no overrides, using converter defaults.
.. versionchanged:: 25.2.0
Slotted dataclasses work on Python 3.14 via :func:`cattrs.subclasses.subclasses`,
which filters out duplicate classes caused by slotting.
"""
# Due to https://github.com/python-attrs/attrs/issues/1047
collect()
@@ -231,7 +238,13 @@ def _include_subclasses_with_union_strategy(
return cls is _cl
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
subclasses = tuple(
[
c
for c in union_classes
if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)
]
)
if len(subclasses) > 1:
u = Union[subclasses] # type: ignore
union_strategy(u, converter)
+65 -45
View File
@@ -52,23 +52,10 @@ def configure_tagged_union(
if is_type_alias(union):
union = union.__value__
args = union.__args__
tag_to_hook = {}
exact_cl_unstruct_hooks = {}
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}
cl_to_tag = {}
if default is not NOTHING:
default_handler = converter.get_structure_hook(default)
@@ -76,36 +63,9 @@ def configure_tagged_union(
def structure_default(val: dict, _cl=default, _h=default_handler):
return _h(val, _cl)
tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
cl_to_tag = defaultdict(lambda: default, cl_to_tag)
tag_to_hook = defaultdict(lambda: structure_default)
cl_to_tag = defaultdict(lambda: default)
def unstructure_tagged_union(
val: union,
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
_cl_to_tag=cl_to_tag,
_tag_name=tag_name,
) -> dict:
res = _exact_cl_unstruct_hooks[val.__class__](val)
res[_tag_name] = _cl_to_tag[val.__class__]
return res
if default is NOTHING:
if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
val = val.copy()
return _tag_to_cl[val.pop(_tag_name)](val)
else:
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
return _tag_to_cl[val[_tag_name]](val)
else:
if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union(
@@ -135,11 +95,54 @@ def configure_tagged_union(
return _tag_to_hook[val[_tag_name]](val)
return _dh(val, _default)
else:
if getattr(converter, "forbid_extra_keys", False):
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
val = val.copy()
return _tag_to_cl[val.pop(_tag_name)](val)
else:
def structure_tagged_union(
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
) -> union:
return _tag_to_cl[val[_tag_name]](val)
def unstructure_tagged_union(
val: union,
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
_cl_to_tag=cl_to_tag,
_tag_name=tag_name,
) -> dict:
res = _exact_cl_unstruct_hooks[val.__class__](val)
res[_tag_name] = _cl_to_tag[val.__class__]
return res
converter.register_unstructure_hook(union, unstructure_tagged_union)
converter.register_structure_hook(union, structure_tagged_union)
for cl in args:
tag = tag_generator(cl)
struct_handler = converter.get_structure_hook(cl)
unstruct_handler = converter.get_unstructure_hook(cl)
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
return _h(val, _cl)
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
return _h(val)
tag_to_hook[tag] = structure_union_member
exact_cl_unstruct_hooks[cl] = unstructure_union_member
cl_to_tag[cl] = tag
def configure_union_passthrough(
union: Any, converter: BaseConverter, accept_ints_as_floats: bool = True
) -> None:
"""
Configure the converter to support validating and passing through unions of the
provided types and their subsets.
@@ -162,7 +165,14 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
If the union contains a class and one or more of its subclasses, the subclasses
will also be included when validating the superclass.
:param accept_ints_as_floats: When set (the default), if the provided union
contains both ints and floats, actual unions containing only floats will also accept
ints. See https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
for more information.
.. versionadded:: 23.2.0
.. versionchanged:: 25.2.0
Introduced the `accept_ints_as_floats` parameter.
"""
args = set(union.__args__)
@@ -205,6 +215,16 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
and not is_literal(a)
}
# By default, when floats are part of the union, accept ints too.
if (
accept_ints_as_floats
and int in args
and float in args
and float in non_literal_classes
and int not in non_literal_classes
):
non_literal_classes.add(int)
if spillover:
spillover_type = (
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
+27
View File
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,19 @@
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 @@
pip
uv
@@ -1,4 +1,4 @@
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
importlib_metadata-6.8.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
importlib_metadata-6.8.0.dist-info/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
importlib_metadata-6.8.0.dist-info/RECORD,,
@@ -6,15 +6,6 @@ importlib_metadata-6.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
importlib_metadata-6.8.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
importlib_metadata/__init__.py,sha256=EiH0qTKP_6oa6pRGJgPrq0kvjnL3hJ18BJH8VaAYSBA,30749
importlib_metadata/__pycache__/__init__.cpython-311.pyc,,
importlib_metadata/__pycache__/_adapters.cpython-311.pyc,,
importlib_metadata/__pycache__/_collections.cpython-311.pyc,,
importlib_metadata/__pycache__/_compat.cpython-311.pyc,,
importlib_metadata/__pycache__/_functools.cpython-311.pyc,,
importlib_metadata/__pycache__/_itertools.cpython-311.pyc,,
importlib_metadata/__pycache__/_meta.cpython-311.pyc,,
importlib_metadata/__pycache__/_py39compat.cpython-311.pyc,,
importlib_metadata/__pycache__/_text.cpython-311.pyc,,
importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554
@@ -1 +1 @@
pip
uv
@@ -1,15 +1,10 @@
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
lsprotocol-2023.0.1.dist-info/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
lsprotocol-2023.0.1.dist-info/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
lsprotocol-2023.0.1.dist-info/RECORD,,
lsprotocol-2023.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
lsprotocol/__pycache__/__init__.cpython-311.pyc,,
lsprotocol/__pycache__/_hooks.cpython-311.pyc,,
lsprotocol/__pycache__/converters.cpython-311.pyc,,
lsprotocol/__pycache__/types.cpython-311.pyc,,
lsprotocol/__pycache__/validators.cpython-311.pyc,,
lsprotocol/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1 +0,0 @@
pip
@@ -1,41 +0,0 @@
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
@@ -0,0 +1 @@
uv
@@ -1,14 +1,13 @@
Metadata-Version: 2.4
Name: packaging
Version: 25.0
Version: 26.2
Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-Expression: Apache-2.0 OR BSD-2-Clause
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
@@ -18,8 +17,10 @@ Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
Classifier: Typing :: Typed
License-File: LICENSE
License-File: LICENSE.APACHE
@@ -42,7 +43,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`).
.. end-intro
The ``packaging`` project includes the following: version handling, specifiers,
markers, requirements, tags, utilities.
markers, requirements, tags, metadata, lockfiles, utilities.
Documentation
-------------
@@ -52,8 +53,14 @@ The `documentation`_ provides information and the API for the following:
- Version Handling
- Specifiers
- Markers
- Licenses
- Requirements
- Metadata
- Tags
- Lockfiles (pylock)
- Direct URL helpers
- Dependency groups
- Errors
- Utilities
Installation
@@ -70,11 +77,11 @@ Discussion
If you run into bugs, you can file them in our `issue tracker`_.
You can also join ``#pypa`` on Freenode to ask questions or get involved.
You can also join discussions on `GitHub Discussions`_ to ask questions or get involved.
.. _`documentation`: https://packaging.pypa.io/
.. _`issue tracker`: https://github.com/pypa/packaging/issues
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
Code of Conduct
@@ -0,0 +1,29 @@
packaging-26.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
packaging-26.2.dist-info/METADATA,sha256=T5y815M0FaR5P3dnyYoralEsgj_IHIczeBVwXyMOyr8,3543
packaging-26.2.dist-info/RECORD,,
packaging-26.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging-26.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
packaging-26.2.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
packaging-26.2.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
packaging-26.2.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
packaging/__init__.py,sha256=QhMEdPu2XogrJzV3S0KWS6t7l0I9k8EeDRJl4fnw87s,494
packaging/_elffile.py,sha256=-sKkptYqzYw2-x3QByJa5mB4rfPWu1pxkZHRx1WAFCY,3211
packaging/_manylinux.py,sha256=Hf6nB0cOrayEs96-p3oIXAgGnFquv20DO5l-o2_Xnv0,9559
packaging/_musllinux.py,sha256=Z6swjH3MA7XS3qXnmMN7QPhqP3fnoYI0eQ18e9-HgAE,2707
packaging/_parser.py,sha256=Kf2nsDw4c54X82pY8ba4F02Bve6OygGMAjL-Begqcew,11698
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
packaging/_tokenizer.py,sha256=tFU2Wr-ZZJdAbkXLEJo7qUQDJaIkfft9DqaifiEND7A,5391
packaging/dependency_groups.py,sha256=XZIAVFK9uHG4RCGprmJn3VInUWMesxha_kytJuMO9eY,10218
packaging/direct_url.py,sha256=eKmbDiPP1sLV4Mj_kCSZqqknrIyVO9Sr7JpF8KCjp4U,10917
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
packaging/licenses/__init__.py,sha256=_Jx0XRiD_58palsWnyLrLuh59ZpGCPIPXLKdZo9OJvQ,7293
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
packaging/markers.py,sha256=8fDIUhAF6YMnCNB5FSiwh9pEIusiFzAF73J-0OB8bTk,17055
packaging/metadata.py,sha256=crAh0E3GVGVqPlu6EdRFsaG-Y6UYznTUqjuGKRGPv6c,38770
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging/pylock.py,sha256=G_1gncTmDbRLY1jo4VDI9Uw-b5IErh_Q9V_BbVJTmD8,33890
packaging/requirements.py,sha256=dd1c9aa1gp5NI6btF6UFRQjPn1nxQXnE_T34yDDTEpc,4383
packaging/specifiers.py,sha256=Mfp8avQg0lVot17to9lVKBtZD1FsWBTItoGwFUZ3wtg,71514
packaging/tags.py,sha256=NQ1weo69_Sjte3xBZ1I_G63CIgCmaN0C24mz-z3hGYo,34224
packaging/utils.py,sha256=M7-JMKic2sP1YtV_8aW7eVGB-x3ADuKCiSrsVeCd2Uo,9848
packaging/version.py,sha256=Y1aTtxe3sn2xOMa5BdI85-AcHuybbanOVkEvvSRRC8I,38369
+1 -1
View File
@@ -6,7 +6,7 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
__version__ = "25.0"
__version__ = "26.2"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
-1
View File
@@ -4,7 +4,6 @@ ELF file parser.
This provides a class ``ELFFile`` that parses an ELF executable in a similar
interface to ``ZipFile``. Only the read interface is implemented.
Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
+36 -36
View File
@@ -15,6 +15,16 @@ EF_ARM_ABIMASK = 0xFF000000
EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400
_ALLOWED_ARCHS = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then.
@@ -57,16 +67,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
return _is_linux_armhf(executable)
if "i686" in archs:
return _is_linux_i686(executable)
allowed_archs = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
return any(arch in allowed_archs for arch in archs)
return any(arch in _ALLOWED_ARCHS for arch in archs)
# If glibc ever changes its major version, we need to know what the last
@@ -106,7 +107,7 @@ def _glibc_version_string_ctypes() -> str | None:
Fallback implementation of glibc_version_string using ctypes.
"""
try:
import ctypes
import ctypes # noqa: PLC0415
except ImportError:
return None
@@ -150,7 +151,7 @@ def _glibc_version_string() -> str | None:
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
def _parse_glibc_version(version_str: str) -> tuple[int, int]:
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
"""Parse glibc version.
We use a regexp instead of str.split because we want to discard any
@@ -165,15 +166,15 @@ def _parse_glibc_version(version_str: str) -> tuple[int, int]:
RuntimeWarning,
stacklevel=2,
)
return -1, -1
return int(m.group("major")), int(m.group("minor"))
return _GLibCVersion(-1, -1)
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
@functools.lru_cache
def _get_glibc_version() -> tuple[int, int]:
def _get_glibc_version() -> _GLibCVersion:
version_str = _glibc_version_string()
if version_str is None:
return (-1, -1)
return _GLibCVersion(-1, -1)
return _parse_glibc_version(version_str)
@@ -184,7 +185,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
return False
# Check for presence of _manylinux module.
try:
import _manylinux
import _manylinux # noqa: PLC0415
except ImportError:
return True
if hasattr(_manylinux, "manylinux_compatible"):
@@ -192,25 +193,26 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
if result is not None:
return bool(result)
return True
if version == _GLibCVersion(2, 5):
if hasattr(_manylinux, "manylinux1_compatible"):
return bool(_manylinux.manylinux1_compatible)
if version == _GLibCVersion(2, 12):
if hasattr(_manylinux, "manylinux2010_compatible"):
return bool(_manylinux.manylinux2010_compatible)
if version == _GLibCVersion(2, 17):
if hasattr(_manylinux, "manylinux2014_compatible"):
return bool(_manylinux.manylinux2014_compatible)
if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"):
return bool(_manylinux.manylinux1_compatible)
if version == _GLibCVersion(2, 12) and hasattr(
_manylinux, "manylinux2010_compatible"
):
return bool(_manylinux.manylinux2010_compatible)
if version == _GLibCVersion(2, 17) and hasattr(
_manylinux, "manylinux2014_compatible"
):
return bool(_manylinux.manylinux2014_compatible)
return True
_LEGACY_MANYLINUX_MAP = {
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
(2, 17): "manylinux2014",
_GLibCVersion(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
(2, 12): "manylinux2010",
_GLibCVersion(2, 12): "manylinux2010",
# CentOS 5 w/ glibc 2.5 (PEP 513)
(2, 5): "manylinux1",
_GLibCVersion(2, 5): "manylinux1",
}
@@ -252,11 +254,9 @@ def platform_tags(archs: Sequence[str]) -> Iterator[str]:
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, glibc_version):
yield f"{tag}_{arch}"
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(arch, glibc_version):
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
yield f"{legacy_tag}_{arch}"
+1 -1
View File
@@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None:
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
return _parse_musl_version(proc.stderr)
+50 -10
View File
@@ -7,12 +7,14 @@ the implementation.
from __future__ import annotations
import ast
from typing import NamedTuple, Sequence, Tuple, Union
from typing import List, Literal, NamedTuple, Sequence, Tuple, Union
from ._tokenizer import DEFAULT_RULES, Tokenizer
class Node:
__slots__ = ("value",)
def __init__(self, value: str) -> None:
self.value = value
@@ -20,31 +22,66 @@ class Node:
return self.value
def __repr__(self) -> str:
return f"<{self.__class__.__name__}('{self}')>"
return f"<{self.__class__.__name__}({self.value!r})>"
def serialize(self) -> str:
raise NotImplementedError
def __getstate__(self) -> str:
# Return just the value string for compactness and stability.
return self.value
def _restore_value(self, value: object) -> None:
if not isinstance(value, str):
raise TypeError(
f"Cannot restore {self.__class__.__name__} value from {value!r}"
)
self.value = value
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the value string.
self._restore_value(state)
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "value" in slot_dict:
self._restore_value(slot_dict["value"])
return
if isinstance(state, dict) and "value" in state:
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
self._restore_value(state["value"])
return
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
class Variable(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
class Value(Node):
__slots__ = ()
def serialize(self) -> str:
return f'"{self}"'
class Op(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
MarkerLogical = Literal["and", "or"]
MarkerVar = Union[Variable, Value]
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]
class ParsedRequirement(NamedTuple):
@@ -111,7 +148,9 @@ def _parse_requirement_details(
return (url, specifier, marker)
marker = _parse_requirement_marker(
tokenizer, span_start=url_start, after="URL and whitespace"
tokenizer,
span_start=url_start,
expected="semicolon (after URL and whitespace)",
)
else:
specifier_start = tokenizer.position
@@ -124,10 +163,10 @@ def _parse_requirement_details(
marker = _parse_requirement_marker(
tokenizer,
span_start=specifier_start,
after=(
"version specifier"
expected=(
"comma (within version specifier), semicolon (after version specifier)"
if specifier
else "name and no valid version specifier"
else "semicolon (after name with no version specifier)"
),
)
@@ -135,7 +174,7 @@ def _parse_requirement_details(
def _parse_requirement_marker(
tokenizer: Tokenizer, *, span_start: int, after: str
tokenizer: Tokenizer, *, span_start: int, expected: str
) -> MarkerList:
"""
requirement_marker = SEMICOLON marker WS?
@@ -143,8 +182,9 @@ def _parse_requirement_marker(
if not tokenizer.check("SEMICOLON"):
tokenizer.raise_syntax_error(
f"Expected end or semicolon (after {after})",
f"Expected {expected} or end",
span_start=span_start,
span_end=None,
)
tokenizer.read()
@@ -307,7 +347,7 @@ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
return (marker_var_left, marker_op, marker_var_right)
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
"""
marker_var = VARIABLE | QUOTED_STRING
"""
+17 -45
View File
@@ -2,60 +2,32 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""Backward-compatibility shim for unpickling Version objects serialized before
packaging 26.1.
Old pickles reference ``packaging._structures.InfinityType`` and
``packaging._structures.NegativeInfinityType``. This module provides minimal
stand-in classes so that ``pickle.loads()`` can resolve those references.
The deserialized objects are not used for comparisons — ``Version.__setstate__``
discards the stale ``_key`` cache and recomputes it from the core version fields.
"""
from __future__ import annotations
class InfinityType:
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
def __repr__(self) -> str:
return "Infinity"
def __hash__(self) -> int:
return hash(repr(self))
def __lt__(self, other: object) -> bool:
return False
def __le__(self, other: object) -> bool:
return False
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__)
def __gt__(self, other: object) -> bool:
return True
def __ge__(self, other: object) -> bool:
return True
def __neg__(self: object) -> "NegativeInfinityType":
return NegativeInfinity
Infinity = InfinityType()
class NegativeInfinityType:
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
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
Infinity = InfinityType()
NegativeInfinity = NegativeInfinityType()
+24 -26
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import contextlib
import re
from dataclasses import dataclass
from typing import Iterator, NoReturn
from typing import Generator, Mapping, NoReturn
from .specifiers import Specifier
@@ -33,16 +33,16 @@ class ParserSyntaxError(Exception):
def __str__(self) -> str:
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
return "\n ".join([self.message, self.source, marker])
return f"{self.message}\n {self.source}\n {marker}"
DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
"LEFT_PARENTHESIS": r"\(",
"RIGHT_PARENTHESIS": r"\)",
"LEFT_BRACKET": r"\[",
"RIGHT_BRACKET": r"\]",
"SEMICOLON": r";",
"COMMA": r",",
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
"LEFT_PARENTHESIS": re.compile(r"\("),
"RIGHT_PARENTHESIS": re.compile(r"\)"),
"LEFT_BRACKET": re.compile(r"\["),
"RIGHT_BRACKET": re.compile(r"\]"),
"SEMICOLON": re.compile(r";"),
"COMMA": re.compile(r","),
"QUOTED_STRING": re.compile(
r"""
(
@@ -53,10 +53,10 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
""",
re.VERBOSE,
),
"OP": r"(===|==|~=|!=|<=|>=|<|>)",
"BOOLOP": r"\b(or|and)\b",
"IN": r"\bin\b",
"NOT": r"\bnot\b",
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
"BOOLOP": re.compile(r"\b(or|and)\b"),
"IN": re.compile(r"\bin\b"),
"NOT": re.compile(r"\bnot\b"),
"VARIABLE": re.compile(
r"""
\b(
@@ -75,16 +75,16 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
re.VERBOSE,
),
"SPECIFIER": re.compile(
Specifier._operator_regex_str + Specifier._version_regex_str,
Specifier._specifier_regex_str,
re.VERBOSE | re.IGNORECASE,
),
"AT": r"\@",
"URL": r"[^ \t]+",
"IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
"VERSION_PREFIX_TRAIL": r"\.\*",
"VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
"WS": r"[ \t]+",
"END": r"$",
"AT": re.compile(r"\@"),
"URL": re.compile(r"[^ \t]+"),
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
"WS": re.compile(r"[ \t]+"),
"END": re.compile(r"$"),
}
@@ -99,12 +99,10 @@ class Tokenizer:
self,
source: str,
*,
rules: dict[str, str | re.Pattern[str]],
rules: Mapping[str, re.Pattern[str]],
) -> None:
self.source = source
self.rules: dict[str, re.Pattern[str]] = {
name: re.compile(pattern) for name, pattern in rules.items()
}
self.rules = rules
self.next_token: Token | None = None
self.position = 0
@@ -174,7 +172,7 @@ class Tokenizer:
@contextlib.contextmanager
def enclosing_tokens(
self, open_token: str, close_token: str, *, around: str
) -> Iterator[None]:
) -> Generator[None, None, None]:
if self.check(open_token):
open_position = self.position
self.read()
+302
View File
@@ -0,0 +1,302 @@
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
@@ -0,0 +1,325 @@
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
@@ -0,0 +1,94 @@
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)
+52 -11
View File
@@ -34,7 +34,7 @@ from __future__ import annotations
import re
from typing import NewType, cast
from packaging.licenses._spdx import EXCEPTIONS, LICENSES
from ._spdx import EXCEPTIONS, LICENSES
__all__ = [
"InvalidLicenseExpression",
@@ -42,14 +42,25 @@ __all__ = [
"canonicalize_license_expression",
]
# Simple __dir__ implementation since there are no public submodules
def __dir__() -> list[str]:
return __all__
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized
License-Expression.
"""
class InvalidLicenseExpression(ValueError):
"""Raised when a license-expression string is invalid
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("invalid")
Traceback (most recent call last):
...
@@ -60,6 +71,34 @@ class InvalidLicenseExpression(ValueError):
def canonicalize_license_expression(
raw_license_expression: str,
) -> NormalizedLicenseExpression:
"""
This function takes a valid License-Expression, and returns the normalized
form of it.
The return type is typed as :class:`NormalizedLicenseExpression`. This
allows type checkers to help require that a string has passed through this
function before use.
:param str raw_license_expression: The License-Expression to canonicalize.
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
invalid/unknown license identifier or invalid syntax.
.. doctest::
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("mit")
'MIT'
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
>>> canonicalize_license_expression("(mit")
Traceback (most recent call last):
...
InvalidLicenseExpression: Invalid license expression: '(mit'
>>> canonicalize_license_expression("Use-it-after-midnight")
Traceback (most recent call last):
...
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
"""
if not raw_license_expression:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
@@ -80,16 +119,21 @@ def canonicalize_license_expression(
tokens = license_expression.split()
# Rather than implementing boolean logic, we create an expression that Python can
# parse. Everything that is not involved with the grammar itself is treated as
# `False` and the expression should evaluate as such.
# Rather than implementing a parenthesis/boolean logic parser, create an
# expression that Python can parse. Everything that is not involved with the
# grammar itself is replaced with the placeholder `False` and the resultant
# expression should become a valid Python expression.
python_tokens = []
for token in tokens:
if token not in {"or", "and", "with", "(", ")"}:
python_tokens.append("False")
elif token == "with":
python_tokens.append("or")
elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}:
elif (
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}"
raise InvalidLicenseExpression(message)
else:
@@ -97,11 +141,8 @@ def canonicalize_license_expression(
python_expression = " ".join(python_tokens)
try:
invalid = eval(python_expression, globals(), locals())
except Exception:
invalid = True
if invalid is not False:
compile(python_expression, "", "eval")
except SyntaxError:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) from None
@@ -140,6 +181,6 @@ def canonicalize_license_expression(
normalized_expression = " ".join(normalized_tokens)
return cast(
NormalizedLicenseExpression,
"NormalizedLicenseExpression",
normalized_expression.replace("( ", "(").replace(" )", ")"),
)
+41 -1
View File
@@ -12,7 +12,7 @@ class SPDXException(TypedDict):
deprecated: bool
VERSION = '3.25.0'
VERSION = '3.27.0'
LICENSES: dict[str, SPDXLicense] = {
'0bsd': {'id': '0BSD', 'deprecated': False},
@@ -46,6 +46,7 @@ LICENSES: dict[str, SPDXLicense] = {
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
'any-osi': {'id': 'any-OSI', 'deprecated': False},
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
@@ -61,6 +62,8 @@ LICENSES: dict[str, SPDXLicense] = {
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
@@ -75,6 +78,7 @@ LICENSES: dict[str, SPDXLicense] = {
'blessing': {'id': 'blessing', 'deprecated': False},
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
'borceux': {'id': 'Borceux', 'deprecated': False},
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
@@ -85,6 +89,7 @@ LICENSES: dict[str, SPDXLicense] = {
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
@@ -176,6 +181,8 @@ LICENSES: dict[str, SPDXLicense] = {
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
@@ -215,6 +222,7 @@ LICENSES: dict[str, SPDXLicense] = {
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
'cronyx': {'id': 'Cronyx', 'deprecated': False},
'crossword': {'id': 'Crossword', 'deprecated': False},
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
'cube': {'id': 'Cube', 'deprecated': False},
@@ -226,7 +234,9 @@ LICENSES: dict[str, SPDXLicense] = {
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
'doc': {'id': 'DOC', 'deprecated': False},
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
@@ -263,12 +273,17 @@ LICENSES: dict[str, SPDXLicense] = {
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
'fsful': {'id': 'FSFUL', 'deprecated': False},
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
'ftl': {'id': 'FTL', 'deprecated': False},
'furuseth': {'id': 'Furuseth', 'deprecated': False},
'fwlw': {'id': 'fwlw', 'deprecated': False},
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
'gd': {'id': 'GD', 'deprecated': False},
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
@@ -320,6 +335,7 @@ LICENSES: dict[str, SPDXLicense] = {
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
'gutmann': {'id': 'Gutmann', 'deprecated': False},
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
'hdf5': {'id': 'HDF5', 'deprecated': False},
'hdparm': {'id': 'hdparm', 'deprecated': False},
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
@@ -360,6 +376,7 @@ LICENSES: dict[str, SPDXLicense] = {
'imlib2': {'id': 'Imlib2', 'deprecated': False},
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
'intel': {'id': 'Intel', 'deprecated': False},
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
@@ -369,6 +386,7 @@ LICENSES: dict[str, SPDXLicense] = {
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
'jam': {'id': 'Jam', 'deprecated': False},
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
'jove': {'id': 'jove', 'deprecated': False},
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
'jpnic': {'id': 'JPNIC', 'deprecated': False},
'json': {'id': 'JSON', 'deprecated': False},
@@ -394,6 +412,7 @@ LICENSES: dict[str, SPDXLicense] = {
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
'libpng': {'id': 'Libpng', 'deprecated': False},
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
'libtiff': {'id': 'libtiff', 'deprecated': False},
@@ -424,14 +443,17 @@ LICENSES: dict[str, SPDXLicense] = {
'magaz': {'id': 'magaz', 'deprecated': False},
'mailprio': {'id': 'mailprio', 'deprecated': False},
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
'man2html': {'id': 'man2html', 'deprecated': False},
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
'metamail': {'id': 'metamail', 'deprecated': False},
'minpack': {'id': 'Minpack', 'deprecated': False},
'mips': {'id': 'MIPS', 'deprecated': False},
'miros': {'id': 'MirOS', 'deprecated': False},
'mit': {'id': 'MIT', 'deprecated': False},
'mit-0': {'id': 'MIT-0', 'deprecated': False},
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
@@ -472,6 +494,7 @@ LICENSES: dict[str, SPDXLicense] = {
'netcdf': {'id': 'NetCDF', 'deprecated': False},
'newsletr': {'id': 'Newsletr', 'deprecated': False},
'ngpl': {'id': 'NGPL', 'deprecated': False},
'ngrep': {'id': 'ngrep', 'deprecated': False},
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
@@ -486,6 +509,7 @@ LICENSES: dict[str, SPDXLicense] = {
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
'nrl': {'id': 'NRL', 'deprecated': False},
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
'ntp': {'id': 'NTP', 'deprecated': False},
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
'nunit': {'id': 'Nunit', 'deprecated': True},
@@ -580,6 +604,7 @@ LICENSES: dict[str, SPDXLicense] = {
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
'sendmail': {'id': 'Sendmail', 'deprecated': False},
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
@@ -592,10 +617,12 @@ LICENSES: dict[str, SPDXLicense] = {
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
'sl': {'id': 'SL', 'deprecated': False},
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
'smppl': {'id': 'SMPPL', 'deprecated': False},
'snia': {'id': 'SNIA', 'deprecated': False},
'snprintf': {'id': 'snprintf', 'deprecated': False},
'sofa': {'id': 'SOFA', 'deprecated': False},
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
'soundex': {'id': 'Soundex', 'deprecated': False},
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
@@ -609,6 +636,7 @@ LICENSES: dict[str, SPDXLicense] = {
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
'sunpro': {'id': 'SunPro', 'deprecated': False},
@@ -620,12 +648,14 @@ LICENSES: dict[str, SPDXLicense] = {
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
'tmate': {'id': 'TMate', 'deprecated': False},
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
'tosl': {'id': 'TOSL', 'deprecated': False},
'tpdl': {'id': 'TPDL', 'deprecated': False},
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
'ttwl': {'id': 'TTWL', 'deprecated': False},
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
@@ -641,6 +671,8 @@ LICENSES: dict[str, SPDXLicense] = {
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
'unlicense': {'id': 'Unlicense', 'deprecated': False},
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
'vim': {'id': 'Vim', 'deprecated': False},
@@ -654,6 +686,7 @@ LICENSES: dict[str, SPDXLicense] = {
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
'wwl': {'id': 'wwl', 'deprecated': False},
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
'x11': {'id': 'X11', 'deprecated': False},
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
@@ -695,9 +728,11 @@ EXCEPTIONS: dict[str, SPDXException] = {
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
@@ -714,13 +749,16 @@ EXCEPTIONS: dict[str, SPDXException] = {
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
@@ -730,12 +768,14 @@ EXCEPTIONS: dict[str, SPDXException] = {
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
+185 -55
View File
@@ -8,7 +8,7 @@ import operator
import os
import platform
import sys
from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast
from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
from ._parser import parse_marker as _parse_marker
@@ -17,6 +17,7 @@ from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
__all__ = [
"Environment",
"EvaluateContext",
"InvalidMarker",
"Marker",
@@ -25,31 +26,58 @@ __all__ = [
"default_environment",
]
def __dir__() -> list[str]:
return __all__
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
* ``"metadata"`` (for core metadata; default)
* ``"lock_file"`` (for lock files)
* ``"requirement"`` (i.e. all other situations)
"""
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
MARKERS_REQUIRING_VERSION = {
"implementation_version",
"platform_release",
"python_full_version",
"python_version",
}
class InvalidMarker(ValueError):
"""
An invalid marker was found, users should refer to PEP 508.
"""Raised when attempting to create a :class:`Marker` from invalid input.
This error indicates that the given marker string does not conform to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
"""
class UndefinedComparison(ValueError):
"""
An invalid operation was attempted on a value that doesn't support it.
"""Raised when evaluating an unsupported marker comparison.
This can happen when marker values are compared as versions but do not
conform to the :ref:`specification of version specifiers
<pypug:version-specifiers>`.
"""
class UndefinedEnvironmentName(ValueError):
"""
A name was attempted to be used that does not exist inside of the
environment.
"""
"""Raised when evaluating a marker that references a missing environment key."""
class Environment(TypedDict):
"""
A dictionary that represents a Python environment as captured by
:func:`default_environment`. All fields are required.
"""
implementation_name: str
"""The implementation's identifier, e.g. ``'cpython'``."""
@@ -121,20 +149,28 @@ class Environment(TypedDict):
"""
def _normalize_extra_values(results: Any) -> Any:
def _normalize_extras(
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.
"""
if isinstance(results[0], tuple):
lhs, op, rhs = results[0]
if isinstance(lhs, Variable) and lhs.value == "extra":
normalized_extra = canonicalize_name(rhs.value)
rhs = Value(normalized_extra)
elif isinstance(rhs, Variable) and rhs.value == "extra":
normalized_extra = canonicalize_name(lhs.value)
lhs = Value(normalized_extra)
results[0] = lhs, op, rhs
return results
return [_normalize_extras(r) for r in results]
def _format_marker(
@@ -168,25 +204,26 @@ def _format_marker(
_operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": operator.lt,
"<=": operator.le,
"<": lambda _lhs, _rhs: False,
"<=": operator.eq,
"==": operator.eq,
"!=": operator.ne,
">=": operator.ge,
">": operator.gt,
">=": operator.eq,
">": lambda _lhs, _rhs: False,
}
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
if isinstance(rhs, str):
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
op_str = op.serialize()
if key in MARKERS_REQUIRING_VERSION:
try:
spec = Specifier("".join([op.serialize(), rhs]))
spec = Specifier(f"{op_str}{rhs}")
except InvalidSpecifier:
pass
else:
return spec.contains(lhs, prereleases=True)
oper: Operator | None = _operators.get(op.serialize())
oper: Operator | None = _operators.get(op_str)
if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
@@ -196,13 +233,14 @@ def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
def _normalize(
lhs: str, rhs: str | AbstractSet[str], key: str
) -> tuple[str, str | AbstractSet[str]]:
# PEP 685 Comparison of extra names for optional distribution dependencies
# PEP 685 - Comparison of extra names for optional distribution dependencies
# https://peps.python.org/pep-0685/
# > When comparing extra names, tools MUST normalize the names being
# > compared using the semantics outlined in PEP 503 for names
if key == "extra":
assert isinstance(rhs, str), "extra value must be a string"
return (canonicalize_name(lhs), canonicalize_name(rhs))
# Both sides are normalized at this point already
return (lhs, rhs)
if key in MARKERS_ALLOWING_SET:
if isinstance(rhs, str): # pragma: no cover
return (canonicalize_name(lhs), canonicalize_name(rhs))
@@ -219,8 +257,6 @@ def _evaluate_markers(
groups: list[list[bool]] = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, str))
if isinstance(marker, list):
groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple):
@@ -234,18 +270,21 @@ def _evaluate_markers(
lhs_value = lhs.value
environment_key = rhs.value
rhs_value = environment[environment_key]
assert isinstance(lhs_value, str), "lhs must be a string"
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
groups[-1].append(_eval_op(lhs_value, op, rhs_value))
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
elif marker == "or":
groups.append([])
elif marker == "and":
pass
else: # pragma: nocover
raise TypeError(f"Unexpected marker {marker!r}")
return any(all(item) for item in groups)
def format_full_version(info: sys._version_info) -> str:
def _format_full_version(info: sys._version_info) -> str:
version = f"{info.major}.{info.minor}.{info.micro}"
kind = info.releaselevel
if kind != "final":
@@ -254,7 +293,11 @@ def format_full_version(info: sys._version_info) -> str:
def default_environment() -> Environment:
iver = format_full_version(sys.implementation.version)
"""Return the default marker environment for the current Python process.
This is the base environment used by :meth:`Marker.evaluate`.
"""
iver = _format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
return {
"implementation_name": implementation_name,
@@ -272,10 +315,36 @@ def default_environment() -> Environment:
class Marker:
"""Represents a parsed dependency marker expression.
Marker expressions are parsed according to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
:param marker: The string representation of a marker expression.
:raises InvalidMarker: If ``marker`` cannot be parsed.
Instances are safe to serialize with :mod:`pickle`. They use a stable
format so the same pickle can be loaded in future packaging releases.
.. versionchanged:: 26.2
Added a stable pickle format. Pickles created with packaging 26.2+ can
be unpickled with future releases. Backward compatibility with pickles
from packaging < 26.2 is supported but may be removed in a future
release.
"""
__slots__ = ("_markers",)
def __init__(self, marker: str) -> None:
# Note: We create a Marker object without calling this constructor in
# packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement.
# If this fails and throws an error, the repr still expects _markers to
# be defined.
self._markers: MarkerList = []
try:
self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a recursive type:
@@ -297,36 +366,94 @@ class Marker:
except ParserSyntaxError as e:
raise InvalidMarker(str(e)) from e
@classmethod
def _from_markers(cls, markers: MarkerList) -> Marker:
"""Create a Marker instance from a pre-parsed marker tree.
This avoids re-parsing serialised marker strings when combining markers.
"""
new = cls.__new__(cls)
new._markers = markers
return new
def __str__(self) -> str:
return _format_marker(self._markers)
def __repr__(self) -> str:
return f"<Marker('{self}')>"
return f"<{self.__class__.__name__}({str(self)!r})>"
def __hash__(self) -> int:
return hash((self.__class__.__name__, str(self)))
return hash(str(self))
def __eq__(self, other: Any) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Marker):
return NotImplemented
return str(self) == str(other)
def __getstate__(self) -> str:
# Return the marker expression string for compactness and stability.
# Internal Node objects are excluded; the string is re-parsed on load.
return str(self)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the marker expression string.
try:
self._markers = _normalize_extra_values(_parse_marker(state))
except ParserSyntaxError as exc:
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
return
if isinstance(state, dict) and "_markers" in state:
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
markers = state["_markers"]
if isinstance(markers, list):
self._markers = markers
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
markers = slot_dict["_markers"]
if isinstance(markers, list):
self._markers = markers
return
raise TypeError(f"Cannot restore Marker from {state!r}")
def __and__(self, other: Marker) -> Marker:
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "and", other._markers])
def __or__(self, other: Marker) -> Marker:
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "or", other._markers])
def evaluate(
self,
environment: dict[str, str] | None = None,
environment: Mapping[str, str | AbstractSet[str]] | None = None,
context: EvaluateContext = "metadata",
) -> bool:
"""Evaluate a marker.
Return the boolean from evaluating the given marker against the
environment. environment is an optional argument to override all or
part of the determined environment. The *context* parameter specifies what
context the markers are being evaluated for, which influences what markers
are considered valid. Acceptable values are "metadata" (for core metadata;
default), "lock_file", and "requirement" (i.e. all other situations).
Return the boolean from evaluating this marker against the environment.
The environment is determined from the current Python process unless
passed in explicitly.
:param environment: Mapping containing keys and values to override the
detected environment.
:param EvaluateContext context: The context in which the marker is
evaluated, which influences what marker names are considered valid.
Accepted values are ``"metadata"`` (for core metadata; default),
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
:raises UndefinedComparison: If the marker uses a comparison on values
that are not valid versions per the :ref:`specification of version
specifiers <pypug:version-specifiers>`.
:raises UndefinedEnvironmentName: If the marker references a value that
is missing from the evaluation environment.
:returns: ``True`` if the marker matches, otherwise ``False``.
The environment is determined from the current Python process.
"""
current_environment = cast(
"dict[str, str | AbstractSet[str]]", default_environment()
@@ -337,12 +464,15 @@ class Marker:
)
elif context == "metadata":
current_environment["extra"] = ""
if environment is not None:
current_environment.update(environment)
# The API used to allow setting extra to None. We need to handle this
# case for backwards compatibility.
if "extra" in current_environment and current_environment["extra"] is None:
current_environment["extra"] = ""
if "extra" in current_environment:
# The API used to allow setting extra to None. We need to handle
# this case for backwards compatibility. Also skip running
# normalize name if extra is empty.
extra = cast("str | None", current_environment["extra"])
current_environment["extra"] = canonicalize_name(extra) if extra else ""
return _evaluate_markers(
self._markers, _repair_python_full_version(current_environment)
@@ -356,7 +486,7 @@ def _repair_python_full_version(
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
python_full_version = cast(str, env["python_full_version"])
python_full_version = cast("str", env["python_full_version"])
if python_full_version.endswith("+"):
env["python_full_version"] = f"{python_full_version}local"
return env

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