Compare commits

...
111 Commits
Author SHA1 Message Date
Christoph 4ca1b0cce9 Update version to 2026.6.201 2026-06-22 06:50:56 +00:00
Christoph 6051260a8d Merge pull request 'Fix single-line brace formatting to preserve internal whitespace' (#31) from bug_fixing into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #31
2026-06-22 06:50:16 +00:00
Christoph Brandau dfe15dd3db Fix single-line brace formatting to preserve internal whitespace
The formatter now calculates and passes explicit leading and trailing whitespace around expression content to the `_brace` method. This ensures correct rendering of single-line expressions that include braces.
2026-06-22 08:49:22 +02:00
Christoph 5d1c402d2a Update version to 2026.6.200 2026-06-22 06:34:00 +00:00
Christoph 88f834ae79 Merge pull request 'Fix Bug for Formatting' (#30) from bug_fixing into main
/ build_and_publish (release) Successful in 37s
Reviewed-on: #30
2026-06-22 06:32:26 +00:00
Christoph Brandau 3b233389c8 Fix Tcl formatting bug by adjusting formatter options 2026-06-22 08:29:48 +02:00
Christoph 67bd9de18a Update version to 2026.6.100 2026-06-18 19:03:03 +00:00
Christoph 495b7b7a07 Update package.json
/ build_and_publish (release) Failing after 14m37s
2026-06-18 19:01:47 +00:00
Christoph 8b39c4d250 Update version to 2026.6.100 2026-06-18 18:41:28 +00:00
Christoph 49e8cdd7df Merge pull request 'Bug fixing' (#29) from bug_fixing into main
/ build_and_publish (release) Has been cancelled
Reviewed-on: #29
2026-06-18 18:39:23 +00:00
Christoph Brandau f60c4563e4 Delegate TCL folding to server, fix bugs, and enhance stability
- Move TCL folding range computation from client to language server, avoiding duplicate regions.
- Serialize language server restarts to prevent multiple server instances running concurrently.
- Pin server-side Python dependencies to specific minor versions for improved stability and predictability.
- Make debug server connection non-fatal, allowing the language server to start even if the debugger isn't attached.
- Optimize semantic token generation by caching Tclint plugin commands.
2026-06-18 20:38:47 +02:00
Christoph Brandau 3ad3847045 add new features and fix bugs 2026-06-18 17:31:51 +02:00
Christoph Brandau 41f331d4c4 add updates libs 2026-06-18 17:21:58 +02:00
Christoph 91ce762570 Update version to 2025.9.200 2025-09-01 13:24:13 +00:00
Christoph d60d3ec942 Update package.json
/ build_and_publish (release) Successful in 40s
2025-09-01 13:22:58 +00:00
Christoph e52722e353 Update README.md 2025-09-01 13:21:36 +00:00
Christoph a8bf55882e Merge pull request 'feat: use uv instead of pip' (#28) from change_to_uv_ into main
Reviewed-on: #28
2025-08-16 20:23:34 +00:00
Christoph 377bddf579 feat: use uv instead of pip 2025-08-16 22:22:28 +02:00
Christoph cdc1cb4805 Update version to 2025.9.100 2025-08-16 16:35:58 +00:00
Christoph 34430097d8 Update README.md
/ build_and_publish (release) Failing after 25s
2025-08-16 09:43:44 +00:00
Christoph dcf447ed4f Merge branch 'relicense-agpl3' 2025-08-16 11:33:32 +02:00
Christoph 783a9aa7d7 Relicense to AGPL-3.0-or-later 2025-08-16 11:29:09 +02:00
Christoph 63638d6b3b Update LICENSE 2025-08-16 09:16:37 +00:00
Christoph Brandau af8cd58fda Merge branch 'main' of https://git.cbsk-tech.de/Christoph/nx_post_support 2025-08-15 13:13:15 +02:00
Christoph Brandau df0f3d7a61 add description 2025-08-15 13:12:41 +02:00
Christoph 72906a1930 Update version to 2025.8.400 2025-08-15 07:34:53 +00:00
Christoph 068cf2d1c3 Update README.md
/ build_and_publish (release) Successful in 42s
2025-08-15 07:33:26 +00:00
Christoph ad18a3dcf3 Update README.md 2025-08-15 07:32:58 +00:00
Christoph Brandau bd4b17825c add: Outline support for DEF and CDL files 2025-08-15 07:29:01 +02:00
Christoph Brandau 5d6182e91f double value MOM_add_to_block_buffer rename to MOM_add_to_line_buffer 2025-08-15 07:18:38 +02:00
Christoph Brandau 63e9c429bd update ReadMe 2025-08-14 07:57:40 +02:00
Christoph 7fdafe484e Update version to 2025.8.300 2025-08-12 11:26:06 +00:00
Christoph Brandau 5cba6763f1 Merge branch 'new_features'
/ build_and_publish (release) Successful in 36s
2025-08-12 13:21:27 +02:00
Christoph Brandau e20bb85ac6 fix: wrong highligthing 2025-08-12 13:18:08 +02:00
Christoph 98a0b2197d Update version to 2025.8.202 2025-08-12 09:38:34 +00:00
Christoph Brandau 579847b56d fix formatter
/ build_and_publish (release) Successful in 33s
2025-08-12 11:35:54 +02:00
Christoph Brandau 618d6b478b fix: formatter when \ was deleting 2025-08-12 11:28:10 +02:00
Christoph Brandau 480978d6bb Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features
/ build_and_publish (release) Failing after 31s
2025-08-12 11:27:20 +02:00
Christoph Brandau 04798328bd fix: formatter 2025-08-12 11:27:14 +02:00
Christoph Brandau ae835089b6 fix: formatter when \ was deleting 2025-08-12 11:26:52 +02:00
Christoph 0ed1e887fc Update version to 2025.9.100 2025-08-12 07:21:23 +00:00
Christoph Brandau 12b7b62aa5 add variable indexing keep track of postion
/ build_and_publish (release) Successful in 39s
2025-08-12 09:07:04 +02:00
Christoph Brandau 7263b6f530 optimze Server 2025-08-12 07:48:05 +02:00
Christoph Brandau c0635e48ea Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features 2025-08-12 07:15:21 +02:00
Christoph Brandau 8fb98d2f40 refacxtor the code 2025-08-12 07:15:18 +02:00
Christoph f24b280edf mod: Keywords 2025-08-11 22:06:49 +02:00
Christoph b61cff3b3b Update version to 2025.9.4 2025-08-11 19:46:20 +00:00
Christoph cf62044d3e Update version to 2025.8.201 2025-08-11 19:37:32 +00:00
Christoph feba88212f fix: some highlight error
/ build_and_publish (release) Successful in 29s
2025-08-11 21:36:04 +02:00
Christoph aeebb5c964 fix: some highlight error
/ build_and_publish (release) Successful in 38s
2025-08-11 21:35:14 +02:00
Christoph b141818251 Update version to 2025.9.3 2025-08-11 16:41:00 +00:00
Christoph Brandau 72f894b05d add goto
/ build_and_publish (release) Successful in 31s
2025-08-11 18:40:06 +02:00
Christoph Brandau 0a26cb5c0f add outline to lsp 2025-08-11 18:28:23 +02:00
Christoph b3cac5c292 Update version to 2025.9.2 2025-08-11 15:20:46 +00:00
Christoph Brandau ef558c3aa3 update pipline
/ build_and_publish (release) Successful in 28s
2025-08-11 17:19:47 +02:00
Christoph Brandau 9d5ef23162 update pipline
/ build_and_publish (release) Failing after 26s
2025-08-11 17:16:27 +02:00
Christoph f659db1147 Update package.json 2025-08-11 15:12:55 +00:00
Christoph Brandau c25bf79427 update pipline
/ build_and_publish (release) Failing after 32s
2025-08-11 17:00:14 +02:00
Christoph b059a75205 Update version to 2025.9.001 2025-08-11 14:24:17 +00:00
Christoph a90e15514b Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (release) Failing after 31s
2025-08-11 14:23:08 +00:00
Christoph Brandau 2c2579319d update hover feature
/ build_and_publish (release) Failing after 27s
2025-08-11 16:20:14 +02:00
Christoph Brandau b38fab1428 Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features 2025-08-11 16:05:58 +02:00
Christoph Brandau bc745622b9 add first proc doc 2025-08-11 15:59:09 +02:00
Christoph Brandau 0bb4ef731e add first proc doc 2025-08-11 10:15:58 +02:00
Christoph 27e2b3bf6e Merge pull request 'update' (#25) from tcl_language_support into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #25
2025-08-09 21:35:09 +00:00
Christoph f55c3ba904 Update version to 2025.8.200 2025-08-09 12:09:07 +00:00
Christoph cb430a5553 Update version to 2025.8.2
/ build_and_publish (release) Successful in 33s
2025-08-09 12:06:50 +00:00
Christoph 637a28ad82 Update version to 2025.08.1
/ build_and_publish (release) Successful in 43s
2025-08-09 11:57:08 +00:00
Christoph 499df9b51d Update README.md
/ build_and_publish (release) Successful in 35s
2025-08-09 11:55:59 +00:00
Christoph Brandau 31ccae9a40 update 2025-08-08 09:18:22 +02:00
Christoph 9b373e85b7 Update version to 2025.08.101 2025-08-07 05:35:45 +00:00
Christoph 0c5f4ea6fb Merge pull request 'tcl_language_support' (#24) from tcl_language_support into main
/ build_and_publish (release) Successful in 47s
Reviewed-on: #24
2025-08-07 05:34:22 +00:00
Christoph Brandau 793b35f39f Merge branch 'tcl_language_support' of https://git.cbsk-tech.de/Christoph/nx_post_support into tcl_language_support 2025-08-07 07:27:09 +02:00
Christoph Brandau 3780fc27dc mod Readme 2025-08-07 07:27:08 +02:00
Christoph 6541830ef6 Update version to 2025.8.100 2025-08-06 20:10:08 +00:00
Christoph 509b13e99f add icon
/ build_and_publish (release) Successful in 46s
2025-08-06 22:08:39 +02:00
Christoph 396cfdc4ec Update version to 0.5.0 2025-08-06 18:22:29 +00:00
Christoph ccd22e4343 Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (release) Successful in 41s
2025-08-06 18:21:06 +00:00
Christoph 7d83ae1012 Update package.json 2025-08-06 18:19:22 +00:00
Christoph 029eaebd57 Update version to 0.5.0-beta.1 2025-08-06 18:13:14 +00:00
Christoph 2a2db8f217 Merge pull request 'tcl_language_support' (#23) from tcl_language_support into main
/ build_and_publish (push) Failing after 34s
Reviewed-on: #23
2025-08-06 18:09:48 +00:00
Christoph 8be9aae05d update pipline 2025-08-06 20:09:15 +02:00
Christoph Brandau 6bded783a1 add functions to completion 2025-08-06 15:26:53 +02:00
Christoph f5c61267e3 read all procs from psc file 2025-08-05 21:45:22 +02:00
Christoph Brandau b680c01989 update file sourcing 2025-08-05 18:18:17 +02:00
Christoph 1abf89fe56 load all files for completion 2025-08-04 21:11:20 +02:00
Christoph Brandau 6a051a6dee del test file 2025-08-04 16:06:50 +02:00
Christoph d22d897d23 Update version to 0.4.14 2025-08-04 12:52:21 +00:00
Christoph 7c834176a3 Merge pull request 'fix wired namspace call in poco' (#22) from tcl_language_support into main
/ build_and_publish (push) Successful in 32s
Reviewed-on: #22
2025-08-04 12:51:43 +00:00
Christoph Brandau 72be83c282 fix wired namspace call in poco 2025-08-04 14:51:07 +02:00
Christoph Brandau fa5ea00384 add test cases 2025-08-04 14:11:55 +02:00
Christoph a8b4fd9d1e Update .gitea/workflows/build_and_puplish.yml 2025-08-04 11:42:53 +00:00
Christoph 35d0f20fba Update version to 0.4.13 2025-08-04 11:33:48 +00:00
Christoph 440ebc4df5 Update package.json
/ build_and_publish (push) Successful in 26s
2025-08-04 11:31:39 +00:00
Christoph 1b48cc1983 Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (push) Failing after 23s
2025-08-04 11:30:13 +00:00
Christoph 6cde05b786 Update version to 0.4.13 2025-08-04 10:59:08 +00:00
Christoph cde58db3d7 Merge pull request 'tcl_language_support' (#21) from tcl_language_support into main
/ build_and_publish (push) Successful in 29s
Reviewed-on: #21
2025-08-04 10:58:10 +00:00
Christoph Brandau 3613391c23 fix semantics with var in array 2025-08-04 12:57:01 +02:00
Christoph Brandau 894a5b8b52 add more test cases 2025-08-04 12:48:21 +02:00
Christoph Brandau c644ac9bd3 add changes from tclint 2025-08-04 12:48:02 +02:00
Christoph Brandau fd5b1f2e71 add poco replace plugin 2025-08-04 12:47:49 +02:00
Christoph Brandau 57c65221d3 add LIB_GE_command_buffer_edit_replace to plugins 2025-08-04 09:17:30 +02:00
Christoph 528c7a6b9e add more ignore files 2025-08-03 21:54:45 +02:00
Christoph 2d034838b4 Update package.json 2025-08-03 19:45:39 +00:00
Christoph f0ed8ac708 Update version to 0.5.0 2025-08-03 19:44:23 +00:00
Christoph 6a30c1ab53 Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (push) Successful in 37s
2025-08-03 19:43:20 +00:00
Christoph 6d404f26f2 Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (push) Failing after 27s
2025-08-03 19:39:13 +00:00
Christoph afc675f47f Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (push) Failing after 22s
2025-08-03 19:37:43 +00:00
Christoph d2936ec5f6 Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (push) Failing after 37s
2025-08-03 19:36:28 +00:00
Christoph 8b44b41d68 Update .gitea/workflows/build_and_puplish.yml 2025-08-03 19:35:31 +00:00
Christoph 5124c12bc6 Update version to 0.4.12 2025-08-03 19:30:13 +00:00
191 changed files with 11801 additions and 3644 deletions
+35 -8
View File
@@ -1,13 +1,15 @@
on: on:
push: release:
tags: types: [published]
- "*"
jobs: jobs:
build_and_publish: build_and_publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout branch from release
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.release.target_commitish }}
fetch-depth: 0
- name: GitHub Tag Name example - name: GitHub Tag Name example
run: | run: |
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME" echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
@@ -33,16 +35,41 @@ jobs:
git config user.name "${{ vars.USERNAME_GIT }}" git config user.name "${{ vars.USERNAME_GIT }}"
git config user.email "${{ vars.EMAIL_GIT }}" git config user.email "${{ vars.EMAIL_GIT }}"
git add package.json git add package.json
git commit -m "Update version to ${{ github.ref_name }}" git commit -m "Update version to ${{ github.ref_name }}" || echo "No changes to commit."
git push origin HEAD:main
- name: Publish to Visual Studio Marketplace # Get current branch; if detached, resolve to a remote branch that contains this commit
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$BRANCH" == "HEAD" || -z "$BRANCH" ]]; then
# If target_commitish is a branch, use it; if its a SHA, find a branch that contains it
CANDIDATE='${{ github.event.release.target_commitish }}'
if [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]]; then
BRANCH="$(git branch -r --contains "$GITHUB_SHA" | sed -n 's|.*origin/||p' | head -n1)"
else
BRANCH="$CANDIDATE"
fi
fi
if [[ -z "$BRANCH" ]]; then
echo "Could not determine branch for push."; git branch -a; exit 1
fi
git push origin HEAD:refs/heads/$BRANCH
- name: Publish to Visual Studio Marketplace (Pre-Release)
if: ${{ github.event.release.prerelease }}
run: vsce publish --pre-release
env:
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
- name: Publish to Visual Studio Marketplace (Stable)
if: ${{ !github.event.release.prerelease }}
run: vsce publish run: vsce publish
env: env:
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }} VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
- name: Pack Extension for Release
run: vsce pack
- name: Upload VSIX to Release - name: Upload VSIX to Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: "*.vsix" files: "nx-post-support-*.vsix"
# generate_release_notes: true # Automatische Release Notes # generate_release_notes: true # Automatische Release Notes
# draft: false # N # draft: false # N
# prerelease: false # prerelease: false
+2 -1
View File
@@ -8,4 +8,5 @@ __pycache__
*.pyc *.pyc
.nox .nox
*.g4 *.g4
.antlr .antlr
.claude
+2 -1
View File
@@ -2,6 +2,7 @@
"ruff.configuration": { "ruff.configuration": {
"lint": { "lint": {
"extend-ignore": ["F821", "E402"] "extend-ignore": ["F821", "E402"]
} },
"line-length": 200
} }
} }
+4 -1
View File
@@ -11,8 +11,11 @@ esbuild.js
.gitea .gitea
.venv/** .venv/**
.nox/** .nox/**
server/.venv/**
server/.nox/**
**/__pycache__/** **/__pycache__/**
**/requirements.txt **/requirements.txt
**/requirements.in **/requirements.in
**/server/src/_debug_server.py **/server/src/_debug_server.py
noxfile.py server/noxfile.py
client/**
+9 -1
View File
@@ -12,4 +12,12 @@
## [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
+657 -17
View File
@@ -1,21 +1,661 @@
MIT License GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (c) 2025 Christoph Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy 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/>.
+50
View File
@@ -0,0 +1,50 @@
# NX Postprocessor Support
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
## Features
- **Syntax Highlighting** - Full syntax highlighting for CDL, TCL, and DEF files
- **Code Formatting** - Automatic code formatting for all supported file types
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
- **Auto-completion** - Context-aware code completion for faster development
## Supported File Types
- `.cdl` - NX User Defined Event File
- `.tcl` - NX TCL postprocessor files
- `.def` - NX Definition files
## Installation
1. Install from the VS Code Marketplace
2. Install Python 3.8 or higher
3. Open any `.cdl`, `.tcl`, or `.def` file
4. The extension will automatically activate and provide language support
## Configuration
The extension can be configured through VS Code settings:
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
## Usage
Simply open any supported file type and enjoy:
- Syntax highlighting
- Error detection and linting
- Code completion
- Code formatting (Format Document command)
- Hover information
## Contributing
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
## License
AGPL-3.0 License - see LICENSE file for details.
+177
View File
@@ -271,3 +271,180 @@ export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode
return collectSymbols(rootScope) return collectSymbols(rootScope)
} }
export function cdlDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const lines = document.getText().split("\n")
interface Scope {
symbol: vscode.DocumentSymbol
startLine: number
}
const root = new vscode.DocumentSymbol(
"root",
"",
vscode.SymbolKind.Namespace,
new vscode.Range(0, 0, lines.length, 0),
new vscode.Range(0, 0, 0, 0)
)
root.children = []
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()
const createSymbol = () => {
if (!pending) {
return
}
const startRange = new vscode.Range(
pending.startLine,
0,
pending.startLine,
lines[pending.startLine].length
)
const symbol = new vscode.DocumentSymbol(
pending.name,
"",
pending.kind,
startRange,
startRange
)
symbol.children = []
stack[stack.length - 1].symbol.children?.push(symbol)
stack.push({ symbol, startLine: pending.startLine })
pending = null
}
if (pending && trimmed.includes("{")) {
createSymbol()
}
if (trimmed.startsWith("}")) {
const scope = stack.pop()
if (scope) {
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
}
continue
}
let match
if ((match = /^\s*EVENT\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Event,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
} else if ((match = /^\s*PARAM\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Field,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
}
}
return root.children || []
}
export function defDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const lines = document.getText().split("\n")
interface Scope {
symbol: vscode.DocumentSymbol
startLine: number
}
const root = new vscode.DocumentSymbol(
"root",
"",
vscode.SymbolKind.Namespace,
new vscode.Range(0, 0, lines.length, 0),
new vscode.Range(0, 0, 0, 0)
)
root.children = []
const stack: Scope[] = [{ symbol: root, startLine: 0 }]
let pending: { name: string; kind: vscode.SymbolKind; startLine: number } | null = null
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmed = line.trim()
const createSymbol = () => {
if (!pending) {
return
}
const startRange = new vscode.Range(
pending.startLine,
0,
pending.startLine,
lines[pending.startLine].length
)
const symbol = new vscode.DocumentSymbol(
pending.name,
"",
pending.kind,
startRange,
startRange
)
symbol.children = []
stack[stack.length - 1].symbol.children?.push(symbol)
stack.push({ symbol, startLine: pending.startLine })
pending = null
}
if (pending && trimmed.includes("{")) {
createSymbol()
}
if (trimmed.startsWith("}")) {
const scope = stack.pop()
if (scope) {
scope.symbol.range = new vscode.Range(scope.startLine, 0, i, line.length)
}
continue
}
let match
if ((match = /^\s*FORMATTING\b/.exec(line))) {
pending = {
name: "FORMATTING",
kind: vscode.SymbolKind.Module,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
} else if ((match = /^\s*ADDRESS\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Field,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
} else if ((match = /^\s*BLOCK_TEMPLATE\s+(\w+)/.exec(line))) {
pending = {
name: match[1],
kind: vscode.SymbolKind.Function,
startLine: i
}
if (line.includes("{")) {
createSymbol()
}
}
}
return root.children || []
}
+6 -3
View File
@@ -79,7 +79,8 @@ export async function getWorkspaceSettings(
interpreter: resolveVariables(interpreter, workspace), interpreter: resolveVariables(interpreter, workspace),
importStrategy: config.get<string>(`importStrategy`) ?? "useBundled", importStrategy: config.get<string>(`importStrategy`) ?? "useBundled",
showNotifications: config.get<string>(`showNotifications`) ?? "off", showNotifications: config.get<string>(`showNotifications`) ?? "off",
formatter: config.get<boolean>(`formatter`) ?? true formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true
} }
return workspaceSetting return workspaceSetting
} }
@@ -111,7 +112,8 @@ export async function getGlobalSettings(
interpreter: interpreter, interpreter: interpreter,
importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"), importStrategy: getGlobalValue<string>(config, "importStrategy", "useBundled"),
showNotifications: getGlobalValue<string>(config, "showNotifications", "off"), showNotifications: getGlobalValue<string>(config, "showNotifications", "off"),
formatter: config.get<boolean>(`formatter`) ?? true formatter: config.get<boolean>(`formatter`) ?? true,
inlayHint: config.get<boolean>(`inlayHint`) ?? true
} }
return setting return setting
} }
@@ -126,7 +128,8 @@ export function checkIfConfigurationChanged(
`${namespace}.interpreter`, `${namespace}.interpreter`,
`${namespace}.importStrategy`, `${namespace}.importStrategy`,
`${namespace}.showNotifications`, `${namespace}.showNotifications`,
`${namespace}.formatter` `${namespace}.formatter`,
`${namespace}.inlayHint`
] ]
const changed = settings.map((s) => e.affectsConfiguration(s)) const changed = settings.map((s) => e.affectsConfiguration(s))
return changed.includes(true) return changed.includes(true)
+56 -25
View File
@@ -12,7 +12,8 @@ import {
formatDefFile, formatDefFile,
isFirstLineMachine, isFirstLineMachine,
diagnosticHandler, diagnosticHandler,
tclDocumentSymbolProvider cdlDocumentSymbolProvider,
defDocumentSymbolProvider
} 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 {
@@ -28,7 +29,7 @@ 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
@@ -60,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext) {
traceLog(`Module: ${serverInfo.module}`) traceLog(`Module: ${serverInfo.module}`)
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`) traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
const 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))) {
@@ -89,6 +90,16 @@ 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
}
context.subscriptions.push( context.subscriptions.push(
onDidChangePythonInterpreter(async () => { onDidChangePythonInterpreter(async () => {
await runServer() await runServer()
@@ -114,6 +125,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" },
@@ -179,11 +194,26 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(formatDefProvider) context.subscriptions.push(formatDefProvider)
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider( const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
{ scheme: "file", language: "tcl" }, { scheme: "file", language: "cdl" },
{ provideDocumentSymbols: tclDocumentSymbolProvider } {
provideDocumentSymbols(document: vscode.TextDocument) {
return cdlDocumentSymbolProvider(document)
}
}
) )
context.subscriptions.push(tclOutlineProvider) context.subscriptions.push(cdlSymbolProvider)
// Outline now provided by the language server (Document Symbols). Removing TS provider.
const defSymbolProvider = vscode.languages.registerDocumentSymbolProvider(
{ scheme: "file", language: "def" },
{
provideDocumentSymbols(document: vscode.TextDocument) {
return defDocumentSymbolProvider(document)
}
}
)
context.subscriptions.push(defSymbolProvider)
// Diagnostics collection // Diagnostics collection
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl") const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
@@ -191,26 +221,27 @@ 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"
vscode.workspace.onDidOpenTextDocument((document) => { context.subscriptions.push(
if (document.languageId === "cdl" || document.languageId === "def") { vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl") { if (document.languageId === "cdl" || document.languageId === "def") {
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document)) if (document.languageId === "cdl") {
} else if (document.languageId === "def") { diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document)) } else if (document.languageId === "def") {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
}
} }
} }),
}) vscode.workspace.onDidChangeTextDocument((event) => {
const document = event.document
vscode.workspace.onDidChangeTextDocument((event) => { if (document.languageId === "cdl" || document.languageId === "def") {
const document = event.document if (document.languageId === "cdl") {
if (document.languageId === "cdl" || document.languageId === "def") { diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document))
if (document.languageId === "cdl") { } else if (document.languageId === "def") {
diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document)) diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
} else if (document.languageId === "def") { }
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
} }
} })
}) )
} }
export function deactivate(): Thenable<void> | undefined { export function deactivate(): Thenable<void> | undefined {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+33 -26
View File
@@ -1,28 +1,35 @@
{ {
"comments": { "comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments // symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "#" "lineComment": "#"
}, },
// symbols used as brackets "indentationRules": {
"brackets": [ // 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 that are auto closed when typing },
"autoClosingPairs": [ // symbols used as brackets
["{", "}"], "brackets": [
["[", "]"], ["{", "}"],
["(", ")"], ["[", "]"],
["\"", "\""], ["(", ")"]
["'", "'"] ],
], // symbols that are auto closed when typing
// symbols that can be used to surround a selection "autoClosingPairs": [
"surroundingPairs": [ ["{", "}"],
["{", "}"], ["[", "]"],
["[", "]"], ["(", ")"],
["(", ")"], ["\"", "\""],
["\"", "\""], ["'", "'"]
["'", "'"] ],
] // symbols that can be used to surround a selection
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "nx-post-support", "name": "nx-post-support",
"version": "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",
+15 -3
View File
@@ -1,9 +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": "0.4.11", "version": "2026.6.201",
"publisher": "Christoph", "publisher": "Christoph",
"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"
@@ -16,7 +20,10 @@
"keywords": [ "keywords": [
"cdl", "cdl",
"NX CDL", "NX CDL",
"UDE" "UDE",
"Postprocessor",
"NX CAM",
"Siemens NX"
], ],
"engines": { "engines": {
"vscode": "^1.96.0" "vscode": "^1.96.0"
@@ -84,6 +91,11 @@
"default": false, "default": false,
"description": "Use the TCL formatter from `NX Postprocessor Support`" "description": "Use the TCL formatter from `NX Postprocessor Support`"
}, },
"nx-post-support.inlayHint": {
"type": "boolean",
"default": true,
"description": "Use the Inlay Hints in from `NX Postprocessor Support`"
},
"nx-post-support.importStrategy": { "nx-post-support.importStrategy": {
"default": "useBundled", "default": "useBundled",
"description": "Defines where `NX Postprocessor Support` is imported from.", "description": "Defines where `NX Postprocessor Support` is imported from.",
+5 -5
View File
@@ -176,7 +176,7 @@ def attrib(
type: None = ..., type: None = ...,
converter: None = ..., converter: None = ...,
factory: None = ..., factory: None = ...,
kw_only: bool = ..., 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(
+8 -3
View File
@@ -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
View File
@@ -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:
+396 -113
View File
@@ -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, cls, eq_, auto_detect, ("__eq__", "__ne__")
these, )
slots,
is_frozen, Hashability = ClassProps.Hashability
weakref_slot,
_determine_whether_to_implement( if is_exc:
hashability = Hashability.LEAVE_ALONE
elif hash is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif hash is False:
hashability = Hashability.LEAVE_ALONE
elif hash is None:
if auto_detect is True and _has_own_attribute(cls, "__hash__"):
hashability = Hashability.LEAVE_ALONE
elif eq is True and is_frozen is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif eq is False:
hashability = Hashability.LEAVE_ALONE
else:
hashability = Hashability.UNHASHABLE
else:
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)
KeywordOnly = ClassProps.KeywordOnly
if kw_only:
kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES
else:
kwo = KeywordOnly.NO
props = ClassProps(
is_exception=is_exc,
is_frozen=is_frozen,
is_slotted=slots,
collected_fields_by_mro=collect_by_mro,
added_init=_determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
),
added_repr=_determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
),
added_eq=eq,
added_ordering=not is_exc
and _determine_whether_to_implement(
cls,
order_,
auto_detect,
("__lt__", "__le__", "__gt__", "__ge__"),
),
hashability=hashability,
added_match_args=match_args,
kw_only=kwo,
has_weakref_slot=weakref_slot,
added_str=str,
added_pickling=_determine_whether_to_implement(
cls, 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,12 +1910,17 @@ 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)
raise TypeError(msg) if getattr(type_, "__attrs_attrs__", None) is None:
msg = "Passed object must be a class or attrs instance."
raise TypeError(msg)
return fields(type_)
attrs = getattr(cls, "__attrs_attrs__", None) attrs = getattr(cls, "__attrs_attrs__", 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:
+58 -7
View File
@@ -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
+3
View File
@@ -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))
+4 -4
View File
@@ -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
""" """
msg = "can't set attribute" def __init__(self):
args: ClassVar[tuple[str]] = [msg] msg = "can't set attribute"
super().__init__(msg)
self.msg = msg
class FrozenInstanceError(FrozenError): class FrozenInstanceError(FrozenError):
+55 -15
View File
@@ -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:
self.key_validator(inst, attr, key) if self.key_validator is not None:
self.value_validator(inst, attr, value[key]) self.key_validator(inst, attr, key)
if self.value_validator is not None:
self.value_validator(inst, attr, value[key])
def __repr__(self): def __repr__(self):
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>" return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
def deep_mapping(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
+60 -6
View File
@@ -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
-56
View File
@@ -1,56 +0,0 @@
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
attr/__init__.pyi,sha256=QIXnnHPoucmDWkbpNsWTP-cgJ1bn8le7DjyRa_wYdew,11281
attr/__pycache__/__init__.cpython-311.pyc,,
attr/__pycache__/_cmp.cpython-311.pyc,,
attr/__pycache__/_compat.cpython-311.pyc,,
attr/__pycache__/_config.cpython-311.pyc,,
attr/__pycache__/_funcs.cpython-311.pyc,,
attr/__pycache__/_make.cpython-311.pyc,,
attr/__pycache__/_next_gen.cpython-311.pyc,,
attr/__pycache__/_version_info.cpython-311.pyc,,
attr/__pycache__/converters.cpython-311.pyc,,
attr/__pycache__/exceptions.cpython-311.pyc,,
attr/__pycache__/filters.cpython-311.pyc,,
attr/__pycache__/setters.cpython-311.pyc,,
attr/__pycache__/validators.cpython-311.pyc,,
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
attr/_compat.py,sha256=4hlXbWhdDjQCDK6FKF1EgnZ3POiHgtpp54qE0nxaGHg,2704
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
attr/_funcs.py,sha256=5-tUKJtp3h5El55EcDl6GWXFp68fT8D8U7uCRN6497I,15854
attr/_make.py,sha256=lBUPPmxiA1BeHzB6OlHoCEh--tVvM1ozXO8eXOa6g4c,96664
attr/_next_gen.py,sha256=7FRkbtl_N017SuBhf_Vw3mw2c2pGZhtCGOzadgz7tp4,24395
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
attr/_version_info.py,sha256=exSqb3b5E-fMSsgZAlEw9XcLpEgobPORCZpcaEglAM4,2121
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
attr/exceptions.py,sha256=HRFq4iybmv7-DcZwyjl6M1euM2YeJVK_hFxuaBGAngI,1977
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
attr/validators.py,sha256=WaB1HLAHHqRHWsrv_K9H-sJ7ESil3H3Cmv2d8TtVZx4,20046
attr/validators.pyi,sha256=s2WhKPqskxbsckJfKk8zOuuB088GfgpyxcCYSNFLqNU,2603
attrs-25.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
attrs-25.3.0.dist-info/METADATA,sha256=W38cREj7s1wqNf1fg4hVwZmL1xh0AdSp4IhtTMROinw,10993
attrs-25.3.0.dist-info/RECORD,,
attrs-25.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs-25.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
attrs-25.3.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
attrs/__init__.py,sha256=qeQJZ4O08yczSn840v9bYOaZyRE81WsVi-QCrY3krCU,1107
attrs/__init__.pyi,sha256=nZmInocjM7tHV4AQw0vxO_fo6oJjL_PonlV9zKKW8DY,7931
attrs/__pycache__/__init__.cpython-311.pyc,,
attrs/__pycache__/converters.cpython-311.pyc,,
attrs/__pycache__/exceptions.cpython-311.pyc,,
attrs/__pycache__/filters.cpython-311.pyc,,
attrs/__pycache__/setters.cpython-311.pyc,,
attrs/__pycache__/validators.cpython-311.pyc,,
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
@@ -0,0 +1 @@
uv
@@ -1,6 +1,6 @@
Metadata-Version: 2.4 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)
+37
View File
@@ -0,0 +1,37 @@
attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057
attr/__init__.pyi,sha256=pVGImAUVovq2_TYl_r_HIYnGlyOaoCuEhxo-EvsnnSc,11325
attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117
attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368
attr/_compat.py,sha256=x0g7iEUOnBVJC72zyFCgb1eKqyxS-7f2LGnNyZ_r95s,2829
attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843
attr/_funcs.py,sha256=Ix5IETTfz5F01F-12MF_CSFomIn2h8b67EVVz2gCtBE,16479
attr/_make.py,sha256=H7OH2eWS5CnBzLUjNFE1WymfPrmF1r8fv2RPdt9MuYA,106129
attr/_next_gen.py,sha256=BQtCUlzwg2gWHTYXBQvrEYBnzBUrDvO57u0Py6UCPhc,26274
attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469
attr/_version_info.py,sha256=w4R-FYC3NK_kMkGUWJlYP4cVAlH9HRaC-um3fcjYkHM,2222
attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209
attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861
attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643
attr/exceptions.py,sha256=b4vMbnoQ3VpwWZhqrYi_ssXVCK8o2c4HQSS09cSUM9o,1990
attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539
attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795
attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208
attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617
attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584
attr/validators.py,sha256=m3QRzZTANr4f2C4eVdUoFg11NgXWak8Wat4qQTGhvcs,21553
attr/validators.pyi,sha256=gM1ZmHaBckyYWI2EirpRNzqm3B19cw5Iq6B4Kno9YCM,4087
attrs-26.1.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
attrs-26.1.0.dist-info/METADATA,sha256=TNQOaQ8jvzfLytNO_WdY4GLfHfB8hoM_fjzpW_H6OMw,8754
attrs-26.1.0.dist-info/RECORD,,
attrs-26.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs-26.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
attrs-26.1.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109
attrs/__init__.py,sha256=RxaAZNwYiEh-fcvHLZNpQ_DWKni73M_jxEPEftiq1Zc,1183
attrs/__init__.pyi,sha256=2gV79g9UxJppGSM48hAZJ6h_MHb70dZoJL31ZNJeZYI,9416
attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76
attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76
attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73
attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73
attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76
@@ -1,4 +1,4 @@
Wheel-Version: 1.0 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
+4 -1
View File
@@ -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",
+58 -7
View File
@@ -77,7 +77,7 @@ def field(
metadata: Mapping[Any, Any] | None = ..., metadata: Mapping[Any, Any] | None = ...,
converter: None = ..., converter: None = ...,
factory: None = ..., factory: None = ...,
kw_only: bool = ..., 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
-102
View File
@@ -1,102 +0,0 @@
cattr/__init__.py,sha256=bYrmwTYSdYC_ut1xW31V7mxhXBlJQKs8EECgtUBgAuc,906
cattr/__pycache__/__init__.cpython-311.pyc,,
cattr/__pycache__/converters.cpython-311.pyc,,
cattr/__pycache__/disambiguators.cpython-311.pyc,,
cattr/__pycache__/dispatch.cpython-311.pyc,,
cattr/__pycache__/errors.cpython-311.pyc,,
cattr/__pycache__/gen.cpython-311.pyc,,
cattr/converters.py,sha256=rQhY4J8r7QTZh5WICuFe4GWO1v0DS3DgQ9r569zd6jg,192
cattr/disambiguators.py,sha256=ugD1fq1Z5x1pGu5P1lMzcT-IEi1q7IfQJIHEdmg62vM,103
cattr/dispatch.py,sha256=uVEOgHWR9Hn5tm-wIw-bDccqrxJByVi8yRKaYyvL67k,125
cattr/errors.py,sha256=V4RhoCObwGrlaM3oyn1H_FYxGR8iAB9dG5NxFDYM548,343
cattr/gen.py,sha256=hWyKoZ_d2D36Jz_npspyGw8s9pWtUA69sXf0R3uOvgM,597
cattr/preconf/__init__.py,sha256=NqPE7uhVfcP-PggkUpsbfAutMo8oHjcoB1cvjgLft-s,78
cattr/preconf/__pycache__/__init__.cpython-311.pyc,,
cattr/preconf/__pycache__/bson.cpython-311.pyc,,
cattr/preconf/__pycache__/json.cpython-311.pyc,,
cattr/preconf/__pycache__/msgpack.cpython-311.pyc,,
cattr/preconf/__pycache__/orjson.cpython-311.pyc,,
cattr/preconf/__pycache__/pyyaml.cpython-311.pyc,,
cattr/preconf/__pycache__/tomlkit.cpython-311.pyc,,
cattr/preconf/__pycache__/ujson.cpython-311.pyc,,
cattr/preconf/bson.py,sha256=Bn4hJxac7OthGg_CR4LCPeBp_fz4kx3QniBVOZhguGs,195
cattr/preconf/json.py,sha256=LpqYuO3oePDxbQtKFKB0SaoeAi3Z_agIgyNn1VQSIVo,206
cattr/preconf/msgpack.py,sha256=pyJ9L9ekNlZ0IQHbJ9Ay_fi_NOqY5_rE_q-UnD94-RM,207
cattr/preconf/orjson.py,sha256=Adh-7csx4eqCjx22zipMFgSlDXbR554wvgNHEb8Q5JM,203
cattr/preconf/pyyaml.py,sha256=Fy40bejjp7uqgoLhTA_p4wZYF0uFaguHbUK9zs9LoC0,203
cattr/preconf/tomlkit.py,sha256=_gADJ_UYpj3EiNXGYjAfSOkcoFIkLpYVOFfLEqBfIJQ,207
cattr/preconf/ujson.py,sha256=IzEa7QUcYOaSUMiLQsFEWJnBihmmOLhehsM-5cPY9NI,199
cattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs-25.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
cattrs-25.1.1.dist-info/METADATA,sha256=ODqSak3dhIZZjmFa-SZT8Si32_3ey_oo2tUefYx0QtU,8388
cattrs-25.1.1.dist-info/RECORD,,
cattrs-25.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs-25.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
cattrs-25.1.1.dist-info/licenses/LICENSE,sha256=9fudHt43qIykf0IMSZ3KD0oFvJk-Esd9I1IKrSkcAb8,1074
cattrs/__init__.py,sha256=UhiFdxf81gCuBBA6FutoE1oOzthzF_PkAdoE2AVslIo,1901
cattrs/__pycache__/__init__.cpython-311.pyc,,
cattrs/__pycache__/_compat.cpython-311.pyc,,
cattrs/__pycache__/_generics.cpython-311.pyc,,
cattrs/__pycache__/cols.cpython-311.pyc,,
cattrs/__pycache__/converters.cpython-311.pyc,,
cattrs/__pycache__/disambiguators.cpython-311.pyc,,
cattrs/__pycache__/dispatch.cpython-311.pyc,,
cattrs/__pycache__/errors.cpython-311.pyc,,
cattrs/__pycache__/fns.cpython-311.pyc,,
cattrs/__pycache__/literals.cpython-311.pyc,,
cattrs/__pycache__/typealiases.cpython-311.pyc,,
cattrs/__pycache__/types.cpython-311.pyc,,
cattrs/__pycache__/v.cpython-311.pyc,,
cattrs/_compat.py,sha256=dMRB8a8RkxFdnQDKRpamresVF_SBkksM2_ZMAuL0s2w,11987
cattrs/_generics.py,sha256=keExDE2CGIer8ci12SoJ_rXYTLva9P29uLlvyb_fxtM,966
cattrs/cols.py,sha256=mWDchfvjMQ6uACKSfdZs05YiXrWdph2HOJfaqY3D_EI,8848
cattrs/converters.py,sha256=ui4BSAxnV1J6Oh0pYN2oOHgp-mIybsXW-M8SXPDNzlo,54262
cattrs/disambiguators.py,sha256=eUyWMtW6bQJcXGOWiraq1pFcMlUbo9BJHxFY--KF6Lk,6867
cattrs/dispatch.py,sha256=9qA-pmsPvgrM6MGP8Ev2gVP6YL2rXvoBla_C0VgHxQ0,6780
cattrs/errors.py,sha256=6IGfE-wVQbOaDNN4xAQJf7Hk_t2QNV0Zem64D6yMZrU,4168
cattrs/fns.py,sha256=z5z1VZOZv8t5LwG8cBM_tIXg-_PlQUOyZb9wIrXNqlw,626
cattrs/gen/__init__.py,sha256=bpRGHd3G0UTpWcHRcJTNAUcWmX-evoPHGvpH6u29FDU,38842
cattrs/gen/__pycache__/__init__.cpython-311.pyc,,
cattrs/gen/__pycache__/_consts.cpython-311.pyc,,
cattrs/gen/__pycache__/_generics.cpython-311.pyc,,
cattrs/gen/__pycache__/_lc.cpython-311.pyc,,
cattrs/gen/__pycache__/_shared.cpython-311.pyc,,
cattrs/gen/__pycache__/typeddicts.cpython-311.pyc,,
cattrs/gen/_consts.py,sha256=ZwT_m2J3S7p-UjltpbA1WtfQZLNj9KhmFYCAv6Zl-g0,511
cattrs/gen/_generics.py,sha256=_DyXCGql2QIxGhAv3_B1hsi80uPK8PhK2hhZa95YOlo,3011
cattrs/gen/_lc.py,sha256=4fjeUsmgQcCAIjnNndBic0gf5qKmxVS3CZHqUQ9Rw5g,882
cattrs/gen/_shared.py,sha256=xKsfcVtpyYIir9AW8VuOVoiSbaEI7tsSL0JpUCIUX-g,2296
cattrs/gen/typeddicts.py,sha256=Ck3QMr_B1T7vwxyRjfZPHafKphN2hndL181dpQNxzPs,21254
cattrs/literals.py,sha256=0kzAewmWk9ikJGoKq4ysnAR22DMawG3iNqLl8NLgpk0,331
cattrs/preconf/__init__.py,sha256=P7czFRcjeN6zBcdwUyeBloniltlJptCa8Yd2uFGlz9w,1527
cattrs/preconf/__pycache__/__init__.cpython-311.pyc,,
cattrs/preconf/__pycache__/bson.cpython-311.pyc,,
cattrs/preconf/__pycache__/cbor2.cpython-311.pyc,,
cattrs/preconf/__pycache__/json.cpython-311.pyc,,
cattrs/preconf/__pycache__/msgpack.cpython-311.pyc,,
cattrs/preconf/__pycache__/msgspec.cpython-311.pyc,,
cattrs/preconf/__pycache__/orjson.cpython-311.pyc,,
cattrs/preconf/__pycache__/pyyaml.cpython-311.pyc,,
cattrs/preconf/__pycache__/tomlkit.cpython-311.pyc,,
cattrs/preconf/__pycache__/ujson.cpython-311.pyc,,
cattrs/preconf/bson.py,sha256=6p1kmOFMjswSXFCb1hKJeNvr3kNsAm1gfX_DA6igq8E,4201
cattrs/preconf/cbor2.py,sha256=LnREcjpOp_402poUGRVIhDWI4f_R1wvJkdKvs5MrTGU,2022
cattrs/preconf/json.py,sha256=zTrkfjOxXZFwwabNESeafy-C7MEpn7Cw5AdhkkeOjU4,2631
cattrs/preconf/msgpack.py,sha256=dZE9tsAA5qX3pSc3MZmlGuvJ5q_wI6mANDyugKXKj-E,2325
cattrs/preconf/msgspec.py,sha256=Ds0rPW4900zsBLqfupF-smkg9_Kwyx7D_Vh9a0yJB8M,7250
cattrs/preconf/orjson.py,sha256=5MBcUsyp3eGsHgLfLtt8-q90L2mxjD0ttnrWBUIwouo,3870
cattrs/preconf/pyyaml.py,sha256=w0aM_gJ6VhZf-Zpu_UlJki7rdgv4mfaSXElPofB3nlE,2378
cattrs/preconf/tomlkit.py,sha256=gJWGJjMONCViTMZuphOg2xXzjQt3SCEVFVdoKgDjqc8,3148
cattrs/preconf/ujson.py,sha256=wRLidBM8aWucFkCQ9haiktY8xYoCdanDhQuKJLQJgGM,2425
cattrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cattrs/strategies/__init__.py,sha256=nkZWCzSRYcS-75FMfk52mioZSuWykaN8hB39Vig5Xkg,339
cattrs/strategies/__pycache__/__init__.cpython-311.pyc,,
cattrs/strategies/__pycache__/_class_methods.cpython-311.pyc,,
cattrs/strategies/__pycache__/_subclasses.cpython-311.pyc,,
cattrs/strategies/__pycache__/_unions.cpython-311.pyc,,
cattrs/strategies/_class_methods.py,sha256=O5xhQCzNpuFiDNDMlbcyeOVqyrV65NhMZNRsG3jnoBU,2591
cattrs/strategies/_subclasses.py,sha256=aCE2UQjevZQHMnPOPyl2qR_hgRpgRUt1j9lE4qZ3hNc,9365
cattrs/strategies/_unions.py,sha256=YBBklVSWJ-7DSkLDLpumwAJJ39ALuSGyB6W0Ptz5Rz4,9355
cattrs/typealiases.py,sha256=toHavC2kJsIcxThwvATPO5JShzKeC8kIl9KqteFohbw,1619
cattrs/types.py,sha256=cqvfmzliYfrvPswxlW_tN4DmhQ2xpAKQvVbNBJaxiWs,278
cattrs/v.py,sha256=IqUajgJFCKJYf-4S9TCKRtJcmmK4c3En69TGuf2FKOs,4126
@@ -0,0 +1 @@
uv
@@ -1,6 +1,6 @@
Metadata-Version: 2.4 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,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
+44 -42
View File
@@ -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,10 +208,7 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet OriginMutableSet = AbcMutableSet
signature = _signature signature = partial(_signature, eval_str=True)
if sys.version_info >= (3, 10):
signature = partial(_signature, eval_str=True)
try: try:
@@ -256,10 +252,25 @@ def is_tuple(type):
) )
if sys.version_info >= (3, 10): if sys.version_info >= (3, 14):
def is_union_type(obj): 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
) )
+2 -2
View File
@@ -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
View File
@@ -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()
] ]
+34 -13
View File
@@ -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(
+1 -2
View File
@@ -7,7 +7,7 @@ from collections.abc import Mapping
from dataclasses import MISSING from dataclasses import MISSING
from functools import reduce from functools import reduce
from operator import or_ from operator import or_
from typing import TYPE_CHECKING, Any, Callable, Literal, Union 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,
+36
View File
@@ -0,0 +1,36 @@
from collections.abc import Callable
from enum import Enum
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from .converters import BaseConverter
def enum_unstructure_factory(
type: type[Enum], converter: "BaseConverter"
) -> Callable[[Enum], Any]:
"""A factory for generating enum unstructure hooks.
If the enum is a typed enum (has `_value_`), we use the underlying value's hook.
Otherwise, we use the value directly.
"""
if "_value_" in type.__annotations__:
return lambda e: converter.unstructure(e.value)
return lambda e: e.value
def enum_structure_factory(
type: type[Enum], converter: "BaseConverter"
) -> Callable[[Any, type[Enum]], Enum]:
"""A factory for generating enum structure hooks.
If the enum is a typed enum (has `_value_`), we structure the value first.
Otherwise, we use the value directly.
"""
if "_value_" in type.__annotations__:
val_type = type.__annotations__["_value_"]
val_hook = converter.get_structure_hook(val_type)
return lambda v, _: type(val_hook(v, val_type))
return lambda v, _: type(v)
+16 -10
View File
@@ -13,14 +13,18 @@ class StructureHandlerNotFoundError(Exception):
""" """
def __init__(self, message: str, type_: type) -> None: 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))}"
) )
+108 -24
View File
@@ -4,7 +4,7 @@ import re
from collections.abc import Callable, Iterable, Mapping from collections.abc import Callable, Iterable, Mapping
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
from attrs import NOTHING, Attribute, 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]]:
@@ -95,7 +95,17 @@ def make_dict_unstructure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 24.1.0 .. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
.. versionchanged:: 26.1.0
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
specified, the converter is applied to the default value before checking if it
is equal to the attribute's value.
""" """
fn_name = "unstructure_" + cl.__name__ fn_name = "unstructure_" + cl.__name__
@@ -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,16 +192,32 @@ 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
lines.append(f" res['{kn}'] = {invoke}")
c = a.converter
if c is not None:
conv_name = f"__c_conv_{attr_name}"
if isinstance(c, Converter):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
field_name = f"__c_field_{attr_name}"
globs[field_name] = a
internal_arg_parts[field_name] = a
def_str = f"{conv_name}({def_str}, instance, {field_name})"
elif isinstance(d, Factory):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
def_str = f"{conv_name}({def_str})"
else:
globs[def_name] = c(d)
internal_arg_parts[def_name] = c(d)
lines.append(f" if instance.{attr_name} != {def_str}:")
lines.append(f" res['{kn}'] = {invoke}")
else: else:
# No default or no override. # No default or no override.
@@ -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]]:
@@ -235,13 +273,22 @@ def make_dict_unstructure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 23.2.0 *_cattrs_use_alias* .. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false* .. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
""" """
origin = get_origin(cl) origin = get_origin(cl)
attrs = adapted_fields(origin or cl) # type: ignore attrs = adapted_fields(origin or cl) # type: ignore
mapping = {} mapping = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if is_generic(cl): if is_generic(cl):
mapping = generate_mapping(cl, mapping) mapping = generate_mapping(cl, mapping)
@@ -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]:
@@ -314,7 +361,13 @@ def make_dict_structure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 24.1.0 .. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
""" """
cl_name = cl.__name__ cl_name = cl.__name__
@@ -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]:
@@ -706,14 +784,20 @@ def make_dict_structure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False` :param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included. will be included.
.. versionadded:: 23.2.0 *_cattrs_use_alias* .. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false* .. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0 .. versionchanged:: 23.2.0
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
take their values from the given converter by default. take their values from the given converter by default.
.. versionchanged:: 24.1.0 .. versionchanged:: 24.1.0
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
converter by default. converter by default.
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
""" """
mapping = {} mapping = {}
+17 -1
View File
@@ -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:
+61 -35
View File
@@ -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]:
+2 -7
View File
@@ -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):
+2 -2
View File
@@ -99,11 +99,11 @@ def configure_converter(converter: BaseConverter):
# datetime inherits from date, so identity unstructure hook used # datetime inherits from date, so identity unstructure hook used
# here to prevent the date unstructure hook running. # here to prevent the date unstructure hook running.
converter.register_unstructure_hook(datetime, 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
) )
+1 -1
View File
@@ -37,7 +37,7 @@ def configure_converter(converter: BaseConverter):
) )
converter.register_unstructure_hook(date, lambda v: v.isoformat()) converter.register_unstructure_hook(date, lambda v: v.isoformat())
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v)) converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
converter.register_unstructure_hook_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
) )
+1 -1
View File
@@ -52,7 +52,7 @@ def configure_converter(converter: BaseConverter) -> None:
converter.register_unstructure_hook_factory( converter.register_unstructure_hook_factory(
is_literal_containing_enums, literals_with_enums_unstructure_factory is_literal_containing_enums, literals_with_enums_unstructure_factory
) )
converter.register_unstructure_hook_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)
+1 -1
View File
@@ -46,7 +46,7 @@ def configure_converter(converter: BaseConverter) -> None:
converter.register_structure_hook( converter.register_structure_hook(
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date() date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
) )
converter.register_unstructure_hook_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
) )
+14 -3
View File
@@ -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))
+2 -2
View File
@@ -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
+9 -3
View File
@@ -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
) )
+94
View File
@@ -0,0 +1,94 @@
"""Preconfigured converters for tomllib."""
from base64 import b85decode, b85encode
from collections.abc import Set
from datetime import date, datetime
from enum import Enum
from operator import attrgetter
from typing import Any, TypeVar, Union
try:
from tomllib import loads
except ImportError:
from tomli import loads
try:
from tomli_w import dumps
except ImportError: # pragma: nocover
dumps = None
from .._compat import is_mapping, is_subclass
from ..converters import BaseConverter, Converter
from ..fns import identity
from ..strategies import configure_union_passthrough
from . import validate_datetime, wrap
__all__ = ["TomllibConverter", "configure_converter", "make_converter"]
T = TypeVar("T")
_enum_value_getter = attrgetter("_value_")
class TomllibConverter(Converter):
"""A converter subclass specialized for tomllib."""
if dumps is not None:
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
def loads(self, data: str, cl: type[T], **kwargs: Any) -> T:
return self.structure(loads(data, **kwargs), cl)
def configure_converter(converter: BaseConverter):
"""
Configure the converter for use with the tomllib library.
* bytes are serialized as base85 strings
* sets are serialized as lists
* tuples are serializas as lists
* mapping keys are coerced into strings when unstructuring
* dates and datetimes are left for tomllib to handle
"""
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
converter.register_unstructure_hook(
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
)
@converter.register_unstructure_hook_factory(is_mapping)
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
key_handler = str
args = getattr(cl, "__args__", None)
if args:
if is_subclass(args[0], str):
key_handler = _enum_value_getter if is_subclass(args[0], Enum) else None
elif is_subclass(args[0], bytes):
def key_handler(k: bytes):
return b85encode(k).decode("utf8")
return converter.gen_unstructure_mapping(
cl, unstructure_to=unstructure_to, key_handler=key_handler
)
converter.register_unstructure_hook(datetime, identity)
converter.register_structure_hook(datetime, validate_datetime)
converter.register_unstructure_hook(date, identity)
converter.register_structure_hook(
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
)
configure_union_passthrough(Union[str, int, float, bool], converter)
@wrap(TomllibConverter)
def make_converter(*args: Any, **kwargs: Any) -> TomllibConverter:
kwargs["unstruct_collection_overrides"] = {
Set: list,
tuple: list,
**kwargs.get("unstruct_collection_overrides", {}),
}
res = TomllibConverter(*args, **kwargs)
configure_converter(res)
return res
+1 -1
View File
@@ -47,7 +47,7 @@ def configure_converter(converter: BaseConverter):
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v)) converter.register_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
) )
+20 -7
View File
@@ -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)
+65 -45
View File
@@ -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))
+27
View File
@@ -0,0 +1,27 @@
import sys
if sys.version_info <= (3, 13):
def subclasses(cls: type) -> list[type]:
"""A proxy for `cls.__subclasses__()` on older Pythons."""
return cls.__subclasses__()
else:
def subclasses(cls: type) -> list[type]:
"""A helper for getting subclasses of a class.
Filters out duplicate subclasses of slot dataclasses and attrs classes.
"""
return [
cl
for cl in cls.__subclasses__()
if (
not (
"__slots__" not in cl.__dict__
and hasattr(cls, "__dataclass_params__")
and cls.__dataclass_params__.slots
)
and not hasattr(cls, "__attrs_base_of_slotted__")
)
]
+19
View File
@@ -0,0 +1,19 @@
from __future__ import annotations
class ExceptionGroup(Exception):
"""Minimal backport used by bundled libs on Python < 3.11."""
def __new__(cls, message, exceptions):
obj = super().__new__(cls, message)
obj.message = message
obj.exceptions = tuple(exceptions)
return obj
def __init__(self, message, exceptions):
super().__init__(message)
self.message = message
self.exceptions = tuple(exceptions)
def derive(self, exceptions):
return self.__class__(self.message, exceptions)
@@ -1 +1 @@
pip uv
@@ -1,4 +1,4 @@
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
importlib_metadata-6.8.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 importlib_metadata-6.8.0.dist-info/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
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages" __summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging" __uri__ = "https://github.com/pypa/packaging"
__version__ = "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"
-1
View File
@@ -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
""" """
+36 -36
View File
@@ -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) and hasattr(
if version == _GLibCVersion(2, 12): _manylinux, "manylinux2010_compatible"
if hasattr(_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.
if glibc_version in _LEGACY_MANYLINUX_MAP: # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
if _is_compatible(arch, glibc_version):
yield f"{legacy_tag}_{arch}" yield f"{legacy_tag}_{arch}"
+1 -1
View File
@@ -49,7 +49,7 @@ def _get_musl_version(executable: str) -> _MuslVersion | None:
return None return None
if ld is None or "musl" not in ld: if ld is None or "musl" not in ld:
return None return None
proc = subprocess.run([ld], 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)
+50 -10
View File
@@ -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
""" """
+17 -45
View File
@@ -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()
+24 -26
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import contextlib import contextlib
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import 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()
+302
View File
@@ -0,0 +1,302 @@
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from .errors import _ErrorCollector
from .requirements import Requirement
__all__ = [
"CyclicDependencyGroup",
"DependencyGroupInclude",
"DependencyGroupResolver",
"DuplicateGroupNames",
"InvalidDependencyGroupObject",
"resolve_dependency_groups",
]
def __dir__() -> list[str]:
return __all__
# -----------
# Error Types
# -----------
class DuplicateGroupNames(ValueError):
"""
The same dependency groups were defined twice, with different non-normalized names.
"""
class CyclicDependencyGroup(ValueError):
"""
The dependency group includes form a cycle.
"""
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
self.requested_group = requested_group
self.group = group
self.include_group = include_group
if include_group == group:
reason = f"{group} includes itself"
else:
reason = f"{include_group} -> {group}, {group} -> {include_group}"
super().__init__(
"Cyclic dependency group include while resolving "
f"{requested_group}: {reason}"
)
# in the PEP 735 spec, the tables in dependency group lists were described as
# "Dependency Object Specifiers", but the only defined type of object was a
# "Dependency Group Include" -- hence the naming of this error as "Object"
class InvalidDependencyGroupObject(ValueError):
"""
A member of a dependency group was identified as a dict, but was not in a valid
format.
"""
# ------------------------
# Object Model & Interface
# ------------------------
class DependencyGroupInclude:
__slots__ = ("include_group",)
def __init__(self, include_group: str) -> None:
"""
Initialize a DependencyGroupInclude.
:param include_group: The name of the group referred to by this include.
"""
self.include_group = include_group
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.include_group!r})"
class DependencyGroupResolver:
"""
A resolver for Dependency Group data.
This class handles caching, name normalization, cycle detection, and other
parsing requirements. There are only two public methods for exploring the data:
``lookup()`` and ``resolve()``.
:param dependency_groups: A mapping, as provided via pyproject
``[dependency-groups]``.
"""
def __init__(
self,
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
) -> None:
errors = _ErrorCollector()
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
# a map of group names to parsed data
self._parsed_groups: dict[
str, tuple[Requirement | DependencyGroupInclude, ...]
] = {}
# a map of group names to their ancestors, used for cycle detection
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
# a cache of completed resolutions to Requirement lists
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
errors.finalize("[dependency-groups] data was invalid")
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
"""
Lookup a group name, returning the parsed dependency data for that group.
This will not resolve includes.
:param group: the name of the group to lookup
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._parse_group(group, errors)
def resolve(self, group: str) -> tuple[Requirement, ...]:
"""
Resolve a dependency group to a list of requirements.
:param group: the name of the group to resolve
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._resolve(group, group, errors)
def _resolve(
self, group: str, requested_group: str, errors: _ErrorCollector
) -> tuple[Requirement, ...]:
"""
This is a helper for cached resolution to strings. It preserves the name of the
group which the user initially requested in order to present a clearer error in
the event that a cycle is detected.
:param group: The normalized name of the group to resolve.
:param requested_group: The group which was used in the original, user-facing
request.
"""
if group in self._resolve_cache:
return self._resolve_cache[group]
parsed = self._parse_group(group, errors)
resolved_group = []
for item in parsed:
if isinstance(item, Requirement):
resolved_group.append(item)
elif isinstance(item, DependencyGroupInclude):
include_group = _normalize_name(item.include_group)
# if a group is cyclic, record the error
# otherwise, follow the include_group reference
#
# this allows us to examine all includes in a group, even in the
# presence of errors
if include_group in self._include_graph_ancestors.get(group, ()):
errors.error(
CyclicDependencyGroup(
requested_group, group, item.include_group
)
)
else:
self._include_graph_ancestors[include_group] = (
*self._include_graph_ancestors.get(group, ()),
group,
)
resolved_group.extend(
self._resolve(include_group, requested_group, errors)
)
else: # pragma: no cover
raise NotImplementedError(
f"Invalid dependency group item after parse: {item}"
)
# in the event that errors were detected, present the group as empty and do not
# cache the result
# this ensures that repeated access to a cyclic group will raise multiple errors
if errors.errors:
return ()
self._resolve_cache[group] = tuple(resolved_group)
return self._resolve_cache[group]
def _parse_group(
self, group: str, errors: _ErrorCollector
) -> tuple[Requirement | DependencyGroupInclude, ...]:
# short circuit -- never do the work twice
if group in self._parsed_groups:
return self._parsed_groups[group]
if group not in self.dependency_groups:
errors.error(LookupError(f"Dependency group '{group}' not found"))
return ()
raw_group = self.dependency_groups[group]
if isinstance(raw_group, str):
errors.error(
TypeError(
f"Dependency group {group!r} contained a string rather than a list."
)
)
return ()
if not isinstance(raw_group, Sequence):
errors.error(
TypeError(f"Dependency group {group!r} is not a sequence type.")
)
return ()
elements: list[Requirement | DependencyGroupInclude] = []
for item in raw_group:
if isinstance(item, str):
# packaging.requirements.Requirement parsing ensures that this is a
# valid PEP 508 Dependency Specifier
# raises InvalidRequirement on failure
elements.append(Requirement(item))
elif isinstance(item, Mapping):
if tuple(item.keys()) != ("include-group",):
errors.error(
InvalidDependencyGroupObject(
f"Invalid dependency group item: {item!r}"
)
)
else:
include_group = item["include-group"]
elements.append(DependencyGroupInclude(include_group=include_group))
else:
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
self._parsed_groups[group] = tuple(elements)
return self._parsed_groups[group]
# --------------------
# Functional Interface
# --------------------
def resolve_dependency_groups(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
) -> tuple[str, ...]:
"""
Resolve a dependency group to a tuple of requirements, as strings.
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
from ``pyproject.toml``
:param groups: the name of the group(s) to resolve
"""
resolver = DependencyGroupResolver(dependency_groups)
return tuple(str(r) for group in groups for r in resolver.resolve(group))
# ----------------
# internal helpers
# ----------------
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
def _normalize_name(name: str) -> str:
return _NORMALIZE_PATTERN.sub("-", name).lower()
def _normalize_group_names(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
errors: _ErrorCollector,
) -> dict[str, Sequence[str | Mapping[str, str]]]:
original_names: dict[str, list[str]] = {}
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
for group_name, value in dependency_groups.items():
normed_group_name = _normalize_name(group_name)
original_names.setdefault(normed_group_name, []).append(group_name)
normalized_groups[normed_group_name] = value
for normed_name, names in original_names.items():
if len(names) > 1:
errors.error(
DuplicateGroupNames(
"Duplicate dependency group names: "
f"{normed_name} ({', '.join(names)})"
)
)
return normalized_groups
+325
View File
@@ -0,0 +1,325 @@
from __future__ import annotations
import dataclasses
import re
import urllib.parse
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
if TYPE_CHECKING: # pragma: no cover
import sys
from collections.abc import Collection
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
__all__ = [
"ArchiveInfo",
"DirInfo",
"DirectUrl",
"DirectUrlValidationError",
"VcsInfo",
]
def __dir__() -> list[str]:
return __all__
_T = TypeVar("_T")
class _FromMappingProtocol(Protocol): # pragma: no cover
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
return {key: value for key, value in data if value is not None}
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
"""Get a value from the dictionary and verify it's the expected type."""
if (value := d.get(key)) is None:
return None
if not isinstance(value, expected_type):
raise DirectUrlValidationError(
f"Unexpected type {type(value).__name__} "
f"(expected {expected_type.__name__})",
context=key,
)
return value
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
"""Get a required value from the dictionary and verify it's the expected type."""
if (value := _get(d, expected_type, key)) is None:
raise _DirectUrlRequiredKeyError(key)
return value
def _get_object(
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
) -> _FromMappingProtocolT | None:
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
try:
return target_type._from_dict(value)
except Exception as e:
raise DirectUrlValidationError(e, context=key) from e
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
)
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
if "@" not in netloc:
return netloc
user_pass, netloc_no_user_pass = netloc.split("@", 1)
if user_pass in safe_user_passwords:
return netloc
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
return netloc
return netloc_no_user_pass
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
"""url with user:password part removed unless it is formed with
environment variables as specified in PEP 610, or it is a safe user:password
such as `git`.
"""
parsed_url = urllib.parse.urlsplit(url)
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
return urllib.parse.urlunsplit(
(
parsed_url.scheme,
netloc,
parsed_url.path,
parsed_url.query,
parsed_url.fragment,
)
)
class DirectUrlValidationError(Exception):
"""Raised when when input data is not spec-compliant."""
context: str | None = None
message: str
def __init__(
self,
cause: str | Exception,
*,
context: str | None = None,
) -> None:
if isinstance(cause, DirectUrlValidationError):
if cause.context:
self.context = (
f"{context}.{cause.context}" if context else cause.context
)
else:
self.context = context # pragma: no cover
self.message = cause.message
else:
self.context = context
self.message = str(cause)
def __str__(self) -> str:
if self.context:
return f"{self.message} in {self.context!r}"
return self.message
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
def __init__(self, key: str) -> None:
super().__init__("Missing required value", context=key)
@dataclasses.dataclass(frozen=True, init=False)
class VcsInfo:
vcs: str
commit_id: str
requested_revision: str | None = None
def __init__(
self,
*,
vcs: str,
commit_id: str,
requested_revision: str | None = None,
) -> None:
object.__setattr__(self, "vcs", vcs)
object.__setattr__(self, "commit_id", commit_id)
object.__setattr__(self, "requested_revision", requested_revision)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
# We can't validate vcs value because is not closed.
return cls(
vcs=_get_required(d, str, "vcs"),
requested_revision=_get(d, str, "requested_revision"),
commit_id=_get_required(d, str, "commit_id"),
)
@dataclasses.dataclass(frozen=True, init=False)
class ArchiveInfo:
hashes: Mapping[str, str] | None = None
def __init__(
self,
*,
hashes: Mapping[str, str] | None = None,
) -> None:
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
raise DirectUrlValidationError(
"Hash values must be strings", context="hashes"
)
legacy_hash = _get(d, str, "hash")
if legacy_hash is not None:
if "=" not in legacy_hash:
raise DirectUrlValidationError(
"Invalid hash format (expected '<algorithm>=<hash>')",
context="hash",
)
hash_algorithm, hash_value = legacy_hash.split("=", 1)
if hashes is None:
# if `hashes` are not present, we can derive it from the legacy `hash`
hashes = {hash_algorithm: hash_value}
else:
# if `hashes` are present, the legacy `hash` must match one of them
if hash_algorithm not in hashes:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"is not present in hashes field",
context="hashes",
)
if hashes[hash_algorithm] != hash_value:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"has different value in hashes field",
context="hash",
)
return cls(hashes=hashes)
@dataclasses.dataclass(frozen=True, init=False)
class DirInfo:
editable: bool | None = None
def __init__(
self,
*,
editable: bool | None = None,
) -> None:
object.__setattr__(self, "editable", editable)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
return cls(
editable=_get(d, bool, "editable"),
)
@dataclasses.dataclass(frozen=True, init=False)
class DirectUrl:
"""A class representing a direct URL."""
url: str
archive_info: ArchiveInfo | None = None
vcs_info: VcsInfo | None = None
dir_info: DirInfo | None = None
subdirectory: str | None = None # XXX Path or str?
def __init__(
self,
*,
url: str,
archive_info: ArchiveInfo | None = None,
vcs_info: VcsInfo | None = None,
dir_info: DirInfo | None = None,
subdirectory: str | None = None,
) -> None:
object.__setattr__(self, "url", url)
object.__setattr__(self, "archive_info", archive_info)
object.__setattr__(self, "vcs_info", vcs_info)
object.__setattr__(self, "dir_info", dir_info)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
direct_url = cls(
url=_get_required(d, str, "url"),
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
dir_info=_get_object(d, DirInfo, "dir_info"),
subdirectory=_get(d, str, "subdirectory"),
)
if (
bool(direct_url.vcs_info)
+ bool(direct_url.archive_info)
+ bool(direct_url.dir_info)
) != 1:
raise DirectUrlValidationError(
"Exactly one of vcs_info, archive_info, dir_info must be present"
)
if direct_url.dir_info is not None and not direct_url.url.startswith("file://"):
raise DirectUrlValidationError(
"URL scheme must be file:// when dir_info is present",
context="url",
)
# XXX subdirectory must be relative, can we, should we validate that here?
return direct_url
@classmethod
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
"""Create and validate a DirectUrl instance from a JSON dictionary."""
return cls._from_dict(d)
def to_dict(
self,
*,
generate_legacy_hash: bool = False,
strip_user_password: bool = True,
safe_user_passwords: Collection[str] = ("git",),
) -> Mapping[str, Any]:
"""Convert the DirectUrl instance to a JSON dictionary.
:param generate_legacy_hash: If True, include a legacy `hash` field in
`archive_info` for backward compatibility with tools that don't
support the `hashes` field.
:param strip_user_password: If True, strip user:password from the URL
unless it is formed with environment variables as specified in PEP
610, or it is a safe user:password such as `git`.
:param safe_user_passwords: A collection of user:password strings that
should not be stripped from the URL even if `strip_user_password` is
True.
"""
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
if strip_user_password:
res["url"] = _strip_url(self.url, safe_user_passwords)
return res
def validate(self) -> None:
"""Validate the DirectUrl instance against the specification.
Raises :class:`DirectUrlValidationError` if invalid.
"""
self.from_dict(self.to_dict())
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
import contextlib
import dataclasses
import sys
import typing
__all__ = ["ExceptionGroup"]
def __dir__() -> list[str]:
return __all__
if sys.version_info >= (3, 11): # pragma: no cover
from builtins import ExceptionGroup
else: # pragma: no cover
class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
If :external:exc:`ExceptionGroup` is already defined by Python itself,
that version is used instead.
"""
message: str
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
@dataclasses.dataclass
class _ErrorCollector:
"""
Collect errors into ExceptionGroups.
Used like this:
collector = _ErrorCollector()
# Add a single exception
collector.error(ValueError("one"))
# Supports nesting, including combining ExceptionGroups
with collector.collect():
raise ValueError("two")
collector.finalize("Found some errors")
Since making a collector and then calling finalize later is a common pattern,
a convenience method ``on_exit`` is provided.
"""
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
def finalize(self, msg: str) -> None:
"""Raise a group exception if there are any errors."""
if self.errors:
raise ExceptionGroup(msg, self.errors)
@contextlib.contextmanager
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
"""
Calls finalize if no uncollected errors were present.
Uncollected errors are raised normally.
"""
yield self
self.finalize(msg)
@contextlib.contextmanager
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
"""
Context manager to collect errors into the error list.
Must be inside loops, as only one error can be collected at a time.
"""
error_classes = err_cls or (Exception,)
try:
yield
except ExceptionGroup as error:
self.errors.extend(error.exceptions)
except error_classes as error:
self.errors.append(error)
def error(
self,
error: Exception,
) -> None:
"""Add an error to the list."""
self.errors.append(error)
+52 -11
View File
@@ -34,7 +34,7 @@ from __future__ import annotations
import re 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(" )", ")"),
) )
+41 -1
View File
@@ -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},
+185 -55
View File
@@ -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,
) -> MarkerList | MarkerAtom | str:
if not isinstance(result, tuple):
return result
lhs, op, rhs = result
if isinstance(lhs, Variable) and lhs.value == "extra":
normalized_extra = canonicalize_name(rhs.value)
rhs = Value(normalized_extra)
elif isinstance(rhs, Variable) and rhs.value == "extra":
normalized_extra = canonicalize_name(lhs.value)
lhs = Value(normalized_extra)
return lhs, op, rhs
def _normalize_extra_values(results: MarkerList) -> MarkerList:
""" """
Normalize extra values. Normalize extra values.
""" """
if isinstance(results[0], tuple):
lhs, op, rhs = results[0] return [_normalize_extras(r) for r in results]
if isinstance(lhs, Variable) and lhs.value == "extra":
normalized_extra = canonicalize_name(rhs.value)
rhs = Value(normalized_extra)
elif isinstance(rhs, Variable) and rhs.value == "extra":
normalized_extra = canonicalize_name(lhs.value)
lhs = Value(normalized_extra)
results[0] = lhs, op, rhs
return results
def _format_marker( def _format_marker(
@@ -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"] groups.append([])
if marker == "or": elif marker == "and":
groups.append([]) 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
+171 -69
View File
@@ -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 .licenses import NormalizedLicenseExpression from .errors import ExceptionGroup, _ErrorCollector
if typing.TYPE_CHECKING:
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)
+905
View File
@@ -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
+48 -10
View File
@@ -3,7 +3,7 @@
# for complete details. # for complete details.
from __future__ import annotations from __future__ import annotations
from typing import Any, Iterator from typing import Iterator
from ._parser import parse_requirement as _parse_requirement from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError from ._tokenizer import ParserSyntaxError
@@ -11,6 +11,15 @@ from .markers import Marker, _normalize_extra_values
from .specifiers import SpecifierSet from .specifiers import SpecifierSet
from .utils import canonicalize_name from .utils import canonicalize_name
__all__ = [
"InvalidRequirement",
"Requirement",
]
def __dir__() -> list[str]:
return __all__
class InvalidRequirement(ValueError): class InvalidRequirement(ValueError):
""" """
@@ -24,6 +33,16 @@ class Requirement:
Parse a given requirement string into its parts, such as name, specifier, Parse a given requirement string into its parts, such as name, specifier,
URL, and extras. Raises InvalidRequirement on a badly-formed requirement URL, and extras. Raises InvalidRequirement on a badly-formed requirement
string. string.
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.
""" """
# TODO: Can we test whether something is contained within a requirement? # TODO: Can we test whether something is contained within a requirement?
@@ -57,28 +76,47 @@ class Requirement:
yield str(self.specifier) yield str(self.specifier)
if self.url: if self.url:
yield f"@ {self.url}" yield f" @ {self.url}"
if self.marker: if self.marker:
yield " " yield " "
if self.marker: if self.marker:
yield f"; {self.marker}" yield f"; {self.marker}"
def __getstate__(self) -> str:
# Return the requirement string for compactness and stability.
# Re-parsed on load to reconstruct all fields.
return str(self)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the requirement string.
try:
tmp = Requirement(state)
except InvalidRequirement as exc:
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
self.name = tmp.name
self.url = tmp.url
self.extras = tmp.extras
self.specifier = tmp.specifier
self.marker = tmp.marker
return
if isinstance(state, dict):
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
self.__dict__.update(state)
return
raise TypeError(f"Cannot restore Requirement from {state!r}")
def __str__(self) -> str: def __str__(self) -> str:
return "".join(self._iter_parts(self.name)) return "".join(self._iter_parts(self.name))
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Requirement('{self}')>" return f"<{self.__class__.__name__}({str(self)!r})>"
def __hash__(self) -> int: def __hash__(self) -> int:
return hash( return hash(tuple(self._iter_parts(canonicalize_name(self.name))))
(
self.__class__.__name__,
*self._iter_parts(canonicalize_name(self.name)),
)
)
def __eq__(self, other: Any) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, Requirement): if not isinstance(other, Requirement):
return NotImplemented return NotImplemented
File diff suppressed because it is too large Load Diff

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