Compare commits
92
Commits
0.4.14
...
33cf282b0a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33cf282b0a | ||
|
|
35a4357551 | ||
|
|
f5bd79f067 | ||
|
|
a39aee1b9d | ||
|
|
4ca1b0cce9 | ||
|
|
6051260a8d | ||
|
|
dfe15dd3db | ||
|
|
5d1c402d2a | ||
|
|
88f834ae79 | ||
|
|
3b233389c8 | ||
|
|
67bd9de18a | ||
|
|
495b7b7a07 | ||
|
|
8b39c4d250 | ||
|
|
49e8cdd7df | ||
|
|
f60c4563e4 | ||
|
|
3ad3847045 | ||
|
|
41f331d4c4 | ||
|
|
91ce762570 | ||
|
|
d60d3ec942 | ||
|
|
e52722e353 | ||
|
|
a8bf55882e | ||
|
|
377bddf579 | ||
|
|
cdc1cb4805 | ||
|
|
34430097d8 | ||
|
|
dcf447ed4f | ||
|
|
783a9aa7d7 | ||
|
|
63638d6b3b | ||
|
|
af8cd58fda | ||
|
|
df0f3d7a61 | ||
|
|
72906a1930 | ||
|
|
068cf2d1c3 | ||
|
|
ad18a3dcf3 | ||
|
|
bd4b17825c | ||
|
|
5d6182e91f | ||
|
|
63e9c429bd | ||
|
|
7fdafe484e | ||
|
|
5cba6763f1 | ||
|
|
e20bb85ac6 | ||
|
|
98a0b2197d | ||
|
|
579847b56d | ||
|
|
618d6b478b | ||
|
|
480978d6bb | ||
|
|
04798328bd | ||
|
|
ae835089b6 | ||
|
|
0ed1e887fc | ||
|
|
12b7b62aa5 | ||
|
|
7263b6f530 | ||
|
|
c0635e48ea | ||
|
|
8fb98d2f40 | ||
|
|
f24b280edf | ||
|
|
b61cff3b3b | ||
|
|
cf62044d3e | ||
|
|
feba88212f | ||
|
|
aeebb5c964 | ||
|
|
b141818251 | ||
|
|
72f894b05d | ||
|
|
0a26cb5c0f | ||
|
|
b3cac5c292 | ||
|
|
ef558c3aa3 | ||
|
|
9d5ef23162 | ||
|
|
f659db1147 | ||
|
|
c25bf79427 | ||
|
|
b059a75205 | ||
|
|
a90e15514b | ||
|
|
2c2579319d | ||
|
|
b38fab1428 | ||
|
|
bc745622b9 | ||
|
|
0bb4ef731e | ||
|
|
27e2b3bf6e | ||
|
|
f55c3ba904 | ||
|
|
cb430a5553 | ||
|
|
637a28ad82 | ||
|
|
499df9b51d | ||
|
|
31ccae9a40 | ||
|
|
9b373e85b7 | ||
|
|
0c5f4ea6fb | ||
|
|
793b35f39f | ||
|
|
3780fc27dc | ||
|
|
6541830ef6 | ||
|
|
509b13e99f | ||
|
|
396cfdc4ec | ||
|
|
ccd22e4343 | ||
|
|
7d83ae1012 | ||
|
|
029eaebd57 | ||
|
|
2a2db8f217 | ||
|
|
8be9aae05d | ||
|
|
6bded783a1 | ||
|
|
f5c61267e3 | ||
|
|
b680c01989 | ||
|
|
1abf89fe56 | ||
|
|
6a051a6dee | ||
|
|
d22d897d23 |
@@ -1,13 +1,15 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
release:
|
||||
types: [published]
|
||||
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"
|
||||
@@ -33,9 +35,32 @@ 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
|
||||
- name: Publish to Visual Studio Marketplace
|
||||
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 it’s 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
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
||||
- name: Publish to Visual Studio Marketplace (Stable)
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
run: vsce publish
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
||||
|
||||
@@ -9,3 +9,4 @@ __pycache__
|
||||
.nox
|
||||
*.g4
|
||||
.antlr
|
||||
.claude
|
||||
Vendored
+35
-12
@@ -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": "",
|
||||
|
||||
Vendored
+57
@@ -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
|
||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
||||
"ruff.configuration": {
|
||||
"lint": {
|
||||
"extend-ignore": ["F821", "E402"]
|
||||
}
|
||||
},
|
||||
"line-length": 200
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+26
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# NX Postprocessor Support
|
||||
|
||||
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||
|
||||
## Features
|
||||
|
||||
- **Syntax Highlighting** - Full syntax highlighting for CDL, TCL, and DEF files
|
||||
- **Code Formatting** - Automatic code formatting for all supported file types
|
||||
- **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 User Defined Event File
|
||||
- `.tcl` - NX TCL postprocessor files
|
||||
- `.def` - NX Definition files
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install from the VS Code Marketplace
|
||||
2. Install Python 3.11 or higher
|
||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||
4. The extension will automatically activate and provide language support
|
||||
|
||||
## Configuration
|
||||
|
||||
The extension can be configured through VS Code settings:
|
||||
|
||||
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
|
||||
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
|
||||
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
|
||||
|
||||
## Usage
|
||||
|
||||
Simply open any supported file type and enjoy:
|
||||
|
||||
- Syntax highlighting
|
||||
- Error detection and linting
|
||||
- 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
|
||||
|
||||
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0 License - see LICENSE file for details.
|
||||
|
||||
Generated
+22
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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: [
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
+33
-26
@@ -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": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
+16
-3
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "",
|
||||
"version": "0.4.13",
|
||||
"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"
|
||||
@@ -16,7 +20,10 @@
|
||||
"keywords": [
|
||||
"cdl",
|
||||
"NX CDL",
|
||||
"UDE"
|
||||
"UDE",
|
||||
"Postprocessor",
|
||||
"NX CAM",
|
||||
"Siemens NX"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.96.0"
|
||||
@@ -84,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.",
|
||||
@@ -112,6 +124,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "node esbuild.js --production",
|
||||
"compile:debug": "node esbuild.js",
|
||||
"watch": "node esbuild.js --watch",
|
||||
"package": "node esbuild.js --production"
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+27
-60
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
-1
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+9
-6
@@ -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
-1
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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))}"
|
||||
)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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__")
|
||||
)
|
||||
]
|
||||
@@ -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
|
||||
+13
-6
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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(" )", ")"),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user