Compare commits

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

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

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

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

- Adds signature_help module to parse and present signatures
- Integrates with LSP server to provide signature help on the client
- Adds tests for custom and built-in procedures
2026-08-17 08:31:15 +02:00
Christoph 4ca1b0cce9 Update version to 2026.6.201 2026-06-22 06:50:56 +00:00
Christoph 6051260a8d Merge pull request 'Fix single-line brace formatting to preserve internal whitespace' (#31) from bug_fixing into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #31
2026-06-22 06:50:16 +00:00
Christoph Brandau dfe15dd3db Fix single-line brace formatting to preserve internal whitespace
The formatter now calculates and passes explicit leading and trailing whitespace around expression content to the `_brace` method. This ensures correct rendering of single-line expressions that include braces.
2026-06-22 08:49:22 +02:00
Christoph 5d1c402d2a Update version to 2026.6.200 2026-06-22 06:34:00 +00:00
Christoph 88f834ae79 Merge pull request 'Fix Bug for Formatting' (#30) from bug_fixing into main
/ build_and_publish (release) Successful in 37s
Reviewed-on: #30
2026-06-22 06:32:26 +00:00
Christoph Brandau 3b233389c8 Fix Tcl formatting bug by adjusting formatter options 2026-06-22 08:29:48 +02:00
Christoph 67bd9de18a Update version to 2026.6.100 2026-06-18 19:03:03 +00:00
Christoph 495b7b7a07 Update package.json
/ build_and_publish (release) Failing after 14m37s
2026-06-18 19:01:47 +00:00
Christoph 8b39c4d250 Update version to 2026.6.100 2026-06-18 18:41:28 +00:00
Christoph 49e8cdd7df Merge pull request 'Bug fixing' (#29) from bug_fixing into main
/ build_and_publish (release) Has been cancelled
Reviewed-on: #29
2026-06-18 18:39:23 +00:00
Christoph Brandau f60c4563e4 Delegate TCL folding to server, fix bugs, and enhance stability
- Move TCL folding range computation from client to language server, avoiding duplicate regions.
- Serialize language server restarts to prevent multiple server instances running concurrently.
- Pin server-side Python dependencies to specific minor versions for improved stability and predictability.
- Make debug server connection non-fatal, allowing the language server to start even if the debugger isn't attached.
- Optimize semantic token generation by caching Tclint plugin commands.
2026-06-18 20:38:47 +02:00
Christoph Brandau 3ad3847045 add new features and fix bugs 2026-06-18 17:31:51 +02:00
Christoph Brandau 41f331d4c4 add updates libs 2026-06-18 17:21:58 +02:00
Christoph 91ce762570 Update version to 2025.9.200 2025-09-01 13:24:13 +00:00
Christoph d60d3ec942 Update package.json
/ build_and_publish (release) Successful in 40s
2025-09-01 13:22:58 +00:00
Christoph e52722e353 Update README.md 2025-09-01 13:21:36 +00:00
Christoph a8bf55882e Merge pull request 'feat: use uv instead of pip' (#28) from change_to_uv_ into main
Reviewed-on: #28
2025-08-16 20:23:34 +00:00
Christoph 377bddf579 feat: use uv instead of pip 2025-08-16 22:22:28 +02:00
Christoph cdc1cb4805 Update version to 2025.9.100 2025-08-16 16:35:58 +00:00
Christoph 34430097d8 Update README.md
/ build_and_publish (release) Failing after 25s
2025-08-16 09:43:44 +00:00
Christoph dcf447ed4f Merge branch 'relicense-agpl3' 2025-08-16 11:33:32 +02:00
Christoph 783a9aa7d7 Relicense to AGPL-3.0-or-later 2025-08-16 11:29:09 +02:00
Christoph 63638d6b3b Update LICENSE 2025-08-16 09:16:37 +00:00
Christoph Brandau af8cd58fda Merge branch 'main' of https://git.cbsk-tech.de/Christoph/nx_post_support 2025-08-15 13:13:15 +02:00
Christoph Brandau df0f3d7a61 add description 2025-08-15 13:12:41 +02:00
Christoph 72906a1930 Update version to 2025.8.400 2025-08-15 07:34:53 +00:00
Christoph 068cf2d1c3 Update README.md
/ build_and_publish (release) Successful in 42s
2025-08-15 07:33:26 +00:00
Christoph ad18a3dcf3 Update README.md 2025-08-15 07:32:58 +00:00
Christoph Brandau bd4b17825c add: Outline support for DEF and CDL files 2025-08-15 07:29:01 +02:00
Christoph Brandau 5d6182e91f double value MOM_add_to_block_buffer rename to MOM_add_to_line_buffer 2025-08-15 07:18:38 +02:00
Christoph Brandau 63e9c429bd update ReadMe 2025-08-14 07:57:40 +02:00
Christoph 7fdafe484e Update version to 2025.8.300 2025-08-12 11:26:06 +00:00
Christoph Brandau 5cba6763f1 Merge branch 'new_features'
/ build_and_publish (release) Successful in 36s
2025-08-12 13:21:27 +02:00
Christoph Brandau e20bb85ac6 fix: wrong highligthing 2025-08-12 13:18:08 +02:00
Christoph 98a0b2197d Update version to 2025.8.202 2025-08-12 09:38:34 +00:00
Christoph Brandau 579847b56d fix formatter
/ build_and_publish (release) Successful in 33s
2025-08-12 11:35:54 +02:00
Christoph Brandau 618d6b478b fix: formatter when \ was deleting 2025-08-12 11:28:10 +02:00
Christoph Brandau 480978d6bb Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features
/ build_and_publish (release) Failing after 31s
2025-08-12 11:27:20 +02:00
Christoph Brandau 04798328bd fix: formatter 2025-08-12 11:27:14 +02:00
Christoph Brandau ae835089b6 fix: formatter when \ was deleting 2025-08-12 11:26:52 +02:00
Christoph 0ed1e887fc Update version to 2025.9.100 2025-08-12 07:21:23 +00:00
Christoph Brandau 12b7b62aa5 add variable indexing keep track of postion
/ build_and_publish (release) Successful in 39s
2025-08-12 09:07:04 +02:00
Christoph Brandau 7263b6f530 optimze Server 2025-08-12 07:48:05 +02:00
Christoph Brandau c0635e48ea Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features 2025-08-12 07:15:21 +02:00
Christoph Brandau 8fb98d2f40 refacxtor the code 2025-08-12 07:15:18 +02:00
Christoph f24b280edf mod: Keywords 2025-08-11 22:06:49 +02:00
Christoph b61cff3b3b Update version to 2025.9.4 2025-08-11 19:46:20 +00:00
Christoph cf62044d3e Update version to 2025.8.201 2025-08-11 19:37:32 +00:00
Christoph feba88212f fix: some highlight error
/ build_and_publish (release) Successful in 29s
2025-08-11 21:36:04 +02:00
Christoph aeebb5c964 fix: some highlight error
/ build_and_publish (release) Successful in 38s
2025-08-11 21:35:14 +02:00
Christoph b141818251 Update version to 2025.9.3 2025-08-11 16:41:00 +00:00
Christoph Brandau 72f894b05d add goto
/ build_and_publish (release) Successful in 31s
2025-08-11 18:40:06 +02:00
Christoph Brandau 0a26cb5c0f add outline to lsp 2025-08-11 18:28:23 +02:00
Christoph b3cac5c292 Update version to 2025.9.2 2025-08-11 15:20:46 +00:00
Christoph Brandau ef558c3aa3 update pipline
/ build_and_publish (release) Successful in 28s
2025-08-11 17:19:47 +02:00
Christoph Brandau 9d5ef23162 update pipline
/ build_and_publish (release) Failing after 26s
2025-08-11 17:16:27 +02:00
Christoph f659db1147 Update package.json 2025-08-11 15:12:55 +00:00
Christoph Brandau c25bf79427 update pipline
/ build_and_publish (release) Failing after 32s
2025-08-11 17:00:14 +02:00
Christoph b059a75205 Update version to 2025.9.001 2025-08-11 14:24:17 +00:00
Christoph a90e15514b Update .gitea/workflows/build_and_puplish.yml
/ build_and_publish (release) Failing after 31s
2025-08-11 14:23:08 +00:00
Christoph Brandau 2c2579319d update hover feature
/ build_and_publish (release) Failing after 27s
2025-08-11 16:20:14 +02:00
Christoph Brandau b38fab1428 Merge branch 'new_features' of https://git.cbsk-tech.de/Christoph/nx_post_support into new_features 2025-08-11 16:05:58 +02:00
Christoph Brandau bc745622b9 add first proc doc 2025-08-11 15:59:09 +02:00
Christoph Brandau 0bb4ef731e add first proc doc 2025-08-11 10:15:58 +02:00
Christoph 27e2b3bf6e Merge pull request 'update' (#25) from tcl_language_support into main
/ build_and_publish (release) Successful in 33s
Reviewed-on: #25
2025-08-09 21:35:09 +00:00
Christoph f55c3ba904 Update version to 2025.8.200 2025-08-09 12:09:07 +00:00
Christoph cb430a5553 Update version to 2025.8.2
/ build_and_publish (release) Successful in 33s
2025-08-09 12:06:50 +00:00
Christoph 637a28ad82 Update version to 2025.08.1
/ build_and_publish (release) Successful in 43s
2025-08-09 11:57:08 +00:00
Christoph 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
Christoph 32061809a7 Merge pull request 'test new pipline' (#20) from tcl_language_support into main
/ build_and_publish (push) Successful in 47s
Reviewed-on: #20
2025-08-03 19:27:59 +00:00
Christoph f762c42e00 test new pipline 2025-08-03 21:27:09 +02:00
Christoph 4ccdf9e9f3 Update version to 0.4.11 2025-08-03 19:09:05 +00:00
Christoph b86d4f7b58 Merge pull request 'tcl_language_support' (#19) from tcl_language_support into main
/ build_and_publish (push) Successful in 30s
Reviewed-on: #19
2025-08-03 19:07:19 +00:00
Christoph 0318532308 add some features 2025-08-03 21:04:14 +02:00
Christoph Brandau 5e2e85830b update 2025-08-01 13:35:02 +02:00
Christoph Brandau 98e2d104a8 update lsp_server 2025-07-31 16:19:09 +02:00
Christoph Brandau 44fd772151 Merge branch 'tcl_language_support' of https://git.cbsk-tech.de/Christoph/nx_post_support into tcl_language_support 2025-07-31 09:38:10 +02:00
Christoph Brandau d90f9b5425 adjsut completion Items 2025-07-31 09:38:02 +02:00
Christoph fed8b84b46 add more semantic tokens 2025-07-30 22:10:27 +02:00
Christoph Brandau ed045aa459 update completion items 2025-07-30 17:28:18 +02:00
Christoph Brandau 8da61abfce Merge branch 'main' into tcl_language_support 2025-07-28 22:23:08 +02:00
Christoph Brandau 0ea2cbec87 add noxfile.py 2025-07-28 22:21:49 +02:00
Christoph d99d7c6bf6 Update version to 0.4.10 2025-07-28 20:19:47 +00:00
Christoph 3a7c8ea774 Merge pull request 'tcl_language_support' (#18) from tcl_language_support into main
/ build_and_publish (push) Successful in 34s
Reviewed-on: #18
2025-07-28 20:17:42 +00:00
Christoph Brandau 9a56aca6ef modify linter / checks 2025-07-28 22:15:19 +02:00
Christoph 4d79f268b8 add linter 2025-07-27 22:11:16 +02:00
Christoph c6f0758b97 use tclint as parser / formatter 2025-07-27 17:55:48 +02:00
Christoph Brandau c34dac847a update parser 2025-07-25 16:02:39 +02:00
Christoph Brandau 94181497bd fix item list 2025-07-25 12:19:38 +02:00
Christoph Brandau d391b5dd79 add more items 2025-07-25 11:40:09 +02:00
Christoph 666c537f07 add tclint 2025-07-24 21:31:08 +02:00
Christoph Brandau e842d9d74e update completion list 2025-07-24 17:16:38 +02:00
Christoph Brandau c9a07ae1b9 use tclint 2025-07-23 17:12:24 +02:00
Christoph Brandau 64fa802cd6 update parser 2025-07-22 18:41:20 +02:00
Christoph 7984b1648c Update version to 0.4.9 2025-07-22 05:25:58 +00:00
294 changed files with 28876 additions and 11660 deletions
+41 -7
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,9 +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
uses: softprops/action-gh-release@v1
with:
files: "nx-post-support-*.vsix"
# generate_release_notes: true # Automatische Release Notes
# draft: false # N
# prerelease: false
+1
View File
@@ -9,3 +9,4 @@ __pycache__
.nox .nox
*.g4 *.g4
.antlr .antlr
.claude
+35 -12
View File
@@ -10,13 +10,21 @@
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"runtimeExecutable": "${execPath}", "runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"], "args": [
"outFiles": ["${workspaceFolder}/client/**/*.js"], "--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
"!**/node_modules/**"
],
"skipFiles": ["<node_internals>/**"],
"autoAttachChildProcesses": true, "autoAttachChildProcesses": true,
"preLaunchTask": { "preLaunchTask": "NX Post Support: Compile Debug"
"type": "npm",
"script": "watch"
}
}, },
{ {
"name": "Python Attach", "name": "Python Attach",
@@ -34,10 +42,24 @@
"name": "Debug Extension (hidden)", "name": "Debug Extension (hidden)",
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"], "runtimeExecutable": "${execPath}",
"outFiles": ["${workspaceFolder}/client/**/*.js"], "args": [
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug",
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
],
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
"!**/node_modules/**"
],
"skipFiles": ["<node_internals>/**"],
"env": { "env": {
"USE_DEBUGPY": "True" "USE_DEBUGPY": "True",
"NXPS_DEBUG_HOST": "127.0.0.1",
"NXPS_DEBUG_PORT": "5678"
}, },
"presentation": { "presentation": {
"hidden": true, "hidden": true,
@@ -49,8 +71,9 @@
"name": "Python debug server (hidden)", "name": "Python debug server (hidden)",
"type": "debugpy", "type": "debugpy",
"request": "attach", "request": "attach",
"listen": { "host": "localhost", "port": 5678 }, "listen": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": true, "justMyCode": false,
"logToFile": true,
"presentation": { "presentation": {
"hidden": true, "hidden": true,
"group": "", "group": "",
@@ -63,7 +86,7 @@
"name": "Debug Extension and Python", "name": "Debug Extension and Python",
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"], "configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
"stopAll": true, "stopAll": true,
"preLaunchTask": "npm: watch", "preLaunchTask": "NX Post Support: Compile Debug",
"presentation": { "presentation": {
"hidden": false, "hidden": false,
"group": "", "group": "",
+57
View File
@@ -0,0 +1,57 @@
param(
[Parameter(Mandatory = $true)]
[string]$WorkspaceRoot,
[Parameter(Mandatory = $true)]
[string]$DebugRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function ConvertFrom-ExtendedWindowsPath {
param([string]$Path)
if ($Path.StartsWith("\\?\UNC\", [System.StringComparison]::OrdinalIgnoreCase)) {
return "\\" + $Path.Substring(8)
}
if ($Path.StartsWith("\\?\", [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring(4)
}
return $Path
}
$workspacePath = ConvertFrom-ExtendedWindowsPath $WorkspaceRoot
$workspaceItem = Get-Item -LiteralPath $workspacePath
if (-not $workspaceItem.PSIsContainer) {
throw "Workspace root is not a directory: $workspacePath"
}
$workspacePath = $workspaceItem.FullName
$debugPath = ConvertFrom-ExtendedWindowsPath $DebugRoot
$tempPath = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd("\")
$debugParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $debugPath)).TrimEnd("\")
if (-not $debugParent.Equals($tempPath, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Debug alias must be located directly below the user temp directory: $debugPath"
}
if (Test-Path -LiteralPath $debugPath) {
$debugItem = Get-Item -LiteralPath $debugPath -Force
if ($debugItem.LinkType -ne "Junction") {
throw "Debug alias exists but is not a junction: $debugPath"
}
$currentTarget = (Get-Item -LiteralPath $debugItem.Target).FullName
if (-not $currentTarget.Equals($workspacePath, [System.StringComparison]::OrdinalIgnoreCase)) {
# Removing a junction removes only the link, never the target directory.
Remove-Item -LiteralPath $debugPath -Force
}
}
if (-not (Test-Path -LiteralPath $debugPath)) {
New-Item -ItemType Junction -Path $debugPath -Target $workspacePath | Out-Null
}
Write-Output "Debug extension path: $debugPath -> $workspacePath"
& npm.cmd --prefix $workspacePath run compile:debug
exit $LASTEXITCODE
+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
} }
} }
+26
View File
@@ -0,0 +1,26 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "NX Post Support: Compile Debug",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoLogo",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"& { param([string]$WorkspaceRoot, [string]$DebugRoot); $scriptRoot = $WorkspaceRoot; if ($scriptRoot.StartsWith('\\\\?\\')) { $scriptRoot = $scriptRoot.Substring(4) }; & (Join-Path $scriptRoot '.vscode\\prepare-debug.ps1') -WorkspaceRoot $WorkspaceRoot -DebugRoot $DebugRoot; exit $LASTEXITCODE }",
"${workspaceFolder}",
"${env:TEMP}\\nx-post-support-vscode-debug"
],
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated",
"clear": true
}
}
]
}
+4
View File
@@ -11,7 +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
server/noxfile.py
client/**
+14
View File
@@ -1,3 +1,9 @@
## Unreleased
- Add signature help for custom TCL procedures and built-in NX/MOM procedures
- Clean stale TCL indexes on close, delete, and rename operations
- Make background parsing and index updates thread-safe
## [0.0.1] ## [0.0.1]
- Initial release - Initial release
@@ -13,3 +19,11 @@
## [0.2.0] ## [0.2.0]
- Add DEF File Support - Add DEF File Support
## [2026.6.100]
- Fix several bugs
## [2026.6.200]
- Fix foramtting bug
+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/>.
+75
View File
@@ -0,0 +1,75 @@
# NX Postprocessor Support
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
## Features
- **Syntax Highlighting** - Full syntax highlighting for CDL, TCL, and DEF files
- **Code Formatting** - Automatic code formatting for all supported file types
- **Multi-language Support** - Supports NX CDL, TCL, and DEF file formats
- **Intelligent Code Analysis** - Linting and error detection for postprocessor code
- **Auto-completion** - Context-aware code completion for faster development
- **Signature Help** - Shows parameters and documentation for custom and NX procedures
## Supported File Types
- `.cdl` - NX User Defined Event File
- `.tcl` - NX TCL postprocessor files
- `.def` - NX Definition files
## Installation
1. Install from the VS Code Marketplace
2. Install Python 3.11 or higher
3. Open any `.cdl`, `.tcl`, or `.def` file
4. The extension will automatically activate and provide language support
## Configuration
The extension can be configured through VS Code settings:
- `nx-post-support.interpreter` - Specify custom Python interpreter path for the language server
- `nx-post-support.formatter` - Enable/disable the TCL formatter (default: false)
- `nx-post-support.inlayHint` - Enable/disable inlay Hints (default: true)
## Usage
Simply open any supported file type and enjoy:
- Syntax highlighting
- Error detection and linting
- Code completion
- Code formatting (Format Document command)
- Hover information
- Signature help while entering procedure arguments
## Development and debugging
Install the root and client dependencies before the first debug session:
```powershell
npm install
npm install --prefix client
```
Use one of the checked-in VS Code launch configurations:
- **Run Extension** debugs the TypeScript extension host.
- **Debug Extension and Python** debugs both the TypeScript extension and the
Python language server. This is the recommended configuration for LSP work.
- **Python Attach** attaches manually to an already running Python process.
The launch configuration creates a fresh non-minified bundle with embedded
source maps and opens `test/test.tcl` so the extension activates immediately.
For combined debugging, the Python adapter listens on `127.0.0.1:5678`; the
language server waits for that adapter before initialization. The NX
Postprocessor Support output channel reports `Python debug mode: enabled` and
shows `_debug_server.py` in the server command when the debug path is active.
## Contributing
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
## License
AGPL-3.0 License - see LICENSE file for details.
+22
View File
@@ -14,6 +14,7 @@
"vscode-languageclient": "^9.0.1" "vscode-languageclient": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/vscode": "^1.96.0" "@types/vscode": "^1.96.0"
}, },
@@ -21,6 +22,27 @@
"vscode": "^1.96.0" "vscode": "^1.96.0"
} }
}, },
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.10.5", "version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
+1
View File
@@ -12,6 +12,7 @@
"vscode-languageclient": "^9.0.1" "vscode-languageclient": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"@types/vscode": "^1.96.0" "@types/vscode": "^1.96.0"
} }
+273
View File
@@ -137,6 +137,102 @@ export function hoverCdlHandler(document: vscode.TextDocument, position: vscode.
return undefined return undefined
} }
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export function cdlEventAtPosition(
document: vscode.TextDocument,
position: vscode.Position
): string | undefined {
const line = document.lineAt(position.line).text
const match = /^\s*EVENT\s+([^\s{]+)/.exec(line)
if (!match) {
return undefined
}
const declarationStart = match.index
const eventEnd = line.indexOf(match[1], match.index) + match[1].length
if (position.character < declarationStart || position.character > eventEnd) {
return undefined
}
return match[1]
}
export async function definitionCdlEventHandler(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Location[] | undefined> {
const eventName = cdlEventAtPosition(document, position)
if (!eventName) {
return undefined
}
const handlerName = `MOM_${eventName}`
try {
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
"vscode.executeWorkspaceSymbolProvider",
handlerName
)
const indexedLocations = (symbols || [])
.filter(
(symbol) =>
symbol.kind === vscode.SymbolKind.Function &&
(symbol.name === handlerName ||
symbol.name.endsWith(`::${handlerName}`))
)
.map((symbol) => symbol.location)
if (indexedLocations.length > 0) {
return indexedLocations
}
} catch {
// The Tcl language server may still be starting; use the file fallback below.
}
const declaration = new RegExp(
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
)
const tclFiles = await vscode.workspace.findFiles(
"**/*.tcl",
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
)
const locations: vscode.Location[] = []
for (const uri of tclFiles) {
if (token.isCancellationRequested) {
return undefined
}
let tclDocument: vscode.TextDocument
try {
tclDocument = await vscode.workspace.openTextDocument(uri)
} catch {
continue
}
for (let lineNumber = 0; lineNumber < tclDocument.lineCount; lineNumber++) {
const line = tclDocument.lineAt(lineNumber).text
const match = declaration.exec(line)
if (!match) {
continue
}
const start = line.indexOf(handlerName, match.index)
locations.push(
new vscode.Location(
uri,
new vscode.Range(
lineNumber,
start,
lineNumber,
start + handlerName.length
)
)
)
}
}
return locations.length > 0 ? locations : undefined
}
export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] { export function tclDocumentSymbolProvider(document: vscode.TextDocument): vscode.DocumentSymbol[] {
const symbols: vscode.DocumentSymbol[] = [] const symbols: vscode.DocumentSymbol[] = []
const lines = document.getText().split("\n") const lines = document.getText().split("\n")
@@ -271,3 +367,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 || []
}
+50 -18
View File
@@ -2,7 +2,7 @@
// Licensed under the MIT License. // Licensed under the MIT License.
import * as fsapi from "fs-extra" import * as fsapi from "fs-extra"
import { Disposable, env, LogOutputChannel } from "vscode" import { Disposable, env, LogOutputChannel, workspace } from "vscode"
import { State } from "vscode-languageclient" import { State } from "vscode-languageclient"
import { import {
LanguageClient, LanguageClient,
@@ -24,6 +24,13 @@ import { isVirtualWorkspace } from "./vscodeapi"
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings } export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
let _disposables: Disposable[] = []
export function disposeServerResources(): void {
_disposables.forEach((disposable) => disposable.dispose())
_disposables = []
}
async function createServer( async function createServer(
settings: ISettings, settings: ISettings,
serverId: string, serverId: string,
@@ -32,13 +39,26 @@ async function createServer(
initializationOptions: IInitOptions initializationOptions: IInitOptions
): Promise<LanguageClient> { ): Promise<LanguageClient> {
const command = settings.interpreter[0] const command = settings.interpreter[0]
if (!command) {
throw new Error("No Python interpreter is configured for the language server.")
}
const cwd = settings.cwd const cwd = settings.cwd
// Set debugger path needed for debugging python code. // Set debugger path needed for debugging python code.
const newEnv = { ...process.env } const newEnv = { ...process.env }
const debuggerPath = await getDebuggerPath()
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH) const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
if (newEnv.USE_DEBUGPY && debuggerPath) { const debugRequested = newEnv.USE_DEBUGPY?.toLowerCase() === "true"
if (debugRequested && !isDebugScript) {
throw new Error(`Python debug bootstrap not found: ${DEBUG_SERVER_SCRIPT_PATH}`)
}
const debuggerPath = debugRequested ? await getDebuggerPath() : undefined
if (debugRequested && !debuggerPath) {
throw new Error(
"Python debugging was requested, but the Python Debugger extension did not provide debugpy."
)
}
if (debugRequested && debuggerPath) {
newEnv.DEBUGPY_PATH = debuggerPath newEnv.DEBUGPY_PATH = debuggerPath
} else { } else {
newEnv.USE_DEBUGPY = "False" newEnv.USE_DEBUGPY = "False"
@@ -50,10 +70,13 @@ async function createServer(
// Set notification type // Set notification type
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
const args = const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH
newEnv.USE_DEBUGPY === "False" || !isDebugScript const interpreterArgs = settings.interpreter.slice(1)
? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH]) if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) {
: settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH]) interpreterArgs.push("-Xfrozen_modules=off")
}
const args = interpreterArgs.concat([serverScript])
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
traceInfo(`Server run command: ${[command, ...args].join(" ")}`) traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
const serverOptions: ServerOptions = { const serverOptions: ServerOptions = {
@@ -63,6 +86,7 @@ async function createServer(
} }
// Options to control the language client // Options to control the language client
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
const clientOptions: LanguageClientOptions = { const clientOptions: LanguageClientOptions = {
// Register the server for python documents // Register the server for python documents
documentSelector: isVirtualWorkspace() documentSelector: isVirtualWorkspace()
@@ -76,13 +100,16 @@ async function createServer(
outputChannel: outputChannel, outputChannel: outputChannel,
traceOutputChannel: outputChannel, traceOutputChannel: outputChannel,
revealOutputChannelOn: RevealOutputChannelOn.Never, revealOutputChannelOn: RevealOutputChannelOn.Never,
synchronize: {
fileEvents: tclFileWatcher
},
initializationOptions initializationOptions
} }
_disposables.push(tclFileWatcher)
return new LanguageClient(serverId, serverName, serverOptions, clientOptions) return new LanguageClient(serverId, serverName, serverOptions, clientOptions)
} }
let _disposables: Disposable[] = []
export async function restartServer( export async function restartServer(
serverId: string, serverId: string,
serverName: string, serverName: string,
@@ -92,16 +119,22 @@ export async function restartServer(
if (lsClient) { if (lsClient) {
traceInfo(`Server: Stop requested`) traceInfo(`Server: Stop requested`)
await lsClient.stop() await lsClient.stop()
_disposables.forEach((d) => d.dispose()) disposeServerResources()
_disposables = []
} }
const projectRoot = await getProjectRoot() const projectRoot = await getProjectRoot()
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true) const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, { try {
const newLSClient = await createServer(
workspaceSetting,
serverId,
serverName,
outputChannel,
{
settings: await getExtensionSettings(serverId, true), settings: await getExtensionSettings(serverId, true),
globalSettings: await getGlobalSettings(serverId, false) globalSettings: await getGlobalSettings(serverId, false)
}) }
)
traceInfo(`Server: Start requested.`) traceInfo(`Server: Start requested.`)
_disposables.push( _disposables.push(
newLSClient.onDidChangeState((e) => { newLSClient.onDidChangeState((e) => {
@@ -118,14 +151,13 @@ export async function restartServer(
} }
}) })
) )
try {
await newLSClient.start() await newLSClient.start()
} catch (ex) {
traceError(`Server: Start failed: ${ex}`)
return undefined
}
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel) const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level) await newLSClient.setTrace(level)
return newLSClient return newLSClient
} catch (ex) {
traceError(`Server: Start failed: ${ex}`)
disposeServerResources()
return undefined
}
} }
+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)
+70 -12
View File
@@ -12,7 +12,9 @@ import {
formatDefFile, formatDefFile,
isFirstLineMachine, isFirstLineMachine,
diagnosticHandler, diagnosticHandler,
tclDocumentSymbolProvider cdlDocumentSymbolProvider,
defDocumentSymbolProvider,
definitionCdlEventHandler
} from "./common/handlers" } from "./common/handlers"
import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging" import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging"
import { import {
@@ -22,13 +24,13 @@ import {
onDidChangePythonInterpreter, onDidChangePythonInterpreter,
resolveInterpreter resolveInterpreter
} from "./common/python" } from "./common/python"
import { restartServer } from "./common/server" import { disposeServerResources, restartServer } from "./common/server"
import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings" import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings"
import { loadServerDefaults } from "./common/setup" import { loadServerDefaults } from "./common/setup"
import { getLSClientTraceLevel } from "./common/utilities" import { getLSClientTraceLevel } from "./common/utilities"
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi" import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
let client: LanguageClient let client: LanguageClient | undefined
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
// This is required to get server name and module. This should be // This is required to get server name and module. This should be
@@ -36,6 +38,7 @@ export async function activate(context: vscode.ExtensionContext) {
const serverInfo = loadServerDefaults() const serverInfo = loadServerDefaults()
const serverName = serverInfo.name const serverName = serverInfo.name
const serverId = serverInfo.module const serverId = serverInfo.module
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
// Setup logging // Setup logging
const outputChannel = createOutputChannel(serverName) const outputChannel = createOutputChannel(serverName)
@@ -60,7 +63,7 @@ export async function activate(context: vscode.ExtensionContext) {
traceLog(`Module: ${serverInfo.module}`) traceLog(`Module: ${serverInfo.module}`)
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`) traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`)
const runServer = async () => { const runServerImpl = async () => {
const interpreter = getInterpreterFromSetting(serverId) const interpreter = getInterpreterFromSetting(serverId)
if (interpreter && interpreter.length > 0) { if (interpreter && interpreter.length > 0) {
if (checkVersion(await resolveInterpreter(interpreter))) { if (checkVersion(await resolveInterpreter(interpreter))) {
@@ -89,10 +92,25 @@ export async function activate(context: vscode.ExtensionContext) {
) )
} }
// Serialize server (re)starts. Overlapping triggers (interpreter change,
// config change, restart command, initial activation) would otherwise each
// read the stale module-level `client`, start a new server and leave the
// previous one running orphaned -> hints/hover shown multiple times.
let runServerQueue: Promise<void> = Promise.resolve()
const runServer = () => {
runServerQueue = runServerQueue.catch(() => undefined).then(() => runServerImpl())
return runServerQueue
}
if (!pythonDebugMode) {
context.subscriptions.push( context.subscriptions.push(
onDidChangePythonInterpreter(async () => { onDidChangePythonInterpreter(async () => {
await runServer() await runServer()
}), })
)
}
context.subscriptions.push(
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
if (checkIfConfigurationChanged(e, serverId)) { if (checkIfConfigurationChanged(e, serverId)) {
await runServer() await runServer()
@@ -104,6 +122,15 @@ export async function activate(context: vscode.ExtensionContext) {
) )
setImmediate(async () => { setImmediate(async () => {
if (pythonDebugMode) {
// A debugpy listen session is attached to exactly one process. Do not
// subscribe to interpreter changes during startup, as the Python
// extension can emit a duplicate event and restart that process.
traceLog("Python debug mode: starting one stable server session")
await runServer()
return
}
const interpreter = getInterpreterFromSetting(serverId) const interpreter = getInterpreterFromSetting(serverId)
if (interpreter === undefined || interpreter.length === 0) { if (interpreter === undefined || interpreter.length === 0) {
traceLog(`Python extension loading`) traceLog(`Python extension loading`)
@@ -114,6 +141,10 @@ export async function activate(context: vscode.ExtensionContext) {
} }
}) })
// Folding ranges for TCL are provided by the language server
// (folding_range_provider in lsp_server.py). No client-side provider here to
// avoid duplicate folding regions.
// //
const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider( const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "cdl" }, { scheme: "file", language: "cdl" },
@@ -162,6 +193,16 @@ export async function activate(context: vscode.ExtensionContext) {
) )
context.subscriptions.push(hoverCdlProvider) context.subscriptions.push(hoverCdlProvider)
const definitionCdlEventProvider = vscode.languages.registerDefinitionProvider(
{ scheme: "file", language: "cdl" },
{
provideDefinition(document, position, token) {
return definitionCdlEventHandler(document, position, token)
}
}
)
context.subscriptions.push(definitionCdlEventProvider)
const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider( const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider(
{ scheme: "file", language: "def" }, { scheme: "file", language: "def" },
{ {
@@ -179,11 +220,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,6 +247,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef) context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef)
// Check if the first line of the CDL file contains "MACHINE" // Check if the first line of the CDL file contains "MACHINE"
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => { vscode.workspace.onDidOpenTextDocument((document) => {
if (document.languageId === "cdl" || document.languageId === "def") { if (document.languageId === "cdl" || document.languageId === "def") {
if (document.languageId === "cdl") { if (document.languageId === "cdl") {
@@ -199,8 +256,7 @@ export async function activate(context: vscode.ExtensionContext) {
diagnosticCollectionDef.set(document.uri, diagnosticHandler(document)) diagnosticCollectionDef.set(document.uri, diagnosticHandler(document))
} }
} }
}) }),
vscode.workspace.onDidChangeTextDocument((event) => { vscode.workspace.onDidChangeTextDocument((event) => {
const document = event.document const document = event.document
if (document.languageId === "cdl" || document.languageId === "def") { if (document.languageId === "cdl" || document.languageId === "def") {
@@ -211,11 +267,13 @@ export async function activate(context: vscode.ExtensionContext) {
} }
} }
}) })
)
} }
export function deactivate(): Thenable<void> | undefined { export function deactivate(): Thenable<void> | undefined {
if (!client) { if (!client) {
disposeServerResources()
return undefined return undefined
} }
return client.stop() return client.stop().finally(disposeServerResources)
} }
+4 -2
View File
@@ -1,19 +1,21 @@
const esbuild = require("esbuild") const esbuild = require("esbuild")
const path = require("path")
const production = process.argv.includes("--production") const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch") const watch = process.argv.includes("--watch")
async function main() { async function main() {
const ctx = await esbuild.context({ const ctx = await esbuild.context({
absWorkingDir: __dirname,
entryPoints: ["client/src/extension.ts"], entryPoints: ["client/src/extension.ts"],
bundle: true, bundle: true,
format: "cjs", format: "cjs",
minify: production, minify: production,
sourcemap: !production, sourcemap: !production,
sourcesContent: false, sourcesContent: !production,
platform: "node", platform: "node",
// outdir: "out", // outdir: "out",
outfile: "./dist/extension.js", outfile: path.join(__dirname, "dist", "extension.js"),
external: ["vscode"], external: ["vscode"],
logLevel: "silent", logLevel: "silent",
plugins: [ plugins: [
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+7
View File
@@ -3,6 +3,13 @@
// symbol used for single line comment. Remove this entry if your language does not support line comments // symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "#" "lineComment": "#"
}, },
"indentationRules": {
// Ignore pure comment lines so they don't terminate folds inside Tcl brace blocks.
"unIndentedLinePattern": "^\\s*#.*$",
// Opening braces on their own line are common in NX Tcl.
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*)$",
"decreaseIndentPattern": "^\\s*[\\}\\]\\)].*$"
},
// symbols used as brackets // symbols used as brackets
"brackets": [ "brackets": [
["{", "}"], ["{", "}"],
+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",
+16 -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.8", "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.",
@@ -112,6 +124,7 @@
}, },
"scripts": { "scripts": {
"compile": "node esbuild.js --production", "compile": "node esbuild.js --production",
"compile:debug": "node esbuild.js",
"watch": "node esbuild.js --watch", "watch": "node esbuild.js --watch",
"package": "node esbuild.js --production" "package": "node esbuild.js --production"
}, },
+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:
+394 -111
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, eq_, auto_detect, ("__eq__", "__ne__")
)
Hashability = ClassProps.Hashability
if is_exc:
hashability = Hashability.LEAVE_ALONE
elif hash is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif hash is False:
hashability = Hashability.LEAVE_ALONE
elif hash is None:
if auto_detect is True and _has_own_attribute(cls, "__hash__"):
hashability = Hashability.LEAVE_ALONE
elif eq is True and is_frozen is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif eq is False:
hashability = Hashability.LEAVE_ALONE
else:
hashability = Hashability.UNHASHABLE
else:
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)
KeywordOnly = ClassProps.KeywordOnly
if kw_only:
kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES
else:
kwo = KeywordOnly.NO
props = ClassProps(
is_exception=is_exc,
is_frozen=is_frozen,
is_slotted=slots,
collected_fields_by_mro=collect_by_mro,
added_init=_determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
),
added_repr=_determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
),
added_eq=eq,
added_ordering=not is_exc
and _determine_whether_to_implement(
cls, cls,
these, order_,
slots, auto_detect,
is_frozen, ("__lt__", "__le__", "__gt__", "__ge__"),
weakref_slot, ),
_determine_whether_to_implement( hashability=hashability,
added_match_args=match_args,
kw_only=kwo,
has_weakref_slot=weakref_slot,
added_str=str,
added_pickling=_determine_whether_to_implement(
cls, cls,
getstate_setstate, getstate_setstate,
auto_detect, auto_detect,
("__getstate__", "__setstate__"), ("__getstate__", "__setstate__"),
default=slots, default=slots,
), ),
auto_attribs, on_setattr_hook=on_setattr,
kw_only, field_transformer=field_transformer,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_own_setattr,
field_transformer,
) )
if _determine_whether_to_implement( if not props.is_hashable and cache_hash:
cls, repr, auto_detect, ("__repr__",) msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
): raise TypeError(msg)
builder = _ClassBuilder(
cls,
these,
auto_attribs=auto_attribs,
props=props,
has_custom_setattr=has_own_setattr,
)
if props.added_repr:
builder.add_repr(repr_ns) builder.add_repr(repr_ns)
if str is True: if props.added_str:
builder.add_str() builder.add_str()
eq = _determine_whether_to_implement( if props.added_eq:
cls, eq_, auto_detect, ("__eq__", "__ne__")
)
if not is_exc and eq is True:
builder.add_eq() builder.add_eq()
if not is_exc and _determine_whether_to_implement( if props.added_ordering:
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
):
builder.add_order() builder.add_order()
if not frozen: if not frozen:
builder.add_setattr() builder.add_setattr()
nonlocal hash if props.is_hashable:
if (
hash is None
and auto_detect is True
and _has_own_attribute(cls, "__hash__")
):
hash = False
if hash is not True and hash is not False and hash is not None:
# Can't use `hash in` because 1 == True for example.
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)
if hash is False or (hash is None and eq is False) or is_exc:
# Don't do anything. Should fall back to __object__'s __hash__
# which is by id.
if cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
elif hash is True or (
hash is None and eq is True and is_frozen is True
):
# Build a __hash__ if told so, or if it's safe.
builder.add_hash() builder.add_hash()
else: elif props.hashability is Hashability.UNHASHABLE:
# Raise TypeError on attempts to hash.
if cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
builder.make_unhashable() builder.make_unhashable()
if _determine_whether_to_implement( if props.added_init:
cls, init, auto_detect, ("__init__",)
):
builder.add_init() builder.add_init()
else: else:
builder.add_attrs_init() builder.add_attrs_init()
@@ -1835,16 +1890,16 @@ def _add_repr(cls, ns=None, attrs=None):
def fields(cls): def fields(cls):
""" """
Return the tuple of *attrs* attributes for a class. Return the tuple of *attrs* attributes for a class or instance.
The tuple also allows accessing the fields by their names (see below for The tuple also allows accessing the fields by their names (see below for
examples). examples).
Args: Args:
cls (type): Class to introspect. cls (type): Class or instance to introspect.
Raises: Raises:
TypeError: If *cls* is not a class. TypeError: If *cls* is neither a class nor an *attrs* instance.
attrs.exceptions.NotAnAttrsClassError: attrs.exceptions.NotAnAttrsClassError:
If *cls* is not an *attrs* class. If *cls* is not an *attrs* class.
@@ -1855,13 +1910,18 @@ def fields(cls):
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
by name. by name.
.. versionchanged:: 23.1.0 Add support for generic classes. .. versionchanged:: 23.1.0 Add support for generic classes.
.. versionchanged:: 26.1.0 Add support for instances.
""" """
generic_base = get_generic_base(cls) generic_base = get_generic_base(cls)
if generic_base is None and not isinstance(cls, type): if generic_base is None and not isinstance(cls, type):
msg = "Passed object must be a class." type_ = type(cls)
if getattr(type_, "__attrs_attrs__", None) is None:
msg = "Passed object must be a class or attrs instance."
raise TypeError(msg) raise TypeError(msg)
return fields(type_)
attrs = getattr(cls, "__attrs_attrs__", None) attrs = getattr(cls, "__attrs_attrs__", None)
if attrs is None: if attrs is None:
@@ -1967,7 +2027,7 @@ def _make_init_script(
attr_dict[a.name] = a attr_dict[a.name] = a
if a.on_setattr is not None: if a.on_setattr is not None:
if frozen is True: if frozen is True and a.on_setattr is not setters.NO_OP:
msg = "Frozen classes can't use on_setattr." msg = "Frozen classes can't use on_setattr."
raise ValueError(msg) raise ValueError(msg)
@@ -2126,8 +2186,9 @@ def _attrs_to_init_script(
) )
lines.extend(extra_lines) lines.extend(extra_lines)
args = [] args = [] # Parameters in the definition of __init__
kw_only_args = [] pre_init_args = [] # Parameters in the call to __attrs_pre_init__
kw_only_args = [] # Used for both 'args' and 'pre_init_args' above
attrs_to_validate = [] attrs_to_validate = []
# This is a dictionary of names to validator and converter callables. # This is a dictionary of names to validator and converter callables.
@@ -2205,6 +2266,7 @@ def _attrs_to_init_script(
kw_only_args.append(arg) kw_only_args.append(arg)
else: else:
args.append(arg) args.append(arg)
pre_init_args.append(arg_name)
if converter is not None: if converter is not None:
lines.append( lines.append(
@@ -2224,6 +2286,7 @@ def _attrs_to_init_script(
kw_only_args.append(arg) kw_only_args.append(arg)
else: else:
args.append(arg) args.append(arg)
pre_init_args.append(arg_name)
lines.append(f"if {arg_name} is not NOTHING:") lines.append(f"if {arg_name} is not NOTHING:")
init_factory_name = _INIT_FACTORY_PAT % (a.name,) init_factory_name = _INIT_FACTORY_PAT % (a.name,)
@@ -2266,6 +2329,7 @@ def _attrs_to_init_script(
kw_only_args.append(arg_name) kw_only_args.append(arg_name)
else: else:
args.append(arg_name) args.append(arg_name)
pre_init_args.append(arg_name)
if converter is not None: if converter is not None:
lines.append( lines.append(
@@ -2322,7 +2386,7 @@ def _attrs_to_init_script(
lines.append(f"BaseException.__init__(self, {vals})") lines.append(f"BaseException.__init__(self, {vals})")
args = ", ".join(args) args = ", ".join(args)
pre_init_args = args pre_init_args = ", ".join(pre_init_args)
if kw_only_args: if kw_only_args:
# leading comma & kw_only args # leading comma & kw_only args
args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}"
@@ -2337,7 +2401,7 @@ def _attrs_to_init_script(
pre_init_args += pre_init_kw_only_args pre_init_args += pre_init_kw_only_args
if call_pre_init and pre_init_has_args: if call_pre_init and pre_init_has_args:
# If pre init method has arguments, pass same arguments as `__init__`. # If pre init method has arguments, pass the values given to __init__.
lines[0] = f"self.__attrs_pre_init__({pre_init_args})" lines[0] = f"self.__attrs_pre_init__({pre_init_args})"
# Python <3.12 doesn't allow backslashes in f-strings. # Python <3.12 doesn't allow backslashes in f-strings.
@@ -2376,6 +2440,8 @@ class Attribute:
- ``name`` (`str`): The name of the attribute. - ``name`` (`str`): The name of the attribute.
- ``alias`` (`str`): The __init__ parameter name of the attribute, after - ``alias`` (`str`): The __init__ parameter name of the attribute, after
any explicit overrides and default private-attribute-name handling. any explicit overrides and default private-attribute-name handling.
- ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically
generated (``True``) or explicitly provided by the user (``False``).
- ``inherited`` (`bool`): Whether or not that attribute has been inherited - ``inherited`` (`bool`): Whether or not that attribute has been inherited
from a base class. from a base class.
- ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The
@@ -2401,6 +2467,7 @@ class Attribute:
equality checks and hashing anymore. equality checks and hashing anymore.
.. versionadded:: 21.1.0 *eq_key* and *order_key* .. versionadded:: 21.1.0 *eq_key* and *order_key*
.. versionadded:: 22.2.0 *alias* .. versionadded:: 22.2.0 *alias*
.. versionadded:: 26.1.0 *alias_is_default*
For the full version history of the fields, see `attr.ib`. For the full version history of the fields, see `attr.ib`.
""" """
@@ -2425,6 +2492,7 @@ class Attribute:
"inherited", "inherited",
"on_setattr", "on_setattr",
"alias", "alias",
"alias_is_default",
) )
def __init__( def __init__(
@@ -2447,6 +2515,7 @@ class Attribute:
order_key=None, order_key=None,
on_setattr=None, on_setattr=None,
alias=None, alias=None,
alias_is_default=None,
): ):
eq, eq_key, order, order_key = _determine_attrib_eq_order( eq, eq_key, order, order_key = _determine_attrib_eq_order(
cmp, eq_key or eq, order_key or order, True cmp, eq_key or eq, order_key or order, True
@@ -2481,12 +2550,20 @@ class Attribute:
bound_setattr("inherited", inherited) bound_setattr("inherited", inherited)
bound_setattr("on_setattr", on_setattr) bound_setattr("on_setattr", on_setattr)
bound_setattr("alias", alias) bound_setattr("alias", alias)
bound_setattr(
"alias_is_default",
alias is None if alias_is_default is None else alias_is_default,
)
def __setattr__(self, name, value): def __setattr__(self, name, value):
raise FrozenInstanceError raise FrozenInstanceError
@classmethod @classmethod
def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None): def from_counting_attr(
cls, name: str, ca: _CountingAttr, kw_only: bool, type=None
):
# The 'kw_only' argument is the class-level setting, and is used if the
# attribute itself does not explicitly set 'kw_only'.
# type holds the annotated value. deal with conflicts: # type holds the annotated value. deal with conflicts:
if type is None: if type is None:
type = ca.type type = ca.type
@@ -2505,13 +2582,14 @@ class Attribute:
ca.metadata, ca.metadata,
type, type,
ca.converter, ca.converter,
ca.kw_only, kw_only if ca.kw_only is None else ca.kw_only,
ca.eq, ca.eq,
ca.eq_key, ca.eq_key,
ca.order, ca.order,
ca.order_key, ca.order_key,
ca.on_setattr, ca.on_setattr,
ca.alias, ca.alias,
ca.alias is None,
) )
# Don't use attrs.evolve since fields(Attribute) doesn't work # Don't use attrs.evolve since fields(Attribute) doesn't work
@@ -2530,6 +2608,20 @@ class Attribute:
new._setattrs(changes.items()) new._setattrs(changes.items())
if "alias" in changes and "alias_is_default" not in changes:
# Explicit alias provided -- no longer the default.
_OBJ_SETATTR.__get__(new)("alias_is_default", False)
elif (
"name" in changes
and "alias" not in changes
# Don't auto-generate alias if the user picked picked the old one.
and self.alias_is_default
):
# Name changed, alias was auto-generated -- update it.
_OBJ_SETATTR.__get__(new)(
"alias", _default_init_alias_for(new.name)
)
return new return new
# Don't use _add_pickle since fields(Attribute) doesn't work # Don't use _add_pickle since fields(Attribute) doesn't work
@@ -2546,6 +2638,17 @@ class Attribute:
""" """
Play nice with pickle. Play nice with pickle.
""" """
if len(state) < len(self.__slots__):
# Pre-26.1.0 pickle without alias_is_default -- infer it
# heuristically.
state_dict = dict(zip(self.__slots__, state))
alias_is_default = state_dict.get(
"alias"
) is None or state_dict.get("alias") == _default_init_alias_for(
state_dict["name"]
)
state = (*state, alias_is_default)
self._setattrs(zip(self.__slots__, state)) self._setattrs(zip(self.__slots__, state))
def _setattrs(self, name_values_pairs): def _setattrs(self, name_values_pairs):
@@ -2569,7 +2672,7 @@ _a = [
name=name, name=name,
default=NOTHING, default=NOTHING,
validator=None, validator=None,
repr=True, repr=(name != "alias_is_default"),
cmp=None, cmp=None,
eq=True, eq=True,
order=False, order=False,
@@ -2741,6 +2844,188 @@ class _CountingAttr:
_CountingAttr = _add_eq(_add_repr(_CountingAttr)) _CountingAttr = _add_eq(_add_repr(_CountingAttr))
class ClassProps:
"""
Effective class properties as derived from parameters to `attr.s()` or
`define()` decorators.
This is the same data structure that *attrs* uses internally to decide how
to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
is_exception (bool):
Whether the class is treated as an exception class.
is_slotted (bool):
Whether the class is `slotted <slotted classes>`.
has_weakref_slot (bool):
Whether the class has a slot for weak references.
is_frozen (bool):
Whether the class is frozen.
kw_only (KeywordOnly):
Whether / how the class enforces keyword-only arguments on the
``__init__`` method.
collected_fields_by_mro (bool):
Whether the class fields were collected by method resolution order.
That is, correctly but unlike `dataclasses`.
added_init (bool):
Whether the class has an *attrs*-generated ``__init__`` method.
added_repr (bool):
Whether the class has an *attrs*-generated ``__repr__`` method.
added_eq (bool):
Whether the class has *attrs*-generated equality methods.
added_ordering (bool):
Whether the class has *attrs*-generated ordering methods.
hashability (Hashability): How `hashable <hashing>` the class is.
added_match_args (bool):
Whether the class supports positional `match <match>` over its
fields.
added_str (bool):
Whether the class has an *attrs*-generated ``__str__`` method.
added_pickling (bool):
Whether the class has *attrs*-generated ``__getstate__`` and
``__setstate__`` methods for `pickle`.
on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None):
The class's ``__setattr__`` hook.
field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None):
The class's `field transformers <transform-fields>`.
.. versionadded:: 25.4.0
"""
class Hashability(enum.Enum):
"""
The hashability of a class.
.. versionadded:: 25.4.0
"""
HASHABLE = "hashable"
"""Write a ``__hash__``."""
HASHABLE_CACHED = "hashable_cache"
"""Write a ``__hash__`` and cache the hash."""
UNHASHABLE = "unhashable"
"""Set ``__hash__`` to ``None``."""
LEAVE_ALONE = "leave_alone"
"""Don't touch ``__hash__``."""
class KeywordOnly(enum.Enum):
"""
How attributes should be treated regarding keyword-only parameters.
.. versionadded:: 25.4.0
"""
NO = "no"
"""Attributes are not keyword-only."""
YES = "yes"
"""Attributes in current class without kw_only=False are keyword-only."""
FORCE = "force"
"""All attributes are keyword-only."""
__slots__ = ( # noqa: RUF023 -- order matters for __init__
"is_exception",
"is_slotted",
"has_weakref_slot",
"is_frozen",
"kw_only",
"collected_fields_by_mro",
"added_init",
"added_repr",
"added_eq",
"added_ordering",
"hashability",
"added_match_args",
"added_str",
"added_pickling",
"on_setattr_hook",
"field_transformer",
)
def __init__(
self,
is_exception,
is_slotted,
has_weakref_slot,
is_frozen,
kw_only,
collected_fields_by_mro,
added_init,
added_repr,
added_eq,
added_ordering,
hashability,
added_match_args,
added_str,
added_pickling,
on_setattr_hook,
field_transformer,
):
self.is_exception = is_exception
self.is_slotted = is_slotted
self.has_weakref_slot = has_weakref_slot
self.is_frozen = is_frozen
self.kw_only = kw_only
self.collected_fields_by_mro = collected_fields_by_mro
self.added_init = added_init
self.added_repr = added_repr
self.added_eq = added_eq
self.added_ordering = added_ordering
self.hashability = hashability
self.added_match_args = added_match_args
self.added_str = added_str
self.added_pickling = added_pickling
self.on_setattr_hook = on_setattr_hook
self.field_transformer = field_transformer
@property
def is_hashable(self):
return (
self.hashability is ClassProps.Hashability.HASHABLE
or self.hashability is ClassProps.Hashability.HASHABLE_CACHED
)
_cas = [
Attribute(
name=name,
default=NOTHING,
validator=None,
repr=True,
cmp=None,
eq=True,
order=False,
hash=True,
init=True,
inherited=False,
alias=_default_init_alias_for(name),
)
for name in ClassProps.__slots__
]
ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas)
class Factory: class Factory:
""" """
Stores a factory callable. Stores a factory callable.
@@ -2848,9 +3133,7 @@ class Converter:
value, field value, field
) )
else: else:
self.__call__ = lambda value, instance, field: self.converter( self.__call__ = self.converter
value, instance, field
)
rt = ex.get_return_type() rt = ex.get_return_type()
if rt is not None: if rt is not None:
+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))
+3 -3
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
""" """
def __init__(self):
msg = "can't set attribute" msg = "can't set attribute"
args: ClassVar[tuple[str]] = [msg] super().__init__(msg)
self.msg = msg
class FrozenInstanceError(FrozenError): class FrozenInstanceError(FrozenError):
+53 -13
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:
if self.key_validator is not None:
self.key_validator(inst, attr, key) self.key_validator(inst, attr, key)
if self.value_validator is not None:
self.value_validator(inst, attr, value[key]) self.value_validator(inst, attr, value[key])
def __repr__(self): def __repr__(self):
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>" return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
def deep_mapping(key_validator, value_validator, mapping_validator=None): def deep_mapping(
key_validator=None, value_validator=None, mapping_validator=None
):
""" """
A validator that performs deep validation of a dictionary. A validator that performs deep validation of a dictionary.
Args: All validators are optional, but at least one of *key_validator* or
key_validator: Validator to apply to dictionary keys. *value_validator* must be provided.
value_validator: Validator to apply to dictionary values. Args:
key_validator: Validator(s) to apply to dictionary keys.
value_validator: Validator(s) to apply to dictionary values.
mapping_validator: mapping_validator:
Validator to apply to top-level mapping attribute (optional). Validator(s) to apply to top-level mapping attribute.
.. versionadded:: 19.1.0 .. versionadded:: 19.1.0
.. versionchanged:: 25.4.0
*key_validator* and *value_validator* are now optional, but at least one
of them must be provided.
.. versionchanged:: 25.4.0
*key_validator*, *value_validator*, and *mapping_validator* can now be a
list or tuple of validators.
Raises: Raises:
TypeError: if any sub-validators fail TypeError: If any sub-validator fails on validation.
ValueError:
If neither *key_validator* nor *value_validator* is provided on
instantiation.
""" """
if key_validator is None and value_validator is None:
msg = (
"At least one of key_validator or value_validator must be provided"
)
raise ValueError(msg)
if isinstance(key_validator, (list, tuple)):
key_validator = and_(*key_validator)
if isinstance(value_validator, (list, tuple)):
value_validator = and_(*value_validator)
if isinstance(mapping_validator, (list, tuple)):
mapping_validator = and_(*mapping_validator)
return _DeepMapping(key_validator, value_validator, mapping_validator) return _DeepMapping(key_validator, value_validator, mapping_validator)
@@ -485,7 +525,7 @@ def gt(val):
A validator that raises `ValueError` if the initializer is called with a A validator that raises `ValueError` if the initializer is called with a
number smaller or equal to *val*. number smaller or equal to *val*.
The validator uses `operator.ge` to compare the values. The validator uses `operator.gt` to compare the values.
Args: Args:
val: Exclusive lower bound for values val: Exclusive lower bound for values
+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))}"
) )
+99 -15
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]]:
@@ -96,6 +96,16 @@ def make_dict_unstructure_fn_from_attrs(
will be included. will be included.
.. versionadded:: 24.1.0 .. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
.. versionchanged:: 26.1.0
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
specified, the converter is applied to the default value before checking if it
is equal to the attribute's value.
""" """
fn_name = "unstructure_" + cl.__name__ fn_name = "unstructure_" + cl.__name__
@@ -104,15 +114,27 @@ def make_dict_unstructure_fn_from_attrs(
invocation_lines = [] invocation_lines = []
internal_arg_parts = {} internal_arg_parts = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
for a in attrs: for a in attrs:
attr_name = a.name attr_name = a.name
override = kwargs.get(attr_name, neutral) if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[attr_name] = override
if override.omit: if override.omit:
continue continue
if override.omit is None and not a.init and not _cattrs_include_init_false: if override.omit is None and not a.init and not _cattrs_include_init_false:
continue continue
if override.rename is None: if override.rename is None:
kn = attr_name if not _cattrs_use_alias else a.alias kn = attr_name if not _cattrs_use_alias else a.alias
if kn != attr_name:
kwargs[attr_name] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
d = a.default d = a.default
@@ -170,15 +192,31 @@ def make_dict_unstructure_fn_from_attrs(
if isinstance(d, Factory): if isinstance(d, Factory):
globs[def_name] = d.factory globs[def_name] = d.factory
internal_arg_parts[def_name] = d.factory internal_arg_parts[def_name] = d.factory
if d.takes_self: def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()"
lines.append(f" if instance.{attr_name} != {def_name}(instance):")
else:
lines.append(f" if instance.{attr_name} != {def_name}():")
lines.append(f" res['{kn}'] = {invoke}")
else: else:
globs[def_name] = d globs[def_name] = d
internal_arg_parts[def_name] = d internal_arg_parts[def_name] = d
lines.append(f" if instance.{attr_name} != {def_name}:") def_str = def_name
c = a.converter
if c is not None:
conv_name = f"__c_conv_{attr_name}"
if isinstance(c, Converter):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
field_name = f"__c_field_{attr_name}"
globs[field_name] = a
internal_arg_parts[field_name] = a
def_str = f"{conv_name}({def_str}, instance, {field_name})"
elif isinstance(d, Factory):
globs[conv_name] = c
internal_arg_parts[conv_name] = c
def_str = f"{conv_name}({def_str})"
else:
globs[def_name] = c(d)
internal_arg_parts[def_name] = c(d)
lines.append(f" if instance.{attr_name} != {def_str}:")
lines.append(f" res['{kn}'] = {invoke}") lines.append(f" res['{kn}'] = {invoke}")
else: else:
@@ -217,7 +255,7 @@ def make_dict_unstructure_fn(
converter: BaseConverter, converter: BaseConverter,
_cattrs_omit_if_default: bool = False, _cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True, _cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool = False, _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]: ) -> Callable[[T], dict[str, Any]]:
@@ -237,11 +275,20 @@ def make_dict_unstructure_fn(
.. versionadded:: 23.2.0 *_cattrs_use_alias* .. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false* .. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
""" """
origin = get_origin(cl) origin = get_origin(cl)
attrs = adapted_fields(origin or cl) # type: ignore attrs = adapted_fields(origin or cl) # type: ignore
mapping = {} mapping = {}
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if is_generic(cl): if is_generic(cl):
mapping = generate_mapping(cl, mapping) mapping = generate_mapping(cl, mapping)
@@ -289,7 +336,7 @@ def make_dict_structure_fn_from_attrs(
bool | Literal["from_converter"] bool | Literal["from_converter"]
) = "from_converter", ) = "from_converter",
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter", _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False, _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]: ) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -315,6 +362,12 @@ def make_dict_structure_fn_from_attrs(
will be included. will be included.
.. versionadded:: 24.1.0 .. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
""" """
cl_name = cl.__name__ cl_name = cl.__name__
@@ -350,6 +403,9 @@ def make_dict_structure_fn_from_attrs(
if _cattrs_forbid_extra_keys == "from_converter": if _cattrs_forbid_extra_keys == "from_converter":
# BaseConverter doesn't have it so we're careful. # BaseConverter doesn't have it so we're careful.
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False) _cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
if _cattrs_use_alias == "from_converter":
# BaseConverter doesn't have it so we're careful.
_cattrs_use_alias = getattr(converter, "use_alias", False)
if _cattrs_detailed_validation == "from_converter": if _cattrs_detailed_validation == "from_converter":
_cattrs_detailed_validation = converter.detailed_validation _cattrs_detailed_validation = converter.detailed_validation
if _cattrs_prefer_attrib_converters == "from_converter": if _cattrs_prefer_attrib_converters == "from_converter":
@@ -367,7 +423,13 @@ def make_dict_structure_fn_from_attrs(
internal_arg_parts["__c_avn"] = AttributeValidationNote internal_arg_parts["__c_avn"] = AttributeValidationNote
for a in attrs: for a in attrs:
an = a.name an = a.name
override = kwargs.get(an, neutral) if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
if override.omit: if override.omit:
continue continue
if override.omit is None and not a.init and not _cattrs_include_init_false: if override.omit is None and not a.init and not _cattrs_include_init_false:
@@ -396,6 +458,8 @@ def make_dict_structure_fn_from_attrs(
ian = a.alias ian = a.alias
if override.rename is None: if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
@@ -496,14 +560,24 @@ def make_dict_structure_fn_from_attrs(
# The first loop deals with required args. # The first loop deals with required args.
for a in attrs: for a in attrs:
an = a.name an = a.name
override = kwargs.get(an, neutral)
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override
if override.omit: if override.omit:
continue continue
if override.omit is None and not a.init and not _cattrs_include_init_false: if override.omit is None and not a.init and not _cattrs_include_init_false:
continue continue
if a.default is not NOTHING: if a.default is not NOTHING:
non_required.append(a) non_required.append(a)
# The next loop will handle it.
continue continue
t = a.type t = a.type
if isinstance(t, TypeVar): if isinstance(t, TypeVar):
t = typevar_map.get(t.__name__, t) t = typevar_map.get(t.__name__, t)
@@ -523,6 +597,8 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None: if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
allowed_fields.add(kn) allowed_fields.add(kn)
@@ -592,6 +668,8 @@ def make_dict_structure_fn_from_attrs(
if override.rename is None: if override.rename is None:
kn = an if not _cattrs_use_alias else a.alias kn = an if not _cattrs_use_alias else a.alias
if kn != an:
kwargs[an] = evolve(override, rename=kn)
else: else:
kn = override.rename kn = override.rename
allowed_fields.add(kn) allowed_fields.add(kn)
@@ -682,7 +760,7 @@ def make_dict_structure_fn(
bool | Literal["from_converter"] bool | Literal["from_converter"]
) = "from_converter", ) = "from_converter",
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter", _cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
_cattrs_use_alias: bool = False, _cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
_cattrs_include_init_false: bool = False, _cattrs_include_init_false: bool = False,
**kwargs: AttributeOverride, **kwargs: AttributeOverride,
) -> SimpleStructureHook[Mapping[str, Any], T]: ) -> SimpleStructureHook[Mapping[str, Any], T]:
@@ -714,6 +792,12 @@ def make_dict_structure_fn(
.. versionchanged:: 24.1.0 .. versionchanged:: 24.1.0
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
converter by default. converter by default.
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: 26.1.0
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
""" """
mapping = {} mapping = {}
+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)
@@ -0,0 +1 @@
uv
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,138 @@
Metadata-Version: 2.1
Name: importlib-metadata
Version: 6.8.0
Summary: Read metadata from Python packages
Home-page: https://github.com/python/importlib_metadata
Author: Jason R. Coombs
Author-email: jaraco@jaraco.com
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.8
License-File: LICENSE
Requires-Dist: zipp (>=0.5)
Requires-Dist: typing-extensions (>=3.6.4) ; python_version < "3.8"
Provides-Extra: docs
Requires-Dist: sphinx (>=3.5) ; extra == 'docs'
Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs'
Requires-Dist: rst.linker (>=1.9) ; extra == 'docs'
Requires-Dist: furo ; extra == 'docs'
Requires-Dist: sphinx-lint ; extra == 'docs'
Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs'
Provides-Extra: perf
Requires-Dist: ipython ; extra == 'perf'
Provides-Extra: testing
Requires-Dist: pytest (>=6) ; extra == 'testing'
Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing'
Requires-Dist: pytest-cov ; extra == 'testing'
Requires-Dist: pytest-enabler (>=2.2) ; extra == 'testing'
Requires-Dist: pytest-ruff ; extra == 'testing'
Requires-Dist: packaging ; extra == 'testing'
Requires-Dist: pyfakefs ; extra == 'testing'
Requires-Dist: flufl.flake8 ; extra == 'testing'
Requires-Dist: pytest-perf (>=0.9.2) ; extra == 'testing'
Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing'
Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing'
Requires-Dist: importlib-resources (>=1.3) ; (python_version < "3.9") and extra == 'testing'
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
:target: https://pypi.org/project/importlib_metadata
.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg
.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg
:target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22
:alt: tests
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
:target: https://github.com/astral-sh/ruff
:alt: Ruff
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
:alt: Code style: Black
.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest
:target: https://importlib-metadata.readthedocs.io/en/latest/?badge=latest
.. image:: https://img.shields.io/badge/skeleton-2023-informational
:target: https://blog.jaraco.com/skeleton
.. image:: https://tidelift.com/badges/package/pypi/importlib-metadata
:target: https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=readme
Library to access the metadata for a Python package.
This package supplies third-party access to the functionality of
`importlib.metadata <https://docs.python.org/3/library/importlib.metadata.html>`_
including improvements added to subsequent Python versions.
Compatibility
=============
New features are introduced in this third-party library and later merged
into CPython. The following table indicates which versions of this library
were contributed to different versions in the standard library:
.. list-table::
:header-rows: 1
* - importlib_metadata
- stdlib
* - 6.5
- 3.12
* - 4.13
- 3.11
* - 4.6
- 3.10
* - 1.4
- 3.8
Usage
=====
See the `online documentation <https://importlib-metadata.readthedocs.io/>`_
for usage details.
`Finder authors
<https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
also add support for custom package installers. See the above documentation
for details.
Caveats
=======
This project primarily supports third-party packages installed by PyPA
tools (or other conforming packages). It does not support:
- Packages in the stdlib.
- Packages installed without metadata.
Project details
===============
* Project home: https://github.com/python/importlib_metadata
* Report bugs at: https://github.com/python/importlib_metadata/issues
* Code hosting: https://github.com/python/importlib_metadata
* Documentation: https://importlib-metadata.readthedocs.io/
For Enterprise
==============
Available as part of the Tidelift Subscription.
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
`Learn more <https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=referral&utm_campaign=github>`_.
Security Contact
================
To report a security vulnerability, please use the
`Tidelift security contact <https://tidelift.com/security>`_.
Tidelift will coordinate the fix and disclosure.
@@ -0,0 +1,17 @@
importlib_metadata-6.8.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
importlib_metadata-6.8.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
importlib_metadata-6.8.0.dist-info/METADATA,sha256=X79qGRh7gqvuaL_utK5X-MnwHJuIWke0e3eAx0IiLhc,5067
importlib_metadata-6.8.0.dist-info/RECORD,,
importlib_metadata-6.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_metadata-6.8.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
importlib_metadata-6.8.0.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19
importlib_metadata/__init__.py,sha256=EiH0qTKP_6oa6pRGJgPrq0kvjnL3hJ18BJH8VaAYSBA,30749
importlib_metadata/_adapters.py,sha256=i8S6Ib1OQjcILA-l4gkzktMZe18TaeUNI49PLRp6OBU,2454
importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743
importlib_metadata/_compat.py,sha256=zhjcWMfA9SNExFVVVBozOYbuiok0A4tdMsNk9ZDZi-A,1554
importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895
importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068
importlib_metadata/_meta.py,sha256=kypMW_-xSStooSm0WpJc6eupjT-Ipc2ZBIl23PyC3No,1613
importlib_metadata/_py39compat.py,sha256=2Tk5twb_VgLCY-1NEAQjdZp_S9OFMC-pUzP2isuaPsQ,1098
importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166
importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.40.0)
Root-Is-Purelib: true
Tag: py3-none-any
@@ -0,0 +1 @@
importlib_metadata
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,90 @@
import functools
import warnings
import re
import textwrap
import email.message
from ._text import FoldedCase
from ._compat import pypy_partial
# Do not remove prior to 2024-01-01 or Python 3.14
_warn = functools.partial(
warnings.warn,
"Implicit None on return values is deprecated and will raise KeyErrors.",
DeprecationWarning,
stacklevel=pypy_partial(2),
)
class Message(email.message.Message):
multiple_use_keys = set(
map(
FoldedCase,
[
'Classifier',
'Obsoletes-Dist',
'Platform',
'Project-URL',
'Provides-Dist',
'Provides-Extra',
'Requires-Dist',
'Requires-External',
'Supported-Platform',
'Dynamic',
],
)
)
"""
Keys that may be indicated multiple times per PEP 566.
"""
def __new__(cls, orig: email.message.Message):
res = super().__new__(cls)
vars(res).update(vars(orig))
return res
def __init__(self, *args, **kwargs):
self._headers = self._repair_headers()
# suppress spurious error from mypy
def __iter__(self):
return super().__iter__()
def __getitem__(self, item):
"""
Warn users that a ``KeyError`` can be expected when a
mising key is supplied. Ref python/importlib_metadata#371.
"""
res = super().__getitem__(item)
if res is None:
_warn()
return res
def _repair_headers(self):
def redent(value):
"Correct for RFC822 indentation"
if not value or '\n' not in value:
return value
return textwrap.dedent(' ' * 8 + value)
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
if self._payload:
headers.append(('Description', self.get_payload()))
return headers
@property
def json(self):
"""
Convert PackageMetadata to a JSON-compatible format
per PEP 0566.
"""
def transform(key):
value = self.get_all(key) if key in self.multiple_use_keys else self[key]
if key == 'Keywords':
value = re.split(r'\s+', value)
tk = key.lower().replace('-', '_')
return tk, value
return dict(map(transform, map(FoldedCase, self)))
@@ -0,0 +1,30 @@
import collections
# from jaraco.collections 3.3
class FreezableDefaultDict(collections.defaultdict):
"""
Often it is desirable to prevent the mutation of
a default dict after its initial construction, such
as to prevent mutation during iteration.
>>> dd = FreezableDefaultDict(list)
>>> dd[0].append('1')
>>> dd.freeze()
>>> dd[1]
[]
>>> len(dd)
1
"""
def __missing__(self, key):
return getattr(self, '_frozen', super().__missing__)(key)
def freeze(self):
self._frozen = lambda key: self.default_factory()
class Pair(collections.namedtuple('Pair', 'name value')):
@classmethod
def parse(cls, text):
return cls(*map(str.strip, text.split("=", 1)))
+67
View File
@@ -0,0 +1,67 @@
import os
import sys
import platform
from typing import Union
__all__ = ['install', 'NullFinder']
def install(cls):
"""
Class decorator for installation on sys.meta_path.
Adds the backport DistributionFinder to sys.meta_path and
attempts to disable the finder functionality of the stdlib
DistributionFinder.
"""
sys.meta_path.append(cls())
disable_stdlib_finder()
return cls
def disable_stdlib_finder():
"""
Give the backport primacy for discovering path-based distributions
by monkey-patching the stdlib O_O.
See #91 for more background for rationale on this sketchy
behavior.
"""
def matches(finder):
return getattr(
finder, '__module__', None
) == '_frozen_importlib_external' and hasattr(finder, 'find_distributions')
for finder in filter(matches, sys.meta_path): # pragma: nocover
del finder.find_distributions
class NullFinder:
"""
A "Finder" (aka "MetaClassFinder") that never finds any modules,
but may find distributions.
"""
@staticmethod
def find_spec(*args, **kwargs):
return None
def pypy_partial(val):
"""
Adjust for variable stacklevel on partial under PyPy.
Workaround for #327.
"""
is_pypy = platform.python_implementation() == 'PyPy'
return val + is_pypy
if sys.version_info >= (3, 9):
StrPath = Union[str, os.PathLike[str]]
else:
# PathLike is only subscriptable at runtime in 3.9+
StrPath = Union[str, "os.PathLike[str]"] # pragma: no cover
@@ -0,0 +1,104 @@
import types
import functools
# from jaraco.functools 3.3
def method_cache(method, cache_wrapper=None):
"""
Wrap lru_cache to support storing the cache data in the object instances.
Abstracts the common paradigm where the method explicitly saves an
underscore-prefixed protected property on first call and returns that
subsequently.
>>> class MyClass:
... calls = 0
...
... @method_cache
... def method(self, value):
... self.calls += 1
... return value
>>> a = MyClass()
>>> a.method(3)
3
>>> for x in range(75):
... res = a.method(x)
>>> a.calls
75
Note that the apparent behavior will be exactly like that of lru_cache
except that the cache is stored on each instance, so values in one
instance will not flush values from another, and when an instance is
deleted, so are the cached values for that instance.
>>> b = MyClass()
>>> for x in range(35):
... res = b.method(x)
>>> b.calls
35
>>> a.method(0)
0
>>> a.calls
75
Note that if method had been decorated with ``functools.lru_cache()``,
a.calls would have been 76 (due to the cached value of 0 having been
flushed by the 'b' instance).
Clear the cache with ``.cache_clear()``
>>> a.method.cache_clear()
Same for a method that hasn't yet been called.
>>> c = MyClass()
>>> c.method.cache_clear()
Another cache wrapper may be supplied:
>>> cache = functools.lru_cache(maxsize=2)
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
>>> a = MyClass()
>>> a.method2()
3
Caution - do not subsequently wrap the method with another decorator, such
as ``@property``, which changes the semantics of the function.
See also
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
for another implementation and additional justification.
"""
cache_wrapper = cache_wrapper or functools.lru_cache()
def wrapper(self, *args, **kwargs):
# it's the first call, replace the method with a cached, bound method
bound_method = types.MethodType(method, self)
cached_method = cache_wrapper(bound_method)
setattr(self, method.__name__, cached_method)
return cached_method(*args, **kwargs)
# Support cache clear even before cache has been created.
wrapper.cache_clear = lambda: None
return wrapper
# From jaraco.functools 3.3
def pass_none(func):
"""
Wrap func so it's not called if its first param is None
>>> print_text = pass_none(print)
>>> print_text('text')
text
>>> print_text(None)
"""
@functools.wraps(func)
def wrapper(param, *args, **kwargs):
if param is not None:
return func(param, *args, **kwargs)
return wrapper
@@ -0,0 +1,73 @@
from itertools import filterfalse
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
# copied from more_itertools 8.8
def always_iterable(obj, base_type=(str, bytes)):
"""If *obj* is iterable, return an iterator over its items::
>>> obj = (1, 2, 3)
>>> list(always_iterable(obj))
[1, 2, 3]
If *obj* is not iterable, return a one-item iterable containing *obj*::
>>> obj = 1
>>> list(always_iterable(obj))
[1]
If *obj* is ``None``, return an empty iterable:
>>> obj = None
>>> list(always_iterable(None))
[]
By default, binary and text strings are not considered iterable::
>>> obj = 'foo'
>>> list(always_iterable(obj))
['foo']
If *base_type* is set, objects for which ``isinstance(obj, base_type)``
returns ``True`` won't be considered iterable.
>>> obj = {'a': 1}
>>> list(always_iterable(obj)) # Iterate over the dict's keys
['a']
>>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
[{'a': 1}]
Set *base_type* to ``None`` to avoid any special handling and treat objects
Python considers iterable as iterable:
>>> obj = 'foo'
>>> list(always_iterable(obj, base_type=None))
['f', 'o', 'o']
"""
if obj is None:
return iter(())
if (base_type is not None) and isinstance(obj, base_type):
return iter((obj,))
try:
return iter(obj)
except TypeError:
return iter((obj,))
+63
View File
@@ -0,0 +1,63 @@
from typing import Protocol
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
_T = TypeVar("_T")
class PackageMetadata(Protocol):
def __len__(self) -> int:
... # pragma: no cover
def __contains__(self, item: str) -> bool:
... # pragma: no cover
def __getitem__(self, key: str) -> str:
... # pragma: no cover
def __iter__(self) -> Iterator[str]:
... # pragma: no cover
@overload
def get(self, name: str, failobj: None = None) -> Optional[str]:
... # pragma: no cover
@overload
def get(self, name: str, failobj: _T) -> Union[str, _T]:
... # pragma: no cover
# overload per python/importlib_metadata#435
@overload
def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]:
... # pragma: no cover
@overload
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""
@property
def json(self) -> Dict[str, Union[str, List[str]]]:
"""
A JSON-compatible form of the metadata.
"""
class SimplePath(Protocol[_T]):
"""
A minimal subset of pathlib.Path required by PathDistribution.
"""
def joinpath(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
def __truediv__(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
@property
def parent(self) -> _T:
... # pragma: no cover
def read_text(self) -> str:
... # pragma: no cover
@@ -0,0 +1,35 @@
"""
Compatibility layer with Python 3.8/3.9
"""
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING: # pragma: no cover
# Prevent circular imports on runtime.
from . import Distribution, EntryPoint
else:
Distribution = EntryPoint = Any
def normalized_name(dist: Distribution) -> Optional[str]:
"""
Honor name normalization for distributions that don't provide ``_normalized_name``.
"""
try:
return dist._normalized_name
except AttributeError:
from . import Prepared # -> delay to prevent circular imports.
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
def ep_matches(ep: EntryPoint, **params) -> bool:
"""
Workaround for ``EntryPoint`` objects without the ``matches`` method.
"""
try:
return ep.matches(**params)
except AttributeError:
from . import EntryPoint # -> delay to prevent circular imports.
# Reconstruct the EntryPoint object to make sure it is compatible.
return EntryPoint(ep.name, ep.value, ep.group).matches(**params)
+99
View File
@@ -0,0 +1,99 @@
import re
from ._functools import method_cache
# from jaraco.text 3.5
class FoldedCase(str):
"""
A case insensitive string class; behaves just like str
except compares equal when the only variation is case.
>>> s = FoldedCase('hello world')
>>> s == 'Hello World'
True
>>> 'Hello World' == s
True
>>> s != 'Hello World'
False
>>> s.index('O')
4
>>> s.split('O')
['hell', ' w', 'rld']
>>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
['alpha', 'Beta', 'GAMMA']
Sequence membership is straightforward.
>>> "Hello World" in [s]
True
>>> s in ["Hello World"]
True
You may test for set inclusion, but candidate and elements
must both be folded.
>>> FoldedCase("Hello World") in {s}
True
>>> s in {FoldedCase("Hello World")}
True
String inclusion works as long as the FoldedCase object
is on the right.
>>> "hello" in FoldedCase("Hello World")
True
But not if the FoldedCase object is on the left:
>>> FoldedCase('hello') in 'Hello World'
False
In that case, use in_:
>>> FoldedCase('hello').in_('Hello World')
True
>>> FoldedCase('hello') > FoldedCase('Hello')
False
"""
def __lt__(self, other):
return self.lower() < other.lower()
def __gt__(self, other):
return self.lower() > other.lower()
def __eq__(self, other):
return self.lower() == other.lower()
def __ne__(self, other):
return self.lower() != other.lower()
def __hash__(self):
return hash(self.lower())
def __contains__(self, other):
return super().lower().__contains__(other.lower())
def in_(self, other):
"Does self appear in other?"
return self in FoldedCase(other)
# cache lower since it's likely to be called frequently.
@method_cache
def lower(self):
return super().lower()
def index(self, sub):
return self.lower().index(sub.lower())
def split(self, splitter=' ', maxsplit=0):
pattern = re.compile(re.escape(splitter), re.I)
return pattern.split(self, maxsplit)
@@ -1 +0,0 @@
pip
-18
View File
@@ -1,18 +0,0 @@
Copyright © 2017 Erez Shinan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-47
View File
@@ -1,47 +0,0 @@
Metadata-Version: 2.1
Name: lark
Version: 1.2.2
Summary: a modern parsing library
Author-email: Erez Shinan <erezshin@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/lark-parser/lark
Project-URL: Download, https://github.com/lark-parser/lark/tarball/master
Keywords: Earley,LALR,parser,parsing,ast
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: General
Classifier: Topic :: Text Processing :: Linguistic
Classifier: License :: OSI Approved :: MIT License
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: atomic_cache
Requires-Dist: atomicwrites ; extra == 'atomic_cache'
Provides-Extra: interegular
Requires-Dist: interegular <0.4.0,>=0.3.1 ; extra == 'interegular'
Provides-Extra: nearley
Requires-Dist: js2py ; extra == 'nearley'
Provides-Extra: regex
Requires-Dist: regex ; extra == 'regex'
Lark is a modern general-purpose parsing library for Python.
With Lark, you can parse any context-free grammar, efficiently, with very little code.
Main Features:
- Builds a parse-tree (AST) automagically, based on the structure of the grammar
- Earley parser
- Can parse all context-free grammars
- Full support for ambiguous grammars
- LALR(1) parser
- Fast and light, competitive with PLY
- Can generate a stand-alone parser
- CYK parser, for highly ambiguous grammars
- EBNF grammar
- Unicode fully supported
- Automatic line & column tracking
- Standard library of terminals (strings, numbers, names, etc.)
- Import grammars from Nearley.js
- Extensive test suite
- And much more!
Since version 1.2, only Python versions 3.8 and up are supported.
-83
View File
@@ -1,83 +0,0 @@
lark-1.2.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
lark-1.2.2.dist-info/LICENSE,sha256=Lu5g9S1OETV7-J5ysDTQUOKF5H_aE2HlZi-zIu4n13E,1055
lark-1.2.2.dist-info/METADATA,sha256=S-69HuNJr0ktlvb7J5XE48ghb_6ahYn8ksdW9HcB-d0,1831
lark-1.2.2.dist-info/RECORD,,
lark-1.2.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lark-1.2.2.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
lark-1.2.2.dist-info/entry_points.txt,sha256=WXYg_uCUdFlxQDPUhli3HFah37bNNFQfXLdzCqsacGI,61
lark-1.2.2.dist-info/top_level.txt,sha256=dyS6jg8hCHHkXWvsfcIMO8rjlv_bdzAxiE0lkkzJ5hk,5
lark/__init__.py,sha256=bc0tK7h7XwHA-Y4vVeJoNIqSMA-MHVTihq8yy795WXo,744
lark/__pycache__/__init__.cpython-311.pyc,,
lark/__pycache__/ast_utils.cpython-311.pyc,,
lark/__pycache__/common.cpython-311.pyc,,
lark/__pycache__/exceptions.cpython-311.pyc,,
lark/__pycache__/grammar.cpython-311.pyc,,
lark/__pycache__/indenter.cpython-311.pyc,,
lark/__pycache__/lark.cpython-311.pyc,,
lark/__pycache__/lexer.cpython-311.pyc,,
lark/__pycache__/load_grammar.cpython-311.pyc,,
lark/__pycache__/parse_tree_builder.cpython-311.pyc,,
lark/__pycache__/parser_frontends.cpython-311.pyc,,
lark/__pycache__/reconstruct.cpython-311.pyc,,
lark/__pycache__/tree.cpython-311.pyc,,
lark/__pycache__/tree_matcher.cpython-311.pyc,,
lark/__pycache__/tree_templates.cpython-311.pyc,,
lark/__pycache__/utils.cpython-311.pyc,,
lark/__pycache__/visitors.cpython-311.pyc,,
lark/__pyinstaller/__init__.py,sha256=_PpFm44f_mwHlCpvYgv9ZgubLfNDc3PlePVir4sxRfI,182
lark/__pyinstaller/__pycache__/__init__.cpython-311.pyc,,
lark/__pyinstaller/__pycache__/hook-lark.cpython-311.pyc,,
lark/__pyinstaller/hook-lark.py,sha256=5aFHiZWVHPRdHT8qnb4kW4JSOql5GusHodHR25_q9sU,599
lark/ast_utils.py,sha256=jwn44ocNQhZGbfcFsEZnwi_gGvPbNgzjQ-0RuEtwDzI,2117
lark/common.py,sha256=M9-CFAUP3--OkftyyWjke-Kc1-pQMczT1MluHCFwdy4,3008
lark/exceptions.py,sha256=g76ygMPfSMl6ukKqFAZVpR2EAJTOOdyfJ_ALXc_MCR8,10939
lark/grammar.py,sha256=DR17QSLSKCRhMOqx2UQh4n-Ywu4CD-wjdQxtuM8OHkY,3665
lark/grammars/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lark/grammars/__pycache__/__init__.cpython-311.pyc,,
lark/grammars/common.lark,sha256=FV9xGIPiPqHRM4ULAxP6jApXRTVsSwbOe697I9s7DLs,885
lark/grammars/lark.lark,sha256=nq1NTZYqm_DPI2mjRIlpd3ZcxPjGhapA4GUzkcfBTQs,1541
lark/grammars/python.lark,sha256=WMakTkpzCqOd0jUjYONI3LOnSy2KRN9NoL9pFtAZYCI,10641
lark/grammars/unicode.lark,sha256=d9YCz0XWimdl4F8M5YCptavBcFG9D58Yd4aMwxjYtEI,96
lark/indenter.py,sha256=L5uNDYUMNrk4ZTWKmW0Tu-H-3GGErLOHygMC32N_twE,4221
lark/lark.py,sha256=_IHWmTxt43kfd9eYVtwx58zEWWSFAq9_gKH7Oeu5PZs,28184
lark/lexer.py,sha256=OwgQPCpQ-vUi-2aeZztsydd4DLkEgCbZeucvEPvHFi4,24037
lark/load_grammar.py,sha256=WYZDxyO6omhA8NKyMjSckfAMwVKuIMF3liiYXE_-kHo,53946
lark/parse_tree_builder.py,sha256=jT_3gCEkBGZoTXAWSnhMn1kRuJILWB-E7XkUciYNHI4,14412
lark/parser_frontends.py,sha256=mxMXxux2hkfTfE859wuVp4-Fr1no6YVEUt8toDjEdPQ,10165
lark/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lark/parsers/__pycache__/__init__.cpython-311.pyc,,
lark/parsers/__pycache__/cyk.cpython-311.pyc,,
lark/parsers/__pycache__/earley.cpython-311.pyc,,
lark/parsers/__pycache__/earley_common.cpython-311.pyc,,
lark/parsers/__pycache__/earley_forest.cpython-311.pyc,,
lark/parsers/__pycache__/grammar_analysis.cpython-311.pyc,,
lark/parsers/__pycache__/lalr_analysis.cpython-311.pyc,,
lark/parsers/__pycache__/lalr_interactive_parser.cpython-311.pyc,,
lark/parsers/__pycache__/lalr_parser.cpython-311.pyc,,
lark/parsers/__pycache__/lalr_parser_state.cpython-311.pyc,,
lark/parsers/__pycache__/xearley.cpython-311.pyc,,
lark/parsers/cyk.py,sha256=c3GLk3kq23Xwb8MqUOjvivwP488KJY6NUWgxqeR5980,12192
lark/parsers/earley.py,sha256=03sW9vfBkcH4NR72EBt8HkndDKSVSH3IdRnDulXWy24,15117
lark/parsers/earley_common.py,sha256=e2e6NrNucw-WMiNV8HqQ_TpGx6P7v_S8f5aEcF0Tkqo,1620
lark/parsers/earley_forest.py,sha256=w4JTb4tVMewue8dL-gCO96-Uo0wd4BbQUfSfIhr7txY,31332
lark/parsers/grammar_analysis.py,sha256=rQ4Sn9EP8gjXGTZXEiWLW0KByPPpeKpN5hSIQZgNl3I,7141
lark/parsers/lalr_analysis.py,sha256=DGHFk2tIluIyeFEVFfsMRU77DVbd598IJnUUOXO04yo,12207
lark/parsers/lalr_interactive_parser.py,sha256=LsgfT1gdne8pXHTCsN6bl6zD6Pdh2dDqp1rIWOzp7Yw,5757
lark/parsers/lalr_parser.py,sha256=6U8jP1AlUsuGxgJBWMq15WuGuyaolsLPevcf8HZ_zZk,4586
lark/parsers/lalr_parser_state.py,sha256=QZ12p4CtvcvFAIKIqkeDBJYgEU3ntQllBJDYXb419ls,3793
lark/parsers/xearley.py,sha256=DboXMNtuN0G-SXrrDm5zgUDUekz85h0Rih2PRvcf1LM,7825
lark/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
lark/reconstruct.py,sha256=s7CevBXchUG_fe2otdAITxIaSXCEIiSjy4Sbh5QC0hs,3763
lark/tools/__init__.py,sha256=FeKYmVUjXSt-vlQm2ktyWkcxaOCTOkZnHD_kOUWjUuA,2469
lark/tools/__pycache__/__init__.cpython-311.pyc,,
lark/tools/__pycache__/nearley.cpython-311.pyc,,
lark/tools/__pycache__/serialize.cpython-311.pyc,,
lark/tools/__pycache__/standalone.cpython-311.pyc,,
lark/tools/nearley.py,sha256=QaLYdW6mYQdDq8JKMisV3lvPqzF0wPgu8q8BtsSA33g,6265
lark/tools/serialize.py,sha256=nwt46LNxkDm0T_Uh9k2wS4fcfgvZQ2dy4-YC_aKhTQk,965
lark/tools/standalone.py,sha256=6eXDqBuzZSpE5BGZm_Fh6X5yRhAPYxNVyl2aUU3ABzA,5627
lark/tree.py,sha256=aWWHMazid8bbJanhmCjK9XK2jRFJ6N6WmlwXJGTsz28,8522
lark/tree_matcher.py,sha256=jHdZJggn405SXmPpGf9U9HLrrsfP4eNNZaj267UTB00,6003
lark/tree_templates.py,sha256=sSnfw1m8txAkJOYhcQrooG7xajVyVplunzTnNsxY720,6139
lark/utils.py,sha256=3qd1-c0YgHYklvx1hA28qF7N_Ty1Zz6TbtCFMzQanNk,11270
lark/visitors.py,sha256=VJ3T1m8p78MwXJotpOAvn06mYEqKyuIlhsAF51U-a3w,21422
@@ -1,2 +0,0 @@
[pyinstaller40]
hook-dirs = lark.__pyinstaller:get_hook_dirs
@@ -1 +0,0 @@
lark
-38
View File
@@ -1,38 +0,0 @@
from .exceptions import (
GrammarError,
LarkError,
LexError,
ParseError,
UnexpectedCharacters,
UnexpectedEOF,
UnexpectedInput,
UnexpectedToken,
)
from .lark import Lark
from .lexer import Token
from .tree import ParseTree, Tree
from .utils import logger
from .visitors import Discard, Transformer, Transformer_NonRecursive, Visitor, v_args
__version__: str = "1.2.2"
__all__ = (
"GrammarError",
"LarkError",
"LexError",
"ParseError",
"UnexpectedCharacters",
"UnexpectedEOF",
"UnexpectedInput",
"UnexpectedToken",
"Lark",
"Token",
"ParseTree",
"Tree",
"logger",
"Discard",
"Transformer",
"Transformer_NonRecursive",
"Visitor",
"v_args",
)

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