Compare commits
30
Commits
2025.8.400
...
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 |
@@ -9,3 +9,4 @@ __pycache__
|
|||||||
.nox
|
.nox
|
||||||
*.g4
|
*.g4
|
||||||
.antlr
|
.antlr
|
||||||
|
.claude
|
||||||
Vendored
+35
-12
@@ -10,13 +10,21 @@
|
|||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${execPath}",
|
"runtimeExecutable": "${execPath}",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
"args": [
|
||||||
"outFiles": ["${workspaceFolder}/client/**/*.js"],
|
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
|
||||||
|
],
|
||||||
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
|
"sourceMaps": true,
|
||||||
|
"resolveSourceMapLocations": [
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
],
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"autoAttachChildProcesses": true,
|
"autoAttachChildProcesses": true,
|
||||||
"preLaunchTask": {
|
"preLaunchTask": "NX Post Support: Compile Debug"
|
||||||
"type": "npm",
|
|
||||||
"script": "watch"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python Attach",
|
"name": "Python Attach",
|
||||||
@@ -34,10 +42,24 @@
|
|||||||
"name": "Debug Extension (hidden)",
|
"name": "Debug Extension (hidden)",
|
||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
"runtimeExecutable": "${execPath}",
|
||||||
"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>/**"],
|
||||||
"env": {
|
"env": {
|
||||||
"USE_DEBUGPY": "True"
|
"USE_DEBUGPY": "True",
|
||||||
|
"NXPS_DEBUG_HOST": "127.0.0.1",
|
||||||
|
"NXPS_DEBUG_PORT": "5678"
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
@@ -49,8 +71,9 @@
|
|||||||
"name": "Python debug server (hidden)",
|
"name": "Python debug server (hidden)",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"listen": { "host": "localhost", "port": 5678 },
|
"listen": { "host": "127.0.0.1", "port": 5678 },
|
||||||
"justMyCode": true,
|
"justMyCode": false,
|
||||||
|
"logToFile": true,
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"group": "",
|
"group": "",
|
||||||
@@ -63,7 +86,7 @@
|
|||||||
"name": "Debug Extension and Python",
|
"name": "Debug Extension and Python",
|
||||||
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
|
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
|
||||||
"stopAll": true,
|
"stopAll": true,
|
||||||
"preLaunchTask": "npm: watch",
|
"preLaunchTask": "NX Post Support: Compile Debug",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"group": "",
|
"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
+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]
|
## [0.0.1]
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
@@ -13,3 +19,11 @@
|
|||||||
## [0.2.0]
|
## [0.2.0]
|
||||||
|
|
||||||
- Add DEF File Support
|
- Add DEF File Support
|
||||||
|
|
||||||
|
## [2026.6.100]
|
||||||
|
|
||||||
|
- Fix several bugs
|
||||||
|
|
||||||
|
## [2026.6.200]
|
||||||
|
|
||||||
|
- Fix foramtting bug
|
||||||
|
|||||||
@@ -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
|
Preamble
|
||||||
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:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
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
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
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/>.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
|||||||
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
|
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
|
||||||
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
|
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
|
||||||
- **Auto-completion** - Context-aware code completion for faster development
|
- **Auto-completion** - Context-aware code completion for faster development
|
||||||
|
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
|
||||||
|
|
||||||
## Supported File Types
|
## Supported File Types
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install from the VS Code Marketplace
|
1. Install from the VS Code Marketplace
|
||||||
2. Open any `.cdl`, `.tcl`, or `.def` file
|
2. Install Python 3.11 or higher
|
||||||
3. The extension will automatically activate and provide language support
|
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||||
|
4. The extension will automatically activate and provide language support
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -39,6 +41,30 @@ Simply open any supported file type and enjoy:
|
|||||||
- Code completion
|
- Code completion
|
||||||
- Code formatting (Format Document command)
|
- Code formatting (Format Document command)
|
||||||
- Hover information
|
- Hover information
|
||||||
|
- Signature help while entering procedure arguments
|
||||||
|
|
||||||
|
## Development and debugging
|
||||||
|
|
||||||
|
Install the root and client dependencies before the first debug session:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm install --prefix client
|
||||||
|
```
|
||||||
|
|
||||||
|
Use one of the checked-in VS Code launch configurations:
|
||||||
|
|
||||||
|
- **Run Extension** debugs the TypeScript extension host.
|
||||||
|
- **Debug Extension and Python** debugs both the TypeScript extension and the
|
||||||
|
Python language server. This is the recommended configuration for LSP work.
|
||||||
|
- **Python Attach** attaches manually to an already running Python process.
|
||||||
|
|
||||||
|
The launch configuration creates a fresh non-minified bundle with embedded
|
||||||
|
source maps and opens `test/test.tcl` so the extension activates immediately.
|
||||||
|
For combined debugging, the Python adapter listens on `127.0.0.1:5678`; the
|
||||||
|
language server waits for that adapter before initialization. The NX
|
||||||
|
Postprocessor Support output channel reports `Python debug mode: enabled` and
|
||||||
|
shows `_debug_server.py` in the server command when the debug path is active.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -46,4 +72,4 @@ This extension is actively maintained. For issues or feature requests, please vi
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - see LICENSE file for details.
|
AGPL-3.0 License - see LICENSE file for details.
|
||||||
|
|||||||
Generated
+22
@@ -14,6 +14,7 @@
|
|||||||
"vscode-languageclient": "^9.0.1"
|
"vscode-languageclient": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@types/vscode": "^1.96.0"
|
"@types/vscode": "^1.96.0"
|
||||||
},
|
},
|
||||||
@@ -21,6 +22,27 @@
|
|||||||
"vscode": "^1.96.0"
|
"vscode": "^1.96.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/fs-extra": {
|
||||||
|
"version": "11.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
|
||||||
|
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jsonfile": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/jsonfile": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.10.5",
|
"version": "22.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"vscode-languageclient": "^9.0.1"
|
"vscode-languageclient": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@types/vscode": "^1.96.0"
|
"@types/vscode": "^1.96.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,102 @@ export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(value: string): string {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cdlEventAtPosition(
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
position: vscode.Position
|
||||||
|
): string | undefined {
|
||||||
|
const line = document.lineAt(position.line).text
|
||||||
|
const match = /^\s*EVENT\s+([^\s{]+)/.exec(line)
|
||||||
|
if (!match) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const declarationStart = match.index
|
||||||
|
const eventEnd = line.indexOf(match[1], match.index) + match[1].length
|
||||||
|
if (position.character < declarationStart || position.character > eventEnd) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function definitionCdlEventHandler(
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
position: vscode.Position,
|
||||||
|
token: vscode.CancellationToken
|
||||||
|
): Promise<vscode.Location[] | undefined> {
|
||||||
|
const eventName = cdlEventAtPosition(document, position)
|
||||||
|
if (!eventName) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerName = `MOM_${eventName}`
|
||||||
|
try {
|
||||||
|
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
|
||||||
|
"vscode.executeWorkspaceSymbolProvider",
|
||||||
|
handlerName
|
||||||
|
)
|
||||||
|
const indexedLocations = (symbols || [])
|
||||||
|
.filter(
|
||||||
|
(symbol) =>
|
||||||
|
symbol.kind === vscode.SymbolKind.Function &&
|
||||||
|
(symbol.name === handlerName ||
|
||||||
|
symbol.name.endsWith(`::${handlerName}`))
|
||||||
|
)
|
||||||
|
.map((symbol) => symbol.location)
|
||||||
|
if (indexedLocations.length > 0) {
|
||||||
|
return indexedLocations
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// The Tcl language server may still be starting; use the file fallback below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const declaration = new RegExp(
|
||||||
|
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
|
||||||
|
)
|
||||||
|
const tclFiles = await vscode.workspace.findFiles(
|
||||||
|
"**/*.tcl",
|
||||||
|
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
|
||||||
|
)
|
||||||
|
const locations: vscode.Location[] = []
|
||||||
|
|
||||||
|
for (const uri of tclFiles) {
|
||||||
|
if (token.isCancellationRequested) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
let tclDocument: vscode.TextDocument
|
||||||
|
try {
|
||||||
|
tclDocument = await vscode.workspace.openTextDocument(uri)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let lineNumber = 0; lineNumber < tclDocument.lineCount; lineNumber++) {
|
||||||
|
const line = tclDocument.lineAt(lineNumber).text
|
||||||
|
const match = declaration.exec(line)
|
||||||
|
if (!match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const start = line.indexOf(handlerName, match.index)
|
||||||
|
locations.push(
|
||||||
|
new vscode.Location(
|
||||||
|
uri,
|
||||||
|
new vscode.Range(
|
||||||
|
lineNumber,
|
||||||
|
start,
|
||||||
|
lineNumber,
|
||||||
|
start + handlerName.length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return locations.length > 0 ? locations : undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
|
||||||
const symbols: vscode.DocumentSymbol[] = []
|
const symbols: vscode.DocumentSymbol[] = []
|
||||||
const lines = document.getText().split("\n")
|
const lines = document.getText().split("\n")
|
||||||
|
|||||||
+50
-18
@@ -2,7 +2,7 @@
|
|||||||
// Licensed under the MIT License.
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
import * as fsapi from "fs-extra"
|
import * as fsapi from "fs-extra"
|
||||||
import { Disposable, env, LogOutputChannel } from "vscode"
|
import { Disposable, env, LogOutputChannel, workspace } from "vscode"
|
||||||
import { State } from "vscode-languageclient"
|
import { State } from "vscode-languageclient"
|
||||||
import {
|
import {
|
||||||
LanguageClient,
|
LanguageClient,
|
||||||
@@ -24,6 +24,13 @@ import { isVirtualWorkspace } from "./vscodeapi"
|
|||||||
|
|
||||||
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
|
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
|
||||||
|
|
||||||
|
let _disposables: Disposable[] = []
|
||||||
|
|
||||||
|
export function disposeServerResources(): void {
|
||||||
|
_disposables.forEach((disposable) => disposable.dispose())
|
||||||
|
_disposables = []
|
||||||
|
}
|
||||||
|
|
||||||
async function createServer(
|
async function createServer(
|
||||||
settings: ISettings,
|
settings: ISettings,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
@@ -32,13 +39,26 @@ async function createServer(
|
|||||||
initializationOptions: IInitOptions
|
initializationOptions: IInitOptions
|
||||||
): Promise<LanguageClient> {
|
): Promise<LanguageClient> {
|
||||||
const command = settings.interpreter[0]
|
const command = settings.interpreter[0]
|
||||||
|
if (!command) {
|
||||||
|
throw new Error("No Python interpreter is configured for the language server.")
|
||||||
|
}
|
||||||
const cwd = settings.cwd
|
const cwd = settings.cwd
|
||||||
|
|
||||||
// Set debugger path needed for debugging python code.
|
// Set debugger path needed for debugging python code.
|
||||||
const newEnv = { ...process.env }
|
const newEnv = { ...process.env }
|
||||||
const debuggerPath = await getDebuggerPath()
|
|
||||||
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
|
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
|
||||||
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
|
newEnv.DEBUGPY_PATH = debuggerPath
|
||||||
} else {
|
} else {
|
||||||
newEnv.USE_DEBUGPY = "False"
|
newEnv.USE_DEBUGPY = "False"
|
||||||
@@ -50,10 +70,13 @@ async function createServer(
|
|||||||
// Set notification type
|
// Set notification type
|
||||||
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
|
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
|
||||||
|
|
||||||
const args =
|
const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH
|
||||||
newEnv.USE_DEBUGPY === "False" || !isDebugScript
|
const interpreterArgs = settings.interpreter.slice(1)
|
||||||
? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH])
|
if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) {
|
||||||
: settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH])
|
interpreterArgs.push("-Xfrozen_modules=off")
|
||||||
|
}
|
||||||
|
const args = interpreterArgs.concat([serverScript])
|
||||||
|
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
|
||||||
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
|
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
|
||||||
|
|
||||||
const serverOptions: ServerOptions = {
|
const serverOptions: ServerOptions = {
|
||||||
@@ -63,6 +86,7 @@ async function createServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Options to control the language client
|
// Options to control the language client
|
||||||
|
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
|
||||||
const clientOptions: LanguageClientOptions = {
|
const clientOptions: LanguageClientOptions = {
|
||||||
// Register the server for python documents
|
// Register the server for python documents
|
||||||
documentSelector: isVirtualWorkspace()
|
documentSelector: isVirtualWorkspace()
|
||||||
@@ -76,13 +100,16 @@ async function createServer(
|
|||||||
outputChannel: outputChannel,
|
outputChannel: outputChannel,
|
||||||
traceOutputChannel: outputChannel,
|
traceOutputChannel: outputChannel,
|
||||||
revealOutputChannelOn: RevealOutputChannelOn.Never,
|
revealOutputChannelOn: RevealOutputChannelOn.Never,
|
||||||
|
synchronize: {
|
||||||
|
fileEvents: tclFileWatcher
|
||||||
|
},
|
||||||
initializationOptions
|
initializationOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_disposables.push(tclFileWatcher)
|
||||||
return new LanguageClient(serverId, serverName, serverOptions, clientOptions)
|
return new LanguageClient(serverId, serverName, serverOptions, clientOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _disposables: Disposable[] = []
|
|
||||||
export async function restartServer(
|
export async function restartServer(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
serverName: string,
|
serverName: string,
|
||||||
@@ -92,16 +119,22 @@ export async function restartServer(
|
|||||||
if (lsClient) {
|
if (lsClient) {
|
||||||
traceInfo(`Server: Stop requested`)
|
traceInfo(`Server: Stop requested`)
|
||||||
await lsClient.stop()
|
await lsClient.stop()
|
||||||
_disposables.forEach((d) => d.dispose())
|
disposeServerResources()
|
||||||
_disposables = []
|
|
||||||
}
|
}
|
||||||
const projectRoot = await getProjectRoot()
|
const projectRoot = await getProjectRoot()
|
||||||
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
|
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
|
||||||
|
|
||||||
const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, {
|
try {
|
||||||
|
const newLSClient = await createServer(
|
||||||
|
workspaceSetting,
|
||||||
|
serverId,
|
||||||
|
serverName,
|
||||||
|
outputChannel,
|
||||||
|
{
|
||||||
settings: await getExtensionSettings(serverId, true),
|
settings: await getExtensionSettings(serverId, true),
|
||||||
globalSettings: await getGlobalSettings(serverId, false)
|
globalSettings: await getGlobalSettings(serverId, false)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
traceInfo(`Server: Start requested.`)
|
traceInfo(`Server: Start requested.`)
|
||||||
_disposables.push(
|
_disposables.push(
|
||||||
newLSClient.onDidChangeState((e) => {
|
newLSClient.onDidChangeState((e) => {
|
||||||
@@ -118,14 +151,13 @@ export async function restartServer(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
try {
|
|
||||||
await newLSClient.start()
|
await newLSClient.start()
|
||||||
} catch (ex) {
|
|
||||||
traceError(`Server: Start failed: ${ex}`)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
|
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
|
||||||
await newLSClient.setTrace(level)
|
await newLSClient.setTrace(level)
|
||||||
return newLSClient
|
return newLSClient
|
||||||
|
} catch (ex) {
|
||||||
|
traceError(`Server: Start failed: ${ex}`)
|
||||||
|
disposeServerResources()
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-8
@@ -13,7 +13,8 @@ import {
|
|||||||
isFirstLineMachine,
|
isFirstLineMachine,
|
||||||
diagnosticHandler,
|
diagnosticHandler,
|
||||||
cdlDocumentSymbolProvider,
|
cdlDocumentSymbolProvider,
|
||||||
defDocumentSymbolProvider
|
defDocumentSymbolProvider,
|
||||||
|
definitionCdlEventHandler
|
||||||
} from "./common/handlers"
|
} from "./common/handlers"
|
||||||
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
|
||||||
import {
|
import {
|
||||||
@@ -23,13 +24,13 @@ import {
|
|||||||
onDidChangePythonInterpreter,
|
onDidChangePythonInterpreter,
|
||||||
resolveInterpreter
|
resolveInterpreter
|
||||||
} from "./common/python"
|
} from "./common/python"
|
||||||
import { restartServer } from "./common/server"
|
import { disposeServerResources, restartServer } from "./common/server"
|
||||||
import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings"
|
import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings"
|
||||||
import { loadServerDefaults } from "./common/setup"
|
import { loadServerDefaults } from "./common/setup"
|
||||||
import { getLSClientTraceLevel } from "./common/utilities"
|
import { getLSClientTraceLevel } from "./common/utilities"
|
||||||
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
||||||
|
|
||||||
let client: LanguageClient
|
let client: LanguageClient | undefined
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
// This is required to get server name and module. This should be
|
// This is required to get server name and module. This should be
|
||||||
@@ -37,6 +38,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
const serverInfo = loadServerDefaults()
|
const serverInfo = loadServerDefaults()
|
||||||
const serverName = serverInfo.name
|
const serverName = serverInfo.name
|
||||||
const serverId = serverInfo.module
|
const serverId = serverInfo.module
|
||||||
|
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
|
||||||
|
|
||||||
// Setup logging
|
// Setup logging
|
||||||
const outputChannel = createOutputChannel(serverName)
|
const outputChannel = createOutputChannel(serverName)
|
||||||
@@ -61,7 +63,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
traceLog(`Module: ${serverInfo.module}`)
|
traceLog(`Module: ${serverInfo.module}`)
|
||||||
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
|
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
|
||||||
|
|
||||||
const runServer = async () => {
|
const runServerImpl = async () => {
|
||||||
const interpreter = getInterpreterFromSetting(serverId)
|
const interpreter = getInterpreterFromSetting(serverId)
|
||||||
if (interpreter && interpreter.length > 0) {
|
if (interpreter && interpreter.length > 0) {
|
||||||
if (checkVersion(await resolveInterpreter(interpreter))) {
|
if (checkVersion(await resolveInterpreter(interpreter))) {
|
||||||
@@ -90,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(
|
context.subscriptions.push(
|
||||||
onDidChangePythonInterpreter(async () => {
|
onDidChangePythonInterpreter(async () => {
|
||||||
await runServer()
|
await runServer()
|
||||||
}),
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.subscriptions.push(
|
||||||
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
|
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
|
||||||
if (checkIfConfigurationChanged(e, serverId)) {
|
if (checkIfConfigurationChanged(e, serverId)) {
|
||||||
await runServer()
|
await runServer()
|
||||||
@@ -105,6 +122,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
setImmediate(async () => {
|
setImmediate(async () => {
|
||||||
|
if (pythonDebugMode) {
|
||||||
|
// A debugpy listen session is attached to exactly one process. Do not
|
||||||
|
// subscribe to interpreter changes during startup, as the Python
|
||||||
|
// extension can emit a duplicate event and restart that process.
|
||||||
|
traceLog("Python debug mode: starting one stable server session")
|
||||||
|
await runServer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const interpreter = getInterpreterFromSetting(serverId)
|
const interpreter = getInterpreterFromSetting(serverId)
|
||||||
if (interpreter === undefined || interpreter.length === 0) {
|
if (interpreter === undefined || interpreter.length === 0) {
|
||||||
traceLog(`Python extension loading`)
|
traceLog(`Python extension loading`)
|
||||||
@@ -115,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(
|
const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
|
||||||
{ scheme: "file", language: "cdl" },
|
{ scheme: "file", language: "cdl" },
|
||||||
@@ -163,6 +193,16 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
)
|
)
|
||||||
context.subscriptions.push(hoverCdlProvider)
|
context.subscriptions.push(hoverCdlProvider)
|
||||||
|
|
||||||
|
const definitionCdlEventProvider = vscode.languages.registerDefinitionProvider(
|
||||||
|
{ scheme: "file", language: "cdl" },
|
||||||
|
{
|
||||||
|
provideDefinition(document, position, token) {
|
||||||
|
return definitionCdlEventHandler(document, position, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context.subscriptions.push(definitionCdlEventProvider)
|
||||||
|
|
||||||
const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider(
|
const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider(
|
||||||
{ scheme: "file", language: "def" },
|
{ scheme: "file", language: "def" },
|
||||||
{
|
{
|
||||||
@@ -207,6 +247,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
|
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
|
||||||
|
|
||||||
// Check if the first line of the CDL file contains "MACHINE"
|
// Check if the first line of the CDL file contains "MACHINE"
|
||||||
|
context.subscriptions.push(
|
||||||
vscode.workspace.onDidOpenTextDocument((document) => {
|
vscode.workspace.onDidOpenTextDocument((document) => {
|
||||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||||
if (document.languageId === "cdl") {
|
if (document.languageId === "cdl") {
|
||||||
@@ -215,8 +256,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
|
||||||
vscode.workspace.onDidChangeTextDocument((event) => {
|
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||||
const document = event.document
|
const document = event.document
|
||||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||||
@@ -227,11 +267,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): Thenable<void> | undefined {
|
export function deactivate(): Thenable<void> | undefined {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
disposeServerResources()
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return client.stop()
|
return client.stop().finally(disposeServerResources)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,19 +1,21 @@
|
|||||||
const esbuild = require("esbuild")
|
const esbuild = require("esbuild")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
const production = process.argv.includes("--production")
|
const production = process.argv.includes("--production")
|
||||||
const watch = process.argv.includes("--watch")
|
const watch = process.argv.includes("--watch")
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const ctx = await esbuild.context({
|
const ctx = await esbuild.context({
|
||||||
|
absWorkingDir: __dirname,
|
||||||
entryPoints: ["client/src/extension.ts"],
|
entryPoints: ["client/src/extension.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
minify: production,
|
minify: production,
|
||||||
sourcemap: !production,
|
sourcemap: !production,
|
||||||
sourcesContent: false,
|
sourcesContent: !production,
|
||||||
platform: "node",
|
platform: "node",
|
||||||
// outdir: "out",
|
// outdir: "out",
|
||||||
outfile: "./dist/extension.js",
|
outfile: path.join(__dirname, "dist", "extension.js"),
|
||||||
external: ["vscode"],
|
external: ["vscode"],
|
||||||
logLevel: "silent",
|
logLevel: "silent",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
// symbol used for single line comment. Remove this entry if your language does not support line comments
|
// symbol used for single line comment. Remove this entry if your language does not support line comments
|
||||||
"lineComment": "#"
|
"lineComment": "#"
|
||||||
},
|
},
|
||||||
|
"indentationRules": {
|
||||||
|
// 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
|
// symbols used as brackets
|
||||||
"brackets": [
|
"brackets": [
|
||||||
["{", "}"],
|
["{", "}"],
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"version": "0.3.0",
|
"version": "2025.9.200",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"version": "0.3.0",
|
"version": "2025.9.200",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.96.0",
|
"@types/vscode": "^1.96.0",
|
||||||
"@vscode/vsce": "^3.2.1",
|
"@vscode/vsce": "^3.2.1",
|
||||||
|
|||||||
+6
-2
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "nx-post-support",
|
"name": "nx-post-support",
|
||||||
"displayName": "NX Postprocessor Support",
|
"displayName": "NX Postprocessor Support",
|
||||||
"description": "",
|
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
||||||
"version": "2025.8.300",
|
"version": "2026.6.201",
|
||||||
"publisher": "Christoph",
|
"publisher": "Christoph",
|
||||||
"icon": "images/nx-1.png",
|
"icon": "images/nx-1.png",
|
||||||
|
"extensionDependencies": [
|
||||||
|
"ms-python.python"
|
||||||
|
],
|
||||||
"serverInfo": {
|
"serverInfo": {
|
||||||
"name": "NX Postprocessor Support",
|
"name": "NX Postprocessor Support",
|
||||||
"module": "nx-post-support"
|
"module": "nx-post-support"
|
||||||
@@ -121,6 +124,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "node esbuild.js --production",
|
"compile": "node esbuild.js --production",
|
||||||
|
"compile:debug": "node esbuild.js",
|
||||||
"watch": "node esbuild.js --watch",
|
"watch": "node esbuild.js --watch",
|
||||||
"package": "node esbuild.js --production"
|
"package": "node esbuild.js --production"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ def attrib(
|
|||||||
type: None = ...,
|
type: None = ...,
|
||||||
converter: None = ...,
|
converter: None = ...,
|
||||||
factory: None = ...,
|
factory: None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -200,7 +200,7 @@ def attrib(
|
|||||||
| tuple[_ConverterType]
|
| tuple[_ConverterType]
|
||||||
| None = ...,
|
| None = ...,
|
||||||
factory: Callable[[], _T] | None = ...,
|
factory: Callable[[], _T] | None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -223,7 +223,7 @@ def attrib(
|
|||||||
| tuple[_ConverterType]
|
| tuple[_ConverterType]
|
||||||
| None = ...,
|
| None = ...,
|
||||||
factory: Callable[[], _T] | None = ...,
|
factory: Callable[[], _T] | None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -246,7 +246,7 @@ def attrib(
|
|||||||
| tuple[_ConverterType]
|
| tuple[_ConverterType]
|
||||||
| None = ...,
|
| None = ...,
|
||||||
factory: Callable[[], _T] | None = ...,
|
factory: Callable[[], _T] | None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -308,7 +308,7 @@ def attrs(
|
|||||||
match_args: bool = ...,
|
match_args: bool = ...,
|
||||||
unsafe_hash: bool | None = ...,
|
unsafe_hash: bool | None = ...,
|
||||||
) -> Callable[[_C], _C]: ...
|
) -> Callable[[_C], _C]: ...
|
||||||
def fields(cls: type[AttrsInstance]) -> Any: ...
|
def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ...
|
||||||
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
|
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
|
||||||
def validate(inst: AttrsInstance) -> None: ...
|
def validate(inst: AttrsInstance) -> None: ...
|
||||||
def resolve_types(
|
def resolve_types(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from typing import _GenericAlias
|
|||||||
|
|
||||||
|
|
||||||
PYPY = platform.python_implementation() == "PyPy"
|
PYPY = platform.python_implementation() == "PyPy"
|
||||||
PY_3_9_PLUS = sys.version_info[:2] >= (3, 9)
|
|
||||||
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
|
PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
|
||||||
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
|
PY_3_11_PLUS = sys.version_info[:2] >= (3, 11)
|
||||||
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
|
PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
|
||||||
@@ -18,10 +17,16 @@ PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
|
|||||||
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
|
PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
|
||||||
|
|
||||||
|
|
||||||
if PY_3_14_PLUS: # pragma: no cover
|
if PY_3_14_PLUS:
|
||||||
import annotationlib
|
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:
|
else:
|
||||||
|
|
||||||
|
|||||||
+45
-16
@@ -3,11 +3,28 @@
|
|||||||
|
|
||||||
import copy
|
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 ._make import _OBJ_SETATTR, NOTHING, fields
|
||||||
from .exceptions import AttrsAttributeNotFoundError
|
from .exceptions import AttrsAttributeNotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
_ATOMIC_TYPES = frozenset(
|
||||||
|
{
|
||||||
|
type(None),
|
||||||
|
bool,
|
||||||
|
int,
|
||||||
|
float,
|
||||||
|
str,
|
||||||
|
complex,
|
||||||
|
bytes,
|
||||||
|
type(...),
|
||||||
|
type,
|
||||||
|
range,
|
||||||
|
property,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def asdict(
|
def asdict(
|
||||||
inst,
|
inst,
|
||||||
recurse=True,
|
recurse=True,
|
||||||
@@ -71,7 +88,10 @@ def asdict(
|
|||||||
v = value_serializer(inst, a, v)
|
v = value_serializer(inst, a, v)
|
||||||
|
|
||||||
if recurse is True:
|
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(
|
rv[a.name] = asdict(
|
||||||
v,
|
v,
|
||||||
recurse=True,
|
recurse=True,
|
||||||
@@ -80,8 +100,8 @@ def asdict(
|
|||||||
retain_collection_types=retain_collection_types,
|
retain_collection_types=retain_collection_types,
|
||||||
value_serializer=value_serializer,
|
value_serializer=value_serializer,
|
||||||
)
|
)
|
||||||
elif isinstance(v, (tuple, list, set, frozenset)):
|
elif issubclass(value_type, (tuple, list, set, frozenset)):
|
||||||
cf = v.__class__ if retain_collection_types is True else list
|
cf = value_type if retain_collection_types is True else list
|
||||||
items = [
|
items = [
|
||||||
_asdict_anything(
|
_asdict_anything(
|
||||||
i,
|
i,
|
||||||
@@ -101,7 +121,7 @@ def asdict(
|
|||||||
# Workaround for TypeError: cf.__new__() missing 1 required
|
# Workaround for TypeError: cf.__new__() missing 1 required
|
||||||
# positional argument (which appears, for a namedturle)
|
# positional argument (which appears, for a namedturle)
|
||||||
rv[a.name] = cf(*items)
|
rv[a.name] = cf(*items)
|
||||||
elif isinstance(v, dict):
|
elif issubclass(value_type, dict):
|
||||||
df = dict_factory
|
df = dict_factory
|
||||||
rv[a.name] = df(
|
rv[a.name] = df(
|
||||||
(
|
(
|
||||||
@@ -142,7 +162,12 @@ def _asdict_anything(
|
|||||||
"""
|
"""
|
||||||
``asdict`` only works on attrs instances, this works on 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.
|
# Attrs class.
|
||||||
rv = asdict(
|
rv = asdict(
|
||||||
val,
|
val,
|
||||||
@@ -152,7 +177,7 @@ def _asdict_anything(
|
|||||||
retain_collection_types=retain_collection_types,
|
retain_collection_types=retain_collection_types,
|
||||||
value_serializer=value_serializer,
|
value_serializer=value_serializer,
|
||||||
)
|
)
|
||||||
elif isinstance(val, (tuple, list, set, frozenset)):
|
elif issubclass(val_type, (tuple, list, set, frozenset)):
|
||||||
if retain_collection_types is True:
|
if retain_collection_types is True:
|
||||||
cf = val.__class__
|
cf = val.__class__
|
||||||
elif is_key:
|
elif is_key:
|
||||||
@@ -173,7 +198,7 @@ def _asdict_anything(
|
|||||||
for i in val
|
for i in val
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
elif isinstance(val, dict):
|
elif issubclass(val_type, dict):
|
||||||
df = dict_factory
|
df = dict_factory
|
||||||
rv = df(
|
rv = df(
|
||||||
(
|
(
|
||||||
@@ -253,8 +278,11 @@ def astuple(
|
|||||||
v = getattr(inst, a.name)
|
v = getattr(inst, a.name)
|
||||||
if filter is not None and not filter(a, v):
|
if filter is not None and not filter(a, v):
|
||||||
continue
|
continue
|
||||||
|
value_type = type(v)
|
||||||
if recurse is True:
|
if recurse is True:
|
||||||
if has(v.__class__):
|
if value_type in _ATOMIC_TYPES:
|
||||||
|
rv.append(v)
|
||||||
|
elif has(value_type):
|
||||||
rv.append(
|
rv.append(
|
||||||
astuple(
|
astuple(
|
||||||
v,
|
v,
|
||||||
@@ -264,7 +292,7 @@ def astuple(
|
|||||||
retain_collection_types=retain,
|
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
|
cf = v.__class__ if retain is True else list
|
||||||
items = [
|
items = [
|
||||||
(
|
(
|
||||||
@@ -288,8 +316,8 @@ def astuple(
|
|||||||
# Workaround for TypeError: cf.__new__() missing 1 required
|
# Workaround for TypeError: cf.__new__() missing 1 required
|
||||||
# positional argument (which appears, for a namedturle)
|
# positional argument (which appears, for a namedturle)
|
||||||
rv.append(cf(*items))
|
rv.append(cf(*items))
|
||||||
elif isinstance(v, dict):
|
elif issubclass(value_type, dict):
|
||||||
df = v.__class__ if retain is True else dict
|
df = value_type if retain is True else dict
|
||||||
rv.append(
|
rv.append(
|
||||||
df(
|
df(
|
||||||
(
|
(
|
||||||
@@ -450,10 +478,11 @@ def resolve_types(
|
|||||||
if getattr(cls, "__attrs_types_resolved__", None) != cls:
|
if getattr(cls, "__attrs_types_resolved__", None) != cls:
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
kwargs = {"globalns": globalns, "localns": localns}
|
kwargs = {
|
||||||
|
"globalns": globalns,
|
||||||
if PY_3_9_PLUS:
|
"localns": localns,
|
||||||
kwargs["include_extras"] = include_extras
|
"include_extras": include_extras,
|
||||||
|
}
|
||||||
|
|
||||||
hints = typing.get_type_hints(cls, **kwargs)
|
hints = typing.get_type_hints(cls, **kwargs)
|
||||||
for field in fields(cls) if attribs is None else attribs:
|
for field in fields(cls) if attribs is None else attribs:
|
||||||
|
|||||||
+394
-111
@@ -12,6 +12,7 @@ import linecache
|
|||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
import weakref
|
||||||
|
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
@@ -113,7 +114,7 @@ def attrib(
|
|||||||
type=None,
|
type=None,
|
||||||
converter=None,
|
converter=None,
|
||||||
factory=None,
|
factory=None,
|
||||||
kw_only=False,
|
kw_only=None,
|
||||||
eq=None,
|
eq=None,
|
||||||
order=None,
|
order=None,
|
||||||
on_setattr=None,
|
on_setattr=None,
|
||||||
@@ -156,6 +157,9 @@ def attrib(
|
|||||||
*eq*, *order*, and *cmp* also accept a custom callable
|
*eq*, *order*, and *cmp* also accept a custom callable
|
||||||
.. versionchanged:: 21.1.0 *cmp* undeprecated
|
.. versionchanged:: 21.1.0 *cmp* undeprecated
|
||||||
.. versionadded:: 22.2.0 *alias*
|
.. versionadded:: 22.2.0 *alias*
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*kw_only* can now be None, and its default is also changed from False to
|
||||||
|
None.
|
||||||
"""
|
"""
|
||||||
eq, eq_key, order, order_key = _determine_attrib_eq_order(
|
eq, eq_key, order, order_key = _determine_attrib_eq_order(
|
||||||
cmp, eq, order, True
|
cmp, eq, order, True
|
||||||
@@ -373,7 +377,12 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
|
|||||||
|
|
||||||
|
|
||||||
def _transform_attrs(
|
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:
|
) -> _Attributes:
|
||||||
"""
|
"""
|
||||||
Transform all `_CountingAttr`s on a class into `Attribute`s.
|
Transform all `_CountingAttr`s on a class into `Attribute`s.
|
||||||
@@ -428,8 +437,15 @@ def _transform_attrs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fca = Attribute.from_counting_attr
|
fca = Attribute.from_counting_attr
|
||||||
|
no = ClassProps.KeywordOnly.NO
|
||||||
own_attrs = [
|
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:
|
if collect_by_mro:
|
||||||
@@ -441,12 +457,21 @@ def _transform_attrs(
|
|||||||
cls, {a.name for a in own_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]
|
own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
|
||||||
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
|
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
|
||||||
|
|
||||||
attrs = base_attrs + own_attrs
|
attrs = base_attrs + own_attrs
|
||||||
|
|
||||||
|
# Resolve default field alias before executing field_transformer, so that
|
||||||
|
# the transformer receives fully populated Attribute objects with usable
|
||||||
|
# alias values.
|
||||||
|
for a in attrs:
|
||||||
|
if not a.alias:
|
||||||
|
# Evolve is very slow, so we hold our nose and do it dirty.
|
||||||
|
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
|
||||||
|
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
|
||||||
|
|
||||||
if field_transformer is not None:
|
if field_transformer is not None:
|
||||||
attrs = tuple(field_transformer(cls, attrs))
|
attrs = tuple(field_transformer(cls, attrs))
|
||||||
|
|
||||||
@@ -464,13 +489,12 @@ def _transform_attrs(
|
|||||||
if had_default is False and a.default is not NOTHING:
|
if had_default is False and a.default is not NOTHING:
|
||||||
had_default = True
|
had_default = True
|
||||||
|
|
||||||
# Resolve default field alias after executing field_transformer.
|
# Resolve default field alias for any new attributes that the
|
||||||
# This allows field_transformer to differentiate between explicit vs
|
# field_transformer may have added without setting an alias.
|
||||||
# default aliases and supply their own defaults.
|
|
||||||
for a in attrs:
|
for a in attrs:
|
||||||
if not a.alias:
|
if not a.alias:
|
||||||
# Evolve is very slow, so we hold our nose and do it dirty.
|
|
||||||
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
|
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
|
||||||
|
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
|
||||||
|
|
||||||
# Create AttrsClass *after* applying the field_transformer since it may
|
# Create AttrsClass *after* applying the field_transformer since it may
|
||||||
# add or remove attributes!
|
# add or remove attributes!
|
||||||
@@ -553,7 +577,7 @@ def _frozen_delattrs(self, name):
|
|||||||
"""
|
"""
|
||||||
Attached to frozen classes as __delattr__.
|
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)
|
BaseException.__delattr__(self, name)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -651,38 +675,31 @@ class _ClassBuilder:
|
|||||||
self,
|
self,
|
||||||
cls: type,
|
cls: type,
|
||||||
these,
|
these,
|
||||||
slots,
|
auto_attribs: bool,
|
||||||
frozen,
|
props: ClassProps,
|
||||||
weakref_slot,
|
has_custom_setattr: bool,
|
||||||
getstate_setstate,
|
|
||||||
auto_attribs,
|
|
||||||
kw_only,
|
|
||||||
cache_hash,
|
|
||||||
is_exc,
|
|
||||||
collect_by_mro,
|
|
||||||
on_setattr,
|
|
||||||
has_custom_setattr,
|
|
||||||
field_transformer,
|
|
||||||
):
|
):
|
||||||
attrs, base_attrs, base_map = _transform_attrs(
|
attrs, base_attrs, base_map = _transform_attrs(
|
||||||
cls,
|
cls,
|
||||||
these,
|
these,
|
||||||
auto_attribs,
|
auto_attribs,
|
||||||
kw_only,
|
props.kw_only,
|
||||||
collect_by_mro,
|
props.collected_fields_by_mro,
|
||||||
field_transformer,
|
props.field_transformer,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._cls = cls
|
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._attrs = attrs
|
||||||
self._base_names = {a.name for a in base_attrs}
|
self._base_names = {a.name for a in base_attrs}
|
||||||
self._base_attr_map = base_map
|
self._base_attr_map = base_map
|
||||||
self._attr_names = tuple(a.name for a in attrs)
|
self._attr_names = tuple(a.name for a in attrs)
|
||||||
self._slots = slots
|
self._slots = props.is_slotted
|
||||||
self._frozen = frozen
|
self._frozen = props.is_frozen
|
||||||
self._weakref_slot = weakref_slot
|
self._weakref_slot = props.has_weakref_slot
|
||||||
self._cache_hash = cache_hash
|
self._cache_hash = (
|
||||||
|
props.hashability is ClassProps.Hashability.HASHABLE_CACHED
|
||||||
|
)
|
||||||
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
|
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
|
||||||
self._pre_init_has_args = False
|
self._pre_init_has_args = False
|
||||||
if self._has_pre_init:
|
if self._has_pre_init:
|
||||||
@@ -693,20 +710,21 @@ class _ClassBuilder:
|
|||||||
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
|
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
|
||||||
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
|
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
|
||||||
self._delete_attribs = not bool(these)
|
self._delete_attribs = not bool(these)
|
||||||
self._is_exc = is_exc
|
self._is_exc = props.is_exception
|
||||||
self._on_setattr = on_setattr
|
self._on_setattr = props.on_setattr_hook
|
||||||
|
|
||||||
self._has_custom_setattr = has_custom_setattr
|
self._has_custom_setattr = has_custom_setattr
|
||||||
self._wrote_own_setattr = False
|
self._wrote_own_setattr = False
|
||||||
|
|
||||||
self._cls_dict["__attrs_attrs__"] = self._attrs
|
self._cls_dict["__attrs_attrs__"] = self._attrs
|
||||||
|
self._cls_dict["__attrs_props__"] = props
|
||||||
|
|
||||||
if frozen:
|
if props.is_frozen:
|
||||||
self._cls_dict["__setattr__"] = _frozen_setattrs
|
self._cls_dict["__setattr__"] = _frozen_setattrs
|
||||||
self._cls_dict["__delattr__"] = _frozen_delattrs
|
self._cls_dict["__delattr__"] = _frozen_delattrs
|
||||||
|
|
||||||
self._wrote_own_setattr = True
|
self._wrote_own_setattr = True
|
||||||
elif on_setattr in (
|
elif self._on_setattr in (
|
||||||
_DEFAULT_ON_SETATTR,
|
_DEFAULT_ON_SETATTR,
|
||||||
setters.validate,
|
setters.validate,
|
||||||
setters.convert,
|
setters.convert,
|
||||||
@@ -722,18 +740,18 @@ class _ClassBuilder:
|
|||||||
break
|
break
|
||||||
if (
|
if (
|
||||||
(
|
(
|
||||||
on_setattr == _DEFAULT_ON_SETATTR
|
self._on_setattr == _DEFAULT_ON_SETATTR
|
||||||
and not (has_validator or has_converter)
|
and not (has_validator or has_converter)
|
||||||
)
|
)
|
||||||
or (on_setattr == setters.validate and not has_validator)
|
or (self._on_setattr == setters.validate and not has_validator)
|
||||||
or (on_setattr == setters.convert and not has_converter)
|
or (self._on_setattr == setters.convert and not has_converter)
|
||||||
):
|
):
|
||||||
# If class-level on_setattr is set to convert + validate, but
|
# If class-level on_setattr is set to convert + validate, but
|
||||||
# there's no field to convert or validate, pretend like there's
|
# there's no field to convert or validate, pretend like there's
|
||||||
# no on_setattr.
|
# no on_setattr.
|
||||||
self._on_setattr = None
|
self._on_setattr = None
|
||||||
|
|
||||||
if getstate_setstate:
|
if props.added_pickling:
|
||||||
(
|
(
|
||||||
self._cls_dict["__getstate__"],
|
self._cls_dict["__getstate__"],
|
||||||
self._cls_dict["__setstate__"],
|
self._cls_dict["__setstate__"],
|
||||||
@@ -784,6 +802,7 @@ class _ClassBuilder:
|
|||||||
self._eval_snippets()
|
self._eval_snippets()
|
||||||
if self._slots is True:
|
if self._slots is True:
|
||||||
cls = self._create_slots_class()
|
cls = self._create_slots_class()
|
||||||
|
self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)
|
||||||
else:
|
else:
|
||||||
cls = self._patch_original_class()
|
cls = self._patch_original_class()
|
||||||
if PY_3_10_PLUS:
|
if PY_3_10_PLUS:
|
||||||
@@ -845,6 +864,10 @@ class _ClassBuilder:
|
|||||||
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")
|
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 3.14.0rc2+
|
||||||
|
if hasattr(sys, "_clear_type_descriptors"):
|
||||||
|
sys._clear_type_descriptors(self._cls)
|
||||||
|
|
||||||
# If our class doesn't have its own implementation of __setattr__
|
# If our class doesn't have its own implementation of __setattr__
|
||||||
# (either from the user or by us), check the bases, if one of them has
|
# (either from the user or by us), check the bases, if one of them has
|
||||||
# an attrs-made __setattr__, that needs to be reset. We don't walk the
|
# an attrs-made __setattr__, that needs to be reset. We don't walk the
|
||||||
@@ -1081,9 +1104,7 @@ class _ClassBuilder:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def add_replace(self):
|
def add_replace(self):
|
||||||
self._cls_dict["__replace__"] = self._add_method_dunders(
|
self._cls_dict["__replace__"] = self._add_method_dunders(evolve)
|
||||||
lambda self, **changes: evolve(self, **changes)
|
|
||||||
)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_match_args(self):
|
def add_match_args(self):
|
||||||
@@ -1326,6 +1347,7 @@ def attrs(
|
|||||||
field_transformer=None,
|
field_transformer=None,
|
||||||
match_args=True,
|
match_args=True,
|
||||||
unsafe_hash=None,
|
unsafe_hash=None,
|
||||||
|
force_kw_only=True,
|
||||||
):
|
):
|
||||||
r"""
|
r"""
|
||||||
A class decorator that adds :term:`dunder methods` according to the
|
A class decorator that adds :term:`dunder methods` according to the
|
||||||
@@ -1392,6 +1414,10 @@ def attrs(
|
|||||||
If a class has an *inherited* classmethod called
|
If a class has an *inherited* classmethod called
|
||||||
``__attrs_init_subclass__``, it is executed after the class is created.
|
``__attrs_init_subclass__``, it is executed after the class is created.
|
||||||
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
|
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*kw_only* now only applies to attributes defined in the current class,
|
||||||
|
and respects attribute-level ``kw_only=False`` settings.
|
||||||
|
.. versionadded:: 25.4.0 *force_kw_only*
|
||||||
"""
|
"""
|
||||||
if repr_ns is not None:
|
if repr_ns is not None:
|
||||||
import warnings
|
import warnings
|
||||||
@@ -1413,6 +1439,7 @@ def attrs(
|
|||||||
on_setattr = setters.pipe(*on_setattr)
|
on_setattr = setters.pipe(*on_setattr)
|
||||||
|
|
||||||
def wrap(cls):
|
def wrap(cls):
|
||||||
|
nonlocal hash
|
||||||
is_frozen = frozen or _has_frozen_base_class(cls)
|
is_frozen = frozen or _has_frozen_base_class(cls)
|
||||||
is_exc = auto_exc is True and issubclass(cls, BaseException)
|
is_exc = auto_exc is True and issubclass(cls, BaseException)
|
||||||
has_own_setattr = auto_detect and _has_own_attribute(
|
has_own_setattr = auto_detect and _has_own_attribute(
|
||||||
@@ -1423,84 +1450,112 @@ def attrs(
|
|||||||
msg = "Can't freeze a class with a custom __setattr__."
|
msg = "Can't freeze a class with a custom __setattr__."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
builder = _ClassBuilder(
|
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,
|
cls,
|
||||||
these,
|
order_,
|
||||||
slots,
|
auto_detect,
|
||||||
is_frozen,
|
("__lt__", "__le__", "__gt__", "__ge__"),
|
||||||
weakref_slot,
|
),
|
||||||
_determine_whether_to_implement(
|
hashability=hashability,
|
||||||
|
added_match_args=match_args,
|
||||||
|
kw_only=kwo,
|
||||||
|
has_weakref_slot=weakref_slot,
|
||||||
|
added_str=str,
|
||||||
|
added_pickling=_determine_whether_to_implement(
|
||||||
cls,
|
cls,
|
||||||
getstate_setstate,
|
getstate_setstate,
|
||||||
auto_detect,
|
auto_detect,
|
||||||
("__getstate__", "__setstate__"),
|
("__getstate__", "__setstate__"),
|
||||||
default=slots,
|
default=slots,
|
||||||
),
|
),
|
||||||
auto_attribs,
|
on_setattr_hook=on_setattr,
|
||||||
kw_only,
|
field_transformer=field_transformer,
|
||||||
cache_hash,
|
|
||||||
is_exc,
|
|
||||||
collect_by_mro,
|
|
||||||
on_setattr,
|
|
||||||
has_own_setattr,
|
|
||||||
field_transformer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if _determine_whether_to_implement(
|
if not props.is_hashable and cache_hash:
|
||||||
cls, repr, auto_detect, ("__repr__",)
|
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)
|
builder.add_repr(repr_ns)
|
||||||
|
|
||||||
if str is True:
|
if props.added_str:
|
||||||
builder.add_str()
|
builder.add_str()
|
||||||
|
|
||||||
eq = _determine_whether_to_implement(
|
if props.added_eq:
|
||||||
cls, eq_, auto_detect, ("__eq__", "__ne__")
|
|
||||||
)
|
|
||||||
if not is_exc and eq is True:
|
|
||||||
builder.add_eq()
|
builder.add_eq()
|
||||||
if not is_exc and _determine_whether_to_implement(
|
if props.added_ordering:
|
||||||
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
|
|
||||||
):
|
|
||||||
builder.add_order()
|
builder.add_order()
|
||||||
|
|
||||||
if not frozen:
|
if not frozen:
|
||||||
builder.add_setattr()
|
builder.add_setattr()
|
||||||
|
|
||||||
nonlocal hash
|
if props.is_hashable:
|
||||||
if (
|
|
||||||
hash is None
|
|
||||||
and auto_detect is True
|
|
||||||
and _has_own_attribute(cls, "__hash__")
|
|
||||||
):
|
|
||||||
hash = False
|
|
||||||
|
|
||||||
if hash is not True and hash is not False and hash is not None:
|
|
||||||
# Can't use `hash in` because 1 == True for example.
|
|
||||||
msg = "Invalid value for hash. Must be True, False, or None."
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
if hash is False or (hash is None and eq is False) or is_exc:
|
|
||||||
# Don't do anything. Should fall back to __object__'s __hash__
|
|
||||||
# which is by id.
|
|
||||||
if cache_hash:
|
|
||||||
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
|
|
||||||
raise TypeError(msg)
|
|
||||||
elif hash is True or (
|
|
||||||
hash is None and eq is True and is_frozen is True
|
|
||||||
):
|
|
||||||
# Build a __hash__ if told so, or if it's safe.
|
|
||||||
builder.add_hash()
|
builder.add_hash()
|
||||||
else:
|
elif props.hashability is Hashability.UNHASHABLE:
|
||||||
# Raise TypeError on attempts to hash.
|
|
||||||
if cache_hash:
|
|
||||||
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
|
|
||||||
raise TypeError(msg)
|
|
||||||
builder.make_unhashable()
|
builder.make_unhashable()
|
||||||
|
|
||||||
if _determine_whether_to_implement(
|
if props.added_init:
|
||||||
cls, init, auto_detect, ("__init__",)
|
|
||||||
):
|
|
||||||
builder.add_init()
|
builder.add_init()
|
||||||
else:
|
else:
|
||||||
builder.add_attrs_init()
|
builder.add_attrs_init()
|
||||||
@@ -1835,16 +1890,16 @@ def _add_repr(cls, ns=None, attrs=None):
|
|||||||
|
|
||||||
def fields(cls):
|
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
|
The tuple also allows accessing the fields by their names (see below for
|
||||||
examples).
|
examples).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cls (type): Class to introspect.
|
cls (type): Class or instance to introspect.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TypeError: If *cls* is not a class.
|
TypeError: If *cls* is neither a class nor an *attrs* instance.
|
||||||
|
|
||||||
attrs.exceptions.NotAnAttrsClassError:
|
attrs.exceptions.NotAnAttrsClassError:
|
||||||
If *cls* is not an *attrs* class.
|
If *cls* is not an *attrs* class.
|
||||||
@@ -1855,13 +1910,18 @@ def fields(cls):
|
|||||||
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
|
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
|
||||||
by name.
|
by name.
|
||||||
.. versionchanged:: 23.1.0 Add support for generic classes.
|
.. versionchanged:: 23.1.0 Add support for generic classes.
|
||||||
|
.. versionchanged:: 26.1.0 Add support for instances.
|
||||||
"""
|
"""
|
||||||
generic_base = get_generic_base(cls)
|
generic_base = get_generic_base(cls)
|
||||||
|
|
||||||
if generic_base is None and not isinstance(cls, type):
|
if generic_base is None and not isinstance(cls, type):
|
||||||
msg = "Passed object must be a class."
|
type_ = type(cls)
|
||||||
|
if getattr(type_, "__attrs_attrs__", None) is None:
|
||||||
|
msg = "Passed object must be a class or attrs instance."
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
return fields(type_)
|
||||||
|
|
||||||
attrs = getattr(cls, "__attrs_attrs__", None)
|
attrs = getattr(cls, "__attrs_attrs__", None)
|
||||||
|
|
||||||
if attrs is None:
|
if attrs is None:
|
||||||
@@ -1967,7 +2027,7 @@ def _make_init_script(
|
|||||||
attr_dict[a.name] = a
|
attr_dict[a.name] = a
|
||||||
|
|
||||||
if a.on_setattr is not None:
|
if a.on_setattr is not None:
|
||||||
if frozen is True:
|
if frozen is True and a.on_setattr is not setters.NO_OP:
|
||||||
msg = "Frozen classes can't use on_setattr."
|
msg = "Frozen classes can't use on_setattr."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@@ -2126,8 +2186,9 @@ def _attrs_to_init_script(
|
|||||||
)
|
)
|
||||||
lines.extend(extra_lines)
|
lines.extend(extra_lines)
|
||||||
|
|
||||||
args = []
|
args = [] # Parameters in the definition of __init__
|
||||||
kw_only_args = []
|
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 = []
|
attrs_to_validate = []
|
||||||
|
|
||||||
# This is a dictionary of names to validator and converter callables.
|
# This is a dictionary of names to validator and converter callables.
|
||||||
@@ -2205,6 +2266,7 @@ def _attrs_to_init_script(
|
|||||||
kw_only_args.append(arg)
|
kw_only_args.append(arg)
|
||||||
else:
|
else:
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
|
pre_init_args.append(arg_name)
|
||||||
|
|
||||||
if converter is not None:
|
if converter is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -2224,6 +2286,7 @@ def _attrs_to_init_script(
|
|||||||
kw_only_args.append(arg)
|
kw_only_args.append(arg)
|
||||||
else:
|
else:
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
|
pre_init_args.append(arg_name)
|
||||||
lines.append(f"if {arg_name} is not NOTHING:")
|
lines.append(f"if {arg_name} is not NOTHING:")
|
||||||
|
|
||||||
init_factory_name = _INIT_FACTORY_PAT % (a.name,)
|
init_factory_name = _INIT_FACTORY_PAT % (a.name,)
|
||||||
@@ -2266,6 +2329,7 @@ def _attrs_to_init_script(
|
|||||||
kw_only_args.append(arg_name)
|
kw_only_args.append(arg_name)
|
||||||
else:
|
else:
|
||||||
args.append(arg_name)
|
args.append(arg_name)
|
||||||
|
pre_init_args.append(arg_name)
|
||||||
|
|
||||||
if converter is not None:
|
if converter is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -2322,7 +2386,7 @@ def _attrs_to_init_script(
|
|||||||
lines.append(f"BaseException.__init__(self, {vals})")
|
lines.append(f"BaseException.__init__(self, {vals})")
|
||||||
|
|
||||||
args = ", ".join(args)
|
args = ", ".join(args)
|
||||||
pre_init_args = args
|
pre_init_args = ", ".join(pre_init_args)
|
||||||
if kw_only_args:
|
if kw_only_args:
|
||||||
# leading comma & kw_only args
|
# leading comma & kw_only args
|
||||||
args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}"
|
args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}"
|
||||||
@@ -2337,7 +2401,7 @@ def _attrs_to_init_script(
|
|||||||
pre_init_args += pre_init_kw_only_args
|
pre_init_args += pre_init_kw_only_args
|
||||||
|
|
||||||
if call_pre_init and pre_init_has_args:
|
if call_pre_init and pre_init_has_args:
|
||||||
# If pre init method has arguments, pass 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})"
|
lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
|
||||||
|
|
||||||
# Python <3.12 doesn't allow backslashes in f-strings.
|
# Python <3.12 doesn't allow backslashes in f-strings.
|
||||||
@@ -2376,6 +2440,8 @@ class Attribute:
|
|||||||
- ``name`` (`str`): The name of the attribute.
|
- ``name`` (`str`): The name of the attribute.
|
||||||
- ``alias`` (`str`): The __init__ parameter name of the attribute, after
|
- ``alias`` (`str`): The __init__ parameter name of the attribute, after
|
||||||
any explicit overrides and default private-attribute-name handling.
|
any explicit overrides and default private-attribute-name handling.
|
||||||
|
- ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically
|
||||||
|
generated (``True``) or explicitly provided by the user (``False``).
|
||||||
- ``inherited`` (`bool`): Whether or not that attribute has been inherited
|
- ``inherited`` (`bool`): Whether or not that attribute has been inherited
|
||||||
from a base class.
|
from a base class.
|
||||||
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
|
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
|
||||||
@@ -2401,6 +2467,7 @@ class Attribute:
|
|||||||
equality checks and hashing anymore.
|
equality checks and hashing anymore.
|
||||||
.. versionadded:: 21.1.0 *eq_key* and *order_key*
|
.. versionadded:: 21.1.0 *eq_key* and *order_key*
|
||||||
.. versionadded:: 22.2.0 *alias*
|
.. versionadded:: 22.2.0 *alias*
|
||||||
|
.. versionadded:: 26.1.0 *alias_is_default*
|
||||||
|
|
||||||
For the full version history of the fields, see `attr.ib`.
|
For the full version history of the fields, see `attr.ib`.
|
||||||
"""
|
"""
|
||||||
@@ -2425,6 +2492,7 @@ class Attribute:
|
|||||||
"inherited",
|
"inherited",
|
||||||
"on_setattr",
|
"on_setattr",
|
||||||
"alias",
|
"alias",
|
||||||
|
"alias_is_default",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -2447,6 +2515,7 @@ class Attribute:
|
|||||||
order_key=None,
|
order_key=None,
|
||||||
on_setattr=None,
|
on_setattr=None,
|
||||||
alias=None,
|
alias=None,
|
||||||
|
alias_is_default=None,
|
||||||
):
|
):
|
||||||
eq, eq_key, order, order_key = _determine_attrib_eq_order(
|
eq, eq_key, order, order_key = _determine_attrib_eq_order(
|
||||||
cmp, eq_key or eq, order_key or order, True
|
cmp, eq_key or eq, order_key or order, True
|
||||||
@@ -2481,12 +2550,20 @@ class Attribute:
|
|||||||
bound_setattr("inherited", inherited)
|
bound_setattr("inherited", inherited)
|
||||||
bound_setattr("on_setattr", on_setattr)
|
bound_setattr("on_setattr", on_setattr)
|
||||||
bound_setattr("alias", alias)
|
bound_setattr("alias", alias)
|
||||||
|
bound_setattr(
|
||||||
|
"alias_is_default",
|
||||||
|
alias is None if alias_is_default is None else alias_is_default,
|
||||||
|
)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
raise FrozenInstanceError
|
raise FrozenInstanceError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_counting_attr(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:
|
# type holds the annotated value. deal with conflicts:
|
||||||
if type is None:
|
if type is None:
|
||||||
type = ca.type
|
type = ca.type
|
||||||
@@ -2505,13 +2582,14 @@ class Attribute:
|
|||||||
ca.metadata,
|
ca.metadata,
|
||||||
type,
|
type,
|
||||||
ca.converter,
|
ca.converter,
|
||||||
ca.kw_only,
|
kw_only if ca.kw_only is None else ca.kw_only,
|
||||||
ca.eq,
|
ca.eq,
|
||||||
ca.eq_key,
|
ca.eq_key,
|
||||||
ca.order,
|
ca.order,
|
||||||
ca.order_key,
|
ca.order_key,
|
||||||
ca.on_setattr,
|
ca.on_setattr,
|
||||||
ca.alias,
|
ca.alias,
|
||||||
|
ca.alias is None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Don't use attrs.evolve since fields(Attribute) doesn't work
|
# Don't use attrs.evolve since fields(Attribute) doesn't work
|
||||||
@@ -2530,6 +2608,20 @@ class Attribute:
|
|||||||
|
|
||||||
new._setattrs(changes.items())
|
new._setattrs(changes.items())
|
||||||
|
|
||||||
|
if "alias" in changes and "alias_is_default" not in changes:
|
||||||
|
# Explicit alias provided -- no longer the default.
|
||||||
|
_OBJ_SETATTR.__get__(new)("alias_is_default", False)
|
||||||
|
elif (
|
||||||
|
"name" in changes
|
||||||
|
and "alias" not in changes
|
||||||
|
# Don't auto-generate alias if the user picked picked the old one.
|
||||||
|
and self.alias_is_default
|
||||||
|
):
|
||||||
|
# Name changed, alias was auto-generated -- update it.
|
||||||
|
_OBJ_SETATTR.__get__(new)(
|
||||||
|
"alias", _default_init_alias_for(new.name)
|
||||||
|
)
|
||||||
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
# Don't use _add_pickle since fields(Attribute) doesn't work
|
# Don't use _add_pickle since fields(Attribute) doesn't work
|
||||||
@@ -2546,6 +2638,17 @@ class Attribute:
|
|||||||
"""
|
"""
|
||||||
Play nice with pickle.
|
Play nice with pickle.
|
||||||
"""
|
"""
|
||||||
|
if len(state) < len(self.__slots__):
|
||||||
|
# Pre-26.1.0 pickle without alias_is_default -- infer it
|
||||||
|
# heuristically.
|
||||||
|
state_dict = dict(zip(self.__slots__, state))
|
||||||
|
alias_is_default = state_dict.get(
|
||||||
|
"alias"
|
||||||
|
) is None or state_dict.get("alias") == _default_init_alias_for(
|
||||||
|
state_dict["name"]
|
||||||
|
)
|
||||||
|
state = (*state, alias_is_default)
|
||||||
|
|
||||||
self._setattrs(zip(self.__slots__, state))
|
self._setattrs(zip(self.__slots__, state))
|
||||||
|
|
||||||
def _setattrs(self, name_values_pairs):
|
def _setattrs(self, name_values_pairs):
|
||||||
@@ -2569,7 +2672,7 @@ _a = [
|
|||||||
name=name,
|
name=name,
|
||||||
default=NOTHING,
|
default=NOTHING,
|
||||||
validator=None,
|
validator=None,
|
||||||
repr=True,
|
repr=(name != "alias_is_default"),
|
||||||
cmp=None,
|
cmp=None,
|
||||||
eq=True,
|
eq=True,
|
||||||
order=False,
|
order=False,
|
||||||
@@ -2741,6 +2844,188 @@ class _CountingAttr:
|
|||||||
_CountingAttr = _add_eq(_add_repr(_CountingAttr))
|
_CountingAttr = _add_eq(_add_repr(_CountingAttr))
|
||||||
|
|
||||||
|
|
||||||
|
class ClassProps:
|
||||||
|
"""
|
||||||
|
Effective class properties as derived from parameters to `attr.s()` or
|
||||||
|
`define()` decorators.
|
||||||
|
|
||||||
|
This is the same data structure that *attrs* uses internally to decide how
|
||||||
|
to construct the final class.
|
||||||
|
|
||||||
|
Warning:
|
||||||
|
|
||||||
|
This feature is currently **experimental** and is not covered by our
|
||||||
|
strict backwards-compatibility guarantees.
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
is_exception (bool):
|
||||||
|
Whether the class is treated as an exception class.
|
||||||
|
|
||||||
|
is_slotted (bool):
|
||||||
|
Whether the class is `slotted <slotted classes>`.
|
||||||
|
|
||||||
|
has_weakref_slot (bool):
|
||||||
|
Whether the class has a slot for weak references.
|
||||||
|
|
||||||
|
is_frozen (bool):
|
||||||
|
Whether the class is frozen.
|
||||||
|
|
||||||
|
kw_only (KeywordOnly):
|
||||||
|
Whether / how the class enforces keyword-only arguments on the
|
||||||
|
``__init__`` method.
|
||||||
|
|
||||||
|
collected_fields_by_mro (bool):
|
||||||
|
Whether the class fields were collected by method resolution order.
|
||||||
|
That is, correctly but unlike `dataclasses`.
|
||||||
|
|
||||||
|
added_init (bool):
|
||||||
|
Whether the class has an *attrs*-generated ``__init__`` method.
|
||||||
|
|
||||||
|
added_repr (bool):
|
||||||
|
Whether the class has an *attrs*-generated ``__repr__`` method.
|
||||||
|
|
||||||
|
added_eq (bool):
|
||||||
|
Whether the class has *attrs*-generated equality methods.
|
||||||
|
|
||||||
|
added_ordering (bool):
|
||||||
|
Whether the class has *attrs*-generated ordering methods.
|
||||||
|
|
||||||
|
hashability (Hashability): How `hashable <hashing>` the class is.
|
||||||
|
|
||||||
|
added_match_args (bool):
|
||||||
|
Whether the class supports positional `match <match>` over its
|
||||||
|
fields.
|
||||||
|
|
||||||
|
added_str (bool):
|
||||||
|
Whether the class has an *attrs*-generated ``__str__`` method.
|
||||||
|
|
||||||
|
added_pickling (bool):
|
||||||
|
Whether the class has *attrs*-generated ``__getstate__`` and
|
||||||
|
``__setstate__`` methods for `pickle`.
|
||||||
|
|
||||||
|
on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None):
|
||||||
|
The class's ``__setattr__`` hook.
|
||||||
|
|
||||||
|
field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None):
|
||||||
|
The class's `field transformers <transform-fields>`.
|
||||||
|
|
||||||
|
.. versionadded:: 25.4.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Hashability(enum.Enum):
|
||||||
|
"""
|
||||||
|
The hashability of a class.
|
||||||
|
|
||||||
|
.. versionadded:: 25.4.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
HASHABLE = "hashable"
|
||||||
|
"""Write a ``__hash__``."""
|
||||||
|
HASHABLE_CACHED = "hashable_cache"
|
||||||
|
"""Write a ``__hash__`` and cache the hash."""
|
||||||
|
UNHASHABLE = "unhashable"
|
||||||
|
"""Set ``__hash__`` to ``None``."""
|
||||||
|
LEAVE_ALONE = "leave_alone"
|
||||||
|
"""Don't touch ``__hash__``."""
|
||||||
|
|
||||||
|
class KeywordOnly(enum.Enum):
|
||||||
|
"""
|
||||||
|
How attributes should be treated regarding keyword-only parameters.
|
||||||
|
|
||||||
|
.. versionadded:: 25.4.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
NO = "no"
|
||||||
|
"""Attributes are not keyword-only."""
|
||||||
|
YES = "yes"
|
||||||
|
"""Attributes in current class without kw_only=False are keyword-only."""
|
||||||
|
FORCE = "force"
|
||||||
|
"""All attributes are keyword-only."""
|
||||||
|
|
||||||
|
__slots__ = ( # noqa: RUF023 -- order matters for __init__
|
||||||
|
"is_exception",
|
||||||
|
"is_slotted",
|
||||||
|
"has_weakref_slot",
|
||||||
|
"is_frozen",
|
||||||
|
"kw_only",
|
||||||
|
"collected_fields_by_mro",
|
||||||
|
"added_init",
|
||||||
|
"added_repr",
|
||||||
|
"added_eq",
|
||||||
|
"added_ordering",
|
||||||
|
"hashability",
|
||||||
|
"added_match_args",
|
||||||
|
"added_str",
|
||||||
|
"added_pickling",
|
||||||
|
"on_setattr_hook",
|
||||||
|
"field_transformer",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
is_exception,
|
||||||
|
is_slotted,
|
||||||
|
has_weakref_slot,
|
||||||
|
is_frozen,
|
||||||
|
kw_only,
|
||||||
|
collected_fields_by_mro,
|
||||||
|
added_init,
|
||||||
|
added_repr,
|
||||||
|
added_eq,
|
||||||
|
added_ordering,
|
||||||
|
hashability,
|
||||||
|
added_match_args,
|
||||||
|
added_str,
|
||||||
|
added_pickling,
|
||||||
|
on_setattr_hook,
|
||||||
|
field_transformer,
|
||||||
|
):
|
||||||
|
self.is_exception = is_exception
|
||||||
|
self.is_slotted = is_slotted
|
||||||
|
self.has_weakref_slot = has_weakref_slot
|
||||||
|
self.is_frozen = is_frozen
|
||||||
|
self.kw_only = kw_only
|
||||||
|
self.collected_fields_by_mro = collected_fields_by_mro
|
||||||
|
self.added_init = added_init
|
||||||
|
self.added_repr = added_repr
|
||||||
|
self.added_eq = added_eq
|
||||||
|
self.added_ordering = added_ordering
|
||||||
|
self.hashability = hashability
|
||||||
|
self.added_match_args = added_match_args
|
||||||
|
self.added_str = added_str
|
||||||
|
self.added_pickling = added_pickling
|
||||||
|
self.on_setattr_hook = on_setattr_hook
|
||||||
|
self.field_transformer = field_transformer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_hashable(self):
|
||||||
|
return (
|
||||||
|
self.hashability is ClassProps.Hashability.HASHABLE
|
||||||
|
or self.hashability is ClassProps.Hashability.HASHABLE_CACHED
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_cas = [
|
||||||
|
Attribute(
|
||||||
|
name=name,
|
||||||
|
default=NOTHING,
|
||||||
|
validator=None,
|
||||||
|
repr=True,
|
||||||
|
cmp=None,
|
||||||
|
eq=True,
|
||||||
|
order=False,
|
||||||
|
hash=True,
|
||||||
|
init=True,
|
||||||
|
inherited=False,
|
||||||
|
alias=_default_init_alias_for(name),
|
||||||
|
)
|
||||||
|
for name in ClassProps.__slots__
|
||||||
|
]
|
||||||
|
|
||||||
|
ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas)
|
||||||
|
|
||||||
|
|
||||||
class Factory:
|
class Factory:
|
||||||
"""
|
"""
|
||||||
Stores a factory callable.
|
Stores a factory callable.
|
||||||
@@ -2848,9 +3133,7 @@ class Converter:
|
|||||||
value, field
|
value, field
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.__call__ = lambda value, instance, field: self.converter(
|
self.__call__ = self.converter
|
||||||
value, instance, field
|
|
||||||
)
|
|
||||||
|
|
||||||
rt = ex.get_return_type()
|
rt = ex.get_return_type()
|
||||||
if rt is not None:
|
if rt is not None:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from ._make import (
|
|||||||
attrib,
|
attrib,
|
||||||
attrs,
|
attrs,
|
||||||
)
|
)
|
||||||
from .exceptions import UnannotatedAttributeError
|
from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError
|
||||||
|
|
||||||
|
|
||||||
def define(
|
def define(
|
||||||
@@ -43,6 +43,7 @@ def define(
|
|||||||
on_setattr=None,
|
on_setattr=None,
|
||||||
field_transformer=None,
|
field_transformer=None,
|
||||||
match_args=True,
|
match_args=True,
|
||||||
|
force_kw_only=False,
|
||||||
):
|
):
|
||||||
r"""
|
r"""
|
||||||
A class decorator that adds :term:`dunder methods` according to
|
A class decorator that adds :term:`dunder methods` according to
|
||||||
@@ -76,7 +77,7 @@ def define(
|
|||||||
sensible ``__ne__`` by default, so it *should* be enough to only
|
sensible ``__ne__`` by default, so it *should* be enough to only
|
||||||
implement ``__eq__`` in most cases).
|
implement ``__eq__`` in most cases).
|
||||||
|
|
||||||
Passing 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.
|
overrides whatever *auto_detect* would determine.
|
||||||
|
|
||||||
auto_exc (bool):
|
auto_exc (bool):
|
||||||
@@ -214,8 +215,12 @@ def define(
|
|||||||
5. Subclasses of a frozen class are frozen too.
|
5. Subclasses of a frozen class are frozen too.
|
||||||
|
|
||||||
kw_only (bool):
|
kw_only (bool):
|
||||||
Make all attributes keyword-only in the generated ``__init__`` (if
|
Make attributes keyword-only in the generated ``__init__`` (if
|
||||||
*init* is False, this parameter is ignored).
|
*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):
|
weakref_slot (bool):
|
||||||
Make instances weak-referenceable. This has no effect unless
|
Make instances weak-referenceable. This has no effect unless
|
||||||
@@ -244,6 +249,15 @@ def define(
|
|||||||
See also `issue #428
|
See also `issue #428
|
||||||
<https://github.com/python-attrs/attrs/issues/428>`_.
|
<https://github.com/python-attrs/attrs/issues/428>`_.
|
||||||
|
|
||||||
|
force_kw_only (bool):
|
||||||
|
A back-compat flag for restoring pre-25.4.0 behavior. If True and
|
||||||
|
``kw_only=True``, all attributes are made keyword-only, including
|
||||||
|
base class attributes, and those set to ``kw_only=False`` at the
|
||||||
|
attribute level. Defaults to False.
|
||||||
|
|
||||||
|
See also `issue #980
|
||||||
|
<https://github.com/python-attrs/attrs/issues/980>`_.
|
||||||
|
|
||||||
getstate_setstate (bool | None):
|
getstate_setstate (bool | None):
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -319,6 +333,11 @@ def define(
|
|||||||
.. versionadded:: 24.3.0
|
.. versionadded:: 24.3.0
|
||||||
Unless already present, a ``__replace__`` method is automatically
|
Unless already present, a ``__replace__`` method is automatically
|
||||||
created for `copy.replace` (Python 3.13+ only).
|
created for `copy.replace` (Python 3.13+ only).
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*kw_only* now only applies to attributes defined in the current class,
|
||||||
|
and respects attribute-level ``kw_only=False`` settings.
|
||||||
|
.. versionadded:: 25.4.0
|
||||||
|
Added *force_kw_only* to go back to the previous *kw_only* behavior.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -337,6 +356,7 @@ def define(
|
|||||||
- *auto_exc=True*
|
- *auto_exc=True*
|
||||||
- *auto_detect=True*
|
- *auto_detect=True*
|
||||||
- *order=False*
|
- *order=False*
|
||||||
|
- *force_kw_only=False*
|
||||||
- Some options that were only relevant on Python 2 or were kept around
|
- Some options that were only relevant on Python 2 or were kept around
|
||||||
for backwards-compatibility have been removed.
|
for backwards-compatibility have been removed.
|
||||||
|
|
||||||
@@ -366,6 +386,7 @@ def define(
|
|||||||
on_setattr=on_setattr,
|
on_setattr=on_setattr,
|
||||||
field_transformer=field_transformer,
|
field_transformer=field_transformer,
|
||||||
match_args=match_args,
|
match_args=match_args,
|
||||||
|
force_kw_only=force_kw_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
def wrap(cls):
|
def wrap(cls):
|
||||||
@@ -424,7 +445,7 @@ def field(
|
|||||||
type=None,
|
type=None,
|
||||||
converter=None,
|
converter=None,
|
||||||
factory=None,
|
factory=None,
|
||||||
kw_only=False,
|
kw_only=None,
|
||||||
eq=None,
|
eq=None,
|
||||||
order=None,
|
order=None,
|
||||||
on_setattr=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
|
itself. You can use it as part of your own code or for `static type
|
||||||
checking <types>`.
|
checking <types>`.
|
||||||
|
|
||||||
kw_only (bool):
|
kw_only (bool | None):
|
||||||
Make this attribute keyword-only in the generated ``__init__`` (if
|
Make this attribute keyword-only in the generated ``__init__`` (if
|
||||||
``init`` is False, this parameter is ignored).
|
*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]):
|
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]):
|
||||||
Allows to overwrite the *on_setattr* setting from `attr.s`. If left
|
Allows to overwrite the *on_setattr* setting from `attr.s`. If left
|
||||||
@@ -572,6 +594,9 @@ def field(
|
|||||||
.. versionadded:: 23.1.0
|
.. versionadded:: 23.1.0
|
||||||
The *type* parameter has been re-added; mostly for `attrs.make_class`.
|
The *type* parameter has been re-added; mostly for `attrs.make_class`.
|
||||||
Please note that type checkers ignore this metadata.
|
Please note that type checkers ignore this metadata.
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*kw_only* can now be None, and its default is also changed from False to
|
||||||
|
None.
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
@@ -621,3 +646,29 @@ def astuple(inst, *, recurse=True, filter=None):
|
|||||||
return _astuple(
|
return _astuple(
|
||||||
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
|
inst=inst, recurse=recurse, filter=filter, retain_collection_types=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(cls):
|
||||||
|
"""
|
||||||
|
Inspect the class and return its effective build parameters.
|
||||||
|
|
||||||
|
Warning:
|
||||||
|
This feature is currently **experimental** and is not covered by our
|
||||||
|
strict backwards-compatibility guarantees.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cls: The *attrs*-decorated class to inspect.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The effective build parameters of the class.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotAnAttrsClassError: If the class is not an *attrs*-decorated class.
|
||||||
|
|
||||||
|
.. versionadded:: 25.4.0
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return cls.__dict__["__attrs_props__"]
|
||||||
|
except KeyError:
|
||||||
|
msg = f"{cls!r} is not an attrs-decorated class."
|
||||||
|
raise NotAnAttrsClassError(msg) from None
|
||||||
|
|||||||
@@ -84,3 +84,6 @@ class VersionInfo:
|
|||||||
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
|
# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
|
||||||
# have to do anything special with releaselevel for now.
|
# have to do anything special with releaselevel for now.
|
||||||
return us < them
|
return us < them
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.year, self.minor, self.micro, self.releaselevel))
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
|
|
||||||
class FrozenError(AttributeError):
|
class FrozenError(AttributeError):
|
||||||
"""
|
"""
|
||||||
@@ -16,8 +14,10 @@ class FrozenError(AttributeError):
|
|||||||
.. versionadded:: 20.1.0
|
.. versionadded:: 20.1.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
msg = "can't set attribute"
|
msg = "can't set attribute"
|
||||||
args: ClassVar[tuple[str]] = [msg]
|
super().__init__(msg)
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
|
||||||
class FrozenInstanceError(FrozenError):
|
class FrozenInstanceError(FrozenError):
|
||||||
|
|||||||
@@ -79,12 +79,14 @@ def disabled():
|
|||||||
This context manager is not thread-safe!
|
This context manager is not thread-safe!
|
||||||
|
|
||||||
.. versionadded:: 21.3.0
|
.. versionadded:: 21.3.0
|
||||||
|
.. versionchanged:: 26.1.0 The contextmanager is nestable.
|
||||||
"""
|
"""
|
||||||
|
prev = get_run_validators()
|
||||||
set_run_validators(False)
|
set_run_validators(False)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
set_run_validators(True)
|
set_run_validators(prev)
|
||||||
|
|
||||||
|
|
||||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
@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.
|
A validator that performs deep validation of an iterable.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
member_validator: Validator to apply to iterable members.
|
member_validator: Validator(s) to apply to iterable members.
|
||||||
|
|
||||||
iterable_validator:
|
iterable_validator:
|
||||||
Validator to apply to iterable itself (optional).
|
Validator(s) to apply to iterable itself (optional).
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
TypeError: if any sub-validators fail
|
TypeError: if any sub-validators fail
|
||||||
|
|
||||||
.. versionadded:: 19.1.0
|
.. versionadded:: 19.1.0
|
||||||
|
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*member_validator* and *iterable_validator* can now be a list or tuple
|
||||||
|
of validators.
|
||||||
"""
|
"""
|
||||||
if isinstance(member_validator, (list, tuple)):
|
if isinstance(member_validator, (list, tuple)):
|
||||||
member_validator = and_(*member_validator)
|
member_validator = and_(*member_validator)
|
||||||
|
if isinstance(iterable_validator, (list, tuple)):
|
||||||
|
iterable_validator = and_(*iterable_validator)
|
||||||
return _DeepIterable(member_validator, iterable_validator)
|
return _DeepIterable(member_validator, iterable_validator)
|
||||||
|
|
||||||
|
|
||||||
@attrs(repr=False, slots=True, unsafe_hash=True)
|
@attrs(repr=False, slots=True, unsafe_hash=True)
|
||||||
class _DeepMapping:
|
class _DeepMapping:
|
||||||
key_validator = attrib(validator=is_callable())
|
key_validator = attrib(validator=optional(is_callable()))
|
||||||
value_validator = attrib(validator=is_callable())
|
value_validator = attrib(validator=optional(is_callable()))
|
||||||
mapping_validator = attrib(default=None, validator=optional(is_callable()))
|
mapping_validator = attrib(validator=optional(is_callable()))
|
||||||
|
|
||||||
def __call__(self, inst, attr, value):
|
def __call__(self, inst, attr, value):
|
||||||
"""
|
"""
|
||||||
@@ -390,30 +398,62 @@ class _DeepMapping:
|
|||||||
self.mapping_validator(inst, attr, value)
|
self.mapping_validator(inst, attr, value)
|
||||||
|
|
||||||
for key in value:
|
for key in value:
|
||||||
|
if self.key_validator is not None:
|
||||||
self.key_validator(inst, attr, key)
|
self.key_validator(inst, attr, key)
|
||||||
|
if self.value_validator is not None:
|
||||||
self.value_validator(inst, attr, value[key])
|
self.value_validator(inst, attr, value[key])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
|
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
|
||||||
|
|
||||||
|
|
||||||
def deep_mapping(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.
|
A validator that performs deep validation of a dictionary.
|
||||||
|
|
||||||
Args:
|
All validators are optional, but at least one of *key_validator* or
|
||||||
key_validator: Validator to apply to dictionary keys.
|
*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:
|
mapping_validator:
|
||||||
Validator to apply to top-level mapping attribute (optional).
|
Validator(s) to apply to top-level mapping attribute.
|
||||||
|
|
||||||
.. versionadded:: 19.1.0
|
.. versionadded:: 19.1.0
|
||||||
|
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*key_validator* and *value_validator* are now optional, but at least one
|
||||||
|
of them must be provided.
|
||||||
|
|
||||||
|
.. versionchanged:: 25.4.0
|
||||||
|
*key_validator*, *value_validator*, and *mapping_validator* can now be a
|
||||||
|
list or tuple of validators.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TypeError: if any sub-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)
|
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
|
A validator that raises `ValueError` if the initializer is called with a
|
||||||
number smaller or equal to *val*.
|
number smaller or equal to *val*.
|
||||||
|
|
||||||
The validator uses `operator.ge` to compare the values.
|
The validator uses `operator.gt` to compare the values.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
val: Exclusive lower bound for values
|
val: Exclusive lower bound for values
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ _T = TypeVar("_T")
|
|||||||
_T1 = TypeVar("_T1")
|
_T1 = TypeVar("_T1")
|
||||||
_T2 = TypeVar("_T2")
|
_T2 = TypeVar("_T2")
|
||||||
_T3 = TypeVar("_T3")
|
_T3 = TypeVar("_T3")
|
||||||
|
_T4 = TypeVar("_T4")
|
||||||
|
_T5 = TypeVar("_T5")
|
||||||
|
_T6 = TypeVar("_T6")
|
||||||
_I = TypeVar("_I", bound=Iterable)
|
_I = TypeVar("_I", bound=Iterable)
|
||||||
_K = TypeVar("_K")
|
_K = TypeVar("_K")
|
||||||
_V = TypeVar("_V")
|
_V = TypeVar("_V")
|
||||||
@@ -51,7 +54,7 @@ def optional(
|
|||||||
validator: (
|
validator: (
|
||||||
_ValidatorType[_T]
|
_ValidatorType[_T]
|
||||||
| list[_ValidatorType[_T]]
|
| list[_ValidatorType[_T]]
|
||||||
| tuple[_ValidatorType[_T]]
|
| tuple[_ValidatorType[_T], ...]
|
||||||
),
|
),
|
||||||
) -> _ValidatorType[_T | None]: ...
|
) -> _ValidatorType[_T | None]: ...
|
||||||
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
|
def in_(options: Container[_T]) -> _ValidatorType[_T]: ...
|
||||||
@@ -63,12 +66,19 @@ def matches_re(
|
|||||||
) -> _ValidatorType[AnyStr]: ...
|
) -> _ValidatorType[AnyStr]: ...
|
||||||
def deep_iterable(
|
def deep_iterable(
|
||||||
member_validator: _ValidatorArgType[_T],
|
member_validator: _ValidatorArgType[_T],
|
||||||
iterable_validator: _ValidatorType[_I] | None = ...,
|
iterable_validator: _ValidatorArgType[_I] | None = ...,
|
||||||
) -> _ValidatorType[_I]: ...
|
) -> _ValidatorType[_I]: ...
|
||||||
|
@overload
|
||||||
def deep_mapping(
|
def deep_mapping(
|
||||||
key_validator: _ValidatorType[_K],
|
key_validator: _ValidatorArgType[_K],
|
||||||
value_validator: _ValidatorType[_V],
|
value_validator: _ValidatorArgType[_V] | None = ...,
|
||||||
mapping_validator: _ValidatorType[_M] | 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]: ...
|
) -> _ValidatorType[_M]: ...
|
||||||
def is_callable() -> _ValidatorType[_T]: ...
|
def is_callable() -> _ValidatorType[_T]: ...
|
||||||
def lt(val: _T) -> _ValidatorType[_T]: ...
|
def lt(val: _T) -> _ValidatorType[_T]: ...
|
||||||
@@ -83,4 +93,48 @@ def not_(
|
|||||||
msg: str | None = None,
|
msg: str | None = None,
|
||||||
exc_types: type[Exception] | Iterable[type[Exception]] = ...,
|
exc_types: type[Exception] | Iterable[type[Exception]] = ...,
|
||||||
) -> _ValidatorType[_T]: ...
|
) -> _ValidatorType[_T]: ...
|
||||||
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
|
Metadata-Version: 2.4
|
||||||
Name: attrs
|
Name: attrs
|
||||||
Version: 25.3.0
|
Version: 26.1.0
|
||||||
Summary: Classes Without Boilerplate
|
Summary: Classes Without Boilerplate
|
||||||
Project-URL: Documentation, https://www.attrs.org/
|
Project-URL: Documentation, https://www.attrs.org/
|
||||||
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
|
Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html
|
||||||
@@ -12,62 +12,16 @@ License-Expression: MIT
|
|||||||
License-File: LICENSE
|
License-File: LICENSE
|
||||||
Keywords: attribute,boilerplate,class
|
Keywords: attribute,boilerplate,class
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
Classifier: Programming Language :: Python :: 3.8
|
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
Classifier: Programming Language :: Python :: 3.13
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
Classifier: Typing :: Typed
|
Classifier: Typing :: Typed
|
||||||
Requires-Python: >=3.8
|
Requires-Python: >=3.9
|
||||||
Provides-Extra: benchmark
|
|
||||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'benchmark'
|
|
||||||
Requires-Dist: hypothesis; extra == 'benchmark'
|
|
||||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
|
|
||||||
Requires-Dist: pympler; extra == 'benchmark'
|
|
||||||
Requires-Dist: pytest-codspeed; extra == 'benchmark'
|
|
||||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'benchmark'
|
|
||||||
Requires-Dist: pytest-xdist[psutil]; extra == 'benchmark'
|
|
||||||
Requires-Dist: pytest>=4.3.0; extra == 'benchmark'
|
|
||||||
Provides-Extra: cov
|
|
||||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'cov'
|
|
||||||
Requires-Dist: coverage[toml]>=5.3; extra == 'cov'
|
|
||||||
Requires-Dist: hypothesis; extra == 'cov'
|
|
||||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
|
|
||||||
Requires-Dist: pympler; extra == 'cov'
|
|
||||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'cov'
|
|
||||||
Requires-Dist: pytest-xdist[psutil]; extra == 'cov'
|
|
||||||
Requires-Dist: pytest>=4.3.0; extra == 'cov'
|
|
||||||
Provides-Extra: dev
|
|
||||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'dev'
|
|
||||||
Requires-Dist: hypothesis; extra == 'dev'
|
|
||||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
|
|
||||||
Requires-Dist: pre-commit-uv; extra == 'dev'
|
|
||||||
Requires-Dist: pympler; extra == 'dev'
|
|
||||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'dev'
|
|
||||||
Requires-Dist: pytest-xdist[psutil]; extra == 'dev'
|
|
||||||
Requires-Dist: pytest>=4.3.0; extra == 'dev'
|
|
||||||
Provides-Extra: docs
|
|
||||||
Requires-Dist: cogapp; extra == 'docs'
|
|
||||||
Requires-Dist: furo; extra == 'docs'
|
|
||||||
Requires-Dist: myst-parser; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx; extra == 'docs'
|
|
||||||
Requires-Dist: sphinx-notfound-page; extra == 'docs'
|
|
||||||
Requires-Dist: sphinxcontrib-towncrier; extra == 'docs'
|
|
||||||
Requires-Dist: towncrier; extra == 'docs'
|
|
||||||
Provides-Extra: tests
|
|
||||||
Requires-Dist: cloudpickle; (platform_python_implementation == 'CPython') and extra == 'tests'
|
|
||||||
Requires-Dist: hypothesis; extra == 'tests'
|
|
||||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
|
|
||||||
Requires-Dist: pympler; extra == 'tests'
|
|
||||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests'
|
|
||||||
Requires-Dist: pytest-xdist[psutil]; extra == 'tests'
|
|
||||||
Requires-Dist: pytest>=4.3.0; extra == 'tests'
|
|
||||||
Provides-Extra: tests-mypy
|
|
||||||
Requires-Dist: mypy>=1.11.1; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
|
|
||||||
Requires-Dist: pytest-mypy-plugins; (platform_python_implementation == 'CPython' and python_version >= '3.10') and extra == 'tests-mypy'
|
|
||||||
Description-Content-Type: text/markdown
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -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)).
|
*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.
|
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
|
import pathlib, tomllib
|
||||||
|
|
||||||
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]:
|
for sponsor in tomllib.loads(pathlib.Path("pyproject.toml").read_text())["tool"]["sponcon"]["sponsors"]:
|
||||||
print(f'<a href="{sponsor["url"]}"><img title="{sponsor["title"]}" src="https://www.attrs.org/en/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://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/25.3.0/_static/sponsors/Tidelift.svg" width="190" /></a>
|
<a href="https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek"><img title="Tidelift" src="https://www.attrs.org/en/26.1.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://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/25.3.0/_static/sponsors/Privacy-Solutions.svg" width="190" /></a>
|
<a href="https://privacy-solutions.org/"><img title="Privacy Solutions" src="https://www.attrs.org/en/26.1.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/26.1.0/_static/sponsors/FilePreviews.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://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/25.3.0/_static/sponsors/Polar.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]]] -->
|
<!-- [[[end]]] -->
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
@@ -220,10 +174,23 @@ Save time, reduce risk, and improve code health, while paying the maintainers of
|
|||||||
|
|
||||||
## Release Information
|
## Release Information
|
||||||
|
|
||||||
|
### Backwards-incompatible Changes
|
||||||
|
|
||||||
|
- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`.
|
||||||
|
The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`).
|
||||||
|
[#1509](https://github.com/python-attrs/attrs/issues/1509)
|
||||||
|
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Restore support for generator-based `field_transformer`s.
|
- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator.
|
||||||
[#1417](https://github.com/python-attrs/attrs/issues/1417)
|
[#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
|
Wheel-Version: 1.0
|
||||||
Generator: hatchling 1.27.0
|
Generator: hatchling 1.29.0
|
||||||
Root-Is-Purelib: true
|
Root-Is-Purelib: true
|
||||||
Tag: py3-none-any
|
Tag: py3-none-any
|
||||||
@@ -22,7 +22,8 @@ from attr import (
|
|||||||
resolve_types,
|
resolve_types,
|
||||||
validate,
|
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
|
from . import converters, exceptions, filters, setters, validators
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ __all__ = [
|
|||||||
"NOTHING",
|
"NOTHING",
|
||||||
"Attribute",
|
"Attribute",
|
||||||
"AttrsInstance",
|
"AttrsInstance",
|
||||||
|
"ClassProps",
|
||||||
"Converter",
|
"Converter",
|
||||||
"Factory",
|
"Factory",
|
||||||
"NothingType",
|
"NothingType",
|
||||||
@@ -58,6 +60,7 @@ __all__ = [
|
|||||||
"filters",
|
"filters",
|
||||||
"frozen",
|
"frozen",
|
||||||
"has",
|
"has",
|
||||||
|
"inspect",
|
||||||
"make_class",
|
"make_class",
|
||||||
"mutable",
|
"mutable",
|
||||||
"resolve_types",
|
"resolve_types",
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def field(
|
|||||||
metadata: Mapping[Any, Any] | None = ...,
|
metadata: Mapping[Any, Any] | None = ...,
|
||||||
converter: None = ...,
|
converter: None = ...,
|
||||||
factory: None = ...,
|
factory: None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: bool | None = ...,
|
eq: bool | None = ...,
|
||||||
order: bool | None = ...,
|
order: bool | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -98,10 +98,10 @@ def field(
|
|||||||
metadata: Mapping[Any, Any] | None = ...,
|
metadata: Mapping[Any, Any] | None = ...,
|
||||||
converter: _ConverterType
|
converter: _ConverterType
|
||||||
| list[_ConverterType]
|
| list[_ConverterType]
|
||||||
| tuple[_ConverterType]
|
| tuple[_ConverterType, ...]
|
||||||
| None = ...,
|
| None = ...,
|
||||||
factory: Callable[[], _T] | None = ...,
|
factory: Callable[[], _T] | None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -121,10 +121,10 @@ def field(
|
|||||||
metadata: Mapping[Any, Any] | None = ...,
|
metadata: Mapping[Any, Any] | None = ...,
|
||||||
converter: _ConverterType
|
converter: _ConverterType
|
||||||
| list[_ConverterType]
|
| list[_ConverterType]
|
||||||
| tuple[_ConverterType]
|
| tuple[_ConverterType, ...]
|
||||||
| None = ...,
|
| None = ...,
|
||||||
factory: Callable[[], _T] | None = ...,
|
factory: Callable[[], _T] | None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -144,10 +144,10 @@ def field(
|
|||||||
metadata: Mapping[Any, Any] | None = ...,
|
metadata: Mapping[Any, Any] | None = ...,
|
||||||
converter: _ConverterType
|
converter: _ConverterType
|
||||||
| list[_ConverterType]
|
| list[_ConverterType]
|
||||||
| tuple[_ConverterType]
|
| tuple[_ConverterType, ...]
|
||||||
| None = ...,
|
| None = ...,
|
||||||
factory: Callable[[], _T] | None = ...,
|
factory: Callable[[], _T] | None = ...,
|
||||||
kw_only: bool = ...,
|
kw_only: bool | None = ...,
|
||||||
eq: _EqOrderType | None = ...,
|
eq: _EqOrderType | None = ...,
|
||||||
order: _EqOrderType | None = ...,
|
order: _EqOrderType | None = ...,
|
||||||
on_setattr: _OnSetAttrArgType | None = ...,
|
on_setattr: _OnSetAttrArgType | None = ...,
|
||||||
@@ -261,3 +261,54 @@ def frozen(
|
|||||||
field_transformer: _FieldTransformer | None = ...,
|
field_transformer: _FieldTransformer | None = ...,
|
||||||
match_args: bool = ...,
|
match_args: bool = ...,
|
||||||
) -> Callable[[_C], _C]: ...
|
) -> Callable[[_C], _C]: ...
|
||||||
|
|
||||||
|
class ClassProps:
|
||||||
|
# XXX: somehow when defining/using enums Mypy starts looking at our own
|
||||||
|
# (untyped) code and causes tons of errors.
|
||||||
|
Hashability: Any
|
||||||
|
KeywordOnly: Any
|
||||||
|
|
||||||
|
is_exception: bool
|
||||||
|
is_slotted: bool
|
||||||
|
has_weakref_slot: bool
|
||||||
|
is_frozen: bool
|
||||||
|
# kw_only: ClassProps.KeywordOnly
|
||||||
|
kw_only: Any
|
||||||
|
collected_fields_by_mro: bool
|
||||||
|
added_init: bool
|
||||||
|
added_repr: bool
|
||||||
|
added_eq: bool
|
||||||
|
added_ordering: bool
|
||||||
|
# hashability: ClassProps.Hashability
|
||||||
|
hashability: Any
|
||||||
|
added_match_args: bool
|
||||||
|
added_str: bool
|
||||||
|
added_pickling: bool
|
||||||
|
on_setattr_hook: _OnSetAttrType | None
|
||||||
|
field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
is_exception: bool,
|
||||||
|
is_slotted: bool,
|
||||||
|
has_weakref_slot: bool,
|
||||||
|
is_frozen: bool,
|
||||||
|
# kw_only: ClassProps.KeywordOnly
|
||||||
|
kw_only: Any,
|
||||||
|
collected_fields_by_mro: bool,
|
||||||
|
added_init: bool,
|
||||||
|
added_repr: bool,
|
||||||
|
added_eq: bool,
|
||||||
|
added_ordering: bool,
|
||||||
|
# hashability: ClassProps.Hashability
|
||||||
|
hashability: Any,
|
||||||
|
added_match_args: bool,
|
||||||
|
added_str: bool,
|
||||||
|
added_pickling: bool,
|
||||||
|
on_setattr_hook: _OnSetAttrType,
|
||||||
|
field_transformer: Callable[[Attribute[Any]], Attribute[Any]],
|
||||||
|
) -> None: ...
|
||||||
|
@property
|
||||||
|
def is_hashable(self) -> bool: ...
|
||||||
|
|
||||||
|
def inspect(cls: type) -> ClassProps: ...
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
Metadata-Version: 2.4
|
||||||
Name: cattrs
|
Name: cattrs
|
||||||
Version: 25.1.1
|
Version: 26.1.0
|
||||||
Summary: Composable complex class support for attrs and dataclasses.
|
Summary: Composable complex class support for attrs and dataclasses.
|
||||||
Project-URL: Homepage, https://catt.rs
|
Project-URL: Homepage, https://catt.rs
|
||||||
Project-URL: Changelog, https://catt.rs/en/latest/history.html
|
Project-URL: Changelog, https://catt.rs/en/latest/history.html
|
||||||
@@ -14,18 +14,18 @@ Keywords: attrs,dataclasses,serialization
|
|||||||
Classifier: Development Status :: 5 - Production/Stable
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
Classifier: Intended Audience :: Developers
|
Classifier: Intended Audience :: Developers
|
||||||
Classifier: License :: OSI Approved :: MIT License
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
Classifier: Programming Language :: Python :: 3.13
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
Classifier: Typing :: Typed
|
Classifier: Typing :: Typed
|
||||||
Requires-Python: >=3.9
|
Requires-Python: >=3.10
|
||||||
Requires-Dist: attrs>=24.3.0
|
Requires-Dist: attrs>=25.4.0
|
||||||
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11'
|
Requires-Dist: exceptiongroup>=1.1.1; python_version < '3.11'
|
||||||
Requires-Dist: typing-extensions>=4.12.2
|
Requires-Dist: typing-extensions>=4.14.0
|
||||||
Provides-Extra: bson
|
Provides-Extra: bson
|
||||||
Requires-Dist: pymongo>=4.4.0; extra == 'bson'
|
Requires-Dist: pymongo>=4.4.0; extra == 'bson'
|
||||||
Provides-Extra: cbor2
|
Provides-Extra: cbor2
|
||||||
@@ -35,11 +35,14 @@ Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
|
|||||||
Provides-Extra: msgspec
|
Provides-Extra: msgspec
|
||||||
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
|
Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
|
||||||
Provides-Extra: orjson
|
Provides-Extra: orjson
|
||||||
Requires-Dist: orjson>=3.10.7; (implementation_name == 'cpython') and extra == 'orjson'
|
Requires-Dist: orjson>=3.11.3; (implementation_name == 'cpython') and extra == 'orjson'
|
||||||
Provides-Extra: pyyaml
|
Provides-Extra: pyyaml
|
||||||
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
|
Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
|
||||||
Provides-Extra: tomlkit
|
Provides-Extra: tomlkit
|
||||||
Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
|
Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
|
||||||
|
Provides-Extra: tomllib
|
||||||
|
Requires-Dist: tomli-w>=1.1.0; extra == 'tomllib'
|
||||||
|
Requires-Dist: tomli>=1.1.0; (python_version < '3.11') and extra == 'tomllib'
|
||||||
Provides-Extra: ujson
|
Provides-Extra: ujson
|
||||||
Requires-Dist: ujson>=5.10.0; extra == 'ujson'
|
Requires-Dist: ujson>=5.10.0; extra == 'ujson'
|
||||||
Description-Content-Type: text/markdown
|
Description-Content-Type: text/markdown
|
||||||
@@ -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
|
Wheel-Version: 1.0
|
||||||
Generator: hatchling 1.27.0
|
Generator: hatchling 1.28.0
|
||||||
Root-Is-Purelib: true
|
Root-Is-Purelib: true
|
||||||
Tag: py3-none-any
|
Tag: py3-none-any
|
||||||
@@ -29,7 +29,6 @@ from typing import (
|
|||||||
_AnnotatedAlias,
|
_AnnotatedAlias,
|
||||||
_GenericAlias,
|
_GenericAlias,
|
||||||
_SpecialGenericAlias,
|
_SpecialGenericAlias,
|
||||||
_UnionGenericAlias,
|
|
||||||
get_args,
|
get_args,
|
||||||
get_origin,
|
get_origin,
|
||||||
get_type_hints,
|
get_type_hints,
|
||||||
@@ -209,9 +208,6 @@ def get_final_base(type) -> Optional[type]:
|
|||||||
OriginAbstractSet = AbcSet
|
OriginAbstractSet = AbcSet
|
||||||
OriginMutableSet = AbcMutableSet
|
OriginMutableSet = AbcMutableSet
|
||||||
|
|
||||||
signature = _signature
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
|
||||||
signature = partial(_signature, eval_str=True)
|
signature = partial(_signature, eval_str=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -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):
|
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 (
|
return (
|
||||||
obj is Union
|
obj is Union
|
||||||
@@ -277,25 +288,6 @@ if sys.version_info >= (3, 10):
|
|||||||
else:
|
else:
|
||||||
from typing_extensions import NotRequired, Required
|
from typing_extensions import NotRequired, Required
|
||||||
|
|
||||||
else:
|
|
||||||
# 3.9
|
|
||||||
from typing_extensions import NotRequired, Required
|
|
||||||
|
|
||||||
def is_union_type(obj):
|
|
||||||
return obj is Union or (
|
|
||||||
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
|
||||||
supertype = getattr(typ, "__supertype__", None)
|
|
||||||
if (
|
|
||||||
supertype is not None
|
|
||||||
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
|
|
||||||
and typ.__module__ in ("typing", "typing_extensions")
|
|
||||||
):
|
|
||||||
return supertype
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_notrequired_base(type) -> Union[Any, NothingType]:
|
def get_notrequired_base(type) -> Union[Any, NothingType]:
|
||||||
if is_annotated(type):
|
if is_annotated(type):
|
||||||
@@ -306,6 +298,25 @@ def get_notrequired_base(type) -> Union[Any, NothingType]:
|
|||||||
return NOTHING
|
return NOTHING
|
||||||
|
|
||||||
|
|
||||||
|
def is_mutable_sequence(type: Any) -> bool:
|
||||||
|
"""A predicate function for mutable sequences.
|
||||||
|
|
||||||
|
Matches lists, mutable sequences, and deques.
|
||||||
|
"""
|
||||||
|
origin = getattr(type, "__origin__", None)
|
||||||
|
return (
|
||||||
|
type in (List, list, TypingMutableSequence, AbcMutableSequence, deque, Deque)
|
||||||
|
or (
|
||||||
|
type.__class__ is _GenericAlias
|
||||||
|
and (
|
||||||
|
((origin is not tuple) and is_subclass(origin, TypingMutableSequence))
|
||||||
|
or (origin is tuple and type.__args__[1] is ...)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (origin in (list, deque, AbcMutableSequence))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_sequence(type: Any) -> bool:
|
def is_sequence(type: Any) -> bool:
|
||||||
"""A predicate function for sequences.
|
"""A predicate function for sequences.
|
||||||
|
|
||||||
@@ -313,19 +324,8 @@ def is_sequence(type: Any) -> bool:
|
|||||||
tuples.
|
tuples.
|
||||||
"""
|
"""
|
||||||
origin = getattr(type, "__origin__", None)
|
origin = getattr(type, "__origin__", None)
|
||||||
return (
|
return is_mutable_sequence(type) or (
|
||||||
type
|
type in (TypingSequence, tuple, Tuple)
|
||||||
in (
|
|
||||||
List,
|
|
||||||
list,
|
|
||||||
TypingSequence,
|
|
||||||
TypingMutableSequence,
|
|
||||||
AbcMutableSequence,
|
|
||||||
tuple,
|
|
||||||
Tuple,
|
|
||||||
deque,
|
|
||||||
Deque,
|
|
||||||
)
|
|
||||||
or (
|
or (
|
||||||
type.__class__ is _GenericAlias
|
type.__class__ is _GenericAlias
|
||||||
and (
|
and (
|
||||||
@@ -333,7 +333,7 @@ def is_sequence(type: Any) -> bool:
|
|||||||
or (origin is tuple and type.__args__[1] is ...)
|
or (origin is tuple and type.__args__[1] is ...)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
|
or (origin is AbcSequence)
|
||||||
or (origin is tuple and type.__args__[1] is ...)
|
or (origin is tuple and type.__args__[1] is ...)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -403,8 +403,10 @@ def is_generic(type) -> bool:
|
|||||||
"""Whether `type` is a generic type."""
|
"""Whether `type` is a generic type."""
|
||||||
# Inheriting from protocol will inject `Generic` into the MRO
|
# Inheriting from protocol will inject `Generic` into the MRO
|
||||||
# without `__orig_bases__`.
|
# without `__orig_bases__`.
|
||||||
return isinstance(type, (_GenericAlias, GenericAlias)) or (
|
return (
|
||||||
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
|
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 collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any, get_args
|
||||||
|
|
||||||
from attrs import NOTHING
|
from attrs import NOTHING
|
||||||
from typing_extensions import Self
|
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):
|
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 import defaultdict
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import (
|
from typing import TYPE_CHECKING, Any, DefaultDict, Literal, NamedTuple, TypeVar
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
DefaultDict,
|
|
||||||
Literal,
|
|
||||||
NamedTuple,
|
|
||||||
TypeVar,
|
|
||||||
get_type_hints,
|
|
||||||
)
|
|
||||||
|
|
||||||
from attrs import NOTHING, Attribute, NothingType
|
from attrs import NOTHING, Attribute, NothingType
|
||||||
|
|
||||||
from ._compat import (
|
from ._compat import (
|
||||||
ANIES,
|
ANIES,
|
||||||
|
AbcSet,
|
||||||
get_args,
|
get_args,
|
||||||
|
get_full_type_hints,
|
||||||
get_origin,
|
get_origin,
|
||||||
is_bare,
|
is_bare,
|
||||||
is_frozenset,
|
is_frozenset,
|
||||||
is_mapping,
|
is_mapping,
|
||||||
|
is_mutable_sequence,
|
||||||
is_sequence,
|
is_sequence,
|
||||||
is_subclass,
|
is_subclass,
|
||||||
)
|
)
|
||||||
@@ -47,10 +42,13 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"defaultdict_structure_factory",
|
"defaultdict_structure_factory",
|
||||||
|
"homogenous_tuple_structure_factory",
|
||||||
|
"is_abstract_set",
|
||||||
"is_any_set",
|
"is_any_set",
|
||||||
"is_defaultdict",
|
"is_defaultdict",
|
||||||
"is_frozenset",
|
"is_frozenset",
|
||||||
"is_mapping",
|
"is_mapping",
|
||||||
|
"is_mutable_sequence",
|
||||||
"is_namedtuple",
|
"is_namedtuple",
|
||||||
"is_sequence",
|
"is_sequence",
|
||||||
"is_set",
|
"is_set",
|
||||||
@@ -70,6 +68,11 @@ def is_any_set(type) -> bool:
|
|||||||
return is_set(type) or is_frozenset(type)
|
return is_set(type) or is_frozenset(type)
|
||||||
|
|
||||||
|
|
||||||
|
def is_abstract_set(type) -> bool:
|
||||||
|
"""A predicate function for abstract (collection.abc) sets."""
|
||||||
|
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)
|
||||||
|
|
||||||
|
|
||||||
def is_namedtuple(type: Any) -> bool:
|
def is_namedtuple(type: Any) -> bool:
|
||||||
"""A predicate function for named tuples."""
|
"""A predicate function for named tuples."""
|
||||||
|
|
||||||
@@ -151,6 +154,47 @@ def list_structure_factory(type: type, converter: BaseConverter) -> StructureHoo
|
|||||||
return structure_list
|
return structure_list
|
||||||
|
|
||||||
|
|
||||||
|
def homogenous_tuple_structure_factory(
|
||||||
|
type: type, converter: BaseConverter
|
||||||
|
) -> StructureHook:
|
||||||
|
"""A hook factory for homogenous (all elements the same, indeterminate length) tuples.
|
||||||
|
|
||||||
|
Converts any given iterable into a tuple.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if is_bare(type) or type.__args__[0] in ANIES:
|
||||||
|
|
||||||
|
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
|
||||||
|
return tuple(obj)
|
||||||
|
|
||||||
|
return structure_tuple
|
||||||
|
|
||||||
|
elem_type = type.__args__[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
handler = converter.get_structure_hook(elem_type)
|
||||||
|
except RecursionError:
|
||||||
|
# Break the cycle by using late binding.
|
||||||
|
handler = converter.structure
|
||||||
|
|
||||||
|
if converter.detailed_validation:
|
||||||
|
|
||||||
|
# We have to structure into a list first anyway.
|
||||||
|
list_structure = list_structure_factory(type, converter)
|
||||||
|
|
||||||
|
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
|
||||||
|
return tuple(list_structure(obj, _))
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def structure_tuple(
|
||||||
|
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
|
||||||
|
) -> tuple[T, ...]:
|
||||||
|
return tuple([_handler(e, _elem_type) for e in obj])
|
||||||
|
|
||||||
|
return structure_tuple
|
||||||
|
|
||||||
|
|
||||||
def namedtuple_unstructure_factory(
|
def namedtuple_unstructure_factory(
|
||||||
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
|
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
|
||||||
) -> UnstructureHook:
|
) -> UnstructureHook:
|
||||||
@@ -195,7 +239,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
|
|||||||
type=a,
|
type=a,
|
||||||
alias=name,
|
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_hetero_tuple,
|
||||||
is_literal,
|
is_literal,
|
||||||
is_mapping,
|
is_mapping,
|
||||||
|
is_mutable_sequence,
|
||||||
is_mutable_set,
|
is_mutable_set,
|
||||||
is_optional,
|
is_optional,
|
||||||
is_protocol,
|
is_protocol,
|
||||||
is_sequence,
|
is_subclass,
|
||||||
is_tuple,
|
is_tuple,
|
||||||
is_typeddict,
|
is_typeddict,
|
||||||
is_union_type,
|
is_union_type,
|
||||||
@@ -54,8 +55,11 @@ from ._compat import (
|
|||||||
)
|
)
|
||||||
from .cols import (
|
from .cols import (
|
||||||
defaultdict_structure_factory,
|
defaultdict_structure_factory,
|
||||||
|
homogenous_tuple_structure_factory,
|
||||||
|
is_abstract_set,
|
||||||
is_defaultdict,
|
is_defaultdict,
|
||||||
is_namedtuple,
|
is_namedtuple,
|
||||||
|
is_sequence,
|
||||||
iterable_unstructure_factory,
|
iterable_unstructure_factory,
|
||||||
list_structure_factory,
|
list_structure_factory,
|
||||||
mapping_structure_factory,
|
mapping_structure_factory,
|
||||||
@@ -73,6 +77,7 @@ from .dispatch import (
|
|||||||
UnstructuredValue,
|
UnstructuredValue,
|
||||||
UnstructureHook,
|
UnstructureHook,
|
||||||
)
|
)
|
||||||
|
from .enums import enum_structure_factory, enum_unstructure_factory
|
||||||
from .errors import (
|
from .errors import (
|
||||||
IterableValidationError,
|
IterableValidationError,
|
||||||
IterableValidationNote,
|
IterableValidationNote,
|
||||||
@@ -225,6 +230,10 @@ class BaseConverter:
|
|||||||
)
|
)
|
||||||
self._unstructure_func.register_func_list(
|
self._unstructure_func.register_func_list(
|
||||||
[
|
[
|
||||||
|
(
|
||||||
|
lambda t: get_newtype_base(t) is not None,
|
||||||
|
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
is_protocol,
|
is_protocol,
|
||||||
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
||||||
@@ -239,12 +248,12 @@ class BaseConverter:
|
|||||||
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
|
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
|
||||||
True,
|
True,
|
||||||
),
|
),
|
||||||
(is_literal_containing_enums, self.unstructure),
|
|
||||||
(is_mapping, self._unstructure_mapping),
|
(is_mapping, self._unstructure_mapping),
|
||||||
(is_sequence, self._unstructure_seq),
|
(is_sequence, self._unstructure_seq),
|
||||||
(is_mutable_set, self._unstructure_seq),
|
(is_mutable_set, self._unstructure_seq),
|
||||||
(is_frozenset, self._unstructure_seq),
|
(is_frozenset, self._unstructure_seq),
|
||||||
(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),
|
(has, self._unstructure_attrs),
|
||||||
(is_union_type, self._unstructure_union),
|
(is_union_type, self._unstructure_union),
|
||||||
(lambda t: t in ANIES, self.unstructure),
|
(lambda t: t in ANIES, self.unstructure),
|
||||||
@@ -271,20 +280,27 @@ class BaseConverter:
|
|||||||
),
|
),
|
||||||
(is_literal, self._structure_simple_literal),
|
(is_literal, self._structure_simple_literal),
|
||||||
(is_literal_containing_enums, self._structure_enum_literal),
|
(is_literal_containing_enums, self._structure_enum_literal),
|
||||||
(is_sequence, list_structure_factory, "extended"),
|
(is_sequence, homogenous_tuple_structure_factory, "extended"),
|
||||||
|
(is_mutable_sequence, list_structure_factory, "extended"),
|
||||||
(is_deque, self._structure_deque),
|
(is_deque, self._structure_deque),
|
||||||
(is_mutable_set, self._structure_set),
|
(is_mutable_set, self._structure_set),
|
||||||
|
(is_abstract_set, self._structure_frozenset),
|
||||||
(is_frozenset, self._structure_frozenset),
|
(is_frozenset, self._structure_frozenset),
|
||||||
(is_tuple, self._structure_tuple),
|
(is_tuple, self._structure_tuple),
|
||||||
(is_namedtuple, namedtuple_structure_factory, "extended"),
|
(is_namedtuple, namedtuple_structure_factory, "extended"),
|
||||||
(is_mapping, self._structure_dict),
|
(is_mapping, self._structure_dict),
|
||||||
(is_supported_union, self._gen_attrs_union_structure, True),
|
*(
|
||||||
|
[(is_supported_union, self._gen_attrs_union_structure, True)]
|
||||||
|
if unstruct_strat is UnstructureStrategy.AS_DICT
|
||||||
|
else []
|
||||||
|
),
|
||||||
(is_optional, self._structure_optional),
|
(is_optional, self._structure_optional),
|
||||||
(
|
(
|
||||||
lambda t: is_union_type(t) and t in self._union_struct_registry,
|
lambda t: is_union_type(t) and t in self._union_struct_registry,
|
||||||
self._union_struct_registry.__getitem__,
|
self._union_struct_registry.__getitem__,
|
||||||
True,
|
True,
|
||||||
),
|
),
|
||||||
|
(lambda t: is_subclass(t, Enum), enum_structure_factory, "extended"),
|
||||||
(has, self._structure_attrs),
|
(has, self._structure_attrs),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -295,7 +311,6 @@ class BaseConverter:
|
|||||||
(bytes, self._structure_call),
|
(bytes, self._structure_call),
|
||||||
(int, self._structure_call),
|
(int, self._structure_call),
|
||||||
(float, self._structure_call),
|
(float, self._structure_call),
|
||||||
(Enum, self._structure_call),
|
|
||||||
(Path, self._structure_call),
|
(Path, self._structure_call),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -617,10 +632,6 @@ class BaseConverter:
|
|||||||
res.append(dispatch(a.type or v.__class__)(v))
|
res.append(dispatch(a.type or v.__class__)(v))
|
||||||
return tuple(res)
|
return tuple(res)
|
||||||
|
|
||||||
def _unstructure_enum(self, obj: Enum) -> Any:
|
|
||||||
"""Convert an enum to its value."""
|
|
||||||
return obj.value
|
|
||||||
|
|
||||||
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
|
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
|
||||||
"""Convert a sequence to primitive equivalents."""
|
"""Convert a sequence to primitive equivalents."""
|
||||||
# We can reuse the sequence class, so tuples stay tuples.
|
# We can reuse the sequence class, so tuples stay tuples.
|
||||||
@@ -1034,6 +1045,7 @@ class Converter(BaseConverter):
|
|||||||
"forbid_extra_keys",
|
"forbid_extra_keys",
|
||||||
"omit_if_default",
|
"omit_if_default",
|
||||||
"type_overrides",
|
"type_overrides",
|
||||||
|
"use_alias",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -1050,6 +1062,7 @@ class Converter(BaseConverter):
|
|||||||
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
|
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
|
||||||
None, t
|
None, t
|
||||||
),
|
),
|
||||||
|
use_alias: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param detailed_validation: Whether to use a slightly slower mode for detailed
|
:param detailed_validation: Whether to use a slightly slower mode for detailed
|
||||||
@@ -1058,12 +1071,15 @@ class Converter(BaseConverter):
|
|||||||
registered unstructuring hooks match.
|
registered unstructuring hooks match.
|
||||||
:param structure_fallback_factory: A hook factory to be called when no
|
:param structure_fallback_factory: A hook factory to be called when no
|
||||||
registered structuring hooks match.
|
registered structuring hooks match.
|
||||||
|
:param use_alias: Whether to use the field alias instead of the field name as
|
||||||
|
the un/structured dictionary key by default.
|
||||||
|
|
||||||
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
|
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
|
||||||
.. versionadded:: 23.2.0 *structure_fallback_factory*
|
.. versionadded:: 23.2.0 *structure_fallback_factory*
|
||||||
.. versionchanged:: 24.2.0
|
.. versionchanged:: 24.2.0
|
||||||
The default `structure_fallback_factory` now raises errors for missing handlers
|
The default `structure_fallback_factory` now raises errors for missing handlers
|
||||||
more eagerly, surfacing problems earlier.
|
more eagerly, surfacing problems earlier.
|
||||||
|
.. versionadded:: 25.2.0 *use_alias*
|
||||||
"""
|
"""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
dict_factory=dict_factory,
|
dict_factory=dict_factory,
|
||||||
@@ -1076,6 +1092,7 @@ class Converter(BaseConverter):
|
|||||||
self.omit_if_default = omit_if_default
|
self.omit_if_default = omit_if_default
|
||||||
self.forbid_extra_keys = forbid_extra_keys
|
self.forbid_extra_keys = forbid_extra_keys
|
||||||
self.type_overrides = dict(type_overrides)
|
self.type_overrides = dict(type_overrides)
|
||||||
|
self.use_alias = use_alias
|
||||||
|
|
||||||
unstruct_collection_overrides = {
|
unstruct_collection_overrides = {
|
||||||
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
|
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
|
||||||
@@ -1246,7 +1263,7 @@ class Converter(BaseConverter):
|
|||||||
attribs = fields(origin or cl)
|
attribs = fields(origin or cl)
|
||||||
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
||||||
# PEP 563 annotations - need to be resolved.
|
# PEP 563 annotations - need to be resolved.
|
||||||
resolve_types(cl)
|
resolve_types(origin or cl)
|
||||||
attrib_overrides = {
|
attrib_overrides = {
|
||||||
a.name: self.type_overrides[a.type]
|
a.name: self.type_overrides[a.type]
|
||||||
for a in attribs
|
for a in attribs
|
||||||
@@ -1284,10 +1301,11 @@ class Converter(BaseConverter):
|
|||||||
def gen_structure_attrs_fromdict(
|
def gen_structure_attrs_fromdict(
|
||||||
self, cl: type[T]
|
self, cl: type[T]
|
||||||
) -> Callable[[Mapping[str, Any], Any], T]:
|
) -> Callable[[Mapping[str, Any], Any], T]:
|
||||||
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):
|
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
||||||
# PEP 563 annotations - need to be resolved.
|
# PEP 563 annotations - need to be resolved.
|
||||||
resolve_types(cl)
|
resolve_types(origin or cl)
|
||||||
attrib_overrides = {
|
attrib_overrides = {
|
||||||
a.name: self.type_overrides[a.type]
|
a.name: self.type_overrides[a.type]
|
||||||
for a in attribs
|
for a in attribs
|
||||||
@@ -1299,6 +1317,7 @@ class Converter(BaseConverter):
|
|||||||
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
|
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
|
||||||
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
|
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
|
||||||
_cattrs_detailed_validation=self.detailed_validation,
|
_cattrs_detailed_validation=self.detailed_validation,
|
||||||
|
_cattrs_use_alias=self.use_alias,
|
||||||
**attrib_overrides,
|
**attrib_overrides,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1377,6 +1396,7 @@ class Converter(BaseConverter):
|
|||||||
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
|
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
|
||||||
prefer_attrib_converters: bool | None = None,
|
prefer_attrib_converters: bool | None = None,
|
||||||
detailed_validation: bool | None = None,
|
detailed_validation: bool | None = None,
|
||||||
|
use_alias: bool | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Create a copy of the converter, keeping all existing custom hooks.
|
"""Create a copy of the converter, keeping all existing custom hooks.
|
||||||
|
|
||||||
@@ -1416,6 +1436,7 @@ class Converter(BaseConverter):
|
|||||||
if detailed_validation is not None
|
if detailed_validation is not None
|
||||||
else self.detailed_validation
|
else self.detailed_validation
|
||||||
),
|
),
|
||||||
|
use_alias=(use_alias if use_alias is not None else self.use_alias),
|
||||||
)
|
)
|
||||||
|
|
||||||
self._unstructure_func.copy_to(
|
self._unstructure_func.copy_to(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping
|
|||||||
from dataclasses import MISSING
|
from dataclasses import MISSING
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from operator import or_
|
from operator import or_
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
|
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, get_origin
|
||||||
|
|
||||||
from attrs import NOTHING, Attribute, AttrsInstance
|
from attrs import NOTHING, Attribute, AttrsInstance
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@ from ._compat import (
|
|||||||
adapted_fields,
|
adapted_fields,
|
||||||
fields_dict,
|
fields_dict,
|
||||||
get_args,
|
get_args,
|
||||||
get_origin,
|
|
||||||
has,
|
has,
|
||||||
is_literal,
|
is_literal,
|
||||||
is_union_type,
|
is_union_type,
|
||||||
|
|||||||
@@ -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:
|
def __init__(self, message: str, type_: type) -> None:
|
||||||
super().__init__(message)
|
super().__init__(message, type_)
|
||||||
|
self.message = message
|
||||||
self.type_ = type_
|
self.type_ = type_
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
class BaseValidationError(ExceptionGroup):
|
class BaseValidationError(ExceptionGroup):
|
||||||
cl: type
|
cl: type
|
||||||
|
|
||||||
def __new__(cls, message: str, excs: Sequence[Exception], cl: type):
|
def __new__(cls, message: str, excs: Sequence[Exception], cl: type) -> Self:
|
||||||
obj = super().__new__(cls, message, excs)
|
obj = super().__new__(cls, message, excs)
|
||||||
obj.cl = cl
|
obj.cl = cl
|
||||||
return obj
|
return obj
|
||||||
@@ -35,9 +39,7 @@ class IterableValidationNote(str):
|
|||||||
index: Union[int, str] # Ints for list indices, strs for dict keys
|
index: Union[int, str] # Ints for list indices, strs for dict keys
|
||||||
type: Any
|
type: Any
|
||||||
|
|
||||||
def __new__(
|
def __new__(cls, string: str, index: Union[int, str], type: Any) -> Self:
|
||||||
cls, string: str, index: Union[int, str], type: Any
|
|
||||||
) -> "IterableValidationNote":
|
|
||||||
instance = str.__new__(cls, string)
|
instance = str.__new__(cls, string)
|
||||||
instance.index = index
|
instance.index = index
|
||||||
instance.type = type
|
instance.type = type
|
||||||
@@ -76,7 +78,7 @@ class AttributeValidationNote(str):
|
|||||||
name: str
|
name: str
|
||||||
type: Any
|
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 = str.__new__(cls, string)
|
||||||
instance.name = name
|
instance.name = name
|
||||||
instance.type = type
|
instance.type = type
|
||||||
@@ -122,11 +124,15 @@ class ForbiddenExtraKeysError(Exception):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, message: Optional[str], cl: type, extra_fields: set[str]
|
self, message: Optional[str], cl: type, extra_fields: set[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.message = message
|
||||||
self.cl = cl
|
self.cl = cl
|
||||||
self.extra_fields = extra_fields
|
self.extra_fields = extra_fields
|
||||||
cln = cl.__name__
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(message, cl, extra_fields)
|
||||||
message
|
|
||||||
or f"Extra fields in constructor for {cln}: {', '.join(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 collections.abc import Callable, Iterable, Mapping
|
||||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
|
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
|
||||||
|
|
||||||
from attrs import NOTHING, Attribute, Factory
|
from attrs import NOTHING, Attribute, Converter, Factory, evolve
|
||||||
from typing_extensions import NoDefault
|
from typing_extensions import NoDefault
|
||||||
|
|
||||||
from .._compat import (
|
from .._compat import (
|
||||||
@@ -33,7 +33,7 @@ from ..types import SimpleStructureHook
|
|||||||
from ._consts import AttributeOverride, already_generating, neutral
|
from ._consts import AttributeOverride, already_generating, neutral
|
||||||
from ._generics import generate_mapping
|
from ._generics import generate_mapping
|
||||||
from ._lc import generate_unique_filename
|
from ._lc import generate_unique_filename
|
||||||
from ._shared import find_structure_handler
|
from ._shared import _annotated_override_or_default, find_structure_handler
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..converters import BaseConverter
|
from ..converters import BaseConverter
|
||||||
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
|
|||||||
typevar_map: dict[str, Any] = {},
|
typevar_map: dict[str, Any] = {},
|
||||||
_cattrs_omit_if_default: bool = False,
|
_cattrs_omit_if_default: bool = False,
|
||||||
_cattrs_use_linecache: bool = True,
|
_cattrs_use_linecache: bool = True,
|
||||||
_cattrs_use_alias: bool = False,
|
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||||
_cattrs_include_init_false: bool = False,
|
_cattrs_include_init_false: bool = False,
|
||||||
**kwargs: AttributeOverride,
|
**kwargs: AttributeOverride,
|
||||||
) -> Callable[[T], dict[str, Any]]:
|
) -> Callable[[T], dict[str, Any]]:
|
||||||
@@ -96,6 +96,16 @@ def make_dict_unstructure_fn_from_attrs(
|
|||||||
will be included.
|
will be included.
|
||||||
|
|
||||||
.. versionadded:: 24.1.0
|
.. versionadded:: 24.1.0
|
||||||
|
.. versionchanged:: 25.2.0
|
||||||
|
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||||
|
by default.
|
||||||
|
.. versionchanged:: 26.1.0
|
||||||
|
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||||
|
unstructuring.
|
||||||
|
.. versionchanged:: 26.1.0
|
||||||
|
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
|
||||||
|
specified, the converter is applied to the default value before checking if it
|
||||||
|
is equal to the attribute's value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fn_name = "unstructure_" + cl.__name__
|
fn_name = "unstructure_" + cl.__name__
|
||||||
@@ -104,15 +114,27 @@ def make_dict_unstructure_fn_from_attrs(
|
|||||||
invocation_lines = []
|
invocation_lines = []
|
||||||
internal_arg_parts = {}
|
internal_arg_parts = {}
|
||||||
|
|
||||||
|
if _cattrs_use_alias == "from_converter":
|
||||||
|
# BaseConverter doesn't have it so we're careful.
|
||||||
|
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||||
|
|
||||||
for a in attrs:
|
for a in attrs:
|
||||||
attr_name = a.name
|
attr_name = a.name
|
||||||
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:
|
if override.omit:
|
||||||
continue
|
continue
|
||||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||||
continue
|
continue
|
||||||
if override.rename is None:
|
if override.rename is None:
|
||||||
kn = attr_name if not _cattrs_use_alias else a.alias
|
kn = attr_name if not _cattrs_use_alias else a.alias
|
||||||
|
if kn != attr_name:
|
||||||
|
kwargs[attr_name] = evolve(override, rename=kn)
|
||||||
else:
|
else:
|
||||||
kn = override.rename
|
kn = override.rename
|
||||||
d = a.default
|
d = a.default
|
||||||
@@ -170,15 +192,31 @@ def make_dict_unstructure_fn_from_attrs(
|
|||||||
if isinstance(d, Factory):
|
if isinstance(d, Factory):
|
||||||
globs[def_name] = d.factory
|
globs[def_name] = d.factory
|
||||||
internal_arg_parts[def_name] = d.factory
|
internal_arg_parts[def_name] = d.factory
|
||||||
if d.takes_self:
|
def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()"
|
||||||
lines.append(f" if instance.{attr_name} != {def_name}(instance):")
|
|
||||||
else:
|
|
||||||
lines.append(f" if instance.{attr_name} != {def_name}():")
|
|
||||||
lines.append(f" res['{kn}'] = {invoke}")
|
|
||||||
else:
|
else:
|
||||||
globs[def_name] = d
|
globs[def_name] = d
|
||||||
internal_arg_parts[def_name] = d
|
internal_arg_parts[def_name] = d
|
||||||
lines.append(f" if instance.{attr_name} != {def_name}:")
|
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}")
|
lines.append(f" res['{kn}'] = {invoke}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -217,7 +255,7 @@ def make_dict_unstructure_fn(
|
|||||||
converter: BaseConverter,
|
converter: BaseConverter,
|
||||||
_cattrs_omit_if_default: bool = False,
|
_cattrs_omit_if_default: bool = False,
|
||||||
_cattrs_use_linecache: bool = True,
|
_cattrs_use_linecache: bool = True,
|
||||||
_cattrs_use_alias: bool = False,
|
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||||
_cattrs_include_init_false: bool = False,
|
_cattrs_include_init_false: bool = False,
|
||||||
**kwargs: AttributeOverride,
|
**kwargs: AttributeOverride,
|
||||||
) -> Callable[[T], dict[str, Any]]:
|
) -> Callable[[T], dict[str, Any]]:
|
||||||
@@ -237,11 +275,20 @@ def make_dict_unstructure_fn(
|
|||||||
|
|
||||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||||
|
.. versionchanged:: 25.2.0
|
||||||
|
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||||
|
by default.
|
||||||
|
.. versionchanged:: 26.1.0
|
||||||
|
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||||
|
unstructuring.
|
||||||
"""
|
"""
|
||||||
origin = get_origin(cl)
|
origin = get_origin(cl)
|
||||||
attrs = adapted_fields(origin or cl) # type: ignore
|
attrs = adapted_fields(origin or cl) # type: ignore
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
|
if _cattrs_use_alias == "from_converter":
|
||||||
|
# BaseConverter doesn't have it so we're careful.
|
||||||
|
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||||
if is_generic(cl):
|
if is_generic(cl):
|
||||||
mapping = generate_mapping(cl, mapping)
|
mapping = generate_mapping(cl, mapping)
|
||||||
|
|
||||||
@@ -289,7 +336,7 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
bool | Literal["from_converter"]
|
bool | Literal["from_converter"]
|
||||||
) = "from_converter",
|
) = "from_converter",
|
||||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||||
_cattrs_use_alias: bool = False,
|
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||||
_cattrs_include_init_false: bool = False,
|
_cattrs_include_init_false: bool = False,
|
||||||
**kwargs: AttributeOverride,
|
**kwargs: AttributeOverride,
|
||||||
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
||||||
@@ -315,6 +362,12 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
will be included.
|
will be included.
|
||||||
|
|
||||||
.. versionadded:: 24.1.0
|
.. versionadded:: 24.1.0
|
||||||
|
.. versionchanged:: 25.2.0
|
||||||
|
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||||
|
by default.
|
||||||
|
.. versionchanged:: 26.1.0
|
||||||
|
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||||
|
unstructuring.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cl_name = cl.__name__
|
cl_name = cl.__name__
|
||||||
@@ -350,6 +403,9 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
if _cattrs_forbid_extra_keys == "from_converter":
|
if _cattrs_forbid_extra_keys == "from_converter":
|
||||||
# BaseConverter doesn't have it so we're careful.
|
# BaseConverter doesn't have it so we're careful.
|
||||||
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
|
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
|
||||||
|
if _cattrs_use_alias == "from_converter":
|
||||||
|
# BaseConverter doesn't have it so we're careful.
|
||||||
|
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||||
if _cattrs_detailed_validation == "from_converter":
|
if _cattrs_detailed_validation == "from_converter":
|
||||||
_cattrs_detailed_validation = converter.detailed_validation
|
_cattrs_detailed_validation = converter.detailed_validation
|
||||||
if _cattrs_prefer_attrib_converters == "from_converter":
|
if _cattrs_prefer_attrib_converters == "from_converter":
|
||||||
@@ -367,7 +423,13 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
internal_arg_parts["__c_avn"] = AttributeValidationNote
|
internal_arg_parts["__c_avn"] = AttributeValidationNote
|
||||||
for a in attrs:
|
for a in attrs:
|
||||||
an = a.name
|
an = a.name
|
||||||
override = kwargs.get(an, neutral)
|
if an in kwargs:
|
||||||
|
override = kwargs[an]
|
||||||
|
else:
|
||||||
|
override = _annotated_override_or_default(a.type, neutral)
|
||||||
|
if override != neutral:
|
||||||
|
kwargs[an] = override
|
||||||
|
|
||||||
if override.omit:
|
if override.omit:
|
||||||
continue
|
continue
|
||||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||||
@@ -396,6 +458,8 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
ian = a.alias
|
ian = a.alias
|
||||||
if override.rename is None:
|
if override.rename is None:
|
||||||
kn = an if not _cattrs_use_alias else a.alias
|
kn = an if not _cattrs_use_alias else a.alias
|
||||||
|
if kn != an:
|
||||||
|
kwargs[an] = evolve(override, rename=kn)
|
||||||
else:
|
else:
|
||||||
kn = override.rename
|
kn = override.rename
|
||||||
|
|
||||||
@@ -496,14 +560,24 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
# The first loop deals with required args.
|
# The first loop deals with required args.
|
||||||
for a in attrs:
|
for a in attrs:
|
||||||
an = a.name
|
an = a.name
|
||||||
override = kwargs.get(an, neutral)
|
|
||||||
|
if an in kwargs:
|
||||||
|
override = kwargs[an]
|
||||||
|
else:
|
||||||
|
override = _annotated_override_or_default(a.type, neutral)
|
||||||
|
if override != neutral:
|
||||||
|
kwargs[an] = override
|
||||||
|
|
||||||
if override.omit:
|
if override.omit:
|
||||||
continue
|
continue
|
||||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if a.default is not NOTHING:
|
if a.default is not NOTHING:
|
||||||
non_required.append(a)
|
non_required.append(a)
|
||||||
|
# The next loop will handle it.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
t = a.type
|
t = a.type
|
||||||
if isinstance(t, TypeVar):
|
if isinstance(t, TypeVar):
|
||||||
t = typevar_map.get(t.__name__, t)
|
t = typevar_map.get(t.__name__, t)
|
||||||
@@ -523,6 +597,8 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
|
|
||||||
if override.rename is None:
|
if override.rename is None:
|
||||||
kn = an if not _cattrs_use_alias else a.alias
|
kn = an if not _cattrs_use_alias else a.alias
|
||||||
|
if kn != an:
|
||||||
|
kwargs[an] = evolve(override, rename=kn)
|
||||||
else:
|
else:
|
||||||
kn = override.rename
|
kn = override.rename
|
||||||
allowed_fields.add(kn)
|
allowed_fields.add(kn)
|
||||||
@@ -592,6 +668,8 @@ def make_dict_structure_fn_from_attrs(
|
|||||||
|
|
||||||
if override.rename is None:
|
if override.rename is None:
|
||||||
kn = an if not _cattrs_use_alias else a.alias
|
kn = an if not _cattrs_use_alias else a.alias
|
||||||
|
if kn != an:
|
||||||
|
kwargs[an] = evolve(override, rename=kn)
|
||||||
else:
|
else:
|
||||||
kn = override.rename
|
kn = override.rename
|
||||||
allowed_fields.add(kn)
|
allowed_fields.add(kn)
|
||||||
@@ -682,7 +760,7 @@ def make_dict_structure_fn(
|
|||||||
bool | Literal["from_converter"]
|
bool | Literal["from_converter"]
|
||||||
) = "from_converter",
|
) = "from_converter",
|
||||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||||
_cattrs_use_alias: bool = False,
|
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||||
_cattrs_include_init_false: bool = False,
|
_cattrs_include_init_false: bool = False,
|
||||||
**kwargs: AttributeOverride,
|
**kwargs: AttributeOverride,
|
||||||
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
||||||
@@ -714,6 +792,12 @@ def make_dict_structure_fn(
|
|||||||
.. versionchanged:: 24.1.0
|
.. versionchanged:: 24.1.0
|
||||||
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
|
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
|
||||||
converter by default.
|
converter by default.
|
||||||
|
.. versionchanged:: 25.2.0
|
||||||
|
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||||
|
by default.
|
||||||
|
.. versionchanged:: 26.1.0
|
||||||
|
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||||
|
unstructuring.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
|
|||||||
@@ -4,15 +4,31 @@ from typing import TYPE_CHECKING, Any
|
|||||||
|
|
||||||
from attrs import NOTHING, Attribute, Factory
|
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 ..dispatch import StructureHook
|
||||||
from ..errors import StructureHandlerNotFoundError
|
from ..errors import StructureHandlerNotFoundError
|
||||||
from ..fns import raise_error
|
from ..fns import raise_error
|
||||||
|
from ._consts import AttributeOverride
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..converters import BaseConverter
|
from ..converters import BaseConverter
|
||||||
|
|
||||||
|
|
||||||
|
def _annotated_override_or_default(
|
||||||
|
type: Any, default: AttributeOverride
|
||||||
|
) -> AttributeOverride:
|
||||||
|
"""
|
||||||
|
If the type is Annotated containing an AttributeOverride, return it.
|
||||||
|
Otherwise, return the default.
|
||||||
|
"""
|
||||||
|
if is_annotated(type):
|
||||||
|
for arg in get_args(type):
|
||||||
|
if isinstance(arg, AttributeOverride):
|
||||||
|
return arg
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def find_structure_handler(
|
def find_structure_handler(
|
||||||
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
|
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
|
||||||
) -> StructureHook | None:
|
) -> StructureHook | None:
|
||||||
|
|||||||
@@ -3,23 +3,12 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from inspect import get_annotations
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
||||||
|
|
||||||
from attrs import NOTHING, Attribute
|
from attrs import NOTHING, Attribute
|
||||||
from typing_extensions import _TypedDictMeta
|
from typing_extensions import _TypedDictMeta
|
||||||
|
|
||||||
try:
|
|
||||||
from inspect import get_annotations
|
|
||||||
|
|
||||||
def get_annots(cl) -> dict[str, Any]:
|
|
||||||
return get_annotations(cl, eval_str=True)
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
|
|
||||||
def get_annots(cl) -> dict[str, Any]:
|
|
||||||
return cl.__dict__.get("__annotations__", {})
|
|
||||||
|
|
||||||
|
|
||||||
from .._compat import (
|
from .._compat import (
|
||||||
get_full_type_hints,
|
get_full_type_hints,
|
||||||
get_notrequired_base,
|
get_notrequired_base,
|
||||||
@@ -40,7 +29,7 @@ from . import AttributeOverride
|
|||||||
from ._consts import already_generating, neutral
|
from ._consts import already_generating, neutral
|
||||||
from ._generics import generate_mapping
|
from ._generics import generate_mapping
|
||||||
from ._lc import generate_unique_filename
|
from ._lc import generate_unique_filename
|
||||||
from ._shared import find_structure_handler
|
from ._shared import _annotated_override_or_default, find_structure_handler
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..converters import BaseConverter
|
from ..converters import BaseConverter
|
||||||
@@ -50,6 +39,10 @@ __all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def get_annots(cl) -> dict[str, Any]:
|
||||||
|
return get_annotations(cl, eval_str=True)
|
||||||
|
|
||||||
|
|
||||||
def make_dict_unstructure_fn(
|
def make_dict_unstructure_fn(
|
||||||
cl: type[T],
|
cl: type[T],
|
||||||
converter: BaseConverter,
|
converter: BaseConverter,
|
||||||
@@ -109,11 +102,20 @@ def make_dict_unstructure_fn(
|
|||||||
# * all attributes resolve to `converter._unstructure_identity`
|
# * all attributes resolve to `converter._unstructure_identity`
|
||||||
for a in attrs:
|
for a in attrs:
|
||||||
attr_name = a.name
|
attr_name = a.name
|
||||||
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:
|
if override != neutral:
|
||||||
break
|
break
|
||||||
handler = None
|
handler = None
|
||||||
t = a.type
|
|
||||||
|
|
||||||
if isinstance(t, TypeVar):
|
if isinstance(t, TypeVar):
|
||||||
if t.__name__ in mapping:
|
if t.__name__ in mapping:
|
||||||
@@ -125,9 +127,6 @@ def make_dict_unstructure_fn(
|
|||||||
t = deep_copy_with(t, mapping, cl)
|
t = deep_copy_with(t, mapping, cl)
|
||||||
|
|
||||||
if handler is None:
|
if handler is None:
|
||||||
nrb = get_notrequired_base(t)
|
|
||||||
if nrb is not NOTHING:
|
|
||||||
t = nrb
|
|
||||||
try:
|
try:
|
||||||
handler = converter.get_unstructure_hook(t)
|
handler = converter.get_unstructure_hook(t)
|
||||||
except RecursionError:
|
except RecursionError:
|
||||||
@@ -142,10 +141,22 @@ def make_dict_unstructure_fn(
|
|||||||
|
|
||||||
for ix, a in enumerate(attrs):
|
for ix, a in enumerate(attrs):
|
||||||
attr_name = a.name
|
attr_name = a.name
|
||||||
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:
|
if override.omit:
|
||||||
lines.append(f" res.pop('{attr_name}', None)")
|
lines.append(f" res.pop('{attr_name}', None)")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if override.rename is not None:
|
if override.rename is not None:
|
||||||
# We also need to pop when renaming, since we're copying
|
# We also need to pop when renaming, since we're copying
|
||||||
# the original.
|
# the original.
|
||||||
@@ -160,8 +171,6 @@ def make_dict_unstructure_fn(
|
|||||||
if override.unstruct_hook is not None:
|
if override.unstruct_hook is not None:
|
||||||
handler = override.unstruct_hook
|
handler = override.unstruct_hook
|
||||||
else:
|
else:
|
||||||
t = a.type
|
|
||||||
|
|
||||||
if isinstance(t, TypeVar):
|
if isinstance(t, TypeVar):
|
||||||
if t.__name__ in mapping:
|
if t.__name__ in mapping:
|
||||||
t = mapping[t.__name__]
|
t = mapping[t.__name__]
|
||||||
@@ -171,9 +180,6 @@ def make_dict_unstructure_fn(
|
|||||||
t = deep_copy_with(t, mapping, cl)
|
t = deep_copy_with(t, mapping, cl)
|
||||||
|
|
||||||
if handler is None:
|
if handler is None:
|
||||||
nrb = get_notrequired_base(t)
|
|
||||||
if nrb is not NOTHING:
|
|
||||||
t = nrb
|
|
||||||
try:
|
try:
|
||||||
handler = converter.get_unstructure_hook(t)
|
handler = converter.get_unstructure_hook(t)
|
||||||
except RecursionError:
|
except RecursionError:
|
||||||
@@ -220,12 +226,15 @@ def make_dict_unstructure_fn(
|
|||||||
)
|
)
|
||||||
|
|
||||||
eval(compile(script, fname, "exec"), globs)
|
eval(compile(script, fname, "exec"), globs)
|
||||||
|
|
||||||
|
res = globs[fn_name]
|
||||||
|
res.overrides = kwargs
|
||||||
finally:
|
finally:
|
||||||
working_set.remove(cl)
|
working_set.remove(cl)
|
||||||
if not working_set:
|
if not working_set:
|
||||||
del already_generating.working_set
|
del already_generating.working_set
|
||||||
|
|
||||||
return globs[fn_name]
|
return res
|
||||||
|
|
||||||
|
|
||||||
def make_dict_structure_fn(
|
def make_dict_structure_fn(
|
||||||
@@ -326,20 +335,25 @@ def make_dict_structure_fn(
|
|||||||
for ix, a in enumerate(attrs):
|
for ix, a in enumerate(attrs):
|
||||||
an = a.name
|
an = a.name
|
||||||
attr_required = an in req_keys
|
attr_required = an in req_keys
|
||||||
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:
|
if override.omit:
|
||||||
continue
|
continue
|
||||||
t = a.type
|
|
||||||
|
|
||||||
if isinstance(t, TypeVar):
|
if isinstance(t, TypeVar):
|
||||||
t = mapping.get(t.__name__, t)
|
t = mapping.get(t.__name__, t)
|
||||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||||
t = deep_copy_with(t, mapping, cl)
|
t = deep_copy_with(t, mapping, cl)
|
||||||
|
|
||||||
nrb = get_notrequired_base(t)
|
|
||||||
if nrb is not NOTHING:
|
|
||||||
t = nrb
|
|
||||||
|
|
||||||
if is_generic(t) and not is_bare(t) and not is_annotated(t):
|
if is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||||
t = deep_copy_with(t, mapping, cl)
|
t = deep_copy_with(t, mapping, cl)
|
||||||
|
|
||||||
@@ -399,7 +413,12 @@ def make_dict_structure_fn(
|
|||||||
for ix, a in enumerate(attrs):
|
for ix, a in enumerate(attrs):
|
||||||
an = a.name
|
an = a.name
|
||||||
attr_required = an in req_keys
|
attr_required = an in req_keys
|
||||||
override = kwargs.get(an, neutral)
|
if an in kwargs:
|
||||||
|
override = kwargs[an]
|
||||||
|
else:
|
||||||
|
override = _annotated_override_or_default(a.type, neutral)
|
||||||
|
if override != neutral:
|
||||||
|
kwargs[an] = override
|
||||||
if override.omit:
|
if override.omit:
|
||||||
continue
|
continue
|
||||||
if not attr_required:
|
if not attr_required:
|
||||||
@@ -448,13 +467,18 @@ def make_dict_structure_fn(
|
|||||||
if non_required:
|
if non_required:
|
||||||
for ix, a in non_required:
|
for ix, a in non_required:
|
||||||
an = a.name
|
an = a.name
|
||||||
override = kwargs.get(an, neutral)
|
|
||||||
t = a.type
|
t = a.type
|
||||||
|
|
||||||
nrb = get_notrequired_base(t)
|
nrb = get_notrequired_base(t)
|
||||||
if nrb is not NOTHING:
|
if nrb is not NOTHING:
|
||||||
t = nrb
|
t = nrb
|
||||||
|
|
||||||
|
if an in kwargs:
|
||||||
|
override = kwargs[an]
|
||||||
|
else:
|
||||||
|
override = _annotated_override_or_default(t, neutral)
|
||||||
|
if override != neutral:
|
||||||
|
kwargs[an] = override
|
||||||
|
|
||||||
if isinstance(t, TypeVar):
|
if isinstance(t, TypeVar):
|
||||||
t = mapping.get(t.__name__, t)
|
t = mapping.get(t.__name__, t)
|
||||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||||
@@ -514,7 +538,9 @@ def make_dict_structure_fn(
|
|||||||
)
|
)
|
||||||
|
|
||||||
eval(compile(script, fname, "exec"), globs)
|
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]:
|
def _adapted_fields(cls: Any) -> list[Attribute]:
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import sys
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
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 .._compat import is_subclass
|
||||||
from ..converters import Converter, UnstructureHook
|
from ..converters import Converter, UnstructureHook
|
||||||
from ..fns import identity
|
from ..fns import identity
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 10):
|
|
||||||
from typing_extensions import ParamSpec
|
|
||||||
else:
|
|
||||||
from typing import ParamSpec
|
|
||||||
|
|
||||||
|
|
||||||
def validate_datetime(v, _):
|
def validate_datetime(v, _):
|
||||||
if not isinstance(v, datetime):
|
if not isinstance(v, datetime):
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ def configure_converter(converter: BaseConverter):
|
|||||||
|
|
||||||
# datetime inherits from date, so identity unstructure hook used
|
# datetime inherits from date, so identity unstructure hook used
|
||||||
# here to prevent the date unstructure hook running.
|
# here to prevent the date unstructure hook running.
|
||||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
converter.register_unstructure_hook(datetime, identity)
|
||||||
converter.register_structure_hook(datetime, validate_datetime)
|
converter.register_structure_hook(datetime, validate_datetime)
|
||||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||||
converter.register_unstructure_hook_factory(
|
converter.register_unstructure_hook_factory(
|
||||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def configure_converter(converter: BaseConverter):
|
|||||||
)
|
)
|
||||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||||
converter.register_unstructure_hook_factory(
|
converter.register_unstructure_hook_factory(
|
||||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ def configure_converter(converter: BaseConverter) -> None:
|
|||||||
converter.register_unstructure_hook_factory(
|
converter.register_unstructure_hook_factory(
|
||||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||||
)
|
)
|
||||||
converter.register_unstructure_hook_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)
|
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def configure_converter(converter: BaseConverter) -> None:
|
|||||||
converter.register_structure_hook(
|
converter.register_structure_hook(
|
||||||
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
|
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
|
||||||
)
|
)
|
||||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||||
converter.register_unstructure_hook_factory(
|
converter.register_unstructure_hook_factory(
|
||||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,18 +3,27 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import is_dataclass
|
from dataclasses import is_dataclass
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, 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 has as attrs_has
|
||||||
from attrs import resolve_types
|
from attrs import resolve_types
|
||||||
from msgspec import Struct, convert, to_builtins
|
from msgspec import Struct, convert, to_builtins
|
||||||
from msgspec.json import Encoder, decode
|
from msgspec.json import Encoder, decode
|
||||||
|
|
||||||
from .._compat import 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 ..cols import is_namedtuple
|
||||||
from ..converters import BaseConverter, Converter
|
from ..converters import BaseConverter, Converter
|
||||||
from ..dispatch import UnstructureHook
|
from ..dispatch import UnstructureHook
|
||||||
@@ -74,7 +83,9 @@ def configure_converter(converter: Converter) -> None:
|
|||||||
configure_passthroughs(converter)
|
configure_passthroughs(converter)
|
||||||
|
|
||||||
converter.register_unstructure_hook(Struct, to_builtins)
|
converter.register_unstructure_hook(Struct, to_builtins)
|
||||||
converter.register_unstructure_hook(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(Struct, convert)
|
||||||
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
|
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ def configure_converter(converter: Converter) -> None:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
converter.register_unstructure_hook_func(
|
converter.register_unstructure_hook_factory(
|
||||||
partial(is_primitive_enum, include_bare_enums=True), identity
|
partial(is_primitive_enum, include_bare_enums=True), lambda t: identity
|
||||||
)
|
)
|
||||||
converter.register_unstructure_hook_factory(
|
converter.register_unstructure_hook_factory(
|
||||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from tomlkit.items import Float, Integer, String
|
|||||||
|
|
||||||
from .._compat import is_mapping, is_subclass
|
from .._compat import is_mapping, is_subclass
|
||||||
from ..converters import BaseConverter, Converter
|
from ..converters import BaseConverter, Converter
|
||||||
|
from ..fns import identity
|
||||||
from ..strategies import configure_union_passthrough
|
from ..strategies import configure_union_passthrough
|
||||||
from . import validate_datetime, wrap
|
from . import validate_datetime, wrap
|
||||||
|
|
||||||
@@ -37,6 +38,9 @@ def configure_converter(converter: BaseConverter):
|
|||||||
* sets are serialized as lists
|
* sets are serialized as lists
|
||||||
* tuples are serializas as lists
|
* tuples are serializas as lists
|
||||||
* mapping keys are coerced into strings when unstructuring
|
* mapping keys are coerced into strings when unstructuring
|
||||||
|
|
||||||
|
.. versionchanged:: 26.1.0
|
||||||
|
date objects are now passed through to tomlkit without unstructuring.
|
||||||
"""
|
"""
|
||||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||||
converter.register_unstructure_hook(
|
converter.register_unstructure_hook(
|
||||||
@@ -67,10 +71,12 @@ def configure_converter(converter: BaseConverter):
|
|||||||
|
|
||||||
# datetime inherits from date, so identity unstructure hook used
|
# datetime inherits from date, so identity unstructure hook used
|
||||||
# here to prevent the date unstructure hook running.
|
# here to prevent the date unstructure hook running.
|
||||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
converter.register_unstructure_hook(datetime, identity)
|
||||||
converter.register_structure_hook(datetime, validate_datetime)
|
converter.register_structure_hook(datetime, validate_datetime)
|
||||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
converter.register_unstructure_hook(date, identity)
|
||||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
converter.register_structure_hook(
|
||||||
|
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
|
||||||
|
)
|
||||||
configure_union_passthrough(
|
configure_union_passthrough(
|
||||||
Union[str, String, bool, int, Integer, float, Float], converter
|
Union[str, String, bool, int, Integer, float, Float], converter
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
|
||||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||||
converter.register_unstructure_hook_factory(
|
converter.register_unstructure_hook_factory(
|
||||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,21 +9,25 @@ from typing import Any, Callable, TypeVar, Union
|
|||||||
from ..converters import BaseConverter
|
from ..converters import BaseConverter
|
||||||
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
|
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
|
||||||
from ..gen._consts import already_generating
|
from ..gen._consts import already_generating
|
||||||
|
from ..subclasses import subclasses
|
||||||
|
|
||||||
|
|
||||||
def _make_subclasses_tree(cl: type) -> list[type]:
|
def _make_subclasses_tree(cl: type) -> list[type]:
|
||||||
# get class origin for accessing subclasses (see #648 for more info)
|
# get class origin for accessing subclasses (see #648 for more info)
|
||||||
cls_origin = typing.get_origin(cl) or cl
|
cls_origin = typing.get_origin(cl) or cl
|
||||||
return [cl] + [
|
|
||||||
sscl
|
# Use a dict to deduplicate and keep insertion order.
|
||||||
for scl in cls_origin.__subclasses__()
|
seen = {cl: None}
|
||||||
for sscl in _make_subclasses_tree(scl)
|
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:
|
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
|
||||||
"""Whether the given class has subclasses from `given_subclasses`."""
|
"""Whether the given class has subclasses from `given_subclasses`."""
|
||||||
actual = set(cl.__subclasses__())
|
cls_origin = typing.get_origin(cl) or cl
|
||||||
|
actual = set(subclasses(cls_origin))
|
||||||
given = set(given_subclasses)
|
given = set(given_subclasses)
|
||||||
return bool(actual & given)
|
return bool(actual & given)
|
||||||
|
|
||||||
@@ -68,6 +72,9 @@ def include_subclasses(
|
|||||||
.. versionchanged:: 24.1.0
|
.. versionchanged:: 24.1.0
|
||||||
When overrides are not provided, hooks for individual classes are retrieved from
|
When overrides are not provided, hooks for individual classes are retrieved from
|
||||||
the converter instead of generated with no overrides, using converter defaults.
|
the converter instead of generated with no overrides, using converter defaults.
|
||||||
|
.. versionchanged:: 25.2.0
|
||||||
|
Slotted dataclasses work on Python 3.14 via :func:`cattrs.subclasses.subclasses`,
|
||||||
|
which filters out duplicate classes caused by slotting.
|
||||||
"""
|
"""
|
||||||
# Due to https://github.com/python-attrs/attrs/issues/1047
|
# Due to https://github.com/python-attrs/attrs/issues/1047
|
||||||
collect()
|
collect()
|
||||||
@@ -231,7 +238,13 @@ def _include_subclasses_with_union_strategy(
|
|||||||
return cls is _cl
|
return cls is _cl
|
||||||
|
|
||||||
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
|
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
|
||||||
subclasses = tuple([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:
|
if len(subclasses) > 1:
|
||||||
u = Union[subclasses] # type: ignore
|
u = Union[subclasses] # type: ignore
|
||||||
union_strategy(u, converter)
|
union_strategy(u, converter)
|
||||||
|
|||||||
@@ -52,23 +52,10 @@ def configure_tagged_union(
|
|||||||
if is_type_alias(union):
|
if is_type_alias(union):
|
||||||
union = union.__value__
|
union = union.__value__
|
||||||
args = union.__args__
|
args = union.__args__
|
||||||
|
|
||||||
tag_to_hook = {}
|
tag_to_hook = {}
|
||||||
exact_cl_unstruct_hooks = {}
|
exact_cl_unstruct_hooks = {}
|
||||||
for cl in args:
|
cl_to_tag = {}
|
||||||
tag = tag_generator(cl)
|
|
||||||
struct_handler = converter.get_structure_hook(cl)
|
|
||||||
unstruct_handler = converter.get_unstructure_hook(cl)
|
|
||||||
|
|
||||||
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
|
|
||||||
return _h(val, _cl)
|
|
||||||
|
|
||||||
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
|
|
||||||
return _h(val)
|
|
||||||
|
|
||||||
tag_to_hook[tag] = structure_union_member
|
|
||||||
exact_cl_unstruct_hooks[cl] = unstructure_union_member
|
|
||||||
|
|
||||||
cl_to_tag = {cl: tag_generator(cl) for cl in args}
|
|
||||||
|
|
||||||
if default is not NOTHING:
|
if default is not NOTHING:
|
||||||
default_handler = converter.get_structure_hook(default)
|
default_handler = converter.get_structure_hook(default)
|
||||||
@@ -76,36 +63,9 @@ def configure_tagged_union(
|
|||||||
def structure_default(val: dict, _cl=default, _h=default_handler):
|
def structure_default(val: dict, _cl=default, _h=default_handler):
|
||||||
return _h(val, _cl)
|
return _h(val, _cl)
|
||||||
|
|
||||||
tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
|
tag_to_hook = defaultdict(lambda: structure_default)
|
||||||
cl_to_tag = defaultdict(lambda: default, cl_to_tag)
|
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):
|
if getattr(converter, "forbid_extra_keys", False):
|
||||||
|
|
||||||
def structure_tagged_union(
|
def structure_tagged_union(
|
||||||
@@ -135,11 +95,54 @@ def configure_tagged_union(
|
|||||||
return _tag_to_hook[val[_tag_name]](val)
|
return _tag_to_hook[val[_tag_name]](val)
|
||||||
return _dh(val, _default)
|
return _dh(val, _default)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if getattr(converter, "forbid_extra_keys", False):
|
||||||
|
|
||||||
|
def structure_tagged_union(
|
||||||
|
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||||
|
) -> union:
|
||||||
|
val = val.copy()
|
||||||
|
return _tag_to_cl[val.pop(_tag_name)](val)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def structure_tagged_union(
|
||||||
|
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||||
|
) -> union:
|
||||||
|
return _tag_to_cl[val[_tag_name]](val)
|
||||||
|
|
||||||
|
def unstructure_tagged_union(
|
||||||
|
val: union,
|
||||||
|
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
|
||||||
|
_cl_to_tag=cl_to_tag,
|
||||||
|
_tag_name=tag_name,
|
||||||
|
) -> dict:
|
||||||
|
res = _exact_cl_unstruct_hooks[val.__class__](val)
|
||||||
|
res[_tag_name] = _cl_to_tag[val.__class__]
|
||||||
|
return res
|
||||||
|
|
||||||
converter.register_unstructure_hook(union, unstructure_tagged_union)
|
converter.register_unstructure_hook(union, unstructure_tagged_union)
|
||||||
converter.register_structure_hook(union, structure_tagged_union)
|
converter.register_structure_hook(union, structure_tagged_union)
|
||||||
|
|
||||||
|
for cl in args:
|
||||||
|
tag = tag_generator(cl)
|
||||||
|
struct_handler = converter.get_structure_hook(cl)
|
||||||
|
unstruct_handler = converter.get_unstructure_hook(cl)
|
||||||
|
|
||||||
def 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
|
Configure the converter to support validating and passing through unions of the
|
||||||
provided types and their subsets.
|
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
|
If the union contains a class and one or more of its subclasses, the subclasses
|
||||||
will also be included when validating the superclass.
|
will also be included when validating the superclass.
|
||||||
|
|
||||||
|
:param accept_ints_as_floats: When set (the default), if the provided union
|
||||||
|
contains both ints and floats, actual unions containing only floats will also accept
|
||||||
|
ints. See https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||||
|
for more information.
|
||||||
|
|
||||||
.. versionadded:: 23.2.0
|
.. versionadded:: 23.2.0
|
||||||
|
.. versionchanged:: 25.2.0
|
||||||
|
Introduced the `accept_ints_as_floats` parameter.
|
||||||
"""
|
"""
|
||||||
args = set(union.__args__)
|
args = set(union.__args__)
|
||||||
|
|
||||||
@@ -205,6 +215,16 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
|||||||
and not is_literal(a)
|
and not is_literal(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# By default, when floats are part of the union, accept ints too.
|
||||||
|
if (
|
||||||
|
accept_ints_as_floats
|
||||||
|
and int in args
|
||||||
|
and float in args
|
||||||
|
and float in non_literal_classes
|
||||||
|
and int not in non_literal_classes
|
||||||
|
):
|
||||||
|
non_literal_classes.add(int)
|
||||||
|
|
||||||
if spillover:
|
if spillover:
|
||||||
spillover_type = (
|
spillover_type = (
|
||||||
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
|
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
|
||||||
|
|||||||
@@ -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/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
||||||
importlib_metadata-6.8.0.dist-info/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
|
importlib_metadata-6.8.0.dist-info/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
|
||||||
importlib_metadata-6.8.0.dist-info/RECORD,,
|
importlib_metadata-6.8.0.dist-info/RECORD,,
|
||||||
@@ -6,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/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
||||||
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
|
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
|
||||||
importlib_metadata/__init__.py,sha256=EiH0qTKP_6oa6pRGJgPrq0kvjnL3hJ18BJH8VaAYSBA,30749
|
importlib_metadata/__init__.py,sha256=EiH0qTKP_6oa6pRGJgPrq0kvjnL3hJ18BJH8VaAYSBA,30749
|
||||||
importlib_metadata/__pycache__/__init__.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_adapters.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_collections.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_compat.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_functools.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_itertools.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_meta.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_py39compat.cpython-311.pyc,,
|
|
||||||
importlib_metadata/__pycache__/_text.cpython-311.pyc,,
|
|
||||||
importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
|
importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
|
||||||
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
|
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
|
||||||
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554
|
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
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/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
||||||
lsprotocol-2023.0.1.dist-info/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
|
lsprotocol-2023.0.1.dist-info/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
|
||||||
lsprotocol-2023.0.1.dist-info/RECORD,,
|
lsprotocol-2023.0.1.dist-info/RECORD,,
|
||||||
lsprotocol-2023.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
lsprotocol-2023.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
||||||
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
|
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
|
||||||
lsprotocol/__pycache__/__init__.cpython-311.pyc,,
|
|
||||||
lsprotocol/__pycache__/_hooks.cpython-311.pyc,,
|
|
||||||
lsprotocol/__pycache__/converters.cpython-311.pyc,,
|
|
||||||
lsprotocol/__pycache__/types.cpython-311.pyc,,
|
|
||||||
lsprotocol/__pycache__/validators.cpython-311.pyc,,
|
|
||||||
lsprotocol/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
|
lsprotocol/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
|
||||||
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
|
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
|
||||||
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
|||||||
@@ -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
|
Metadata-Version: 2.4
|
||||||
Name: packaging
|
Name: packaging
|
||||||
Version: 25.0
|
Version: 26.2
|
||||||
Summary: Core utilities for Python packages
|
Summary: Core utilities for Python packages
|
||||||
Author-email: Donald Stufft <donald@stufft.io>
|
Author-email: Donald Stufft <donald@stufft.io>
|
||||||
Requires-Python: >=3.8
|
Requires-Python: >=3.8
|
||||||
Description-Content-Type: text/x-rst
|
Description-Content-Type: text/x-rst
|
||||||
|
License-Expression: Apache-2.0 OR BSD-2-Clause
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
Classifier: Intended Audience :: Developers
|
Classifier: Intended Audience :: Developers
|
||||||
Classifier: License :: OSI Approved :: Apache Software License
|
|
||||||
Classifier: License :: OSI Approved :: BSD License
|
|
||||||
Classifier: Programming Language :: Python
|
Classifier: Programming Language :: Python
|
||||||
Classifier: Programming Language :: Python :: 3
|
Classifier: Programming Language :: Python :: 3
|
||||||
Classifier: Programming Language :: Python :: 3 :: Only
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
@@ -18,8 +17,10 @@ Classifier: Programming Language :: Python :: 3.10
|
|||||||
Classifier: Programming Language :: Python :: 3.11
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
Classifier: Programming Language :: Python :: 3.13
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
|
||||||
Classifier: Typing :: Typed
|
Classifier: Typing :: Typed
|
||||||
License-File: LICENSE
|
License-File: LICENSE
|
||||||
License-File: LICENSE.APACHE
|
License-File: LICENSE.APACHE
|
||||||
@@ -42,7 +43,7 @@ or benefit greatly from having a single shared implementation (eg: :pep:`425`).
|
|||||||
.. end-intro
|
.. end-intro
|
||||||
|
|
||||||
The ``packaging`` project includes the following: version handling, specifiers,
|
The ``packaging`` project includes the following: version handling, specifiers,
|
||||||
markers, requirements, tags, utilities.
|
markers, requirements, tags, metadata, lockfiles, utilities.
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
@@ -52,8 +53,14 @@ The `documentation`_ provides information and the API for the following:
|
|||||||
- Version Handling
|
- Version Handling
|
||||||
- Specifiers
|
- Specifiers
|
||||||
- Markers
|
- Markers
|
||||||
|
- Licenses
|
||||||
- Requirements
|
- Requirements
|
||||||
|
- Metadata
|
||||||
- Tags
|
- Tags
|
||||||
|
- Lockfiles (pylock)
|
||||||
|
- Direct URL helpers
|
||||||
|
- Dependency groups
|
||||||
|
- Errors
|
||||||
- Utilities
|
- Utilities
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
@@ -70,11 +77,11 @@ Discussion
|
|||||||
|
|
||||||
If you run into bugs, you can file them in our `issue tracker`_.
|
If you run into bugs, you can file them in our `issue tracker`_.
|
||||||
|
|
||||||
You can also join ``#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/
|
.. _`documentation`: https://packaging.pypa.io/
|
||||||
.. _`issue tracker`: https://github.com/pypa/packaging/issues
|
.. _`issue tracker`: https://github.com/pypa/packaging/issues
|
||||||
|
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
|
||||||
|
|
||||||
|
|
||||||
Code of Conduct
|
Code of Conduct
|
||||||
@@ -0,0 +1,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"
|
__summary__ = "Core utilities for Python packages"
|
||||||
__uri__ = "https://github.com/pypa/packaging"
|
__uri__ = "https://github.com/pypa/packaging"
|
||||||
|
|
||||||
__version__ = "25.0"
|
__version__ = "26.2"
|
||||||
|
|
||||||
__author__ = "Donald Stufft and individual contributors"
|
__author__ = "Donald Stufft and individual contributors"
|
||||||
__email__ = "donald@stufft.io"
|
__email__ = "donald@stufft.io"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ ELF file parser.
|
|||||||
This provides a class ``ELFFile`` that parses an ELF executable in a similar
|
This provides a class ``ELFFile`` that parses an ELF executable in a similar
|
||||||
interface to ``ZipFile``. Only the read interface is implemented.
|
interface to ``ZipFile``. Only the read interface is implemented.
|
||||||
|
|
||||||
Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
|
|
||||||
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ EF_ARM_ABIMASK = 0xFF000000
|
|||||||
EF_ARM_ABI_VER5 = 0x05000000
|
EF_ARM_ABI_VER5 = 0x05000000
|
||||||
EF_ARM_ABI_FLOAT_HARD = 0x00000400
|
EF_ARM_ABI_FLOAT_HARD = 0x00000400
|
||||||
|
|
||||||
|
_ALLOWED_ARCHS = {
|
||||||
|
"x86_64",
|
||||||
|
"aarch64",
|
||||||
|
"ppc64",
|
||||||
|
"ppc64le",
|
||||||
|
"s390x",
|
||||||
|
"loongarch64",
|
||||||
|
"riscv64",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
|
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
|
||||||
# as the type for `path` until then.
|
# as the type for `path` until then.
|
||||||
@@ -57,16 +67,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
|
|||||||
return _is_linux_armhf(executable)
|
return _is_linux_armhf(executable)
|
||||||
if "i686" in archs:
|
if "i686" in archs:
|
||||||
return _is_linux_i686(executable)
|
return _is_linux_i686(executable)
|
||||||
allowed_archs = {
|
return any(arch in _ALLOWED_ARCHS for arch in archs)
|
||||||
"x86_64",
|
|
||||||
"aarch64",
|
|
||||||
"ppc64",
|
|
||||||
"ppc64le",
|
|
||||||
"s390x",
|
|
||||||
"loongarch64",
|
|
||||||
"riscv64",
|
|
||||||
}
|
|
||||||
return any(arch in allowed_archs for arch in archs)
|
|
||||||
|
|
||||||
|
|
||||||
# If glibc ever changes its major version, we need to know what the last
|
# If glibc ever changes its major version, we need to know what the last
|
||||||
@@ -106,7 +107,7 @@ def _glibc_version_string_ctypes() -> str | None:
|
|||||||
Fallback implementation of glibc_version_string using ctypes.
|
Fallback implementation of glibc_version_string using ctypes.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import ctypes
|
import ctypes # noqa: PLC0415
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ def _glibc_version_string() -> str | None:
|
|||||||
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
|
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
|
||||||
|
|
||||||
|
|
||||||
def _parse_glibc_version(version_str: str) -> tuple[int, int]:
|
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
|
||||||
"""Parse glibc version.
|
"""Parse glibc version.
|
||||||
|
|
||||||
We use a regexp instead of str.split because we want to discard any
|
We use a regexp instead of str.split because we want to discard any
|
||||||
@@ -165,15 +166,15 @@ def _parse_glibc_version(version_str: str) -> tuple[int, int]:
|
|||||||
RuntimeWarning,
|
RuntimeWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
return -1, -1
|
return _GLibCVersion(-1, -1)
|
||||||
return int(m.group("major")), int(m.group("minor"))
|
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache
|
@functools.lru_cache
|
||||||
def _get_glibc_version() -> tuple[int, int]:
|
def _get_glibc_version() -> _GLibCVersion:
|
||||||
version_str = _glibc_version_string()
|
version_str = _glibc_version_string()
|
||||||
if version_str is None:
|
if version_str is None:
|
||||||
return (-1, -1)
|
return _GLibCVersion(-1, -1)
|
||||||
return _parse_glibc_version(version_str)
|
return _parse_glibc_version(version_str)
|
||||||
|
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
|
|||||||
return False
|
return False
|
||||||
# Check for presence of _manylinux module.
|
# Check for presence of _manylinux module.
|
||||||
try:
|
try:
|
||||||
import _manylinux
|
import _manylinux # noqa: PLC0415
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return True
|
return True
|
||||||
if hasattr(_manylinux, "manylinux_compatible"):
|
if hasattr(_manylinux, "manylinux_compatible"):
|
||||||
@@ -192,25 +193,26 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
|
|||||||
if result is not None:
|
if result is not None:
|
||||||
return bool(result)
|
return bool(result)
|
||||||
return True
|
return True
|
||||||
if version == _GLibCVersion(2, 5):
|
if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"):
|
||||||
if hasattr(_manylinux, "manylinux1_compatible"):
|
|
||||||
return bool(_manylinux.manylinux1_compatible)
|
return bool(_manylinux.manylinux1_compatible)
|
||||||
if version == _GLibCVersion(2, 12):
|
if version == _GLibCVersion(2, 12) and hasattr(
|
||||||
if hasattr(_manylinux, "manylinux2010_compatible"):
|
_manylinux, "manylinux2010_compatible"
|
||||||
|
):
|
||||||
return bool(_manylinux.manylinux2010_compatible)
|
return bool(_manylinux.manylinux2010_compatible)
|
||||||
if version == _GLibCVersion(2, 17):
|
if version == _GLibCVersion(2, 17) and hasattr(
|
||||||
if hasattr(_manylinux, "manylinux2014_compatible"):
|
_manylinux, "manylinux2014_compatible"
|
||||||
|
):
|
||||||
return bool(_manylinux.manylinux2014_compatible)
|
return bool(_manylinux.manylinux2014_compatible)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
_LEGACY_MANYLINUX_MAP = {
|
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
|
||||||
# CentOS 7 w/ glibc 2.17 (PEP 599)
|
# CentOS 7 w/ glibc 2.17 (PEP 599)
|
||||||
(2, 17): "manylinux2014",
|
_GLibCVersion(2, 17): "manylinux2014",
|
||||||
# CentOS 6 w/ glibc 2.12 (PEP 571)
|
# CentOS 6 w/ glibc 2.12 (PEP 571)
|
||||||
(2, 12): "manylinux2010",
|
_GLibCVersion(2, 12): "manylinux2010",
|
||||||
# CentOS 5 w/ glibc 2.5 (PEP 513)
|
# 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
|
min_minor = -1
|
||||||
for glibc_minor in range(glibc_max.minor, min_minor, -1):
|
for glibc_minor in range(glibc_max.minor, min_minor, -1):
|
||||||
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
|
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
|
||||||
tag = "manylinux_{}_{}".format(*glibc_version)
|
|
||||||
if _is_compatible(arch, glibc_version):
|
if _is_compatible(arch, glibc_version):
|
||||||
yield f"{tag}_{arch}"
|
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
|
||||||
|
|
||||||
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
|
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
|
||||||
if glibc_version in _LEGACY_MANYLINUX_MAP:
|
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
|
||||||
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
|
|
||||||
if _is_compatible(arch, glibc_version):
|
|
||||||
yield f"{legacy_tag}_{arch}"
|
yield f"{legacy_tag}_{arch}"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None:
|
|||||||
return None
|
return None
|
||||||
if ld is None or "musl" not in ld:
|
if ld is None or "musl" not in ld:
|
||||||
return None
|
return None
|
||||||
proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
|
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
|
||||||
return _parse_musl_version(proc.stderr)
|
return _parse_musl_version(proc.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ the implementation.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
from typing import NamedTuple, Sequence, Tuple, Union
|
from typing import List, Literal, NamedTuple, Sequence, Tuple, Union
|
||||||
|
|
||||||
from ._tokenizer import DEFAULT_RULES, Tokenizer
|
from ._tokenizer import DEFAULT_RULES, Tokenizer
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
|
__slots__ = ("value",)
|
||||||
|
|
||||||
def __init__(self, value: str) -> None:
|
def __init__(self, value: str) -> None:
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
@@ -20,31 +22,66 @@ class Node:
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{self.__class__.__name__}('{self}')>"
|
return f"<{self.__class__.__name__}({self.value!r})>"
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __getstate__(self) -> str:
|
||||||
|
# Return just the value string for compactness and stability.
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def _restore_value(self, value: object) -> None:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(
|
||||||
|
f"Cannot restore {self.__class__.__name__} value from {value!r}"
|
||||||
|
)
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __setstate__(self, state: object) -> None:
|
||||||
|
if isinstance(state, str):
|
||||||
|
# New format (26.2+): just the value string.
|
||||||
|
self._restore_value(state)
|
||||||
|
return
|
||||||
|
if isinstance(state, tuple) and len(state) == 2:
|
||||||
|
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
|
||||||
|
_, slot_dict = state
|
||||||
|
if isinstance(slot_dict, dict) and "value" in slot_dict:
|
||||||
|
self._restore_value(slot_dict["value"])
|
||||||
|
return
|
||||||
|
if isinstance(state, dict) and "value" in state:
|
||||||
|
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
|
||||||
|
self._restore_value(state["value"])
|
||||||
|
return
|
||||||
|
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
|
||||||
|
|
||||||
|
|
||||||
class Variable(Node):
|
class Variable(Node):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
class Value(Node):
|
class Value(Node):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
return f'"{self}"'
|
return f'"{self}"'
|
||||||
|
|
||||||
|
|
||||||
class Op(Node):
|
class Op(Node):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
|
MarkerLogical = Literal["and", "or"]
|
||||||
MarkerVar = Union[Variable, Value]
|
MarkerVar = Union[Variable, Value]
|
||||||
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
|
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
|
||||||
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
|
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
|
||||||
MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
|
MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]
|
||||||
|
|
||||||
|
|
||||||
class ParsedRequirement(NamedTuple):
|
class ParsedRequirement(NamedTuple):
|
||||||
@@ -111,7 +148,9 @@ def _parse_requirement_details(
|
|||||||
return (url, specifier, marker)
|
return (url, specifier, marker)
|
||||||
|
|
||||||
marker = _parse_requirement_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:
|
else:
|
||||||
specifier_start = tokenizer.position
|
specifier_start = tokenizer.position
|
||||||
@@ -124,10 +163,10 @@ def _parse_requirement_details(
|
|||||||
marker = _parse_requirement_marker(
|
marker = _parse_requirement_marker(
|
||||||
tokenizer,
|
tokenizer,
|
||||||
span_start=specifier_start,
|
span_start=specifier_start,
|
||||||
after=(
|
expected=(
|
||||||
"version specifier"
|
"comma (within version specifier), semicolon (after version specifier)"
|
||||||
if 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(
|
def _parse_requirement_marker(
|
||||||
tokenizer: Tokenizer, *, span_start: int, after: str
|
tokenizer: Tokenizer, *, span_start: int, expected: str
|
||||||
) -> MarkerList:
|
) -> MarkerList:
|
||||||
"""
|
"""
|
||||||
requirement_marker = SEMICOLON marker WS?
|
requirement_marker = SEMICOLON marker WS?
|
||||||
@@ -143,8 +182,9 @@ def _parse_requirement_marker(
|
|||||||
|
|
||||||
if not tokenizer.check("SEMICOLON"):
|
if not tokenizer.check("SEMICOLON"):
|
||||||
tokenizer.raise_syntax_error(
|
tokenizer.raise_syntax_error(
|
||||||
f"Expected end or semicolon (after {after})",
|
f"Expected {expected} or end",
|
||||||
span_start=span_start,
|
span_start=span_start,
|
||||||
|
span_end=None,
|
||||||
)
|
)
|
||||||
tokenizer.read()
|
tokenizer.read()
|
||||||
|
|
||||||
@@ -307,7 +347,7 @@ def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
|
|||||||
return (marker_var_left, marker_op, marker_var_right)
|
return (marker_var_left, marker_op, marker_var_right)
|
||||||
|
|
||||||
|
|
||||||
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:
|
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
|
||||||
"""
|
"""
|
||||||
marker_var = VARIABLE | QUOTED_STRING
|
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
|
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||||
# for complete details.
|
# for complete details.
|
||||||
|
|
||||||
|
"""Backward-compatibility shim for unpickling Version objects serialized before
|
||||||
|
packaging 26.1.
|
||||||
|
|
||||||
|
Old pickles reference ``packaging._structures.InfinityType`` and
|
||||||
|
``packaging._structures.NegativeInfinityType``. This module provides minimal
|
||||||
|
stand-in classes so that ``pickle.loads()`` can resolve those references.
|
||||||
|
The deserialized objects are not used for comparisons — ``Version.__setstate__``
|
||||||
|
discards the stale ``_key`` cache and recomputes it from the core version fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
class InfinityType:
|
class InfinityType:
|
||||||
|
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "Infinity"
|
return "Infinity"
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(repr(self))
|
|
||||||
|
|
||||||
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:
|
class NegativeInfinityType:
|
||||||
|
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "-Infinity"
|
return "-Infinity"
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(repr(self))
|
|
||||||
|
|
||||||
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()
|
NegativeInfinity = NegativeInfinityType()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import contextlib
|
import contextlib
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterator, NoReturn
|
from typing import Generator, Mapping, NoReturn
|
||||||
|
|
||||||
from .specifiers import Specifier
|
from .specifiers import Specifier
|
||||||
|
|
||||||
@@ -33,16 +33,16 @@ class ParserSyntaxError(Exception):
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
|
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
|
||||||
return "\n ".join([self.message, self.source, marker])
|
return f"{self.message}\n {self.source}\n {marker}"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
|
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
|
||||||
"LEFT_PARENTHESIS": r"\(",
|
"LEFT_PARENTHESIS": re.compile(r"\("),
|
||||||
"RIGHT_PARENTHESIS": r"\)",
|
"RIGHT_PARENTHESIS": re.compile(r"\)"),
|
||||||
"LEFT_BRACKET": r"\[",
|
"LEFT_BRACKET": re.compile(r"\["),
|
||||||
"RIGHT_BRACKET": r"\]",
|
"RIGHT_BRACKET": re.compile(r"\]"),
|
||||||
"SEMICOLON": r";",
|
"SEMICOLON": re.compile(r";"),
|
||||||
"COMMA": r",",
|
"COMMA": re.compile(r","),
|
||||||
"QUOTED_STRING": re.compile(
|
"QUOTED_STRING": re.compile(
|
||||||
r"""
|
r"""
|
||||||
(
|
(
|
||||||
@@ -53,10 +53,10 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
|
|||||||
""",
|
""",
|
||||||
re.VERBOSE,
|
re.VERBOSE,
|
||||||
),
|
),
|
||||||
"OP": r"(===|==|~=|!=|<=|>=|<|>)",
|
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
|
||||||
"BOOLOP": r"\b(or|and)\b",
|
"BOOLOP": re.compile(r"\b(or|and)\b"),
|
||||||
"IN": r"\bin\b",
|
"IN": re.compile(r"\bin\b"),
|
||||||
"NOT": r"\bnot\b",
|
"NOT": re.compile(r"\bnot\b"),
|
||||||
"VARIABLE": re.compile(
|
"VARIABLE": re.compile(
|
||||||
r"""
|
r"""
|
||||||
\b(
|
\b(
|
||||||
@@ -75,16 +75,16 @@ DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
|
|||||||
re.VERBOSE,
|
re.VERBOSE,
|
||||||
),
|
),
|
||||||
"SPECIFIER": re.compile(
|
"SPECIFIER": re.compile(
|
||||||
Specifier._operator_regex_str + Specifier._version_regex_str,
|
Specifier._specifier_regex_str,
|
||||||
re.VERBOSE | re.IGNORECASE,
|
re.VERBOSE | re.IGNORECASE,
|
||||||
),
|
),
|
||||||
"AT": r"\@",
|
"AT": re.compile(r"\@"),
|
||||||
"URL": r"[^ \t]+",
|
"URL": re.compile(r"[^ \t]+"),
|
||||||
"IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
|
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
|
||||||
"VERSION_PREFIX_TRAIL": r"\.\*",
|
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
|
||||||
"VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
|
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
|
||||||
"WS": r"[ \t]+",
|
"WS": re.compile(r"[ \t]+"),
|
||||||
"END": r"$",
|
"END": re.compile(r"$"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -99,12 +99,10 @@ class Tokenizer:
|
|||||||
self,
|
self,
|
||||||
source: str,
|
source: str,
|
||||||
*,
|
*,
|
||||||
rules: dict[str, str | re.Pattern[str]],
|
rules: Mapping[str, re.Pattern[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.source = source
|
self.source = source
|
||||||
self.rules: dict[str, re.Pattern[str]] = {
|
self.rules = rules
|
||||||
name: re.compile(pattern) for name, pattern in rules.items()
|
|
||||||
}
|
|
||||||
self.next_token: Token | None = None
|
self.next_token: Token | None = None
|
||||||
self.position = 0
|
self.position = 0
|
||||||
|
|
||||||
@@ -174,7 +172,7 @@ class Tokenizer:
|
|||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def enclosing_tokens(
|
def enclosing_tokens(
|
||||||
self, open_token: str, close_token: str, *, around: str
|
self, open_token: str, close_token: str, *, around: str
|
||||||
) -> Iterator[None]:
|
) -> Generator[None, None, None]:
|
||||||
if self.check(open_token):
|
if self.check(open_token):
|
||||||
open_position = self.position
|
open_position = self.position
|
||||||
self.read()
|
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
|
import re
|
||||||
from typing import NewType, cast
|
from typing import NewType, cast
|
||||||
|
|
||||||
from packaging.licenses._spdx import EXCEPTIONS, LICENSES
|
from ._spdx import EXCEPTIONS, LICENSES
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"InvalidLicenseExpression",
|
"InvalidLicenseExpression",
|
||||||
@@ -42,14 +42,25 @@ __all__ = [
|
|||||||
"canonicalize_license_expression",
|
"canonicalize_license_expression",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Simple __dir__ implementation since there are no public submodules
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
|
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
|
||||||
|
|
||||||
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
|
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
|
||||||
|
"""
|
||||||
|
A :class:`typing.NewType` of :class:`str`, representing a normalized
|
||||||
|
License-Expression.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class InvalidLicenseExpression(ValueError):
|
class InvalidLicenseExpression(ValueError):
|
||||||
"""Raised when a license-expression string is invalid
|
"""Raised when a license-expression string is invalid
|
||||||
|
|
||||||
|
>>> from packaging.licenses import canonicalize_license_expression
|
||||||
>>> canonicalize_license_expression("invalid")
|
>>> canonicalize_license_expression("invalid")
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
@@ -60,6 +71,34 @@ class InvalidLicenseExpression(ValueError):
|
|||||||
def canonicalize_license_expression(
|
def canonicalize_license_expression(
|
||||||
raw_license_expression: str,
|
raw_license_expression: str,
|
||||||
) -> NormalizedLicenseExpression:
|
) -> NormalizedLicenseExpression:
|
||||||
|
"""
|
||||||
|
This function takes a valid License-Expression, and returns the normalized
|
||||||
|
form of it.
|
||||||
|
|
||||||
|
The return type is typed as :class:`NormalizedLicenseExpression`. This
|
||||||
|
allows type checkers to help require that a string has passed through this
|
||||||
|
function before use.
|
||||||
|
|
||||||
|
:param str raw_license_expression: The License-Expression to canonicalize.
|
||||||
|
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
|
||||||
|
invalid/unknown license identifier or invalid syntax.
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from packaging.licenses import canonicalize_license_expression
|
||||||
|
>>> canonicalize_license_expression("mit")
|
||||||
|
'MIT'
|
||||||
|
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
|
||||||
|
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
|
||||||
|
>>> canonicalize_license_expression("(mit")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
InvalidLicenseExpression: Invalid license expression: '(mit'
|
||||||
|
>>> canonicalize_license_expression("Use-it-after-midnight")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
|
||||||
|
"""
|
||||||
if not raw_license_expression:
|
if not raw_license_expression:
|
||||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
raise InvalidLicenseExpression(message)
|
raise InvalidLicenseExpression(message)
|
||||||
@@ -80,16 +119,21 @@ def canonicalize_license_expression(
|
|||||||
|
|
||||||
tokens = license_expression.split()
|
tokens = license_expression.split()
|
||||||
|
|
||||||
# Rather than implementing boolean logic, we create an expression that Python can
|
# Rather than implementing a parenthesis/boolean logic parser, create an
|
||||||
# parse. Everything that is not involved with the grammar itself is treated as
|
# expression that Python can parse. Everything that is not involved with the
|
||||||
# `False` and the expression should evaluate as such.
|
# grammar itself is replaced with the placeholder `False` and the resultant
|
||||||
|
# expression should become a valid Python expression.
|
||||||
python_tokens = []
|
python_tokens = []
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token not in {"or", "and", "with", "(", ")"}:
|
if token not in {"or", "and", "with", "(", ")"}:
|
||||||
python_tokens.append("False")
|
python_tokens.append("False")
|
||||||
elif token == "with":
|
elif token == "with":
|
||||||
python_tokens.append("or")
|
python_tokens.append("or")
|
||||||
elif 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}"
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
raise InvalidLicenseExpression(message)
|
raise InvalidLicenseExpression(message)
|
||||||
else:
|
else:
|
||||||
@@ -97,11 +141,8 @@ def canonicalize_license_expression(
|
|||||||
|
|
||||||
python_expression = " ".join(python_tokens)
|
python_expression = " ".join(python_tokens)
|
||||||
try:
|
try:
|
||||||
invalid = eval(python_expression, globals(), locals())
|
compile(python_expression, "", "eval")
|
||||||
except Exception:
|
except SyntaxError:
|
||||||
invalid = True
|
|
||||||
|
|
||||||
if invalid is not False:
|
|
||||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||||
raise InvalidLicenseExpression(message) from None
|
raise InvalidLicenseExpression(message) from None
|
||||||
|
|
||||||
@@ -140,6 +181,6 @@ def canonicalize_license_expression(
|
|||||||
normalized_expression = " ".join(normalized_tokens)
|
normalized_expression = " ".join(normalized_tokens)
|
||||||
|
|
||||||
return cast(
|
return cast(
|
||||||
NormalizedLicenseExpression,
|
"NormalizedLicenseExpression",
|
||||||
normalized_expression.replace("( ", "(").replace(" )", ")"),
|
normalized_expression.replace("( ", "(").replace(" )", ")"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class SPDXException(TypedDict):
|
|||||||
deprecated: bool
|
deprecated: bool
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.25.0'
|
VERSION = '3.27.0'
|
||||||
|
|
||||||
LICENSES: dict[str, SPDXLicense] = {
|
LICENSES: dict[str, SPDXLicense] = {
|
||||||
'0bsd': {'id': '0BSD', 'deprecated': False},
|
'0bsd': {'id': '0BSD', 'deprecated': False},
|
||||||
@@ -46,6 +46,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
|
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
|
||||||
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
|
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
|
||||||
'any-osi': {'id': 'any-OSI', 'deprecated': False},
|
'any-osi': {'id': 'any-OSI', 'deprecated': False},
|
||||||
|
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
|
||||||
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
|
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
|
||||||
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
|
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
|
||||||
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
|
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
|
||||||
@@ -61,6 +62,8 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
|
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
|
||||||
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
|
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
|
||||||
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
|
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
|
||||||
|
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
|
||||||
|
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
|
||||||
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
|
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
|
||||||
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
|
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
|
||||||
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
|
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
|
||||||
@@ -75,6 +78,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'blessing': {'id': 'blessing', 'deprecated': False},
|
'blessing': {'id': 'blessing', 'deprecated': False},
|
||||||
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
|
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
|
||||||
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
|
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
|
||||||
|
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
|
||||||
'borceux': {'id': 'Borceux', 'deprecated': False},
|
'borceux': {'id': 'Borceux', 'deprecated': False},
|
||||||
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
|
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
|
||||||
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
|
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
|
||||||
@@ -85,6 +89,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
|
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
|
||||||
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
|
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
|
||||||
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
|
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
|
||||||
|
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
|
||||||
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
|
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
|
||||||
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
|
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
|
||||||
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
|
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
|
||||||
@@ -176,6 +181,8 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
|
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
|
||||||
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
|
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
|
||||||
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
|
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
|
||||||
|
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
|
||||||
|
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
|
||||||
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
|
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
|
||||||
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
|
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
|
||||||
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
|
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
|
||||||
@@ -215,6 +222,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
|
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
|
||||||
'cronyx': {'id': 'Cronyx', 'deprecated': False},
|
'cronyx': {'id': 'Cronyx', 'deprecated': False},
|
||||||
'crossword': {'id': 'Crossword', 'deprecated': False},
|
'crossword': {'id': 'Crossword', 'deprecated': False},
|
||||||
|
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
|
||||||
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
|
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
|
||||||
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
|
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
|
||||||
'cube': {'id': 'Cube', 'deprecated': False},
|
'cube': {'id': 'Cube', 'deprecated': False},
|
||||||
@@ -226,7 +234,9 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
|
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
|
||||||
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
|
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
|
||||||
'doc': {'id': 'DOC', 'deprecated': False},
|
'doc': {'id': 'DOC', 'deprecated': False},
|
||||||
|
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
|
||||||
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
|
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
|
||||||
|
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
|
||||||
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
|
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
|
||||||
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
|
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
|
||||||
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
|
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
|
||||||
@@ -263,12 +273,17 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
|
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
|
||||||
'fsful': {'id': 'FSFUL', 'deprecated': False},
|
'fsful': {'id': 'FSFUL', 'deprecated': False},
|
||||||
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
|
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
|
||||||
|
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
|
||||||
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
|
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
|
||||||
|
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
|
||||||
|
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
|
||||||
'ftl': {'id': 'FTL', 'deprecated': False},
|
'ftl': {'id': 'FTL', 'deprecated': False},
|
||||||
'furuseth': {'id': 'Furuseth', 'deprecated': False},
|
'furuseth': {'id': 'Furuseth', 'deprecated': False},
|
||||||
'fwlw': {'id': 'fwlw', 'deprecated': False},
|
'fwlw': {'id': 'fwlw', 'deprecated': False},
|
||||||
|
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
|
||||||
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
|
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
|
||||||
'gd': {'id': 'GD', 'deprecated': False},
|
'gd': {'id': 'GD', 'deprecated': False},
|
||||||
|
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
|
||||||
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
|
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
|
||||||
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
|
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
|
||||||
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
|
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
|
||||||
@@ -320,6 +335,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
|
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
|
||||||
'gutmann': {'id': 'Gutmann', 'deprecated': False},
|
'gutmann': {'id': 'Gutmann', 'deprecated': False},
|
||||||
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
|
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
|
||||||
|
'hdf5': {'id': 'HDF5', 'deprecated': False},
|
||||||
'hdparm': {'id': 'hdparm', 'deprecated': False},
|
'hdparm': {'id': 'hdparm', 'deprecated': False},
|
||||||
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
|
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
|
||||||
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
|
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
|
||||||
@@ -360,6 +376,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'imlib2': {'id': 'Imlib2', 'deprecated': False},
|
'imlib2': {'id': 'Imlib2', 'deprecated': False},
|
||||||
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
|
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
|
||||||
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
|
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
|
||||||
|
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
|
||||||
'intel': {'id': 'Intel', 'deprecated': False},
|
'intel': {'id': 'Intel', 'deprecated': False},
|
||||||
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
|
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
|
||||||
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
|
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
|
||||||
@@ -369,6 +386,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
|
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
|
||||||
'jam': {'id': 'Jam', 'deprecated': False},
|
'jam': {'id': 'Jam', 'deprecated': False},
|
||||||
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
|
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
|
||||||
|
'jove': {'id': 'jove', 'deprecated': False},
|
||||||
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
|
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
|
||||||
'jpnic': {'id': 'JPNIC', 'deprecated': False},
|
'jpnic': {'id': 'JPNIC', 'deprecated': False},
|
||||||
'json': {'id': 'JSON', 'deprecated': False},
|
'json': {'id': 'JSON', 'deprecated': False},
|
||||||
@@ -394,6 +412,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
|
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
|
||||||
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
|
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
|
||||||
'libpng': {'id': 'Libpng', 'deprecated': False},
|
'libpng': {'id': 'Libpng', 'deprecated': False},
|
||||||
|
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
|
||||||
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
|
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
|
||||||
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
|
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
|
||||||
'libtiff': {'id': 'libtiff', 'deprecated': False},
|
'libtiff': {'id': 'libtiff', 'deprecated': False},
|
||||||
@@ -424,14 +443,17 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'magaz': {'id': 'magaz', 'deprecated': False},
|
'magaz': {'id': 'magaz', 'deprecated': False},
|
||||||
'mailprio': {'id': 'mailprio', 'deprecated': False},
|
'mailprio': {'id': 'mailprio', 'deprecated': False},
|
||||||
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
|
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
|
||||||
|
'man2html': {'id': 'man2html', 'deprecated': False},
|
||||||
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
|
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
|
||||||
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
|
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
|
||||||
'metamail': {'id': 'metamail', 'deprecated': False},
|
'metamail': {'id': 'metamail', 'deprecated': False},
|
||||||
'minpack': {'id': 'Minpack', 'deprecated': False},
|
'minpack': {'id': 'Minpack', 'deprecated': False},
|
||||||
|
'mips': {'id': 'MIPS', 'deprecated': False},
|
||||||
'miros': {'id': 'MirOS', 'deprecated': False},
|
'miros': {'id': 'MirOS', 'deprecated': False},
|
||||||
'mit': {'id': 'MIT', 'deprecated': False},
|
'mit': {'id': 'MIT', 'deprecated': False},
|
||||||
'mit-0': {'id': 'MIT-0', 'deprecated': False},
|
'mit-0': {'id': 'MIT-0', 'deprecated': False},
|
||||||
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
|
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
|
||||||
|
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
|
||||||
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
|
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
|
||||||
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
|
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
|
||||||
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
|
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
|
||||||
@@ -472,6 +494,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'netcdf': {'id': 'NetCDF', 'deprecated': False},
|
'netcdf': {'id': 'NetCDF', 'deprecated': False},
|
||||||
'newsletr': {'id': 'Newsletr', 'deprecated': False},
|
'newsletr': {'id': 'Newsletr', 'deprecated': False},
|
||||||
'ngpl': {'id': 'NGPL', 'deprecated': False},
|
'ngpl': {'id': 'NGPL', 'deprecated': False},
|
||||||
|
'ngrep': {'id': 'ngrep', 'deprecated': False},
|
||||||
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
|
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
|
||||||
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
|
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
|
||||||
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
|
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
|
||||||
@@ -486,6 +509,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
|
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
|
||||||
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
|
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
|
||||||
'nrl': {'id': 'NRL', 'deprecated': False},
|
'nrl': {'id': 'NRL', 'deprecated': False},
|
||||||
|
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
|
||||||
'ntp': {'id': 'NTP', 'deprecated': False},
|
'ntp': {'id': 'NTP', 'deprecated': False},
|
||||||
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
|
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
|
||||||
'nunit': {'id': 'Nunit', 'deprecated': True},
|
'nunit': {'id': 'Nunit', 'deprecated': True},
|
||||||
@@ -580,6 +604,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
|
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
|
||||||
'sendmail': {'id': 'Sendmail', 'deprecated': False},
|
'sendmail': {'id': 'Sendmail', 'deprecated': False},
|
||||||
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
|
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
|
||||||
|
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
|
||||||
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
|
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
|
||||||
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
|
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
|
||||||
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
|
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
|
||||||
@@ -592,10 +617,12 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
|
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
|
||||||
'sl': {'id': 'SL', 'deprecated': False},
|
'sl': {'id': 'SL', 'deprecated': False},
|
||||||
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
|
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
|
||||||
|
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
|
||||||
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
|
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
|
||||||
'smppl': {'id': 'SMPPL', 'deprecated': False},
|
'smppl': {'id': 'SMPPL', 'deprecated': False},
|
||||||
'snia': {'id': 'SNIA', 'deprecated': False},
|
'snia': {'id': 'SNIA', 'deprecated': False},
|
||||||
'snprintf': {'id': 'snprintf', 'deprecated': False},
|
'snprintf': {'id': 'snprintf', 'deprecated': False},
|
||||||
|
'sofa': {'id': 'SOFA', 'deprecated': False},
|
||||||
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
|
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
|
||||||
'soundex': {'id': 'Soundex', 'deprecated': False},
|
'soundex': {'id': 'Soundex', 'deprecated': False},
|
||||||
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
|
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
|
||||||
@@ -609,6 +636,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
|
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
|
||||||
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
|
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
|
||||||
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
|
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
|
||||||
|
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
|
||||||
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
|
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
|
||||||
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
|
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
|
||||||
'sunpro': {'id': 'SunPro', 'deprecated': False},
|
'sunpro': {'id': 'SunPro', 'deprecated': False},
|
||||||
@@ -620,12 +648,14 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
|
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
|
||||||
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
|
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
|
||||||
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
|
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
|
||||||
|
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
|
||||||
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
|
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
|
||||||
'tmate': {'id': 'TMate', 'deprecated': False},
|
'tmate': {'id': 'TMate', 'deprecated': False},
|
||||||
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
|
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
|
||||||
'tosl': {'id': 'TOSL', 'deprecated': False},
|
'tosl': {'id': 'TOSL', 'deprecated': False},
|
||||||
'tpdl': {'id': 'TPDL', 'deprecated': False},
|
'tpdl': {'id': 'TPDL', 'deprecated': False},
|
||||||
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
|
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
|
||||||
|
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
|
||||||
'ttwl': {'id': 'TTWL', 'deprecated': False},
|
'ttwl': {'id': 'TTWL', 'deprecated': False},
|
||||||
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
|
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
|
||||||
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
|
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
|
||||||
@@ -641,6 +671,8 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
|
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
|
||||||
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
|
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
|
||||||
'unlicense': {'id': 'Unlicense', 'deprecated': False},
|
'unlicense': {'id': 'Unlicense', 'deprecated': False},
|
||||||
|
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
|
||||||
|
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
|
||||||
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
|
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
|
||||||
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
|
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
|
||||||
'vim': {'id': 'Vim', 'deprecated': False},
|
'vim': {'id': 'Vim', 'deprecated': False},
|
||||||
@@ -654,6 +686,7 @@ LICENSES: dict[str, SPDXLicense] = {
|
|||||||
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
|
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
|
||||||
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
|
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
|
||||||
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
|
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
|
||||||
|
'wwl': {'id': 'wwl', 'deprecated': False},
|
||||||
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
|
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
|
||||||
'x11': {'id': 'X11', 'deprecated': False},
|
'x11': {'id': 'X11', 'deprecated': False},
|
||||||
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
|
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
|
||||||
@@ -695,9 +728,11 @@ EXCEPTIONS: dict[str, SPDXException] = {
|
|||||||
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
|
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
|
||||||
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
|
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
|
||||||
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
|
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
|
||||||
|
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
|
||||||
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
|
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
|
||||||
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
|
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
|
||||||
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
|
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
|
||||||
|
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
|
||||||
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
|
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
|
||||||
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
|
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
|
||||||
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
|
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
|
||||||
@@ -714,13 +749,16 @@ EXCEPTIONS: dict[str, SPDXException] = {
|
|||||||
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
|
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
|
||||||
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
|
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
|
||||||
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
|
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
|
||||||
|
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
|
||||||
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
|
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
|
||||||
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
|
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
|
||||||
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
|
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
|
||||||
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
|
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
|
||||||
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
|
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
|
||||||
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
|
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
|
||||||
|
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
|
||||||
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
|
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
|
||||||
|
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
|
||||||
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
|
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
|
||||||
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
|
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
|
||||||
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
|
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
|
||||||
@@ -730,12 +768,14 @@ EXCEPTIONS: dict[str, SPDXException] = {
|
|||||||
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
|
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
|
||||||
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
|
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
|
||||||
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
|
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
|
||||||
|
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
|
||||||
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
|
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
|
||||||
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
|
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
|
||||||
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
|
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
|
||||||
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
|
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
|
||||||
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
|
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
|
||||||
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
|
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
|
||||||
|
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
|
||||||
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
|
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
|
||||||
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
|
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
|
||||||
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
|
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import operator
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast
|
from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast
|
||||||
|
|
||||||
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
|
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
|
||||||
from ._parser import parse_marker as _parse_marker
|
from ._parser import parse_marker as _parse_marker
|
||||||
@@ -17,6 +17,7 @@ from .specifiers import InvalidSpecifier, Specifier
|
|||||||
from .utils import canonicalize_name
|
from .utils import canonicalize_name
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Environment",
|
||||||
"EvaluateContext",
|
"EvaluateContext",
|
||||||
"InvalidMarker",
|
"InvalidMarker",
|
||||||
"Marker",
|
"Marker",
|
||||||
@@ -25,31 +26,58 @@ __all__ = [
|
|||||||
"default_environment",
|
"default_environment",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
|
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
|
||||||
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
|
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
|
||||||
|
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
|
||||||
|
|
||||||
|
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
|
||||||
|
|
||||||
|
* ``"metadata"`` (for core metadata; default)
|
||||||
|
* ``"lock_file"`` (for lock files)
|
||||||
|
* ``"requirement"`` (i.e. all other situations)
|
||||||
|
"""
|
||||||
|
|
||||||
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
||||||
|
MARKERS_REQUIRING_VERSION = {
|
||||||
|
"implementation_version",
|
||||||
|
"platform_release",
|
||||||
|
"python_full_version",
|
||||||
|
"python_version",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InvalidMarker(ValueError):
|
class InvalidMarker(ValueError):
|
||||||
"""
|
"""Raised when attempting to create a :class:`Marker` from invalid input.
|
||||||
An invalid marker was found, users should refer to PEP 508.
|
|
||||||
|
This error indicates that the given marker string does not conform to the
|
||||||
|
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class UndefinedComparison(ValueError):
|
class UndefinedComparison(ValueError):
|
||||||
"""
|
"""Raised when evaluating an unsupported marker comparison.
|
||||||
An invalid operation was attempted on a value that doesn't support it.
|
|
||||||
|
This can happen when marker values are compared as versions but do not
|
||||||
|
conform to the :ref:`specification of version specifiers
|
||||||
|
<pypug:version-specifiers>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class UndefinedEnvironmentName(ValueError):
|
class UndefinedEnvironmentName(ValueError):
|
||||||
"""
|
"""Raised when evaluating a marker that references a missing environment key."""
|
||||||
A name was attempted to be used that does not exist inside of the
|
|
||||||
environment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Environment(TypedDict):
|
class Environment(TypedDict):
|
||||||
|
"""
|
||||||
|
A dictionary that represents a Python environment as captured by
|
||||||
|
:func:`default_environment`. All fields are required.
|
||||||
|
"""
|
||||||
|
|
||||||
implementation_name: str
|
implementation_name: str
|
||||||
"""The implementation's identifier, e.g. ``'cpython'``."""
|
"""The implementation's identifier, e.g. ``'cpython'``."""
|
||||||
|
|
||||||
@@ -121,20 +149,28 @@ class Environment(TypedDict):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _normalize_extra_values(results: Any) -> Any:
|
def _normalize_extras(
|
||||||
"""
|
result: MarkerList | MarkerAtom | str,
|
||||||
Normalize extra values.
|
) -> MarkerList | MarkerAtom | str:
|
||||||
"""
|
if not isinstance(result, tuple):
|
||||||
if isinstance(results[0], tuple):
|
return result
|
||||||
lhs, op, rhs = results[0]
|
|
||||||
|
lhs, op, rhs = result
|
||||||
if isinstance(lhs, Variable) and lhs.value == "extra":
|
if isinstance(lhs, Variable) and lhs.value == "extra":
|
||||||
normalized_extra = canonicalize_name(rhs.value)
|
normalized_extra = canonicalize_name(rhs.value)
|
||||||
rhs = Value(normalized_extra)
|
rhs = Value(normalized_extra)
|
||||||
elif isinstance(rhs, Variable) and rhs.value == "extra":
|
elif isinstance(rhs, Variable) and rhs.value == "extra":
|
||||||
normalized_extra = canonicalize_name(lhs.value)
|
normalized_extra = canonicalize_name(lhs.value)
|
||||||
lhs = Value(normalized_extra)
|
lhs = Value(normalized_extra)
|
||||||
results[0] = lhs, op, rhs
|
return lhs, op, rhs
|
||||||
return results
|
|
||||||
|
|
||||||
|
def _normalize_extra_values(results: MarkerList) -> MarkerList:
|
||||||
|
"""
|
||||||
|
Normalize extra values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [_normalize_extras(r) for r in results]
|
||||||
|
|
||||||
|
|
||||||
def _format_marker(
|
def _format_marker(
|
||||||
@@ -168,25 +204,26 @@ def _format_marker(
|
|||||||
_operators: dict[str, Operator] = {
|
_operators: dict[str, Operator] = {
|
||||||
"in": lambda lhs, rhs: lhs in rhs,
|
"in": lambda lhs, rhs: lhs in rhs,
|
||||||
"not in": lambda lhs, rhs: lhs not in rhs,
|
"not in": lambda lhs, rhs: lhs not in rhs,
|
||||||
"<": operator.lt,
|
"<": lambda _lhs, _rhs: False,
|
||||||
"<=": operator.le,
|
"<=": operator.eq,
|
||||||
"==": operator.eq,
|
"==": operator.eq,
|
||||||
"!=": operator.ne,
|
"!=": operator.ne,
|
||||||
">=": operator.ge,
|
">=": operator.eq,
|
||||||
">": operator.gt,
|
">": lambda _lhs, _rhs: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
|
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
|
||||||
if isinstance(rhs, str):
|
op_str = op.serialize()
|
||||||
|
if key in MARKERS_REQUIRING_VERSION:
|
||||||
try:
|
try:
|
||||||
spec = Specifier("".join([op.serialize(), rhs]))
|
spec = Specifier(f"{op_str}{rhs}")
|
||||||
except InvalidSpecifier:
|
except InvalidSpecifier:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
return spec.contains(lhs, prereleases=True)
|
return spec.contains(lhs, prereleases=True)
|
||||||
|
|
||||||
oper: Operator | None = _operators.get(op.serialize())
|
oper: Operator | None = _operators.get(op_str)
|
||||||
if oper is None:
|
if oper is None:
|
||||||
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
|
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
|
||||||
|
|
||||||
@@ -196,13 +233,14 @@ def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool:
|
|||||||
def _normalize(
|
def _normalize(
|
||||||
lhs: str, rhs: str | AbstractSet[str], key: str
|
lhs: str, rhs: str | AbstractSet[str], key: str
|
||||||
) -> tuple[str, str | AbstractSet[str]]:
|
) -> tuple[str, str | AbstractSet[str]]:
|
||||||
# PEP 685 – Comparison of extra names for optional distribution dependencies
|
# PEP 685 - Comparison of extra names for optional distribution dependencies
|
||||||
# https://peps.python.org/pep-0685/
|
# https://peps.python.org/pep-0685/
|
||||||
# > When comparing extra names, tools MUST normalize the names being
|
# > When comparing extra names, tools MUST normalize the names being
|
||||||
# > compared using the semantics outlined in PEP 503 for names
|
# > compared using the semantics outlined in PEP 503 for names
|
||||||
if key == "extra":
|
if key == "extra":
|
||||||
assert isinstance(rhs, str), "extra value must be a string"
|
assert isinstance(rhs, str), "extra value must be a string"
|
||||||
return (canonicalize_name(lhs), canonicalize_name(rhs))
|
# Both sides are normalized at this point already
|
||||||
|
return (lhs, rhs)
|
||||||
if key in MARKERS_ALLOWING_SET:
|
if key in MARKERS_ALLOWING_SET:
|
||||||
if isinstance(rhs, str): # pragma: no cover
|
if isinstance(rhs, str): # pragma: no cover
|
||||||
return (canonicalize_name(lhs), canonicalize_name(rhs))
|
return (canonicalize_name(lhs), canonicalize_name(rhs))
|
||||||
@@ -219,8 +257,6 @@ def _evaluate_markers(
|
|||||||
groups: list[list[bool]] = [[]]
|
groups: list[list[bool]] = [[]]
|
||||||
|
|
||||||
for marker in markers:
|
for marker in markers:
|
||||||
assert isinstance(marker, (list, tuple, str))
|
|
||||||
|
|
||||||
if isinstance(marker, list):
|
if isinstance(marker, list):
|
||||||
groups[-1].append(_evaluate_markers(marker, environment))
|
groups[-1].append(_evaluate_markers(marker, environment))
|
||||||
elif isinstance(marker, tuple):
|
elif isinstance(marker, tuple):
|
||||||
@@ -234,18 +270,21 @@ def _evaluate_markers(
|
|||||||
lhs_value = lhs.value
|
lhs_value = lhs.value
|
||||||
environment_key = rhs.value
|
environment_key = rhs.value
|
||||||
rhs_value = environment[environment_key]
|
rhs_value = environment[environment_key]
|
||||||
|
|
||||||
assert isinstance(lhs_value, str), "lhs must be a string"
|
assert isinstance(lhs_value, str), "lhs must be a string"
|
||||||
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
|
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
|
||||||
groups[-1].append(_eval_op(lhs_value, op, rhs_value))
|
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
|
||||||
else:
|
elif marker == "or":
|
||||||
assert marker in ["and", "or"]
|
|
||||||
if marker == "or":
|
|
||||||
groups.append([])
|
groups.append([])
|
||||||
|
elif marker == "and":
|
||||||
|
pass
|
||||||
|
else: # pragma: nocover
|
||||||
|
raise TypeError(f"Unexpected marker {marker!r}")
|
||||||
|
|
||||||
return any(all(item) for item in groups)
|
return any(all(item) for item in groups)
|
||||||
|
|
||||||
|
|
||||||
def format_full_version(info: sys._version_info) -> str:
|
def _format_full_version(info: sys._version_info) -> str:
|
||||||
version = f"{info.major}.{info.minor}.{info.micro}"
|
version = f"{info.major}.{info.minor}.{info.micro}"
|
||||||
kind = info.releaselevel
|
kind = info.releaselevel
|
||||||
if kind != "final":
|
if kind != "final":
|
||||||
@@ -254,7 +293,11 @@ def format_full_version(info: sys._version_info) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def default_environment() -> Environment:
|
def default_environment() -> Environment:
|
||||||
iver = format_full_version(sys.implementation.version)
|
"""Return the default marker environment for the current Python process.
|
||||||
|
|
||||||
|
This is the base environment used by :meth:`Marker.evaluate`.
|
||||||
|
"""
|
||||||
|
iver = _format_full_version(sys.implementation.version)
|
||||||
implementation_name = sys.implementation.name
|
implementation_name = sys.implementation.name
|
||||||
return {
|
return {
|
||||||
"implementation_name": implementation_name,
|
"implementation_name": implementation_name,
|
||||||
@@ -272,10 +315,36 @@ def default_environment() -> Environment:
|
|||||||
|
|
||||||
|
|
||||||
class Marker:
|
class Marker:
|
||||||
|
"""Represents a parsed dependency marker expression.
|
||||||
|
|
||||||
|
Marker expressions are parsed according to the
|
||||||
|
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||||
|
|
||||||
|
:param marker: The string representation of a marker expression.
|
||||||
|
:raises InvalidMarker: If ``marker`` cannot be parsed.
|
||||||
|
|
||||||
|
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||||
|
format so the same pickle can be loaded in future packaging releases.
|
||||||
|
|
||||||
|
.. versionchanged:: 26.2
|
||||||
|
|
||||||
|
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||||
|
be unpickled with future releases. Backward compatibility with pickles
|
||||||
|
from packaging < 26.2 is supported but may be removed in a future
|
||||||
|
release.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_markers",)
|
||||||
|
|
||||||
def __init__(self, marker: str) -> None:
|
def __init__(self, marker: str) -> None:
|
||||||
# Note: We create a Marker object without calling this constructor in
|
# Note: We create a Marker object without calling this constructor in
|
||||||
# packaging.requirements.Requirement. If any additional logic is
|
# packaging.requirements.Requirement. If any additional logic is
|
||||||
# added here, make sure to mirror/adapt Requirement.
|
# added here, make sure to mirror/adapt Requirement.
|
||||||
|
|
||||||
|
# If this fails and throws an error, the repr still expects _markers to
|
||||||
|
# be defined.
|
||||||
|
self._markers: MarkerList = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._markers = _normalize_extra_values(_parse_marker(marker))
|
self._markers = _normalize_extra_values(_parse_marker(marker))
|
||||||
# The attribute `_markers` can be described in terms of a recursive type:
|
# The attribute `_markers` can be described in terms of a recursive type:
|
||||||
@@ -297,36 +366,94 @@ class Marker:
|
|||||||
except ParserSyntaxError as e:
|
except ParserSyntaxError as e:
|
||||||
raise InvalidMarker(str(e)) from e
|
raise InvalidMarker(str(e)) from e
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_markers(cls, markers: MarkerList) -> Marker:
|
||||||
|
"""Create a Marker instance from a pre-parsed marker tree.
|
||||||
|
|
||||||
|
This avoids re-parsing serialised marker strings when combining markers.
|
||||||
|
"""
|
||||||
|
new = cls.__new__(cls)
|
||||||
|
new._markers = markers
|
||||||
|
return new
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return _format_marker(self._markers)
|
return _format_marker(self._markers)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Marker('{self}')>"
|
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash((self.__class__.__name__, str(self)))
|
return hash(str(self))
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, Marker):
|
if not isinstance(other, Marker):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
return str(self) == str(other)
|
return str(self) == str(other)
|
||||||
|
|
||||||
|
def __getstate__(self) -> str:
|
||||||
|
# Return the marker expression string for compactness and stability.
|
||||||
|
# Internal Node objects are excluded; the string is re-parsed on load.
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def __setstate__(self, state: object) -> None:
|
||||||
|
if isinstance(state, str):
|
||||||
|
# New format (26.2+): just the marker expression string.
|
||||||
|
try:
|
||||||
|
self._markers = _normalize_extra_values(_parse_marker(state))
|
||||||
|
except ParserSyntaxError as exc:
|
||||||
|
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
|
||||||
|
return
|
||||||
|
if isinstance(state, dict) and "_markers" in state:
|
||||||
|
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||||
|
markers = state["_markers"]
|
||||||
|
if isinstance(markers, list):
|
||||||
|
self._markers = markers
|
||||||
|
return
|
||||||
|
if isinstance(state, tuple) and len(state) == 2:
|
||||||
|
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||||
|
_, slot_dict = state
|
||||||
|
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
|
||||||
|
markers = slot_dict["_markers"]
|
||||||
|
if isinstance(markers, list):
|
||||||
|
self._markers = markers
|
||||||
|
return
|
||||||
|
raise TypeError(f"Cannot restore Marker from {state!r}")
|
||||||
|
|
||||||
|
def __and__(self, other: Marker) -> Marker:
|
||||||
|
if not isinstance(other, Marker):
|
||||||
|
return NotImplemented
|
||||||
|
return self._from_markers([self._markers, "and", other._markers])
|
||||||
|
|
||||||
|
def __or__(self, other: Marker) -> Marker:
|
||||||
|
if not isinstance(other, Marker):
|
||||||
|
return NotImplemented
|
||||||
|
return self._from_markers([self._markers, "or", other._markers])
|
||||||
|
|
||||||
def evaluate(
|
def evaluate(
|
||||||
self,
|
self,
|
||||||
environment: dict[str, str] | None = None,
|
environment: Mapping[str, str | AbstractSet[str]] | None = None,
|
||||||
context: EvaluateContext = "metadata",
|
context: EvaluateContext = "metadata",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Evaluate a marker.
|
"""Evaluate a marker.
|
||||||
|
|
||||||
Return the boolean from evaluating the given marker against the
|
Return the boolean from evaluating this marker against the environment.
|
||||||
environment. environment is an optional argument to override all or
|
The environment is determined from the current Python process unless
|
||||||
part of the determined environment. The *context* parameter specifies what
|
passed in explicitly.
|
||||||
context the markers are being evaluated for, which influences what markers
|
|
||||||
are considered valid. Acceptable values are "metadata" (for core metadata;
|
:param environment: Mapping containing keys and values to override the
|
||||||
default), "lock_file", and "requirement" (i.e. all other situations).
|
detected environment.
|
||||||
|
:param EvaluateContext context: The context in which the marker is
|
||||||
|
evaluated, which influences what marker names are considered valid.
|
||||||
|
Accepted values are ``"metadata"`` (for core metadata; default),
|
||||||
|
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
|
||||||
|
:raises UndefinedComparison: If the marker uses a comparison on values
|
||||||
|
that are not valid versions per the :ref:`specification of version
|
||||||
|
specifiers <pypug:version-specifiers>`.
|
||||||
|
:raises UndefinedEnvironmentName: If the marker references a value that
|
||||||
|
is missing from the evaluation environment.
|
||||||
|
:returns: ``True`` if the marker matches, otherwise ``False``.
|
||||||
|
|
||||||
The environment is determined from the current Python process.
|
|
||||||
"""
|
"""
|
||||||
current_environment = cast(
|
current_environment = cast(
|
||||||
"dict[str, str | AbstractSet[str]]", default_environment()
|
"dict[str, str | AbstractSet[str]]", default_environment()
|
||||||
@@ -337,12 +464,15 @@ class Marker:
|
|||||||
)
|
)
|
||||||
elif context == "metadata":
|
elif context == "metadata":
|
||||||
current_environment["extra"] = ""
|
current_environment["extra"] = ""
|
||||||
|
|
||||||
if environment is not None:
|
if environment is not None:
|
||||||
current_environment.update(environment)
|
current_environment.update(environment)
|
||||||
# The API used to allow setting extra to None. We need to handle this
|
if "extra" in current_environment:
|
||||||
# case for backwards compatibility.
|
# The API used to allow setting extra to None. We need to handle
|
||||||
if "extra" in current_environment and current_environment["extra"] is None:
|
# this case for backwards compatibility. Also skip running
|
||||||
current_environment["extra"] = ""
|
# normalize name if extra is empty.
|
||||||
|
extra = cast("str | None", current_environment["extra"])
|
||||||
|
current_environment["extra"] = canonicalize_name(extra) if extra else ""
|
||||||
|
|
||||||
return _evaluate_markers(
|
return _evaluate_markers(
|
||||||
self._markers, _repair_python_full_version(current_environment)
|
self._markers, _repair_python_full_version(current_environment)
|
||||||
@@ -356,7 +486,7 @@ def _repair_python_full_version(
|
|||||||
Work around platform.python_version() returning something that is not PEP 440
|
Work around platform.python_version() returning something that is not PEP 440
|
||||||
compliant for non-tagged Python builds.
|
compliant for non-tagged Python builds.
|
||||||
"""
|
"""
|
||||||
python_full_version = cast(str, env["python_full_version"])
|
python_full_version = cast("str", env["python_full_version"])
|
||||||
if python_full_version.endswith("+"):
|
if python_full_version.endswith("+"):
|
||||||
env["python_full_version"] = f"{python_full_version}local"
|
env["python_full_version"] = f"{python_full_version}local"
|
||||||
return env
|
return env
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import email.feedparser
|
|
||||||
import email.header
|
import email.header
|
||||||
import email.message
|
import email.message
|
||||||
import email.parser
|
import email.parser
|
||||||
import email.policy
|
import email.policy
|
||||||
|
import keyword
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
|
||||||
import typing
|
import typing
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
@@ -19,31 +18,27 @@ from typing import (
|
|||||||
|
|
||||||
from . import licenses, requirements, specifiers, utils
|
from . import licenses, requirements, specifiers, utils
|
||||||
from . import version as version_module
|
from . import version as version_module
|
||||||
|
from .errors import ExceptionGroup, _ErrorCollector
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
from .licenses import NormalizedLicenseExpression
|
from .licenses import NormalizedLicenseExpression
|
||||||
|
|
||||||
T = typing.TypeVar("T")
|
T = typing.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 11): # pragma: no cover
|
__all__ = [
|
||||||
ExceptionGroup = ExceptionGroup
|
"ExceptionGroup", # Keep this for a bit (makes mypy happy w/ 26.0 compat)
|
||||||
else: # pragma: no cover
|
"InvalidMetadata",
|
||||||
|
"Metadata",
|
||||||
|
"RFC822Message",
|
||||||
|
"RFC822Policy",
|
||||||
|
"RawMetadata",
|
||||||
|
"parse_email",
|
||||||
|
]
|
||||||
|
|
||||||
class ExceptionGroup(Exception):
|
|
||||||
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
|
|
||||||
|
|
||||||
If :external:exc:`ExceptionGroup` is already defined by Python itself,
|
def __dir__() -> list[str]:
|
||||||
that version is used instead.
|
return __all__
|
||||||
"""
|
|
||||||
|
|
||||||
message: str
|
|
||||||
exceptions: list[Exception]
|
|
||||||
|
|
||||||
def __init__(self, message: str, exceptions: list[Exception]) -> None:
|
|
||||||
self.message = message
|
|
||||||
self.exceptions = exceptions
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidMetadata(ValueError):
|
class InvalidMetadata(ValueError):
|
||||||
@@ -73,8 +68,8 @@ class RawMetadata(TypedDict, total=False):
|
|||||||
|
|
||||||
Core metadata fields that can be specified multiple times are stored as a
|
Core metadata fields that can be specified multiple times are stored as a
|
||||||
list or dict depending on which is appropriate for the field. Any fields
|
list or dict depending on which is appropriate for the field. Any fields
|
||||||
which hold multiple values in a single field are stored as a list.
|
which hold multiple values in a single field are stored as a list. All fields
|
||||||
|
are considered optional.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Metadata 1.0 - PEP 241
|
# Metadata 1.0 - PEP 241
|
||||||
@@ -126,13 +121,19 @@ class RawMetadata(TypedDict, total=False):
|
|||||||
|
|
||||||
# Metadata 2.3 - PEP 685
|
# Metadata 2.3 - PEP 685
|
||||||
# No new fields were added in PEP 685, just some edge case were
|
# No new fields were added in PEP 685, just some edge case were
|
||||||
# tightened up to provide better interoptability.
|
# tightened up to provide better interoperability.
|
||||||
|
|
||||||
# Metadata 2.4 - PEP 639
|
# Metadata 2.4 - PEP 639
|
||||||
license_expression: str
|
license_expression: str
|
||||||
license_files: list[str]
|
license_files: list[str]
|
||||||
|
|
||||||
|
# Metadata 2.5 - PEP 794
|
||||||
|
import_names: list[str]
|
||||||
|
import_namespaces: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
# 'keywords' is special as it's a string in the core metadata spec, but we
|
||||||
|
# represent it as a list.
|
||||||
_STRING_FIELDS = {
|
_STRING_FIELDS = {
|
||||||
"author",
|
"author",
|
||||||
"author_email",
|
"author_email",
|
||||||
@@ -165,6 +166,8 @@ _LIST_FIELDS = {
|
|||||||
"requires_dist",
|
"requires_dist",
|
||||||
"requires_external",
|
"requires_external",
|
||||||
"supported_platforms",
|
"supported_platforms",
|
||||||
|
"import_names",
|
||||||
|
"import_namespaces",
|
||||||
}
|
}
|
||||||
|
|
||||||
_DICT_FIELDS = {
|
_DICT_FIELDS = {
|
||||||
@@ -193,24 +196,23 @@ def _parse_project_urls(data: list[str]) -> dict[str, str]:
|
|||||||
# be the missing value, then they'd have multiple '' values that
|
# be the missing value, then they'd have multiple '' values that
|
||||||
# overwrite each other in a accumulating dict.
|
# overwrite each other in a accumulating dict.
|
||||||
#
|
#
|
||||||
# The other potentional issue is that it's possible to have the
|
# The other potential issue is that it's possible to have the
|
||||||
# same label multiple times in the metadata, with no solid "right"
|
# same label multiple times in the metadata, with no solid "right"
|
||||||
# answer with what to do in that case. As such, we'll do the only
|
# answer with what to do in that case. As such, we'll do the only
|
||||||
# thing we can, which is treat the field as unparseable and add it
|
# thing we can, which is treat the field as unparsable and add it
|
||||||
# to our list of unparsed fields.
|
# to our list of unparsed fields.
|
||||||
parts = [p.strip() for p in pair.split(",", 1)]
|
#
|
||||||
parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items
|
|
||||||
|
|
||||||
# TODO: The spec doesn't say anything about if the keys should be
|
# TODO: The spec doesn't say anything about if the keys should be
|
||||||
# considered case sensitive or not... logically they should
|
# considered case sensitive or not... logically they should
|
||||||
# be case-preserving and case-insensitive, but doing that
|
# be case-preserving and case-insensitive, but doing that
|
||||||
# would open up more cases where we might have duplicate
|
# would open up more cases where we might have duplicate
|
||||||
# entries.
|
# entries.
|
||||||
label, url = parts
|
label, _, url = (s.strip() for s in pair.partition(","))
|
||||||
|
|
||||||
if label in urls:
|
if label in urls:
|
||||||
# The label already exists in our set of urls, so this field
|
# The label already exists in our set of urls, so this field
|
||||||
# is unparseable, and we can just add the whole thing to our
|
# is unparsable, and we can just add the whole thing to our
|
||||||
# unparseable data and stop processing it.
|
# unparsable data and stop processing it.
|
||||||
raise KeyError("duplicate labels in project urls")
|
raise KeyError("duplicate labels in project urls")
|
||||||
urls[label] = url
|
urls[label] = url
|
||||||
|
|
||||||
@@ -257,6 +259,8 @@ _EMAIL_TO_RAW_MAPPING = {
|
|||||||
"download-url": "download_url",
|
"download-url": "download_url",
|
||||||
"dynamic": "dynamic",
|
"dynamic": "dynamic",
|
||||||
"home-page": "home_page",
|
"home-page": "home_page",
|
||||||
|
"import-name": "import_names",
|
||||||
|
"import-namespace": "import_namespaces",
|
||||||
"keywords": "keywords",
|
"keywords": "keywords",
|
||||||
"license": "license",
|
"license": "license",
|
||||||
"license-expression": "license_expression",
|
"license-expression": "license_expression",
|
||||||
@@ -283,6 +287,45 @@ _EMAIL_TO_RAW_MAPPING = {
|
|||||||
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
|
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
|
||||||
|
|
||||||
|
|
||||||
|
# This class is for writing RFC822 messages
|
||||||
|
class RFC822Policy(email.policy.EmailPolicy):
|
||||||
|
"""
|
||||||
|
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
|
||||||
|
implementation that handles multi-line values, and some nice defaults.
|
||||||
|
"""
|
||||||
|
|
||||||
|
utf8 = True
|
||||||
|
mangle_from_ = False
|
||||||
|
max_line_length = 0
|
||||||
|
|
||||||
|
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
|
||||||
|
size = len(name) + 2
|
||||||
|
value = value.replace("\n", "\n" + " " * size)
|
||||||
|
return (name, value)
|
||||||
|
|
||||||
|
|
||||||
|
# This class is for writing RFC822 messages
|
||||||
|
class RFC822Message(email.message.EmailMessage):
|
||||||
|
"""
|
||||||
|
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
|
||||||
|
our `RFC822Policy`, and it correctly writes unicode when being called
|
||||||
|
with `bytes()`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(policy=RFC822Policy())
|
||||||
|
|
||||||
|
def as_bytes(
|
||||||
|
self, unixfrom: bool = False, policy: email.policy.Policy | None = None
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Return the bytes representation of the message.
|
||||||
|
|
||||||
|
This handles unicode encoding.
|
||||||
|
"""
|
||||||
|
return self.as_string(unixfrom, policy=policy).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
||||||
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
|
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
|
||||||
|
|
||||||
@@ -310,10 +353,10 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|||||||
# We have to wrap parsed.keys() in a set, because in the case of multiple
|
# We have to wrap parsed.keys() in a set, because in the case of multiple
|
||||||
# values for a key (a list), the key will appear multiple times in the
|
# values for a key (a list), the key will appear multiple times in the
|
||||||
# list of keys, but we're avoiding that by using get_all().
|
# list of keys, but we're avoiding that by using get_all().
|
||||||
for name in frozenset(parsed.keys()):
|
for name_with_case in frozenset(parsed.keys()):
|
||||||
# Header names in RFC are case insensitive, so we'll normalize to all
|
# Header names in RFC are case insensitive, so we'll normalize to all
|
||||||
# lower case to make comparisons easier.
|
# lower case to make comparisons easier.
|
||||||
name = name.lower()
|
name = name_with_case.lower()
|
||||||
|
|
||||||
# We use get_all() here, even for fields that aren't multiple use,
|
# We use get_all() here, even for fields that aren't multiple use,
|
||||||
# because otherwise someone could have e.g. two Name fields, and we
|
# because otherwise someone could have e.g. two Name fields, and we
|
||||||
@@ -349,16 +392,16 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|||||||
# can be independently encoded, so we'll need to check each
|
# can be independently encoded, so we'll need to check each
|
||||||
# of them.
|
# of them.
|
||||||
chunks: list[tuple[bytes, str | None]] = []
|
chunks: list[tuple[bytes, str | None]] = []
|
||||||
for bin, encoding in email.header.decode_header(h):
|
for binary, _encoding in email.header.decode_header(h):
|
||||||
try:
|
try:
|
||||||
bin.decode("utf8", "strict")
|
binary.decode("utf8", "strict")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# Enable mojibake.
|
# Enable mojibake.
|
||||||
encoding = "latin1"
|
encoding = "latin1"
|
||||||
valid_encoding = False
|
valid_encoding = False
|
||||||
else:
|
else:
|
||||||
encoding = "utf8"
|
encoding = "utf8"
|
||||||
chunks.append((bin, encoding))
|
chunks.append((binary, encoding))
|
||||||
|
|
||||||
# Turn our chunks back into a Header object, then let that
|
# Turn our chunks back into a Header object, then let that
|
||||||
# Header object do the right thing to turn them into a
|
# Header object do the right thing to turn them into a
|
||||||
@@ -397,6 +440,11 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|||||||
# of unparsed stuff.
|
# of unparsed stuff.
|
||||||
if raw_name in _STRING_FIELDS and len(value) == 1:
|
if raw_name in _STRING_FIELDS and len(value) == 1:
|
||||||
raw[raw_name] = value[0]
|
raw[raw_name] = value[0]
|
||||||
|
# If this is import_names, we need to special case the empty field
|
||||||
|
# case, which converts to an empty list instead of None. We can't let
|
||||||
|
# the empty case slip through, as it will fail validation.
|
||||||
|
elif raw_name == "import_names" and value == [""]:
|
||||||
|
raw[raw_name] = []
|
||||||
# If this is one of our list of string fields, then we can just assign
|
# If this is one of our list of string fields, then we can just assign
|
||||||
# the value, since email *only* has strings, and our get_all() call
|
# the value, since email *only* has strings, and our get_all() call
|
||||||
# above ensures that this is a list.
|
# above ensures that this is a list.
|
||||||
@@ -424,7 +472,7 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
unparsed[name] = value
|
unparsed[name] = value
|
||||||
# Nothing that we've done has managed to parse this, so it'll just
|
# Nothing that we've done has managed to parse this, so it'll just
|
||||||
# throw it in our unparseable data and move on.
|
# throw it in our unparsable data and move on.
|
||||||
else:
|
else:
|
||||||
unparsed[name] = value
|
unparsed[name] = value
|
||||||
|
|
||||||
@@ -441,9 +489,9 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|||||||
else:
|
else:
|
||||||
if payload:
|
if payload:
|
||||||
# Check to see if we've already got a description, if so then both
|
# Check to see if we've already got a description, if so then both
|
||||||
# it, and this body move to unparseable.
|
# it, and this body move to unparsable.
|
||||||
if "description" in raw:
|
if "description" in raw:
|
||||||
description_header = cast(str, raw.pop("description"))
|
description_header = cast("str", raw.pop("description"))
|
||||||
unparsed.setdefault("description", []).extend(
|
unparsed.setdefault("description", []).extend(
|
||||||
[description_header, payload]
|
[description_header, payload]
|
||||||
)
|
)
|
||||||
@@ -456,15 +504,15 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|||||||
# literal key names, but we're computing our key names on purpose, but the
|
# literal key names, but we're computing our key names on purpose, but the
|
||||||
# way this function is implemented, our `TypedDict` can only have valid key
|
# way this function is implemented, our `TypedDict` can only have valid key
|
||||||
# names.
|
# names.
|
||||||
return cast(RawMetadata, raw), unparsed
|
return cast("RawMetadata", raw), unparsed
|
||||||
|
|
||||||
|
|
||||||
_NOT_FOUND = object()
|
_NOT_FOUND = object()
|
||||||
|
|
||||||
|
|
||||||
# Keep the two values in sync.
|
# Keep the two values in sync.
|
||||||
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
|
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||||
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
|
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||||
|
|
||||||
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
|
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
|
||||||
|
|
||||||
@@ -519,7 +567,7 @@ class _Validator(Generic[T]):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return cast(T, value)
|
return cast("T", value)
|
||||||
|
|
||||||
def _invalid_metadata(
|
def _invalid_metadata(
|
||||||
self, msg: str, cause: Exception | None = None
|
self, msg: str, cause: Exception | None = None
|
||||||
@@ -534,7 +582,7 @@ class _Validator(Generic[T]):
|
|||||||
# Implicitly makes Metadata-Version required.
|
# Implicitly makes Metadata-Version required.
|
||||||
if value not in _VALID_METADATA_VERSIONS:
|
if value not in _VALID_METADATA_VERSIONS:
|
||||||
raise self._invalid_metadata(f"{value!r} is not a valid metadata version")
|
raise self._invalid_metadata(f"{value!r} is not a valid metadata version")
|
||||||
return cast(_MetadataVersion, value)
|
return cast("_MetadataVersion", value)
|
||||||
|
|
||||||
def _process_name(self, value: str) -> str:
|
def _process_name(self, value: str) -> str:
|
||||||
if not value:
|
if not value:
|
||||||
@@ -585,7 +633,7 @@ class _Validator(Generic[T]):
|
|||||||
charset = parameters.get("charset", "UTF-8")
|
charset = parameters.get("charset", "UTF-8")
|
||||||
if charset != "UTF-8":
|
if charset != "UTF-8":
|
||||||
raise self._invalid_metadata(
|
raise self._invalid_metadata(
|
||||||
f"{{field}} can only specify the UTF-8 charset, not {list(charset)}"
|
f"{{field}} can only specify the UTF-8 charset, not {charset!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
markdown_variants = {"GFM", "CommonMark"}
|
markdown_variants = {"GFM", "CommonMark"}
|
||||||
@@ -647,9 +695,7 @@ class _Validator(Generic[T]):
|
|||||||
else:
|
else:
|
||||||
return reqs
|
return reqs
|
||||||
|
|
||||||
def _process_license_expression(
|
def _process_license_expression(self, value: str) -> NormalizedLicenseExpression:
|
||||||
self, value: str
|
|
||||||
) -> NormalizedLicenseExpression | None:
|
|
||||||
try:
|
try:
|
||||||
return licenses.canonicalize_license_expression(value)
|
return licenses.canonicalize_license_expression(value)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -683,6 +729,30 @@ class _Validator(Generic[T]):
|
|||||||
paths.append(path)
|
paths.append(path)
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
def _process_import_names(self, value: list[str]) -> list[str]:
|
||||||
|
for import_name in value:
|
||||||
|
name, semicolon, private = import_name.partition(";")
|
||||||
|
name = name.rstrip()
|
||||||
|
for identifier in name.split("."):
|
||||||
|
if not identifier.isidentifier():
|
||||||
|
raise self._invalid_metadata(
|
||||||
|
f"{name!r} is invalid for {{field}}; "
|
||||||
|
f"{identifier!r} is not a valid identifier"
|
||||||
|
)
|
||||||
|
elif keyword.iskeyword(identifier):
|
||||||
|
raise self._invalid_metadata(
|
||||||
|
f"{name!r} is invalid for {{field}}; "
|
||||||
|
f"{identifier!r} is a keyword"
|
||||||
|
)
|
||||||
|
if semicolon and private.lstrip() != "private":
|
||||||
|
raise self._invalid_metadata(
|
||||||
|
f"{import_name!r} is invalid for {{field}}; "
|
||||||
|
"the only valid option is 'private'"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
_process_import_namespaces = _process_import_names
|
||||||
|
|
||||||
|
|
||||||
class Metadata:
|
class Metadata:
|
||||||
"""Representation of distribution metadata.
|
"""Representation of distribution metadata.
|
||||||
@@ -706,13 +776,11 @@ class Metadata:
|
|||||||
ins._raw = data.copy() # Mutations occur due to caching enriched values.
|
ins._raw = data.copy() # Mutations occur due to caching enriched values.
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
exceptions: list[Exception] = []
|
collector = _ErrorCollector()
|
||||||
try:
|
metadata_version = None
|
||||||
|
with collector.collect(InvalidMetadata):
|
||||||
metadata_version = ins.metadata_version
|
metadata_version = ins.metadata_version
|
||||||
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
|
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
|
||||||
except InvalidMetadata as metadata_version_exc:
|
|
||||||
exceptions.append(metadata_version_exc)
|
|
||||||
metadata_version = None
|
|
||||||
|
|
||||||
# Make sure to check for the fields that are present, the required
|
# Make sure to check for the fields that are present, the required
|
||||||
# fields (so their absence can be reported).
|
# fields (so their absence can be reported).
|
||||||
@@ -729,7 +797,7 @@ class Metadata:
|
|||||||
field_metadata_version = cls.__dict__[key].added
|
field_metadata_version = cls.__dict__[key].added
|
||||||
except KeyError:
|
except KeyError:
|
||||||
exc = InvalidMetadata(key, f"unrecognized field: {key!r}")
|
exc = InvalidMetadata(key, f"unrecognized field: {key!r}")
|
||||||
exceptions.append(exc)
|
collector.error(exc)
|
||||||
continue
|
continue
|
||||||
field_age = _VALID_METADATA_VERSIONS.index(
|
field_age = _VALID_METADATA_VERSIONS.index(
|
||||||
field_metadata_version
|
field_metadata_version
|
||||||
@@ -741,14 +809,13 @@ class Metadata:
|
|||||||
f"{field} introduced in metadata version "
|
f"{field} introduced in metadata version "
|
||||||
f"{field_metadata_version}, not {metadata_version}",
|
f"{field_metadata_version}, not {metadata_version}",
|
||||||
)
|
)
|
||||||
exceptions.append(exc)
|
collector.error(exc)
|
||||||
continue
|
continue
|
||||||
getattr(ins, key)
|
getattr(ins, key)
|
||||||
except InvalidMetadata as exc:
|
except InvalidMetadata as exc:
|
||||||
exceptions.append(exc)
|
collector.error(exc)
|
||||||
|
|
||||||
if exceptions:
|
collector.finalize("invalid metadata")
|
||||||
raise ExceptionGroup("invalid metadata", exceptions)
|
|
||||||
|
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
@@ -762,16 +829,13 @@ class Metadata:
|
|||||||
raw, unparsed = parse_email(data)
|
raw, unparsed = parse_email(data)
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
exceptions: list[Exception] = []
|
with _ErrorCollector().on_exit("unparsed") as collector:
|
||||||
for unparsed_key in unparsed:
|
for unparsed_key in unparsed:
|
||||||
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
||||||
message = f"{unparsed_key!r} has invalid data"
|
message = f"{unparsed_key!r} has invalid data"
|
||||||
else:
|
else:
|
||||||
message = f"unrecognized field: {unparsed_key!r}"
|
message = f"unrecognized field: {unparsed_key!r}"
|
||||||
exceptions.append(InvalidMetadata(unparsed_key, message))
|
collector.error(InvalidMetadata(unparsed_key, message))
|
||||||
|
|
||||||
if exceptions:
|
|
||||||
raise ExceptionGroup("unparsed", exceptions)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return cls.from_raw(raw, validate=validate)
|
return cls.from_raw(raw, validate=validate)
|
||||||
@@ -854,9 +918,47 @@ class Metadata:
|
|||||||
""":external:ref:`core-metadata-provides-dist`"""
|
""":external:ref:`core-metadata-provides-dist`"""
|
||||||
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
|
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
|
||||||
""":external:ref:`core-metadata-obsoletes-dist`"""
|
""":external:ref:`core-metadata-obsoletes-dist`"""
|
||||||
|
import_names: _Validator[list[str] | None] = _Validator(added="2.5")
|
||||||
|
""":external:ref:`core-metadata-import-name`"""
|
||||||
|
import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5")
|
||||||
|
""":external:ref:`core-metadata-import-namespace`"""
|
||||||
requires: _Validator[list[str] | None] = _Validator(added="1.1")
|
requires: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||||
"""``Requires`` (deprecated)"""
|
"""``Requires`` (deprecated)"""
|
||||||
provides: _Validator[list[str] | None] = _Validator(added="1.1")
|
provides: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||||
"""``Provides`` (deprecated)"""
|
"""``Provides`` (deprecated)"""
|
||||||
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
|
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||||
"""``Obsoletes`` (deprecated)"""
|
"""``Obsoletes`` (deprecated)"""
|
||||||
|
|
||||||
|
def as_rfc822(self) -> RFC822Message:
|
||||||
|
"""
|
||||||
|
Return an RFC822 message with the metadata.
|
||||||
|
"""
|
||||||
|
message = RFC822Message()
|
||||||
|
self._write_metadata(message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def _write_metadata(self, message: RFC822Message) -> None:
|
||||||
|
"""
|
||||||
|
Return an RFC822 message with the metadata.
|
||||||
|
"""
|
||||||
|
for name, validator in self.__class__.__dict__.items():
|
||||||
|
if isinstance(validator, _Validator) and name != "description":
|
||||||
|
value = getattr(self, name)
|
||||||
|
email_name = _RAW_TO_EMAIL_MAPPING[name]
|
||||||
|
if value is not None:
|
||||||
|
if email_name == "project-url":
|
||||||
|
for label, url in value.items():
|
||||||
|
message[email_name] = f"{label}, {url}"
|
||||||
|
elif email_name == "keywords":
|
||||||
|
message[email_name] = ",".join(value)
|
||||||
|
elif email_name == "import-name" and value == []:
|
||||||
|
message[email_name] = ""
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
message[email_name] = str(item)
|
||||||
|
else:
|
||||||
|
message[email_name] = str(value)
|
||||||
|
|
||||||
|
# The description is a special case because it is in the body of the message.
|
||||||
|
if self.description is not None:
|
||||||
|
message.set_payload(self.description)
|
||||||
|
|||||||
@@ -0,0 +1,905 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Protocol,
|
||||||
|
TypeVar,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from .markers import Environment, Marker, default_environment
|
||||||
|
from .specifiers import SpecifierSet
|
||||||
|
from .tags import create_compatible_tags_selector, sys_tags
|
||||||
|
from .utils import (
|
||||||
|
NormalizedName,
|
||||||
|
is_normalized_name,
|
||||||
|
parse_sdist_filename,
|
||||||
|
parse_wheel_filename,
|
||||||
|
)
|
||||||
|
from .version import Version
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from collections.abc import Collection, Iterator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from .tags import Tag
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Package",
|
||||||
|
"PackageArchive",
|
||||||
|
"PackageDirectory",
|
||||||
|
"PackageSdist",
|
||||||
|
"PackageVcs",
|
||||||
|
"PackageWheel",
|
||||||
|
"Pylock",
|
||||||
|
"PylockUnsupportedVersionError",
|
||||||
|
"PylockValidationError",
|
||||||
|
"is_valid_pylock_path",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return __all__
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
_T2 = TypeVar("_T2")
|
||||||
|
|
||||||
|
|
||||||
|
class _FromMappingProtocol(Protocol): # pragma: no cover
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
|
||||||
|
|
||||||
|
|
||||||
|
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
|
||||||
|
|
||||||
|
|
||||||
|
_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_pylock_path(path: Path) -> bool:
|
||||||
|
"""Check if the given path is a valid pylock file path."""
|
||||||
|
return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_key(key: str) -> str:
|
||||||
|
return key.replace("_", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
|
||||||
|
if isinstance(value, (Version, Marker, SpecifierSet)):
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, Sequence) and key == "environments":
|
||||||
|
return [str(v) for v in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
_toml_key(key): _toml_value(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 PylockValidationError(
|
||||||
|
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 _PylockRequiredKeyError(key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sequence(
|
||||||
|
d: Mapping[str, Any], expected_item_type: type[_T], key: str
|
||||||
|
) -> Sequence[_T] | None:
|
||||||
|
"""Get a list value from the dictionary and verify it's the expected items type."""
|
||||||
|
if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
|
||||||
|
return None
|
||||||
|
if isinstance(value, (str, bytes)):
|
||||||
|
# special case: str and bytes are Sequences, but we want to reject it
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Unexpected type {type(value).__name__} (expected Sequence)",
|
||||||
|
context=key,
|
||||||
|
)
|
||||||
|
for i, item in enumerate(value):
|
||||||
|
if not isinstance(item, expected_item_type):
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Unexpected type {type(item).__name__} "
|
||||||
|
f"(expected {expected_item_type.__name__})",
|
||||||
|
context=f"{key}[{i}]",
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_as(
|
||||||
|
d: Mapping[str, Any],
|
||||||
|
expected_type: type[_T],
|
||||||
|
target_type: Callable[[_T], _T2],
|
||||||
|
key: str,
|
||||||
|
) -> _T2 | None:
|
||||||
|
"""Get a value from the dictionary, verify it's the expected type,
|
||||||
|
and convert to the target type.
|
||||||
|
|
||||||
|
This assumes the target_type constructor accepts the value.
|
||||||
|
"""
|
||||||
|
if (value := _get(d, expected_type, key)) is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return target_type(value)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=key) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_as(
|
||||||
|
d: Mapping[str, Any],
|
||||||
|
expected_type: type[_T],
|
||||||
|
target_type: Callable[[_T], _T2],
|
||||||
|
key: str,
|
||||||
|
) -> _T2:
|
||||||
|
"""Get a required value from the dict, verify it's the expected type,
|
||||||
|
and convert to the target type."""
|
||||||
|
if (value := _get_as(d, expected_type, target_type, key)) is None:
|
||||||
|
raise _PylockRequiredKeyError(key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sequence_as(
|
||||||
|
d: Mapping[str, Any],
|
||||||
|
expected_item_type: type[_T],
|
||||||
|
target_item_type: Callable[[_T], _T2],
|
||||||
|
key: str,
|
||||||
|
) -> list[_T2] | None:
|
||||||
|
"""Get list value from dictionary and verify expected items type."""
|
||||||
|
if (value := _get_sequence(d, expected_item_type, key)) is None:
|
||||||
|
return None
|
||||||
|
result = []
|
||||||
|
try:
|
||||||
|
for item in value:
|
||||||
|
typed_item = target_item_type(item)
|
||||||
|
result.append(typed_item)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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 PylockValidationError(e, context=key) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sequence_of_objects(
|
||||||
|
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
|
||||||
|
) -> list[_FromMappingProtocolT] | None:
|
||||||
|
"""Get a list value from the dictionary and convert its items to a dataclass."""
|
||||||
|
if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||||
|
return None
|
||||||
|
result: list[_FromMappingProtocolT] = []
|
||||||
|
try:
|
||||||
|
for item in value:
|
||||||
|
typed_item = target_item_type._from_dict(item)
|
||||||
|
result.append(typed_item)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_sequence_of_objects(
|
||||||
|
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
|
||||||
|
) -> Sequence[_FromMappingProtocolT]:
|
||||||
|
"""Get a required list value from the dictionary and convert its items to a
|
||||||
|
dataclass."""
|
||||||
|
if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
|
||||||
|
raise _PylockRequiredKeyError(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_normalized_name(name: str) -> NormalizedName:
|
||||||
|
"""Validate that a string is a NormalizedName."""
|
||||||
|
if not is_normalized_name(name):
|
||||||
|
raise PylockValidationError(f"Name {name!r} is not normalized")
|
||||||
|
return NormalizedName(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_path_url(path: str | None, url: str | None) -> None:
|
||||||
|
if not path and not url:
|
||||||
|
raise PylockValidationError("path or url must be provided")
|
||||||
|
|
||||||
|
|
||||||
|
def _path_name(path: str | None) -> str | None:
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
# If the path is relative it MAY use POSIX-style path separators explicitly
|
||||||
|
# for portability
|
||||||
|
if "/" in path:
|
||||||
|
return path.rsplit("/", 1)[-1]
|
||||||
|
elif "\\" in path:
|
||||||
|
return path.rsplit("\\", 1)[-1]
|
||||||
|
else:
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _url_name(url: str | None) -> str | None:
|
||||||
|
if not url:
|
||||||
|
return None
|
||||||
|
url_path = urlparse(url).path
|
||||||
|
return url_path.rsplit("/", 1)[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||||
|
if not hashes:
|
||||||
|
raise PylockValidationError("At least one hash must be provided")
|
||||||
|
if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
|
||||||
|
raise PylockValidationError("Hash values must be strings")
|
||||||
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
class PylockValidationError(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, PylockValidationError):
|
||||||
|
if cause.context:
|
||||||
|
self.context = (
|
||||||
|
f"{context}.{cause.context}" if context else cause.context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.context = context
|
||||||
|
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 _PylockRequiredKeyError(PylockValidationError):
|
||||||
|
def __init__(self, key: str) -> None:
|
||||||
|
super().__init__("Missing required value", context=key)
|
||||||
|
|
||||||
|
|
||||||
|
class PylockUnsupportedVersionError(PylockValidationError):
|
||||||
|
"""Raised when encountering an unsupported `lock_version`."""
|
||||||
|
|
||||||
|
|
||||||
|
class PylockSelectError(Exception):
|
||||||
|
"""Base exception for errors raised by :meth:`Pylock.select`."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageVcs:
|
||||||
|
type: str
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
requested_revision: str | None = None
|
||||||
|
commit_id: str # type: ignore[misc]
|
||||||
|
subdirectory: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
type: str,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
requested_revision: str | None = None,
|
||||||
|
commit_id: str,
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "type", type)
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "requested_revision", requested_revision)
|
||||||
|
object.__setattr__(self, "commit_id", commit_id)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_vcs = cls(
|
||||||
|
type=_get_required(d, str, "type"),
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
requested_revision=_get(d, str, "requested-revision"),
|
||||||
|
commit_id=_get_required(d, str, "commit-id"),
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
_validate_path_url(package_vcs.path, package_vcs.url)
|
||||||
|
return package_vcs
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageDirectory:
|
||||||
|
path: str
|
||||||
|
editable: bool | None = None
|
||||||
|
subdirectory: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
editable: bool | None = None,
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "editable", editable)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
return cls(
|
||||||
|
path=_get_required(d, str, "path"),
|
||||||
|
editable=_get(d, bool, "editable"),
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageArchive:
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
upload_time: datetime | None = None
|
||||||
|
hashes: Mapping[str, str] # type: ignore[misc]
|
||||||
|
subdirectory: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
upload_time: datetime | None = None,
|
||||||
|
hashes: Mapping[str, str],
|
||||||
|
subdirectory: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "size", size)
|
||||||
|
object.__setattr__(self, "upload_time", upload_time)
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
object.__setattr__(self, "subdirectory", subdirectory)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_archive = cls(
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
size=_get(d, int, "size"),
|
||||||
|
upload_time=_get(d, datetime, "upload-time"),
|
||||||
|
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||||
|
subdirectory=_get(d, str, "subdirectory"),
|
||||||
|
)
|
||||||
|
_validate_path_url(package_archive.path, package_archive.url)
|
||||||
|
return package_archive
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageSdist:
|
||||||
|
name: str | None = None
|
||||||
|
upload_time: datetime | None = None
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
hashes: Mapping[str, str] # type: ignore[misc]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
upload_time: datetime | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
hashes: Mapping[str, str],
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "upload_time", upload_time)
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "size", size)
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_sdist = cls(
|
||||||
|
name=_get(d, str, "name"),
|
||||||
|
upload_time=_get(d, datetime, "upload-time"),
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
size=_get(d, int, "size"),
|
||||||
|
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
_validate_path_url(package_sdist.path, package_sdist.url)
|
||||||
|
return package_sdist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
"""Get the filename of the sdist."""
|
||||||
|
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||||
|
if not filename:
|
||||||
|
raise PylockValidationError("Cannot determine sdist filename")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class PackageWheel:
|
||||||
|
name: str | None = None
|
||||||
|
upload_time: datetime | None = None
|
||||||
|
url: str | None = None
|
||||||
|
path: str | None = None
|
||||||
|
size: int | None = None
|
||||||
|
hashes: Mapping[str, str] # type: ignore[misc]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
upload_time: datetime | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
path: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
hashes: Mapping[str, str],
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "upload_time", upload_time)
|
||||||
|
object.__setattr__(self, "url", url)
|
||||||
|
object.__setattr__(self, "path", path)
|
||||||
|
object.__setattr__(self, "size", size)
|
||||||
|
object.__setattr__(self, "hashes", hashes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package_wheel = cls(
|
||||||
|
name=_get(d, str, "name"),
|
||||||
|
upload_time=_get(d, datetime, "upload-time"),
|
||||||
|
url=_get(d, str, "url"),
|
||||||
|
path=_get(d, str, "path"),
|
||||||
|
size=_get(d, int, "size"),
|
||||||
|
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
_validate_path_url(package_wheel.path, package_wheel.url)
|
||||||
|
return package_wheel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
"""Get the filename of the wheel."""
|
||||||
|
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||||
|
if not filename:
|
||||||
|
raise PylockValidationError("Cannot determine wheel filename")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class Package:
|
||||||
|
name: NormalizedName
|
||||||
|
version: Version | None = None
|
||||||
|
marker: Marker | None = None
|
||||||
|
requires_python: SpecifierSet | None = None
|
||||||
|
dependencies: Sequence[Mapping[str, Any]] | None = None
|
||||||
|
vcs: PackageVcs | None = None
|
||||||
|
directory: PackageDirectory | None = None
|
||||||
|
archive: PackageArchive | None = None
|
||||||
|
index: str | None = None
|
||||||
|
sdist: PackageSdist | None = None
|
||||||
|
wheels: Sequence[PackageWheel] | None = None
|
||||||
|
attestation_identities: Sequence[Mapping[str, Any]] | None = None
|
||||||
|
tool: Mapping[str, Any] | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name: NormalizedName,
|
||||||
|
version: Version | None = None,
|
||||||
|
marker: Marker | None = None,
|
||||||
|
requires_python: SpecifierSet | None = None,
|
||||||
|
dependencies: Sequence[Mapping[str, Any]] | None = None,
|
||||||
|
vcs: PackageVcs | None = None,
|
||||||
|
directory: PackageDirectory | None = None,
|
||||||
|
archive: PackageArchive | None = None,
|
||||||
|
index: str | None = None,
|
||||||
|
sdist: PackageSdist | None = None,
|
||||||
|
wheels: Sequence[PackageWheel] | None = None,
|
||||||
|
attestation_identities: Sequence[Mapping[str, Any]] | None = None,
|
||||||
|
tool: Mapping[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "version", version)
|
||||||
|
object.__setattr__(self, "marker", marker)
|
||||||
|
object.__setattr__(self, "requires_python", requires_python)
|
||||||
|
object.__setattr__(self, "dependencies", dependencies)
|
||||||
|
object.__setattr__(self, "vcs", vcs)
|
||||||
|
object.__setattr__(self, "directory", directory)
|
||||||
|
object.__setattr__(self, "archive", archive)
|
||||||
|
object.__setattr__(self, "index", index)
|
||||||
|
object.__setattr__(self, "sdist", sdist)
|
||||||
|
object.__setattr__(self, "wheels", wheels)
|
||||||
|
object.__setattr__(self, "attestation_identities", attestation_identities)
|
||||||
|
object.__setattr__(self, "tool", tool)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
package = cls(
|
||||||
|
name=_get_required_as(d, str, _validate_normalized_name, "name"),
|
||||||
|
version=_get_as(d, str, Version, "version"),
|
||||||
|
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
|
||||||
|
dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
|
||||||
|
marker=_get_as(d, str, Marker, "marker"),
|
||||||
|
vcs=_get_object(d, PackageVcs, "vcs"),
|
||||||
|
directory=_get_object(d, PackageDirectory, "directory"),
|
||||||
|
archive=_get_object(d, PackageArchive, "archive"),
|
||||||
|
index=_get(d, str, "index"),
|
||||||
|
sdist=_get_object(d, PackageSdist, "sdist"),
|
||||||
|
wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
|
||||||
|
attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
|
||||||
|
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
distributions = bool(package.sdist) + len(package.wheels or [])
|
||||||
|
direct_urls = (
|
||||||
|
bool(package.vcs) + bool(package.directory) + bool(package.archive)
|
||||||
|
)
|
||||||
|
if distributions > 0 and direct_urls > 0:
|
||||||
|
raise PylockValidationError(
|
||||||
|
"None of vcs, directory, archive must be set if sdist or wheels are set"
|
||||||
|
)
|
||||||
|
if distributions == 0 and direct_urls != 1:
|
||||||
|
raise PylockValidationError(
|
||||||
|
"Exactly one of vcs, directory, archive must be set "
|
||||||
|
"if sdist and wheels are not set"
|
||||||
|
)
|
||||||
|
for i, wheel in enumerate(package.wheels or []):
|
||||||
|
try:
|
||||||
|
(name, version, _, _) = parse_wheel_filename(wheel.filename)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Invalid wheel filename {wheel.filename!r}",
|
||||||
|
context=f"wheels[{i}]",
|
||||||
|
) from e
|
||||||
|
if name != package.name:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Name in {wheel.filename!r} is not consistent with "
|
||||||
|
f"package name {package.name!r}",
|
||||||
|
context=f"wheels[{i}]",
|
||||||
|
)
|
||||||
|
if package.version and version != package.version:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Version in {wheel.filename!r} is not consistent with "
|
||||||
|
f"package version {str(package.version)!r}",
|
||||||
|
context=f"wheels[{i}]",
|
||||||
|
)
|
||||||
|
if package.sdist:
|
||||||
|
try:
|
||||||
|
name, version = parse_sdist_filename(package.sdist.filename)
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Invalid sdist filename {package.sdist.filename!r}",
|
||||||
|
context="sdist",
|
||||||
|
) from e
|
||||||
|
if name != package.name:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Name in {package.sdist.filename!r} is not consistent with "
|
||||||
|
f"package name {package.name!r}",
|
||||||
|
context="sdist",
|
||||||
|
)
|
||||||
|
if package.version and version != package.version:
|
||||||
|
raise PylockValidationError(
|
||||||
|
f"Version in {package.sdist.filename!r} is not consistent with "
|
||||||
|
f"package version {str(package.version)!r}",
|
||||||
|
context="sdist",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
for i, attestation_identity in enumerate( # noqa: B007
|
||||||
|
package.attestation_identities or []
|
||||||
|
):
|
||||||
|
_get_required(attestation_identity, str, "kind")
|
||||||
|
except Exception as e:
|
||||||
|
raise PylockValidationError(
|
||||||
|
e, context=f"attestation-identities[{i}]"
|
||||||
|
) from e
|
||||||
|
return package
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_direct(self) -> bool:
|
||||||
|
return not (self.sdist or self.wheels)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, init=False)
|
||||||
|
class Pylock:
|
||||||
|
"""A class representing a pylock file."""
|
||||||
|
|
||||||
|
lock_version: Version
|
||||||
|
environments: Sequence[Marker] | None = None
|
||||||
|
requires_python: SpecifierSet | None = None
|
||||||
|
extras: Sequence[NormalizedName] | None = None
|
||||||
|
dependency_groups: Sequence[str] | None = None
|
||||||
|
default_groups: Sequence[str] | None = None
|
||||||
|
created_by: str # type: ignore[misc]
|
||||||
|
packages: Sequence[Package] # type: ignore[misc]
|
||||||
|
tool: Mapping[str, Any] | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
lock_version: Version,
|
||||||
|
environments: Sequence[Marker] | None = None,
|
||||||
|
requires_python: SpecifierSet | None = None,
|
||||||
|
extras: Sequence[NormalizedName] | None = None,
|
||||||
|
dependency_groups: Sequence[str] | None = None,
|
||||||
|
default_groups: Sequence[str] | None = None,
|
||||||
|
created_by: str,
|
||||||
|
packages: Sequence[Package],
|
||||||
|
tool: Mapping[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
# In Python 3.10+ make dataclass kw_only=True and remove __init__
|
||||||
|
object.__setattr__(self, "lock_version", lock_version)
|
||||||
|
object.__setattr__(self, "environments", environments)
|
||||||
|
object.__setattr__(self, "requires_python", requires_python)
|
||||||
|
object.__setattr__(self, "extras", extras)
|
||||||
|
object.__setattr__(self, "dependency_groups", dependency_groups)
|
||||||
|
object.__setattr__(self, "default_groups", default_groups)
|
||||||
|
object.__setattr__(self, "created_by", created_by)
|
||||||
|
object.__setattr__(self, "packages", packages)
|
||||||
|
object.__setattr__(self, "tool", tool)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||||
|
pylock = cls(
|
||||||
|
lock_version=_get_required_as(d, str, Version, "lock-version"),
|
||||||
|
environments=_get_sequence_as(d, str, Marker, "environments"),
|
||||||
|
extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
|
||||||
|
dependency_groups=_get_sequence(d, str, "dependency-groups"),
|
||||||
|
default_groups=_get_sequence(d, str, "default-groups"),
|
||||||
|
created_by=_get_required(d, str, "created-by"),
|
||||||
|
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
|
||||||
|
packages=_get_required_sequence_of_objects(d, Package, "packages"),
|
||||||
|
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
|
||||||
|
)
|
||||||
|
if not Version("1") <= pylock.lock_version < Version("2"):
|
||||||
|
raise PylockUnsupportedVersionError(
|
||||||
|
f"pylock version {pylock.lock_version} is not supported"
|
||||||
|
)
|
||||||
|
if pylock.lock_version > Version("1.0"):
|
||||||
|
_logger.warning(
|
||||||
|
"pylock minor version %s is not supported", pylock.lock_version
|
||||||
|
)
|
||||||
|
return pylock
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
|
||||||
|
"""Create and validate a Pylock instance from a TOML dictionary.
|
||||||
|
|
||||||
|
Raises :class:`PylockValidationError` if the input data is not
|
||||||
|
spec-compliant.
|
||||||
|
"""
|
||||||
|
return cls._from_dict(d)
|
||||||
|
|
||||||
|
def to_dict(self) -> Mapping[str, Any]:
|
||||||
|
"""Convert the Pylock instance to a TOML dictionary."""
|
||||||
|
return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
"""Validate the Pylock instance against the specification.
|
||||||
|
|
||||||
|
Raises :class:`PylockValidationError` otherwise."""
|
||||||
|
self.from_dict(self.to_dict())
|
||||||
|
|
||||||
|
def select(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
environment: Environment | None = None,
|
||||||
|
tags: Sequence[Tag] | None = None,
|
||||||
|
extras: Collection[str] | None = None,
|
||||||
|
dependency_groups: Collection[str] | None = None,
|
||||||
|
) -> Iterator[
|
||||||
|
tuple[
|
||||||
|
Package,
|
||||||
|
PackageVcs
|
||||||
|
| PackageDirectory
|
||||||
|
| PackageArchive
|
||||||
|
| PackageWheel
|
||||||
|
| PackageSdist,
|
||||||
|
]
|
||||||
|
]:
|
||||||
|
"""Select what to install from the lock file.
|
||||||
|
|
||||||
|
The *environment* and *tags* parameters represent the environment being
|
||||||
|
selected for. If unspecified, ``packaging.markers.default_environment()`` and
|
||||||
|
``packaging.tags.sys_tags()`` are used.
|
||||||
|
|
||||||
|
The *extras* parameter represents the extras to install.
|
||||||
|
|
||||||
|
The *dependency_groups* parameter represents the groups to install. If
|
||||||
|
unspecified, the default groups are used.
|
||||||
|
|
||||||
|
This method must be used on valid Pylock instances (i.e. one obtained
|
||||||
|
from :meth:`Pylock.from_dict` or if constructed manually, after calling
|
||||||
|
:meth:`Pylock.validate`).
|
||||||
|
"""
|
||||||
|
compatible_tags_selector = create_compatible_tags_selector(tags or sys_tags())
|
||||||
|
|
||||||
|
# #. Gather the extras and dependency groups to install and set ``extras`` and
|
||||||
|
# ``dependency_groups`` for marker evaluation, respectively.
|
||||||
|
#
|
||||||
|
# #. ``extras`` SHOULD be set to the empty set by default.
|
||||||
|
# #. ``dependency_groups`` SHOULD be the set created from
|
||||||
|
# :ref:`pylock-default-groups` by default.
|
||||||
|
env = cast(
|
||||||
|
"dict[str, str | frozenset[str]]",
|
||||||
|
dict(
|
||||||
|
environment or {}, # Marker.evaluate will fill-up
|
||||||
|
extras=frozenset(extras or []),
|
||||||
|
dependency_groups=frozenset(
|
||||||
|
(self.default_groups or [])
|
||||||
|
if dependency_groups is None # to allow selecting no group
|
||||||
|
else dependency_groups
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
env_python_full_version = (
|
||||||
|
environment["python_full_version"]
|
||||||
|
if environment
|
||||||
|
else default_environment()["python_full_version"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
|
||||||
|
# supported; an error or warning MUST be raised as appropriate.
|
||||||
|
# Covered by lock.validate() which is a precondition for this method.
|
||||||
|
|
||||||
|
# #. If :ref:`pylock-requires-python` is specified, check that the environment
|
||||||
|
# being installed for meets the requirement; an error MUST be raised if it is
|
||||||
|
# not met.
|
||||||
|
if self.requires_python and not self.requires_python.contains(
|
||||||
|
env_python_full_version,
|
||||||
|
):
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"python_full_version {env_python_full_version!r} "
|
||||||
|
f"in provided environment does not satisfy the Python version "
|
||||||
|
f"requirement {str(self.requires_python)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. If :ref:`pylock-environments` is specified, check that at least one of the
|
||||||
|
# environment marker expressions is satisfied; an error MUST be raised if no
|
||||||
|
# expression is satisfied.
|
||||||
|
if self.environments:
|
||||||
|
for env_marker in self.environments:
|
||||||
|
if env_marker.evaluate(
|
||||||
|
cast("dict[str, str]", environment or {}), context="requirement"
|
||||||
|
):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise PylockSelectError(
|
||||||
|
"Provided environment does not satisfy any of the "
|
||||||
|
"environments specified in the lock file"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. For each package listed in :ref:`pylock-packages`:
|
||||||
|
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
|
||||||
|
for package_index, package in enumerate(self.packages):
|
||||||
|
# #. If :ref:`pylock-packages-marker` is specified, check if it is
|
||||||
|
# satisfied;if it isn't, skip to the next package.
|
||||||
|
if package.marker and not package.marker.evaluate(env, context="lock_file"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
|
||||||
|
# satisfied; an error MUST be raised if it isn't.
|
||||||
|
if package.requires_python and not package.requires_python.contains(
|
||||||
|
env_python_full_version,
|
||||||
|
):
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"python_full_version {env_python_full_version!r} "
|
||||||
|
f"in provided environment does not satisfy the Python version "
|
||||||
|
f"requirement {str(package.requires_python)!r} for package "
|
||||||
|
f"{package.name!r} at packages[{package_index}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Check that no other conflicting instance of the package has been slated
|
||||||
|
# to be installed; an error about the ambiguity MUST be raised otherwise.
|
||||||
|
if package.name in selected_packages_by_name:
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"Multiple packages with the name {package.name!r} are "
|
||||||
|
f"selected at packages[{package_index}] and "
|
||||||
|
f"packages[{selected_packages_by_name[package.name][0]}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# #. Check that the source of the package is specified appropriately (i.e.
|
||||||
|
# there are no conflicting sources in the package entry);
|
||||||
|
# an error MUST be raised if any issues are found.
|
||||||
|
# Covered by lock.validate() which is a precondition for this method.
|
||||||
|
|
||||||
|
# #. Add the package to the set of packages to install.
|
||||||
|
selected_packages_by_name[package.name] = (package_index, package)
|
||||||
|
|
||||||
|
# #. For each package to be installed:
|
||||||
|
for package_index, package in selected_packages_by_name.values():
|
||||||
|
# - If :ref:`pylock-packages-vcs` is set:
|
||||||
|
if package.vcs is not None:
|
||||||
|
yield package, package.vcs
|
||||||
|
|
||||||
|
# - Else if :ref:`pylock-packages-directory` is set:
|
||||||
|
elif package.directory is not None:
|
||||||
|
yield package, package.directory
|
||||||
|
|
||||||
|
# - Else if :ref:`pylock-packages-archive` is set:
|
||||||
|
elif package.archive is not None:
|
||||||
|
yield package, package.archive
|
||||||
|
|
||||||
|
# - Else if there are entries for :ref:`pylock-packages-wheels`:
|
||||||
|
elif package.wheels:
|
||||||
|
# #. Look for the appropriate wheel file based on
|
||||||
|
# :ref:`pylock-packages-wheels-name`; if one is not found then move
|
||||||
|
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
|
||||||
|
# a lack of source for the project.
|
||||||
|
best_wheel = next(
|
||||||
|
compatible_tags_selector(
|
||||||
|
(wheel, parse_wheel_filename(wheel.filename)[-1])
|
||||||
|
for wheel in package.wheels
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if best_wheel:
|
||||||
|
yield package, best_wheel
|
||||||
|
elif package.sdist is not None:
|
||||||
|
yield package, package.sdist
|
||||||
|
else:
|
||||||
|
raise PylockSelectError(
|
||||||
|
f"No wheel found matching the provided tags "
|
||||||
|
f"for package {package.name!r} "
|
||||||
|
f"at packages[{package_index}], "
|
||||||
|
f"and no sdist available as a fallback"
|
||||||
|
)
|
||||||
|
|
||||||
|
# - Else if no :ref:`pylock-packages-wheels` file is found or
|
||||||
|
# :ref:`pylock-packages-sdist` is solely set:
|
||||||
|
elif package.sdist is not None:
|
||||||
|
yield package, package.sdist
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Covered by lock.validate() which is a precondition for this method.
|
||||||
|
raise NotImplementedError # pragma: no cover
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user